Implemented confi snapshots
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user