export
This commit is contained in:
240
models/workflow/plantuml.go
Normal file
240
models/workflow/plantuml.go
Normal file
@@ -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, ", ")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user