From 6a907236faec40cd38016c91ac5450ac1ff4416c Mon Sep 17 00:00:00 2001 From: mr Date: Wed, 18 Mar 2026 08:40:39 +0100 Subject: [PATCH] export --- models/workflow/plantuml.go | 240 ++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 models/workflow/plantuml.go diff --git a/models/workflow/plantuml.go b/models/workflow/plantuml.go new file mode 100644 index 0000000..9beaede --- /dev/null +++ b/models/workflow/plantuml.go @@ -0,0 +1,240 @@ +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 +// --------------------------------------------------------------------------- + +// ToPlantUML serializes the workflow graph back to a PlantUML file that is +// compatible with ExtractFromPlantUML (round-trip). +// Resource attributes and instance attributes are merged and written as +// human-readable comments: "key: value, nested.key: value". +func (w *Workflow) ToPlantUML() string { + var sb strings.Builder + sb.WriteString("@startuml\n\n") + + varNames := plantUMLVarNames(w.Graph.Items) + + // --- resource declarations --- + for id, item := range w.Graph.Items { + sb.WriteString(plantUMLItemLine(varNames[id], item)) + sb.WriteByte('\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 that must never appear in comments. +// Checked against the root key of each dot-notation path. +var plantUMLSkipFields = map[string]bool{ + // identity / meta + "uuid": true, "name": true, "created_at": true, "updated_at": true, + "creator_id": true, "is_draft": 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, +} + +// 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 != "" { + 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, ", ") +}