package main import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "sync" "git.k6n.net/mats/go-cart-actor/pkg/flow" ) // flowStore holds the saga/flow definitions the service can run. Definitions // are JSON files in a directory (editable by the backoffice saga editor), // overlaid on a set of built-in seeds so a fresh deployment always has the // defaults. Saving validates against the engine before writing. type flowStore struct { mu sync.RWMutex dir string engine *flow.Engine defs map[string]*flow.Definition } var validFlowName = func(s string) bool { if s == "" || len(s) > 64 { return false } for _, r := range s { if !(r == '-' || r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) { return false } } return true } // newFlowStore seeds with the built-in definitions, then overlays any JSON files // found in dir (on-disk wins, so edits persist and take effect). func newFlowStore(dir string, engine *flow.Engine, seed map[string]*flow.Definition) (*flowStore, error) { s := &flowStore{dir: dir, engine: engine, defs: map[string]*flow.Definition{}} for name, def := range seed { s.defs[name] = def } matches, _ := filepath.Glob(filepath.Join(dir, "*.json")) for _, m := range matches { data, err := os.ReadFile(m) if err != nil { return nil, fmt.Errorf("read flow %s: %w", m, err) } def, err := flow.Parse(data) if err != nil { return nil, fmt.Errorf("parse flow %s: %w", m, err) } if def.Name == "" { def.Name = strings.TrimSuffix(filepath.Base(m), ".json") } s.defs[def.Name] = def } return s, nil } func (s *flowStore) get(name string) (*flow.Definition, bool) { s.mu.RLock() defer s.mu.RUnlock() d, ok := s.defs[name] return d, ok } func (s *flowStore) list() []*flow.Definition { s.mu.RLock() defer s.mu.RUnlock() names := make([]string, 0, len(s.defs)) for n := range s.defs { names = append(names, n) } sort.Strings(names) out := make([]*flow.Definition, 0, len(names)) for _, n := range names { out = append(out, s.defs[n]) } return out } // save validates the definition, persists it to /.json and updates // the in-memory map so the next checkout uses it. func (s *flowStore) save(name string, def *flow.Definition) error { if !validFlowName(name) { return fmt.Errorf("invalid flow name %q", name) } if def.Name == "" { def.Name = name } if err := s.engine.Validate(def); err != nil { return err } data, err := json.MarshalIndent(def, "", " ") if err != nil { return err } if err := os.MkdirAll(s.dir, 0o755); err != nil { return err } if err := os.WriteFile(filepath.Join(s.dir, name+".json"), append(data, '\n'), 0o644); err != nil { return err } s.mu.Lock() s.defs[name] = def s.mu.Unlock() return nil }