package main import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "gopkg.in/yaml.v3" ) // ops_docs_gen: builds per-service operations docs from resources_ops_yaml. // // Env: // - NUBES_OPS_YAML_DIR (default: /resources_ops_yaml) // - NUBES_OPS_DOCS_DIR (default: /../docs/30_registry/resources/operations) func main() { repoRoot, err := findRepoRoot() if err != nil { panic(err) } yamlDir := strings.TrimSpace(os.Getenv("NUBES_OPS_YAML_DIR")) if yamlDir == "" { yamlDir = filepath.Join(repoRoot, "resources_ops_yaml") } docsDir := strings.TrimSpace(os.Getenv("NUBES_OPS_DOCS_DIR")) if docsDir == "" { root := filepath.Dir(repoRoot) docsDir = filepath.Join(root, "docs", "30_registry", "resources", "operations") } if err := os.MkdirAll(docsDir, 0o755); err != nil { panic(err) } entries := []docEntry{} err = filepath.WalkDir(yamlDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") { return nil } b, err := os.ReadFile(path) if err != nil { return err } var spec ServiceOpsSpec if err := yaml.Unmarshal(b, &spec); err != nil { return err } if spec.ServiceID == 0 || spec.Name == "" { return nil } fileName := fmt.Sprintf("%d_%s.md", spec.ServiceID, sanitizeName(spec.Name)) outPath := filepath.Join(docsDir, fileName) if err := writeServiceDoc(outPath, spec); err != nil { return err } entries = append(entries, docEntry{ ServiceID: spec.ServiceID, Name: spec.Name, File: fileName, Title: serviceTitle(spec), OpCount: len(spec.Operations), }) return nil }) if err != nil { panic(err) } sort.Slice(entries, func(i, j int) bool { return entries[i].ServiceID < entries[j].ServiceID }) if err := writeIndex(filepath.Join(docsDir, "index.md"), entries); err != nil { panic(err) } fmt.Printf("Generated %d operations docs in %s\n", len(entries), docsDir) } type docEntry struct { ServiceID int Name string File string Title string OpCount int } func writeServiceDoc(path string, spec ServiceOpsSpec) error { var b strings.Builder b.WriteString(fmt.Sprintf("# Operations for nubes_%s\n\n", spec.Name)) b.WriteString(fmt.Sprintf("Service ID: `%d`\n\n", spec.ServiceID)) if title := serviceTitle(spec); title != "" { b.WriteString(fmt.Sprintf("Service: %s\n\n", title)) } if strings.TrimSpace(spec.ServiceMan) != "" { b.WriteString("## UI description\n\n") b.WriteString(spec.ServiceMan) b.WriteString("\n\n") } b.WriteString("## Operations\n\n") b.WriteString("Non-CRUD operations are typically executed via an action resource.\n") b.WriteString("Domain operations (for example create_user, create_database) are best modeled as separate resources.\n\n") if len(spec.Operations) == 0 { b.WriteString("No operations found.\n") return os.WriteFile(path, []byte(b.String()), 0o644) } for _, op := range spec.Operations { b.WriteString(fmt.Sprintf("- `%s` (id: %d)\n", op.Name, op.ID)) } b.WriteString("\n") for _, op := range spec.Operations { b.WriteString(fmt.Sprintf("## Operation: %s\n\n", op.Name)) b.WriteString(fmt.Sprintf("Operation ID: `%d`\n\n", op.ID)) if strings.TrimSpace(op.Man) != "" { b.WriteString(op.Man) b.WriteString("\n\n") } if len(op.Params) == 0 { b.WriteString("No parameters.\n\n") continue } b.WriteString("| Code | Type | Required | Default | ID | RefSvcId | ValueList | Func | Regex | Min | Max | Sensitive | DependsOn |\n") b.WriteString("|---|---|---|---|---|---|---|---|---|---|---|---|---|\n") for _, p := range op.Params { b.WriteString(fmt.Sprintf("| `%s` | `%s` | `%t` | `%s` | `%d` | `%s` | `%s` | `%s` | `%s` | `%s` | `%s` | `%t` | `%s` |\n", esc(p.Code), esc(p.DataType), p.Required, esc(formatValue(p.Default)), p.ID, esc(formatRef(p.RefSvcId)), esc(formatList(p.ValueList)), esc(p.Func), esc(p.Regex), esc(formatValue(p.MinValue)), esc(formatValue(p.MaxValue)), p.IsSensitive, esc(formatValue(p.DependsOn)), )) } b.WriteString("\n") } return os.WriteFile(path, []byte(b.String()), 0o644) } func writeIndex(path string, entries []docEntry) error { var b strings.Builder b.WriteString("# Operations by service\n\n") b.WriteString("Auto-generated list of available operations per service.\n\n") b.WriteString("Note: non-CRUD operations should be invoked via an action resource,\n") b.WriteString("and domain operations are best represented as dedicated resources.\n\n") for _, e := range entries { b.WriteString(fmt.Sprintf("- %d - nubes_%s (%d ops): [%s](%s)\n", e.ServiceID, e.Name, e.OpCount, e.Title, e.File)) } return os.WriteFile(path, []byte(b.String()), 0o644) } func sanitizeName(name string) string { name = strings.ToLower(strings.TrimSpace(name)) name = strings.ReplaceAll(name, " ", "_") name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "\\", "_") name = strings.ReplaceAll(name, ":", "_") name = strings.ReplaceAll(name, "-", "_") return name } func serviceTitle(spec ServiceOpsSpec) string { if strings.TrimSpace(spec.ServiceDisplayName) != "" { return spec.ServiceDisplayName } if strings.TrimSpace(spec.ServiceShortName) != "" { return spec.ServiceShortName } return "" } func formatList(items []string) string { if len(items) == 0 { return "" } return strings.Join(items, ",") } func formatRef(value *int) string { if value == nil { return "" } return fmt.Sprintf("%d", *value) } func formatValue(value interface{}) string { if value == nil { return "" } switch t := value.(type) { case string: return t default: b, err := json.Marshal(t) if err != nil { return fmt.Sprintf("%v", value) } return string(b) } } func esc(value string) string { return strings.ReplaceAll(value, "|", "\\|") } func findRepoRoot() (string, error) { wd, err := os.Getwd() if err != nil { return "", err } current := wd for i := 0; i < 8; i++ { candidate := filepath.Join(current, "resources_yaml") info, err := os.Stat(candidate) if err == nil && info.IsDir() { return current, nil } parent := filepath.Dir(current) if parent == current { break } current = parent } return "", fmt.Errorf("failed to locate universal_rebuild repo root") } // ===== spec ===== type ServiceOpsSpec struct { Name string `yaml:"name"` ServiceID int `yaml:"service_id"` ServiceDisplayName string `yaml:"service_display_name,omitempty"` ServiceShortName string `yaml:"service_short_name,omitempty"` ServiceMan string `yaml:"service_man,omitempty"` Operations []OperationSpec `yaml:"operations"` } type OperationSpec struct { Name string `yaml:"name"` ID int `yaml:"id"` Man string `yaml:"man,omitempty"` Params []ParamSpec `yaml:"params"` } type ParamSpec struct { ID int `yaml:"id"` Code string `yaml:"code"` DataType string `yaml:"data_type,omitempty"` Required bool `yaml:"required"` Default interface{} `yaml:"default,omitempty"` ValueList []string `yaml:"value_list,omitempty"` RefSvcId *int `yaml:"ref_svc_id,omitempty"` Func string `yaml:"func,omitempty"` Regex string `yaml:"regex,omitempty"` UniqueScope string `yaml:"unique_scope,omitempty"` MaxLength *int `yaml:"maxlength,omitempty"` MinLength *int `yaml:"minlength,omitempty"` MaxValue interface{} `yaml:"maxvalue,omitempty"` MinValue interface{} `yaml:"minvalue,omitempty"` Descr string `yaml:"descr,omitempty"` Man string `yaml:"man,omitempty"` Sort *int `yaml:"sort,omitempty"` DependsOn interface{} `yaml:"depends_on,omitempty"` IsModifiable *bool `yaml:"is_modifiable,omitempty"` IsSensitive bool `yaml:"is_sensitive,omitempty"` }