package confmgr import ( "fmt" "math" "slices" "strconv" ) // ReadFunc reads the current raw value of a target signal. The API/engine layer // supplies a closure backed by the broker (ReadNow) so confmgr stays decoupled // from the transport. The returned value mirrors datasource.Value.Data // (float64 | []float64 | string | int64 | bool; enums arrive as an int64 index). type ReadFunc func(ds, signal string) (any, error) // SnapshotEntry records the outcome of reading one parameter's target signal. type SnapshotEntry struct { Key string `json:"key"` DS string `json:"ds"` Signal string `json:"signal"` Value any `json:"value,omitempty"` OK bool `json:"ok"` Error string `json:"error,omitempty"` } // SnapshotResult summarises a snapshot run. Values holds the captured, // type-coerced values ready to populate a new ConfigInstance. type SnapshotResult struct { SetID string `json:"setId"` Entries []SnapshotEntry `json:"entries"` Captured int `json:"captured"` Failed int `json:"failed"` Values map[string]any `json:"-"` } // Snapshot reads the current value of every parameter's target signal via read // and builds a value map for a new instance. Read failures and values that // cannot be coerced to the parameter's type are recorded per-entry rather than // aborting the whole snapshot, so a partial capture is reported faithfully. func Snapshot(set ConfigSet, read ReadFunc) SnapshotResult { res := SnapshotResult{ SetID: set.ID, Entries: make([]SnapshotEntry, 0, len(set.Parameters)), Values: make(map[string]any, len(set.Parameters)), } for _, p := range set.Parameters { e := SnapshotEntry{Key: p.Key, DS: p.DS, Signal: p.Signal} raw, err := read(p.DS, p.Signal) if err != nil { e.Error = err.Error() res.Failed++ res.Entries = append(res.Entries, e) continue } v, err := p.coerceSnapshot(raw) if err != nil { e.Error = err.Error() res.Failed++ res.Entries = append(res.Entries, e) continue } e.Value = v e.OK = true res.Values[p.Key] = v res.Captured++ res.Entries = append(res.Entries, e) } return res } // coerceSnapshot converts a raw datasource value into the canonical Go value the // parameter's type expects, so the result passes checkValue and serialises // cleanly. Enum signals arrive as an int64 index and are mapped to their string. func (p Parameter) coerceSnapshot(raw any) (any, error) { switch p.Type { case TypeFloat: return toFloat(raw) case TypeInt: f, err := toFloat(raw) if err != nil { return nil, err } return int64(math.Round(f)), nil case TypeBool: switch b := raw.(type) { case bool: return b, nil case string: pb, err := strconv.ParseBool(b) if err != nil { return nil, fmt.Errorf("value %q is not a bool", b) } return pb, nil default: f, err := toFloat(raw) if err != nil { return nil, fmt.Errorf("value %v is not a bool", raw) } return f != 0, nil } case TypeString: if s, ok := raw.(string); ok { return s, nil } return fmt.Sprintf("%v", raw), nil case TypeEnum: // Enum signals deliver an int64 index into the metadata strings; map it // to this parameter's enum value. A string already in range is kept. if s, ok := raw.(string); ok { if slices.Contains(p.EnumValues, s) { return s, nil } return nil, fmt.Errorf("value %q is not an allowed enum value", s) } f, err := toFloat(raw) if err != nil { return nil, fmt.Errorf("enum value %v is not an index", raw) } idx := int(math.Round(f)) if idx < 0 || idx >= len(p.EnumValues) { return nil, fmt.Errorf("enum index %d out of range", idx) } return p.EnumValues[idx], nil case TypeFloatArray: return toFloatArray(raw) default: return raw, nil } }