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