-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathregistry.go
More file actions
385 lines (330 loc) · 9.78 KB
/
registry.go
File metadata and controls
385 lines (330 loc) · 9.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
package seiconfig
import (
"reflect"
"sort"
"strings"
)
// FieldType classifies the Go type of a config field for consumers that
// need to present or validate values without full Go type information
// (e.g. CRD validation webhooks, CLI flag generation, documentation).
type FieldType int
const (
FieldTypeString FieldType = iota
FieldTypeInt // int, int64
FieldTypeUint // uint, uint16, uint32, uint64
FieldTypeFloat // float64
FieldTypeBool
FieldTypeDuration // Duration (encoded as string in TOML)
FieldTypeStringSlice // []string
FieldTypeOther // anything else (nested labels, etc.)
)
func (ft FieldType) String() string {
switch ft {
case FieldTypeString:
return "string"
case FieldTypeInt:
return "int"
case FieldTypeUint:
return "uint"
case FieldTypeFloat:
return "float"
case FieldTypeBool:
return "bool"
case FieldTypeDuration:
return "duration"
case FieldTypeStringSlice:
return "[]string"
default:
return "other"
}
}
// ConfigField describes a single configuration parameter. Fields are
// auto-discovered from SeiConfig's struct tags and enriched with
// hand-authored metadata via Enrich().
type ConfigField struct {
// Key is the dotted TOML key path in the unified schema.
// Example: "evm.http_port", "storage.state_store.keep_recent"
Key string
// EnvVar is the environment variable name (SEI_ prefix).
// Example: "SEI_EVM_HTTP_PORT"
EnvVar string
// FieldPath is the Go struct field path for reflection access.
// Example: "EVM.HTTPPort"
FieldPath string
// Type classifies the field's Go type.
Type FieldType
// Description is a human-readable explanation of the field.
Description string
// Unit describes the value's unit when not obvious from the type.
// Examples: "bytes", "blocks", "connections", "bytes/sec"
Unit string
// HotReload indicates the field can be changed at runtime without
// restarting seid.
HotReload bool
// Deprecated indicates the field is scheduled for removal.
Deprecated bool
// Section is the top-level config section this field belongs to.
// Example: "evm", "storage", "chain"
Section string
// SinceVersion is the config schema version in which this field was
// introduced. Zero means the field has existed since version 1 (the
// initial schema). Used by ValidateIntent to enforce version-aware
// required-field checks.
SinceVersion int
// RequiredForModes lists the node modes for which this field must have
// a non-zero value. Empty means the field is optional for all modes.
RequiredForModes []NodeMode
}
// Registry holds metadata for every field in SeiConfig. It is built once
// via BuildRegistry() and is safe for concurrent read access.
type Registry struct {
fields []ConfigField
byKey map[string]*ConfigField
byEnvVar map[string]*ConfigField
}
// BuildRegistry constructs a Registry by reflecting over SeiConfig's struct
// tags to auto-populate key paths, env var names, field types, and sections.
func BuildRegistry() *Registry {
r := &Registry{
byKey: make(map[string]*ConfigField),
byEnvVar: make(map[string]*ConfigField),
}
buildFieldsRecursive(
reflect.TypeFor[SeiConfig](),
"", // toml prefix
"", // field path prefix
"", // section
r,
)
sort.Slice(r.fields, func(i, j int) bool {
return r.fields[i].Key < r.fields[j].Key
})
// Rebuild index pointers after sort
for i := range r.fields {
f := &r.fields[i]
r.byKey[f.Key] = f
r.byEnvVar[f.EnvVar] = f
}
return r
}
func buildFieldsRecursive(
t reflect.Type,
tomlPrefix, fieldPrefix, section string,
r *Registry,
) {
for i := range t.NumField() {
sf := t.Field(i)
if !sf.IsExported() {
continue
}
tag := sf.Tag.Get("toml")
if tag == "" || tag == "-" {
continue
}
tomlKey := tag
if tomlPrefix != "" {
tomlKey = tomlPrefix + "." + tag
}
fieldPath := sf.Name
if fieldPrefix != "" {
fieldPath = fieldPrefix + "." + sf.Name
}
currentSection := section
if currentSection == "" {
currentSection = tag
}
ft := sf.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && ft != reflect.TypeFor[Duration]() {
buildFieldsRecursive(ft, tomlKey, fieldPath, currentSection, r)
continue
}
envSuffix := strings.ToUpper(strings.ReplaceAll(tomlKey, ".", "_"))
f := ConfigField{
Key: tomlKey,
EnvVar: "SEI_" + envSuffix,
FieldPath: fieldPath,
Type: classifyType(ft),
Section: currentSection,
}
r.fields = append(r.fields, f)
}
}
func classifyType(t reflect.Type) FieldType {
if t == reflect.TypeFor[Duration]() {
return FieldTypeDuration
}
if t == reflect.TypeFor[[]string]() {
return FieldTypeStringSlice
}
switch t.Kind() {
case reflect.String:
return FieldTypeString
case reflect.Bool:
return FieldTypeBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return FieldTypeInt
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return FieldTypeUint
case reflect.Float32, reflect.Float64:
return FieldTypeFloat
default:
return FieldTypeOther
}
}
// ---------------------------------------------------------------------------
// Query methods
// ---------------------------------------------------------------------------
// Fields returns all registered fields in key-sorted order.
func (r *Registry) Fields() []ConfigField {
out := make([]ConfigField, len(r.fields))
copy(out, r.fields)
return out
}
// Field returns the metadata for a single field by its TOML key path,
// or nil if the key is not found.
func (r *Registry) Field(key string) *ConfigField {
return r.byKey[key]
}
// FieldByEnvVar returns the metadata for a field by its environment
// variable name, or nil if not found.
func (r *Registry) FieldByEnvVar(envVar string) *ConfigField {
return r.byEnvVar[envVar]
}
// FieldsInSection returns all fields belonging to a top-level section.
func (r *Registry) FieldsInSection(section string) []ConfigField {
var out []ConfigField
for _, f := range r.fields {
if f.Section == section {
out = append(out, f)
}
}
return out
}
// HotReloadableFields returns all fields marked as safe to change at runtime.
func (r *Registry) HotReloadableFields() []ConfigField {
var out []ConfigField
for _, f := range r.fields {
if f.HotReload {
out = append(out, f)
}
}
return out
}
// DeprecatedFields returns all fields marked as deprecated.
func (r *Registry) DeprecatedFields() []ConfigField {
var out []ConfigField
for _, f := range r.fields {
if f.Deprecated {
out = append(out, f)
}
}
return out
}
// Sections returns the unique top-level section names in sorted order.
func (r *Registry) Sections() []string {
seen := make(map[string]bool)
for _, f := range r.fields {
seen[f.Section] = true
}
out := make([]string, 0, len(seen))
for s := range seen {
out = append(out, s)
}
sort.Strings(out)
return out
}
// Len returns the total number of registered fields.
func (r *Registry) Len() int {
return len(r.fields)
}
// DefaultsByMode returns the default value for every field under the given
// mode as a map from TOML key to its string representation. This is useful
// for documentation generation and diff tooling.
func (r *Registry) DefaultsByMode(mode NodeMode) map[string]any {
cfg := DefaultForMode(mode)
return r.extractValues(cfg)
}
func (r *Registry) extractValues(cfg *SeiConfig) map[string]any {
result := make(map[string]any, len(r.fields))
v := reflect.ValueOf(cfg).Elem()
for _, f := range r.fields {
val := navigateToField(v, f.FieldPath)
if val.IsValid() {
result[f.Key] = val.Interface()
}
}
return result
}
func navigateToField(v reflect.Value, path string) reflect.Value {
for part := range strings.SplitSeq(path, ".") {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return reflect.Value{}
}
v = v.Elem()
}
v = v.FieldByName(part)
if !v.IsValid() {
return v
}
}
return v
}
// ---------------------------------------------------------------------------
// Enrichment
// ---------------------------------------------------------------------------
// FieldOption is a functional option for enriching a ConfigField.
type FieldOption func(*ConfigField)
// WithDescription sets the field's human-readable description.
func WithDescription(desc string) FieldOption {
return func(f *ConfigField) { f.Description = desc }
}
// WithUnit sets the field's value unit label.
func WithUnit(unit string) FieldOption {
return func(f *ConfigField) { f.Unit = unit }
}
// WithHotReload marks the field as safe to change without a restart.
func WithHotReload() FieldOption {
return func(f *ConfigField) { f.HotReload = true }
}
// WithDeprecated marks the field as deprecated.
func WithDeprecated() FieldOption {
return func(f *ConfigField) { f.Deprecated = true }
}
// WithSinceVersion records the config schema version in which this field
// was introduced. Used for version-aware validation.
func WithSinceVersion(v int) FieldOption {
return func(f *ConfigField) { f.SinceVersion = v }
}
// WithRequiredForModes marks this field as required when the node runs
// in any of the specified modes.
func WithRequiredForModes(modes ...NodeMode) FieldOption {
return func(f *ConfigField) { f.RequiredForModes = modes }
}
// Enrich updates a field's metadata by key. Returns false if the key
// was not found in the registry.
func (r *Registry) Enrich(key string, opts ...FieldOption) bool {
f := r.byKey[key]
if f == nil {
return false
}
for _, opt := range opts {
opt(f)
}
return true
}
// EnrichAll applies multiple enrichments at once. The map keys are TOML
// key paths. Returns the keys that were not found.
func (r *Registry) EnrichAll(enrichments map[string][]FieldOption) []string {
var missing []string
for key, opts := range enrichments {
if !r.Enrich(key, opts...) {
missing = append(missing, key)
}
}
sort.Strings(missing)
return missing
}