281 lines
8.2 KiB
Go
281 lines
8.2 KiB
Go
package workflow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"cloud.o-forge.io/core/oc-lib/models/resources"
|
|
"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// PlantUML export
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// plantUMLProcedures defines the !procedure blocks for each resource type.
|
|
// These make the output valid, renderable PlantUML while remaining parseable
|
|
// by ExtractFromPlantUML (which already skips lines containing "!procedure").
|
|
const plantUMLProcedures = `!procedure Processing($var, $name)
|
|
component "$name" as $var <<Processing>>
|
|
!endprocedure
|
|
|
|
!procedure Data($var, $name)
|
|
file "$name" as $var <<Data>>
|
|
!endprocedure
|
|
|
|
!procedure Storage($var, $name)
|
|
database "$name" as $var <<Storage>>
|
|
!endprocedure
|
|
|
|
!procedure ComputeUnit($var, $name)
|
|
node "$name" as $var <<ComputeUnit>>
|
|
!endprocedure
|
|
|
|
!procedure WorkflowEvent($var, $name)
|
|
usecase "$name" as $var <<WorkflowEvent>>
|
|
!endprocedure
|
|
|
|
!procedure Workflow($var, $name)
|
|
frame "$name" as $var <<Workflow>>
|
|
!endprocedure
|
|
`
|
|
|
|
// ToPlantUML serializes the workflow graph to a valid, renderable PlantUML file
|
|
// that is also compatible with ExtractFromPlantUML (round-trip).
|
|
// Resource and instance attributes are written as human-readable comments:
|
|
//
|
|
// Processing(p1, "NDVI") ' access.container.image: myrepo/ndvi:1.2, infrastructure: 0
|
|
func (w *Workflow) ToPlantUML() string {
|
|
var sb strings.Builder
|
|
sb.WriteString("@startuml\n\n")
|
|
sb.WriteString(plantUMLProcedures)
|
|
sb.WriteByte('\n')
|
|
|
|
varNames := plantUMLVarNames(w.Graph.Items)
|
|
|
|
// --- resource declarations ---
|
|
for id, item := range w.Graph.Items {
|
|
if line := plantUMLItemLine(varNames[id], item); line != "" {
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
}
|
|
|
|
sb.WriteByte('\n')
|
|
|
|
// --- links ---
|
|
for _, link := range w.Graph.Links {
|
|
src := varNames[link.Source.ID]
|
|
dst := varNames[link.Destination.ID]
|
|
if src == "" || dst == "" {
|
|
continue
|
|
}
|
|
line := fmt.Sprintf("%s --> %s", src, dst)
|
|
if comment := plantUMLLinkComment(link); comment != "" {
|
|
line += " ' " + comment
|
|
}
|
|
sb.WriteString(line + "\n")
|
|
}
|
|
|
|
sb.WriteString("\n@enduml\n")
|
|
return sb.String()
|
|
}
|
|
|
|
// plantUMLVarNames assigns short, deterministic variable names to each graph
|
|
// item (d1, d2, p1, s1, c1, e1, wf1 …).
|
|
func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
|
|
counters := map[string]int{}
|
|
varNames := map[string]string{}
|
|
// Sort IDs for deterministic output
|
|
ids := make([]string, 0, len(items))
|
|
for id := range items {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
for _, id := range ids {
|
|
prefix := plantUMLPrefix(items[id])
|
|
counters[prefix]++
|
|
varNames[id] = fmt.Sprintf("%s%d", prefix, counters[prefix])
|
|
}
|
|
return varNames
|
|
}
|
|
|
|
func plantUMLPrefix(item graph.GraphItem) string {
|
|
switch {
|
|
case item.NativeTool != nil:
|
|
return "e"
|
|
case item.Data != nil:
|
|
return "d"
|
|
case item.Processing != nil:
|
|
return "p"
|
|
case item.Storage != nil:
|
|
return "s"
|
|
case item.Compute != nil:
|
|
return "c"
|
|
case item.Workflow != nil:
|
|
return "wf"
|
|
}
|
|
return "u"
|
|
}
|
|
|
|
// plantUMLItemLine builds the PlantUML declaration line for one graph item.
|
|
func plantUMLItemLine(varName string, item graph.GraphItem) string {
|
|
switch {
|
|
case item.NativeTool != nil:
|
|
// WorkflowEvent has no instance and no configurable attributes.
|
|
return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.NativeTool.GetName())
|
|
|
|
case item.Data != nil:
|
|
return plantUMLResourceLine("Data", varName, item.Data)
|
|
|
|
case item.Processing != nil:
|
|
return plantUMLResourceLine("Processing", varName, item.Processing)
|
|
|
|
case item.Storage != nil:
|
|
return plantUMLResourceLine("Storage", varName, item.Storage)
|
|
|
|
case item.Compute != nil:
|
|
return plantUMLResourceLine("ComputeUnit", varName, item.Compute)
|
|
|
|
case item.Workflow != nil:
|
|
return plantUMLResourceLine("Workflow", varName, item.Workflow)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func plantUMLResourceLine(macro, varName string, res resources.ResourceInterface) string {
|
|
line := fmt.Sprintf("%s(%s, \"%s\")", macro, varName, res.GetName())
|
|
if comment := plantUMLResourceComment(res); comment != "" {
|
|
line += " ' " + comment
|
|
}
|
|
return line
|
|
}
|
|
|
|
// plantUMLResourceComment merges resource-level fields with the first instance
|
|
// fields (instance overrides resource) and formats them as human-readable pairs.
|
|
func plantUMLResourceComment(res resources.ResourceInterface) string {
|
|
m := plantUMLToFlatMap(res)
|
|
if inst := res.GetSelectedInstance(nil); inst != nil {
|
|
for k, v := range plantUMLToFlatMap(inst) {
|
|
m[k] = v
|
|
}
|
|
}
|
|
return plantUMLFlatMapToComment(m)
|
|
}
|
|
|
|
// plantUMLLinkComment serializes StorageLinkInfos (first entry) as flat
|
|
// human-readable pairs prefixed with "storage_link_infos.".
|
|
func plantUMLLinkComment(link graph.GraphLink) string {
|
|
if len(link.StorageLinkInfos) == 0 {
|
|
return ""
|
|
}
|
|
infoFlat := plantUMLToFlatMap(link.StorageLinkInfos[0])
|
|
prefixed := make(map[string]string, len(infoFlat))
|
|
for k, v := range infoFlat {
|
|
prefixed["storage_link_infos."+k] = v
|
|
}
|
|
return plantUMLFlatMapToComment(prefixed)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Flat-map helpers (shared by import & export)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// plantUMLSkipFields lists JSON field names (root keys) that must never appear
|
|
// in human-readable comments. All names are the actual JSON tags, not Go field names.
|
|
var plantUMLSkipFields = map[string]bool{
|
|
// AbstractObject — identity & audit (json tags)
|
|
"id": true, "name": true, "is_draft": true, "access_mode": true, "signature": true,
|
|
"creator_id": true, "user_creator_id": true,
|
|
"creation_date": true, "update_date": true,
|
|
"updater_id": true, "user_updater_id": true,
|
|
// internal resource type identifier (AbstractResource.Type / GetType())
|
|
"type": true,
|
|
// relationships / pricing
|
|
"instances": true, "partnerships": true,
|
|
"allowed_booking_modes": true, "usage_restrictions": true,
|
|
// display / admin
|
|
"logo": true, "description": true, "short_description": true, "owners": true,
|
|
// runtime params
|
|
"env": true, "inputs": true, "outputs": true,
|
|
// NativeTool internals
|
|
"kind": true, "params": true,
|
|
}
|
|
|
|
// zeroTimeStr is the JSON representation of Go's zero time.Time value.
|
|
// encoding/json does not treat it as "empty" for omitempty, so we filter it explicitly.
|
|
const zeroTimeStr = "0001-01-01T00:00:00Z"
|
|
|
|
// plantUMLToFlatMap marshals v to JSON and flattens the resulting object into
|
|
// a map[string]string using dot notation for nested keys, skipping zero values
|
|
// and known meta fields.
|
|
func plantUMLToFlatMap(v interface{}) map[string]string {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var raw map[string]interface{}
|
|
if err := json.Unmarshal(b, &raw); err != nil {
|
|
return nil
|
|
}
|
|
result := map[string]string{}
|
|
plantUMLFlattenJSON(raw, "", result)
|
|
return result
|
|
}
|
|
|
|
// plantUMLFlattenJSON recursively walks a JSON object and writes scalar leaf
|
|
// values into result using dot-notation keys.
|
|
func plantUMLFlattenJSON(m map[string]interface{}, prefix string, result map[string]string) {
|
|
for k, v := range m {
|
|
fullKey := k
|
|
if prefix != "" {
|
|
fullKey = prefix + "." + k
|
|
}
|
|
// Skip fields whose root key is in the deny-list
|
|
if plantUMLSkipFields[strings.SplitN(fullKey, ".", 2)[0]] {
|
|
continue
|
|
}
|
|
switch val := v.(type) {
|
|
case map[string]interface{}:
|
|
plantUMLFlattenJSON(val, fullKey, result)
|
|
case []interface{}:
|
|
// Arrays are not representable in flat human-readable format; skip.
|
|
case float64:
|
|
if val != 0 {
|
|
if val == float64(int64(val)) {
|
|
result[fullKey] = strconv.FormatInt(int64(val), 10)
|
|
} else {
|
|
result[fullKey] = strconv.FormatFloat(val, 'f', -1, 64)
|
|
}
|
|
}
|
|
case bool:
|
|
if val {
|
|
result[fullKey] = "true"
|
|
}
|
|
case string:
|
|
if val != "" && val != zeroTimeStr {
|
|
result[fullKey] = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// plantUMLFlatMapToComment converts a flat map to a sorted "key: value, …" string.
|
|
func plantUMLFlatMapToComment(m map[string]string) string {
|
|
if len(m) == 0 {
|
|
return ""
|
|
}
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
parts := make([]string, 0, len(keys))
|
|
for _, k := range keys {
|
|
parts = append(parts, k+": "+m[k])
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|