654 lines
13 KiB
Go
654 lines
13 KiB
Go
package settings
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/engine/problems"
|
|
"aego/format/aetext"
|
|
"aego/schema"
|
|
)
|
|
|
|
const diagOwner = "engine"
|
|
|
|
type Key uint32
|
|
|
|
type entry struct {
|
|
d Desc
|
|
project, user, session, eff schema.Value
|
|
hasProject, hasUser, hasSession bool
|
|
src Layer
|
|
}
|
|
|
|
func (e *entry) value() (schema.Value, Layer) {
|
|
switch {
|
|
case e.hasSession:
|
|
return e.session, LayerSession
|
|
case e.hasUser:
|
|
return e.user, LayerUser
|
|
case e.hasProject:
|
|
return e.project, LayerProject
|
|
}
|
|
return e.d.Default, LayerDefault
|
|
}
|
|
|
|
func (e *entry) recompute() bool {
|
|
v, src := e.value()
|
|
changed := !Equal(v, e.eff)
|
|
e.eff, e.src = v, src
|
|
return changed
|
|
}
|
|
|
|
type owner struct {
|
|
version int
|
|
migrate func(*aetext.Node, int, int) error
|
|
userExtra []aetext.Field
|
|
projExtra []aetext.Field
|
|
newer *aetext.Node
|
|
}
|
|
|
|
type sub struct {
|
|
id uint64
|
|
key Key
|
|
fn func(Key)
|
|
}
|
|
|
|
type saved struct {
|
|
k Key
|
|
project, user, session schema.Value
|
|
hp, hu, hs bool
|
|
}
|
|
|
|
type pending struct {
|
|
deadline float64
|
|
snap []saved
|
|
}
|
|
|
|
type Store struct {
|
|
list []entry
|
|
byID map[string]Key
|
|
owners map[string]*owner
|
|
order []string
|
|
frozen bool
|
|
started bool
|
|
subs []sub
|
|
nextSub uint64
|
|
preset Key
|
|
names func(section, list string) []string
|
|
persist func([]byte) error
|
|
afterLoad []func()
|
|
now float64
|
|
confirm *pending
|
|
restart bool
|
|
dirty bool
|
|
readOnly bool
|
|
diags []problems.Diagnostic
|
|
rev uint64
|
|
unknownUser []*aetext.Node
|
|
unknownProject []*aetext.Node
|
|
}
|
|
|
|
func New() *Store {
|
|
return &Store{byID: map[string]Key{}, owners: map[string]*owner{}}
|
|
}
|
|
|
|
func bad(id, msg string) error {
|
|
return errs.New(errs.EngineModule, "setting "+id+" "+msg, log.F("setting", id))
|
|
}
|
|
|
|
func (s *Store) ownerOf(name string) *owner {
|
|
o := s.owners[name]
|
|
if o == nil {
|
|
o = &owner{version: 1}
|
|
s.owners[name] = o
|
|
s.order = append(s.order, name)
|
|
}
|
|
return o
|
|
}
|
|
|
|
func (s *Store) OwnerVersion(v OwnerVersion) error {
|
|
switch {
|
|
case s.frozen:
|
|
return bad(v.Owner, "version is set after the settings were frozen")
|
|
case v.Version < 1:
|
|
return bad(v.Owner, "version must be positive")
|
|
}
|
|
o := s.ownerOf(v.Owner)
|
|
o.version, o.migrate = v.Version, v.Migrate
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Register(d Desc) (Key, error) {
|
|
var zero schema.Kind
|
|
switch {
|
|
case s.frozen:
|
|
return 0, bad(d.ID, "is registered after the settings were frozen")
|
|
case d.Owner == "" || !strings.HasPrefix(d.ID, d.Owner+".") || !ValidPath(d.ID):
|
|
return 0, bad(d.ID, "must be named "+d.Owner+".<name> with letters, digits and dots")
|
|
case !supported(d.Kind):
|
|
return 0, bad(d.ID, "has the unsupported kind "+d.Kind.String())
|
|
}
|
|
if _, dup := s.byID[d.ID]; dup {
|
|
return 0, bad(d.ID, "is registered twice")
|
|
}
|
|
for i := range d.Choices {
|
|
c := &d.Choices[i]
|
|
c.Value, _ = Normalize(d.Kind, c.Value)
|
|
if c.Label == "" {
|
|
c.Label = c.Name
|
|
}
|
|
}
|
|
if d.Default.Kind == zero {
|
|
d.Default = zeroOf(&d)
|
|
}
|
|
def, ok := Normalize(d.Kind, d.Default)
|
|
if !ok {
|
|
return 0, bad(d.ID, "has a default of kind "+d.Default.Kind.String())
|
|
}
|
|
d.Default = def
|
|
if d.Preset {
|
|
if s.preset != 0 {
|
|
return 0, bad(d.ID, "is a second preset")
|
|
}
|
|
if d.Kind != schema.KindEnum || !slices.ContainsFunc(d.Choices, func(c Choice) bool { return c.Name == PresetCustom }) {
|
|
return 0, bad(d.ID, "is a preset and needs enum choices with "+PresetCustom)
|
|
}
|
|
}
|
|
s.ownerOf(d.Owner)
|
|
k := Key(len(s.list) + 1)
|
|
s.list = append(s.list, entry{d: d, eff: def})
|
|
s.byID[d.ID] = k
|
|
if d.Preset {
|
|
s.preset = k
|
|
}
|
|
return k, nil
|
|
}
|
|
|
|
func (s *Store) Freeze() {
|
|
s.frozen = true
|
|
}
|
|
|
|
func (s *Store) Frozen() bool {
|
|
return s.frozen
|
|
}
|
|
|
|
func (s *Store) SetNames(fn func(section, list string) []string) {
|
|
s.names = fn
|
|
}
|
|
|
|
func (s *Store) SetPersist(fn func([]byte) error) {
|
|
s.persist = fn
|
|
}
|
|
|
|
func (s *Store) AfterLoad(fn func()) {
|
|
if s.started {
|
|
fn()
|
|
return
|
|
}
|
|
s.afterLoad = append(s.afterLoad, fn)
|
|
}
|
|
|
|
func (s *Store) Start() {
|
|
if s.started {
|
|
return
|
|
}
|
|
s.started = true
|
|
for i := range s.list {
|
|
e := &s.list[i]
|
|
if e.d.Apply == nil {
|
|
continue
|
|
}
|
|
if err := e.d.Apply(e.eff); err != nil {
|
|
s.report("CFG004", problems.Warning, e.d.ID+" could not be applied: "+err.Error()+", the project value is used", e.d.ID)
|
|
e.hasUser = false
|
|
e.recompute()
|
|
_ = e.d.Apply(e.eff)
|
|
}
|
|
}
|
|
for _, fn := range s.afterLoad {
|
|
fn()
|
|
}
|
|
s.afterLoad = nil
|
|
}
|
|
|
|
func (s *Store) at(k Key) *entry {
|
|
if k == 0 || int(k) > len(s.list) {
|
|
return nil
|
|
}
|
|
return &s.list[k-1]
|
|
}
|
|
|
|
func (s *Store) Key(id string) Key {
|
|
return s.byID[id]
|
|
}
|
|
|
|
func (s *Store) Len() int {
|
|
return len(s.list)
|
|
}
|
|
|
|
func (s *Store) Desc(k Key) *Desc {
|
|
if e := s.at(k); e != nil {
|
|
return &e.d
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Each(fn func(Key, *Desc)) {
|
|
for i := range s.list {
|
|
fn(Key(i+1), &s.list[i].d)
|
|
}
|
|
}
|
|
|
|
func (s *Store) Value(k Key) schema.Value {
|
|
if e := s.at(k); e != nil {
|
|
return e.eff
|
|
}
|
|
return schema.Value{}
|
|
}
|
|
|
|
func (s *Store) Bool(k Key) bool {
|
|
e := s.at(k)
|
|
return e != nil && e.eff.I != 0
|
|
}
|
|
|
|
func (s *Store) Int(k Key) int64 {
|
|
if e := s.at(k); e != nil {
|
|
return e.eff.I
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *Store) Float(k Key) float64 {
|
|
if e := s.at(k); e != nil {
|
|
return e.eff.F
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *Store) Text(k Key) string {
|
|
if e := s.at(k); e != nil {
|
|
return e.eff.S
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Store) Source(k Key) Layer {
|
|
if e := s.at(k); e != nil {
|
|
return e.src
|
|
}
|
|
return LayerDefault
|
|
}
|
|
|
|
func (s *Store) Base(k Key) schema.Value {
|
|
e := s.at(k)
|
|
switch {
|
|
case e == nil:
|
|
return schema.Value{}
|
|
case e.hasProject:
|
|
return e.project
|
|
}
|
|
return e.d.Default
|
|
}
|
|
|
|
func (s *Store) UserValue(k Key) (schema.Value, bool) {
|
|
if e := s.at(k); e != nil && e.hasUser {
|
|
return e.user, true
|
|
}
|
|
return schema.Value{}, false
|
|
}
|
|
|
|
func (s *Store) ProjectValue(k Key) (schema.Value, bool) {
|
|
if e := s.at(k); e != nil && e.hasProject {
|
|
return e.project, true
|
|
}
|
|
return schema.Value{}, false
|
|
}
|
|
|
|
func (s *Store) Enabled(k Key) bool {
|
|
e := s.at(k)
|
|
return e != nil && (e.d.EnabledWhen == nil || e.d.EnabledWhen(s))
|
|
}
|
|
|
|
func (s *Store) Choices(k Key) []Choice {
|
|
if e := s.at(k); e != nil {
|
|
return s.choices(e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) choices(e *entry) []Choice {
|
|
d := &e.d
|
|
switch {
|
|
case len(d.Choices) > 0:
|
|
return d.Choices
|
|
case d.ChoicesWhen != nil:
|
|
return d.ChoicesWhen(s)
|
|
case d.ChoicesFrom.Section != "" && s.names != nil:
|
|
names := s.names(d.ChoicesFrom.Section, d.ChoicesFrom.List)
|
|
out := make([]Choice, len(names))
|
|
for i, n := range names {
|
|
v := schema.String(n)
|
|
if d.Kind == schema.KindInt64 {
|
|
v = schema.Int64(int64(i))
|
|
}
|
|
out[i] = Choice{Name: n, Label: n, Value: v}
|
|
}
|
|
return out
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) choiceOf(e *entry, v schema.Value) (Choice, bool) {
|
|
for _, c := range s.choices(e) {
|
|
if Equal(c.Value, v) {
|
|
return c, true
|
|
}
|
|
}
|
|
return Choice{}, false
|
|
}
|
|
|
|
func (s *Store) ChoiceName(k Key) string {
|
|
e := s.at(k)
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
if c, ok := s.choiceOf(e, e.eff); ok {
|
|
return c.Name
|
|
}
|
|
return e.eff.S
|
|
}
|
|
|
|
func (s *Store) text(e *entry, v schema.Value) string {
|
|
if c, ok := s.choiceOf(e, v); ok {
|
|
if c.Label != "" {
|
|
return c.Label
|
|
}
|
|
return c.Name
|
|
}
|
|
return plain(v)
|
|
}
|
|
|
|
func (s *Store) Format(k Key) string {
|
|
if e := s.at(k); e != nil {
|
|
return s.text(e, e.eff)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Store) FormatValue(k Key, v schema.Value) string {
|
|
if e := s.at(k); e != nil {
|
|
return s.text(e, v)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *Store) check(e *entry, v schema.Value) (schema.Value, error) {
|
|
d := &e.d
|
|
n, ok := Normalize(d.Kind, v)
|
|
if !ok {
|
|
return v, bad(d.ID, "expects "+d.Kind.String()+", got "+v.Kind.String())
|
|
}
|
|
if d.Kind == schema.KindInt64 || d.Kind == schema.KindFloat64 {
|
|
f := numOf(n)
|
|
if (d.HasMin && f < d.Min) || (d.HasMax && f > d.Max) {
|
|
return v, bad(d.ID, "is out of range: "+strconv.FormatFloat(f, 'g', -1, 64))
|
|
}
|
|
}
|
|
if d.Kind != schema.KindList {
|
|
if cs := s.choices(e); len(cs) > 0 && !slices.ContainsFunc(cs, func(c Choice) bool { return Equal(c.Value, n) }) {
|
|
return v, bad(d.ID, "has no choice "+plain(n))
|
|
}
|
|
}
|
|
if d.Validate != nil {
|
|
if err := d.Validate(n); err != nil {
|
|
return v, bad(d.ID, err.Error())
|
|
}
|
|
}
|
|
if d.Supports != nil && !d.Supports(n) {
|
|
return v, bad(d.ID, "value "+s.text(e, n)+" is not supported here")
|
|
}
|
|
return clone(n), nil
|
|
}
|
|
|
|
func (s *Store) Check(k Key, v schema.Value) (schema.Value, error) {
|
|
e := s.at(k)
|
|
if e == nil {
|
|
return v, bad("#"+strconv.Itoa(int(k)), "does not exist")
|
|
}
|
|
return s.check(e, v)
|
|
}
|
|
|
|
func (s *Store) snapshot(keys []Key) []saved {
|
|
out := make([]saved, len(keys))
|
|
for i, k := range keys {
|
|
e := s.at(k)
|
|
out[i] = saved{k: k, project: e.project, user: e.user, session: e.session, hp: e.hasProject, hu: e.hasUser, hs: e.hasSession}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Store) restore(snap []saved) {
|
|
for _, x := range snap {
|
|
e := s.at(x.k)
|
|
e.project, e.user, e.session = x.project, x.user, x.session
|
|
e.hasProject, e.hasUser, e.hasSession = x.hp, x.hu, x.hs
|
|
}
|
|
}
|
|
|
|
func (s *Store) recompute(keys []Key) []Key {
|
|
var changed []Key
|
|
for _, k := range keys {
|
|
if s.at(k).recompute() {
|
|
changed = append(changed, k)
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
func (s *Store) markDirty(snap []saved) {
|
|
for _, x := range snap {
|
|
e := s.at(x.k)
|
|
if e.d.Flags.Has(Transient) {
|
|
continue
|
|
}
|
|
if e.hasUser != x.hu || (e.hasUser && !Equal(e.user, x.user)) {
|
|
s.dirty = true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Store) commit(keys []Key, mutate func()) error {
|
|
snap := s.snapshot(keys)
|
|
mutate()
|
|
changed := s.recompute(keys)
|
|
if !s.started {
|
|
s.markDirty(snap)
|
|
return nil
|
|
}
|
|
for i, k := range changed {
|
|
e := s.at(k)
|
|
if e.d.Apply == nil {
|
|
continue
|
|
}
|
|
if err := e.d.Apply(e.eff); err != nil {
|
|
s.restore(snap)
|
|
s.recompute(keys)
|
|
for _, b := range changed[:i+1] {
|
|
if be := s.at(b); be.d.Apply != nil {
|
|
_ = be.d.Apply(be.eff)
|
|
}
|
|
}
|
|
s.report("CFG003", problems.Error, e.d.ID+" could not be applied: "+err.Error()+"; the previous settings are back", e.d.ID)
|
|
return bad(e.d.ID, "could not be applied: "+err.Error())
|
|
}
|
|
}
|
|
for _, k := range changed {
|
|
if s.at(k).d.Flags.Has(Restart) {
|
|
s.restart = true
|
|
}
|
|
}
|
|
s.markDirty(snap)
|
|
s.notify(changed)
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) Subscribe(k Key, fn func(Key)) func() {
|
|
s.nextSub++
|
|
id := s.nextSub
|
|
s.subs = append(s.subs, sub{id: id, key: k, fn: fn})
|
|
return func() {
|
|
s.subs = slices.DeleteFunc(s.subs, func(x sub) bool { return x.id == id })
|
|
}
|
|
}
|
|
|
|
func (s *Store) notify(keys []Key) {
|
|
for _, k := range keys {
|
|
for i := 0; i < len(s.subs); i++ {
|
|
if sb := s.subs[i]; sb.key == 0 || sb.key == k {
|
|
sb.fn(k)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Store) Override(k Key, v schema.Value) error {
|
|
e := s.at(k)
|
|
if e == nil {
|
|
return bad("#"+strconv.Itoa(int(k)), "does not exist")
|
|
}
|
|
n, err := s.check(e, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.commit([]Key{k}, func() { e.session, e.hasSession = n, true })
|
|
}
|
|
|
|
func (s *Store) ClearOverride(k Key) error {
|
|
e := s.at(k)
|
|
if e == nil || !e.hasSession {
|
|
return nil
|
|
}
|
|
return s.commit([]Key{k}, func() { e.session, e.hasSession = schema.Value{}, false })
|
|
}
|
|
|
|
func (s *Store) ClearOverrides() error {
|
|
var keys []Key
|
|
for i := range s.list {
|
|
if s.list[i].hasSession {
|
|
keys = append(keys, Key(i+1))
|
|
}
|
|
}
|
|
return s.commit(keys, func() {
|
|
for _, k := range keys {
|
|
e := s.at(k)
|
|
e.session, e.hasSession = schema.Value{}, false
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *Store) SetProject(vals map[Key]schema.Value) error {
|
|
clean := make(map[Key]schema.Value, len(vals))
|
|
for k, v := range vals {
|
|
e := s.at(k)
|
|
if e == nil || !e.d.Flags.Has(Project) {
|
|
return bad("#"+strconv.Itoa(int(k)), "has no project value")
|
|
}
|
|
n, err := s.check(e, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clean[k] = n
|
|
}
|
|
var keys []Key
|
|
for i := range s.list {
|
|
k := Key(i + 1)
|
|
if _, ok := clean[k]; ok || s.list[i].hasProject {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
return s.commit(keys, func() {
|
|
for _, k := range keys {
|
|
e := s.at(k)
|
|
v, ok := clean[k]
|
|
e.project, e.hasProject = v, ok
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *Store) Tick(now float64) {
|
|
s.now = now
|
|
if s.confirm != nil && now >= s.confirm.deadline {
|
|
_ = s.RevertPending()
|
|
}
|
|
}
|
|
|
|
func (s *Store) Pending() (float64, bool) {
|
|
if s.confirm == nil {
|
|
return 0, false
|
|
}
|
|
return max(0, s.confirm.deadline-s.now), true
|
|
}
|
|
|
|
func (s *Store) Confirm() {
|
|
s.confirm = nil
|
|
}
|
|
|
|
func (s *Store) RevertPending() error {
|
|
p := s.confirm
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
s.confirm = nil
|
|
keys := make([]Key, len(p.snap))
|
|
for i, x := range p.snap {
|
|
keys[i] = x.k
|
|
}
|
|
return s.commit(keys, func() { s.restore(p.snap) })
|
|
}
|
|
|
|
func (s *Store) NeedsRestart() bool {
|
|
return s.restart
|
|
}
|
|
|
|
func (s *Store) Dirty() bool {
|
|
return s.dirty
|
|
}
|
|
|
|
func (s *Store) ReadOnly() bool {
|
|
return s.readOnly
|
|
}
|
|
|
|
func (s *Store) Save() error {
|
|
if s.persist == nil || s.readOnly {
|
|
return nil
|
|
}
|
|
if err := s.persist(s.EncodeProfile()); err != nil {
|
|
s.report("CFG005", problems.Error, "the settings profile could not be written: "+err.Error(), "")
|
|
return err
|
|
}
|
|
s.dirty = false
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) report(code string, sev problems.Severity, msg, setting string) {
|
|
s.diags = append(s.diags, problems.Diagnostic{
|
|
Owner: diagOwner, Code: code, Severity: sev, Message: msg,
|
|
Subject: problems.Subject{Kind: problems.SubjectSetting, Setting: setting},
|
|
})
|
|
s.rev++
|
|
}
|
|
|
|
func (s *Store) Diagnostics() []problems.Diagnostic {
|
|
return s.diags
|
|
}
|
|
|
|
func (s *Store) Problems() problems.Source {
|
|
return problems.Source{
|
|
Revision: func() uint64 { return s.rev },
|
|
Append: func(dst []problems.Diagnostic) []problems.Diagnostic { return append(dst, s.diags...) },
|
|
}
|
|
}
|