3588AdminBackend/internal/service/config_runtime_render.go

448 lines
14 KiB
Go

package service
import (
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
)
var slotTokenRE = regexp.MustCompile(`^\$\{slot:([A-Za-z0-9_.-]+)\.([A-Za-z0-9_.-]+)\}$`)
func renderRuntimeConfig(templateRaw map[string]any, templatePath string, profileRaw map[string]any, profilePath string, overlays []runtimeOverlayInput, metadata map[string]any) (map[string]any, error) {
tplName, err := runtimeTemplateName(templateRaw, templatePath)
if err != nil {
return nil, err
}
instances, err := runtimeProfileInstances(profileRaw, tplName)
if err != nil {
return nil, err
}
instances, err = mergeRuntimeTemplateParams(instances, runtimeTemplateParams(templateRaw))
if err != nil {
return nil, err
}
renderedTemplates := map[string]any{}
renderedInstances := make([]any, 0, len(instances))
for _, instance := range instances {
boundName, boundTemplate, renderedInstance, err := renderRuntimeSceneInstance(templateRaw, templatePath, instance)
if err != nil {
return nil, err
}
renderedTemplates[boundName] = boundTemplate
renderedInstances = append(renderedInstances, renderedInstance)
}
root := map[string]any{
"templates": renderedTemplates,
"instances": renderedInstances,
}
for _, key := range []string{"global", "queue"} {
if value, ok := profileRaw[key]; ok {
root[key] = deepCopyAny(value)
}
}
for _, overlay := range overlays {
var err error
root, err = applyRuntimeOverlay(root, overlay.Raw)
if err != nil {
return nil, err
}
}
if metadata != nil {
root["metadata"] = buildRuntimeMetadata(tplName, templatePath, profileRaw, profilePath, overlays, root, metadata)
}
return root, nil
}
type runtimeOverlayInput struct {
Name string
Path string
Raw map[string]any
}
func runtimeTemplateName(templateRaw map[string]any, templatePath string) (string, error) {
name := strings.TrimSpace(firstString(templateRaw["name"], strings.TrimSuffix(filepath.Base(templatePath), filepath.Ext(templatePath))))
if name == "" {
return "", fmt.Errorf("%s: template name is empty", templatePath)
}
return name, nil
}
func runtimeTemplateBody(templateRaw map[string]any) (map[string]any, error) {
body := templateRaw
if nested, ok := templateRaw["template"].(map[string]any); ok {
body = nested
}
nodes, hasNodes := body["nodes"].([]any)
edges, hasEdges := body["edges"].([]any)
if !hasNodes || !hasEdges {
return nil, fmt.Errorf("template body must contain nodes[] and edges[]")
}
out := map[string]any{
"nodes": deepCopyAny(nodes),
"edges": deepCopyAny(edges),
}
if executor, ok := body["executor"]; ok {
out["executor"] = deepCopyAny(executor)
}
return out, nil
}
func runtimeTemplateParams(templateRaw map[string]any) map[string]any {
params, _ := templateRaw["params"].(map[string]any)
if params == nil {
return map[string]any{}
}
return cloneMap(params)
}
func runtimeProfileInstances(profileRaw map[string]any, tplName string) ([]map[string]any, error) {
if items, ok := profileRaw["instances"].([]any); ok {
out := make([]map[string]any, 0, len(items))
for _, item := range items {
instance, _ := item.(map[string]any)
if instance == nil {
return nil, fmt.Errorf("profile.instances entries must be objects")
}
cloned := deepCopyMap(instance)
if strings.TrimSpace(stringValue(cloned["template"])) == "" {
cloned["template"] = tplName
}
out = append(out, cloned)
}
return out, nil
}
name := strings.TrimSpace(stringValue(profileRaw["name"]))
if name == "" {
return nil, fmt.Errorf("profile must contain name or instances[]")
}
instance := map[string]any{
"name": name,
"template": tplName,
}
if params, ok := profileRaw["params"].(map[string]any); ok && len(params) > 0 {
instance["params"] = deepCopyMap(params)
}
if override, ok := profileRaw["override"].(map[string]any); ok && len(override) > 0 {
instance["override"] = deepCopyMap(override)
}
return []map[string]any{instance}, nil
}
func mergeRuntimeTemplateParams(instances []map[string]any, sharedParams map[string]any) ([]map[string]any, error) {
if len(sharedParams) == 0 {
return instances, nil
}
out := make([]map[string]any, 0, len(instances))
for _, item := range instances {
inst := deepCopyMap(item)
params, _ := inst["params"].(map[string]any)
if params == nil {
params = map[string]any{}
}
inst["params"] = deepMergeMap(sharedParams, params)
out = append(out, inst)
}
return out, nil
}
func renderRuntimeSceneInstance(templateRaw map[string]any, templatePath string, instance map[string]any) (string, map[string]any, map[string]any, error) {
instanceName := strings.TrimSpace(stringValue(instance["name"]))
if instanceName == "" {
return "", nil, nil, fmt.Errorf("scene instance name is required")
}
tplName, err := runtimeTemplateName(templateRaw, templatePath)
if err != nil {
return "", nil, nil, err
}
boundName := tplName + "__" + instanceName
context, err := buildRuntimeBindingContext(instance)
if err != nil {
return "", nil, nil, err
}
templateBody, err := runtimeTemplateBody(templateRaw)
if err != nil {
return "", nil, nil, err
}
slotRequirements, err := runtimeSlotRequirements(templateRaw)
if err != nil {
return "", nil, nil, err
}
renderedTemplateAny, err := expandRuntimeSlotTokens(templateBody, context, slotRequirements)
if err != nil {
return "", nil, nil, err
}
renderedTemplate, _ := renderedTemplateAny.(map[string]any)
renderedInstance := map[string]any{
"name": instanceName,
"template": boundName,
}
if sceneMeta, ok := instance["scene_meta"].(map[string]any); ok && len(sceneMeta) > 0 {
renderedInstance["scene_meta"] = deepCopyMap(sceneMeta)
}
if params, ok := instance["params"].(map[string]any); ok && len(params) > 0 {
renderedInstance["params"] = deepCopyMap(params)
}
return boundName, renderedTemplate, renderedInstance, nil
}
func runtimeSlotRequirements(templateRaw map[string]any) (map[string]bool, error) {
group, err := parseTemplateSlots(templateRaw)
if err != nil {
return nil, err
}
out := map[string]bool{}
for _, slot := range group.Inputs {
out[slot.Name] = slot.Required
}
for _, slot := range group.Services {
out[slot.Name] = slot.Required
}
for _, slot := range group.Outputs {
out[slot.Name] = slot.Required
}
return out, nil
}
func buildRuntimeBindingContext(instance map[string]any) (map[string]any, error) {
context := map[string]any{}
if sceneMeta, ok := instance["scene_meta"].(map[string]any); ok && len(sceneMeta) > 0 {
context["scene"] = deepCopyMap(sceneMeta)
}
for _, groupName := range []string{"input_bindings", "service_bindings", "output_bindings"} {
group, _ := instance[groupName].(map[string]any)
for slotName, raw := range group {
entry, _ := raw.(map[string]any)
if entry == nil {
return nil, fmt.Errorf("binding entry must be an object")
}
context[slotName] = resolvedRuntimeBindingValue(entry)
}
}
return context, nil
}
func resolvedRuntimeBindingValue(entry map[string]any) map[string]any {
if resolved, ok := entry["resolved"].(map[string]any); ok && resolved != nil {
return deepCopyMap(resolved)
}
return deepCopyMap(entry)
}
func expandRuntimeSlotTokens(value any, context map[string]any, slotRequirements map[string]bool) (any, error) {
switch typed := value.(type) {
case map[string]any:
out := make(map[string]any, len(typed))
for key, item := range typed {
expanded, err := expandRuntimeSlotTokens(item, context, slotRequirements)
if err != nil {
return nil, err
}
out[key] = expanded
}
return out, nil
case []any:
out := make([]any, 0, len(typed))
for _, item := range typed {
expanded, err := expandRuntimeSlotTokens(item, context, slotRequirements)
if err != nil {
return nil, err
}
out = append(out, expanded)
}
return out, nil
case string:
match := slotTokenRE.FindStringSubmatch(strings.TrimSpace(typed))
if len(match) != 3 {
return typed, nil
}
required, known := slotRequirements[match[1]]
if !known {
required = true
}
slotValues, _ := context[match[1]].(map[string]any)
if slotValues == nil {
if !required {
return "", nil
}
return nil, fmt.Errorf("required slot '%s' is not bound", match[1])
}
fieldValue, ok := slotValues[match[2]]
if !ok {
if !required {
return "", nil
}
return nil, fmt.Errorf("required slot field '%s.%s' is not bound", match[1], match[2])
}
return deepCopyAny(fieldValue), nil
default:
return deepCopyAny(value), nil
}
}
func applyRuntimeOverlay(root map[string]any, overlay map[string]any) (map[string]any, error) {
out := deepCopyMap(root)
for _, key := range []string{"global", "queue", "templates"} {
if value, ok := overlay[key]; ok {
out[key] = deepMergeAny(out[key], value)
}
}
if rawPatches, ok := overlay["instance_overrides"]; ok {
patches, _ := rawPatches.(map[string]any)
if patches == nil {
return nil, fmt.Errorf("overlay.instance_overrides must be an object")
}
instances, _ := out["instances"].([]any)
mergedInstances := make([]any, 0, len(instances))
for _, item := range instances {
instance, _ := item.(map[string]any)
merged := deepCopyMap(instance)
if patch, ok := patches["*"].(map[string]any); ok {
merged = mergeRuntimeInstancePatch(merged, patch)
}
name := stringValue(merged["name"])
if patch, ok := patches[name].(map[string]any); ok {
merged = mergeRuntimeInstancePatch(merged, patch)
}
mergedInstances = append(mergedInstances, merged)
}
out["instances"] = mergedInstances
}
if rawInstances, ok := overlay["instances"]; ok {
patchList, _ := rawInstances.([]any)
if patchList == nil {
return nil, fmt.Errorf("overlay.instances must be an array")
}
instances, _ := out["instances"].([]any)
byName := map[string]int{}
for i, item := range instances {
instance, _ := item.(map[string]any)
byName[stringValue(instance["name"])] = i
}
for _, item := range patchList {
patch, _ := item.(map[string]any)
name := stringValue(patch["name"])
if patch == nil || name == "" {
return nil, fmt.Errorf("overlay.instances entries must be objects with name")
}
idx, ok := byName[name]
if !ok {
return nil, fmt.Errorf("overlay instance not found in profile: %s", name)
}
instance, _ := instances[idx].(map[string]any)
instances[idx] = mergeRuntimeInstancePatch(instance, patch)
}
out["instances"] = instances
}
return out, nil
}
func mergeRuntimeInstancePatch(instance map[string]any, patch map[string]any) map[string]any {
merged := deepCopyMap(instance)
if params, ok := patch["params"].(map[string]any); ok {
existing, _ := merged["params"].(map[string]any)
merged["params"] = deepMergeMap(existing, params)
}
if override, ok := patch["override"].(map[string]any); ok {
existing, _ := merged["override"].(map[string]any)
merged["override"] = deepMergeMap(existing, override)
}
for key, value := range patch {
if key == "name" || key == "template" || key == "params" || key == "override" {
continue
}
merged[key] = deepMergeAny(merged[key], value)
}
return merged
}
func buildRuntimeMetadata(templateName string, templatePath string, profileRaw map[string]any, profilePath string, overlays []runtimeOverlayInput, root map[string]any, metadata map[string]any) map[string]any {
instanceNames := make([]string, 0)
instanceDisplayNames := make([]string, 0)
instances, _ := root["instances"].([]any)
for _, item := range instances {
instance, _ := item.(map[string]any)
if name := strings.TrimSpace(stringValue(instance["name"])); name != "" {
instanceNames = append(instanceNames, name)
}
if sceneMeta, ok := instance["scene_meta"].(map[string]any); ok {
if displayName := strings.TrimSpace(stringValue(sceneMeta["display_name"])); displayName != "" {
instanceDisplayNames = append(instanceDisplayNames, displayName)
}
}
}
overlayNames := make([]any, 0, len(overlays))
overlayPaths := make([]any, 0, len(overlays))
for _, overlay := range overlays {
overlayNames = append(overlayNames, overlay.Name)
overlayPaths = append(overlayPaths, overlay.Path)
}
out := map[string]any{
"template": templateName,
"template_path": templatePath,
"profile": strings.TrimSpace(firstString(profileRaw["name"], strings.TrimSuffix(filepath.Base(profilePath), filepath.Ext(profilePath)))),
"business_name": strings.TrimSpace(stringValue(profileRaw["business_name"])),
"profile_path": profilePath,
"instance_names": instanceNames,
"instance_display_names": instanceDisplayNames,
"overlays": overlayNames,
"overlay_paths": overlayPaths,
}
for key, value := range metadata {
out[key] = deepCopyAny(value)
}
return out
}
func deepMergeMap(base map[string]any, override map[string]any) map[string]any {
if base == nil {
base = map[string]any{}
}
return deepMergeAny(base, override).(map[string]any)
}
func deepMergeAny(base any, override any) any {
baseMap, baseIsMap := base.(map[string]any)
overrideMap, overrideIsMap := override.(map[string]any)
if baseIsMap && overrideIsMap {
merged := deepCopyMap(baseMap)
for key, value := range overrideMap {
if existing, ok := merged[key]; ok {
merged[key] = deepMergeAny(existing, value)
} else {
merged[key] = deepCopyAny(value)
}
}
return merged
}
return deepCopyAny(override)
}
func deepCopyMap(in map[string]any) map[string]any {
if in == nil {
return map[string]any{}
}
out, _ := deepCopyAny(in).(map[string]any)
return out
}
func deepCopyAny(value any) any {
if value == nil {
return nil
}
body, err := json.Marshal(value)
if err != nil {
return value
}
var out any
if err := json.Unmarshal(body, &out); err != nil {
return value
}
return out
}