@@ -75,7 +75,10 @@ func (o *ConfigOptions) init() {
7575// core.Println(cfg.String("config.host"))
7676type Config struct {
7777 * ConfigOptions
78- mu RWMutex
78+ features * Registry [bool ] // live feature-flag store; ConfigOptions.Features is declarative init input
79+ root * Config // non-nil for a Group view; the store + lock live on root
80+ prefix string // group key prefix ("" for the root Config)
81+ mu RWMutex
7982}
8083
8184// New initialises a Config with empty settings and features.
@@ -84,21 +87,78 @@ type Config struct {
8487func (e * Config ) New () * Config {
8588 e .ConfigOptions = & ConfigOptions {}
8689 e .ConfigOptions .init ()
90+ e .features = NewRegistry [bool ]()
8791 return e
8892}
8993
94+ // featureFlags returns the live feature-flag Registry, lazily creating it and
95+ // seeding once from the declarative ConfigOptions.Features input. The Registry
96+ // is the source of truth for Enable/Disable/Enabled; ConfigOptions.Features is
97+ // init-only input and is not mutated by later Enable/Disable calls.
98+ func (e * Config ) featureFlags () * Registry [bool ] {
99+ e .mu .Lock ()
100+ defer e .mu .Unlock ()
101+ if e .features == nil {
102+ e .features = NewRegistry [bool ]()
103+ if e .ConfigOptions != nil && e .ConfigOptions .Features != nil {
104+ for k , v := range e .ConfigOptions .Features {
105+ e .features .Set (k , v )
106+ }
107+ }
108+ }
109+ return e .features
110+ }
111+
112+ // lock returns the mutex guarding the underlying store. A Group view shares its
113+ // root's lock so concurrent access to the shared Settings map stays serialised.
114+ func (e * Config ) lock () * RWMutex {
115+ if e .root != nil {
116+ return & e .root .mu
117+ }
118+ return & e .mu
119+ }
120+
121+ // key applies the group prefix to a raw key ("" prefix → key unchanged).
122+ func (e * Config ) key (k string ) string {
123+ if e .prefix == "" {
124+ return k
125+ }
126+ return Concat (e .prefix , "." , k )
127+ }
128+
129+ // Group returns a Config view scoped under name: Set/Get/String/Int/Bool and the
130+ // feature methods operate on "<name>.<key>" in the shared underlying store.
131+ // Nested groups compose; the root Config's ungrouped keys are unaffected.
132+ //
133+ // db := c.Config("database")
134+ // db.Set("host", "localhost") // stores "database.host"
135+ // c.Config().String("database.host") // "localhost"
136+ func (e * Config ) Group (name string ) * Config {
137+ root := e
138+ if e .root != nil {
139+ root = e .root
140+ }
141+ return & Config {
142+ ConfigOptions : root .ConfigOptions ,
143+ features : root .featureFlags (),
144+ root : root ,
145+ prefix : e .key (name ),
146+ }
147+ }
148+
90149// Set stores a configuration value by key.
91150//
92151// cfg := (&core.Config{}).New()
93152// cfg.Set("config.host", "homelab.lthn.sh")
94153func (e * Config ) Set (key string , val any ) {
95- e .mu .Lock ()
154+ mu := e .lock ()
155+ mu .Lock ()
96156 if e .ConfigOptions == nil {
97157 e .ConfigOptions = & ConfigOptions {}
98158 }
99159 e .ConfigOptions .init ()
100- e .Settings [key ] = val
101- e . mu .Unlock ()
160+ e .Settings [e . key ( key ) ] = val
161+ mu .Unlock ()
102162}
103163
104164// Get retrieves a configuration value by key.
@@ -108,12 +168,13 @@ func (e *Config) Set(key string, val any) {
108168// r := cfg.Get("config.host")
109169// if r.OK { core.Println(r.Value.(string)) }
110170func (e * Config ) Get (key string ) Result {
111- e .mu .RLock ()
112- defer e .mu .RUnlock ()
171+ mu := e .lock ()
172+ mu .RLock ()
173+ defer mu .RUnlock ()
113174 if e .ConfigOptions == nil || e .Settings == nil {
114175 return Result {}
115176 }
116- val , ok := e .Settings [key ]
177+ val , ok := e .Settings [e . key ( key ) ]
117178 if ! ok {
118179 return Result {}
119180 }
@@ -157,54 +218,67 @@ func ConfigGet[T any](e *Config, key string) T {
157218//
158219// c.Config().Enable("dark-mode")
159220func (e * Config ) Enable (feature string ) {
160- e .mu .Lock ()
161- if e .ConfigOptions == nil {
162- e .ConfigOptions = & ConfigOptions {}
163- }
164- e .ConfigOptions .init ()
165- e .Features [feature ] = true
166- e .mu .Unlock ()
221+ e .featureFlags ().Set (e .key (feature ), true )
167222}
168223
169224// Disable deactivates a feature flag.
170225//
171226// c.Config().Disable("dark-mode")
172227func (e * Config ) Disable (feature string ) {
173- e .mu .Lock ()
174- if e .ConfigOptions == nil {
175- e .ConfigOptions = & ConfigOptions {}
176- }
177- e .ConfigOptions .init ()
178- e .Features [feature ] = false
179- e .mu .Unlock ()
228+ e .featureFlags ().Set (e .key (feature ), false )
180229}
181230
182231// Enabled returns true if a feature flag is active.
183232//
184233// if c.Config().Enabled("dark-mode") { ... }
185234func (e * Config ) Enabled (feature string ) bool {
186- e .mu .RLock ()
187- defer e .mu .RUnlock ()
188- if e .ConfigOptions == nil || e .Features == nil {
189- return false
190- }
191- return e .Features [feature ]
235+ r := e .featureFlags ().Get (e .key (feature ))
236+ return r .OK && r .Value .(bool )
237+ }
238+
239+ // Feature is a keyed handle to a single feature flag, bound to a Config.
240+ //
241+ // f := c.Feature("dark-mode")
242+ // f.Enable()
243+ // if f.Enabled() { core.Println("on") }
244+ type Feature struct {
245+ cfg * Config
246+ name string
192247}
193248
249+ // Name returns the feature's name.
250+ //
251+ // c.Feature("dark-mode").Name() // "dark-mode"
252+ func (f Feature ) Name () string { return f .name }
253+
254+ // Enable activates the feature.
255+ //
256+ // c.Feature("dark-mode").Enable()
257+ func (f Feature ) Enable () { f .cfg .Enable (f .name ) }
258+
259+ // Disable deactivates the feature.
260+ //
261+ // c.Feature("dark-mode").Disable()
262+ func (f Feature ) Disable () { f .cfg .Disable (f .name ) }
263+
264+ // Enabled reports whether the feature is active.
265+ //
266+ // if c.Feature("dark-mode").Enabled() { core.Println("on") }
267+ func (f Feature ) Enabled () bool { return f .cfg .Enabled (f .name ) }
268+
194269// EnabledFeatures returns all active feature flag names.
195270//
196271// features := c.Config().EnabledFeatures()
197272func (e * Config ) EnabledFeatures () []string {
198- e .mu .RLock ()
199- defer e .mu .RUnlock ()
200- if e .ConfigOptions == nil || e .Features == nil {
201- return nil
202- }
203- var result []string
204- for k , v := range e .Features {
205- if v {
206- result = append (result , k )
273+ ff := e .featureFlags ()
274+ result := make ([]string , 0 , ff .Len ())
275+ ff .Each (func (name string , on bool ) {
276+ if on {
277+ result = append (result , name )
207278 }
279+ })
280+ if len (result ) == 0 {
281+ return nil // byte-identical to the old var-nil behaviour
208282 }
209283 return result
210284}
0 commit comments