130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
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
|
|
}
|
|
}
|