mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
c0869cb878
补齐 Value/String/Bool/Int/Duration/Strings/WasSet/Origin 只读访问器与 按 secret 脱敏的 Entries 导出,新增 core.ConfigGet[T] 泛型读取入口,并用 编译期断言钉住 ConfigRegistry 对 ConfigExtension 的完整实现。
285 lines
9.3 KiB
Go
285 lines
9.3 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package extpoints
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Sentinel errors returned by the configuration extension point.
|
|
var (
|
|
// ErrConfigConflict is returned when the same key is declared with disagreeing metadata.
|
|
ErrConfigConflict = errors.New("extpoints: conflicting configuration declarations")
|
|
|
|
// ErrConfigType is returned when a value cannot be converted to the declared type.
|
|
ErrConfigType = errors.New("extpoints: configuration value type mismatch")
|
|
|
|
// ErrConfigUnknownKey is returned when a configuration key was never declared.
|
|
ErrConfigUnknownKey = errors.New("extpoints: unknown configuration key")
|
|
|
|
// ErrConfigNotResolved is returned when typed reads happen before resolution.
|
|
ErrConfigNotResolved = errors.New("extpoints: configuration not resolved; run App.Prepare first")
|
|
|
|
// ErrConfigTarget is returned when a binding target is not an addressable struct pointer.
|
|
ErrConfigTarget = errors.New("extpoints: configuration binding target must be a non-nil struct pointer")
|
|
|
|
// ErrConfigNoSource is returned when resolution is attempted without a registered source.
|
|
ErrConfigNoSource = errors.New("extpoints: no configuration source registered")
|
|
)
|
|
|
|
// Configuration origin labels reported by ConfigView.Origin and ConfigEntry.Origin.
|
|
const (
|
|
// OriginEnv marks a value that came from an environment variable.
|
|
OriginEnv = "env"
|
|
// OriginAutoEnable marks a boolean enabled by the presence of another environment variable.
|
|
OriginAutoEnable = "auto-enable"
|
|
// OriginFile marks a value that came from the configuration file.
|
|
OriginFile = "file"
|
|
// OriginDefault marks a value that came from a declaration default.
|
|
OriginDefault = "default"
|
|
)
|
|
|
|
// RedactedValue replaces the printed value of keys declared with secret:"true".
|
|
const RedactedValue = "******"
|
|
|
|
// durationType distinguishes time.Duration from plain int64 during tag walking and decoding.
|
|
var durationType = reflect.TypeFor[time.Duration]()
|
|
|
|
// ConfigRegistry must satisfy the full extension contract, so a missing accessor is a
|
|
// compile error rather than a runtime surprise inside a plugin Apply.
|
|
var _ ConfigExtension = (*ConfigRegistry)(nil)
|
|
|
|
// ConfigSource abstracts where raw configuration values come from, keeping the
|
|
// micro-kernel free of concrete loaders such as viper.
|
|
type ConfigSource interface {
|
|
// Lookup returns the raw value stored at a dotted path in the configuration file.
|
|
Lookup(path string) (any, bool)
|
|
// LookupEnv returns the raw value of an environment variable.
|
|
LookupEnv(name string) (string, bool)
|
|
// Describe returns a human readable identity for the source, used in diagnostics.
|
|
Describe() string
|
|
}
|
|
|
|
// ConfigBinding declares that a plugin reads every `config` tagged field of Target
|
|
// under a dotted configuration prefix.
|
|
type ConfigBinding struct {
|
|
// Prefix is the dotted configuration path, e.g. "redis". An empty prefix means
|
|
// each field's `config` tag is already a full path.
|
|
Prefix string
|
|
// Target must be a non-nil pointer to a struct carrying `config` tags.
|
|
Target any
|
|
}
|
|
|
|
// configField is a single leaf discovered while walking a binding struct's tags.
|
|
// key is the fully qualified dotted path used for resolution; path is the raw `config`
|
|
// tag value used to locate the Go field again during Bind.
|
|
type configField struct {
|
|
key string
|
|
path string
|
|
env string
|
|
autoEnable string
|
|
def string
|
|
secret bool
|
|
typ reflect.Type
|
|
}
|
|
|
|
// configDecl is the registered form of a configField, attributed to its declaring plugin.
|
|
type configDecl struct {
|
|
key string
|
|
pluginID string
|
|
env string
|
|
autoEnable string
|
|
def string
|
|
secret bool
|
|
typ reflect.Type
|
|
}
|
|
|
|
// ConfigEntry is a redacted, self-describing view of one effective configuration key.
|
|
type ConfigEntry struct {
|
|
Key string
|
|
PluginID string
|
|
Env string
|
|
Origin string
|
|
Value string
|
|
}
|
|
|
|
// ConfigView is the read-only surface over effective configuration values.
|
|
// Keys are dotted paths such as "redis.enabled".
|
|
type ConfigView interface {
|
|
Value(key string) (any, bool)
|
|
String(key, fallback string) string
|
|
Bool(key string, fallback bool) bool
|
|
Int(key string, fallback int) int
|
|
Duration(key string, fallback time.Duration) time.Duration
|
|
Strings(key string) []string
|
|
WasSet(envName string) bool
|
|
Origin(key string) string
|
|
}
|
|
|
|
// ConfigExtension is the plugin-facing configuration extension point mounted on the
|
|
// root Context and shared by every forked plugin scope.
|
|
type ConfigExtension interface {
|
|
ConfigView
|
|
|
|
// SetSource installs the raw value source after construction, letting the composition
|
|
// root build the adapter once the kernel Context already exists.
|
|
SetSource(src ConfigSource)
|
|
// Declare registers plugin-owned configuration bindings before Apply runs.
|
|
Declare(pluginID string, bindings ...ConfigBinding) error
|
|
// Bind resolves and assigns the configuration values for a tagged struct.
|
|
Bind(prefix string, target any) error
|
|
// Resolve computes the effective value of every declared key once.
|
|
Resolve() error
|
|
// Resolved reports whether Resolve has already run.
|
|
Resolved() bool
|
|
// Entries returns the redacted effective configuration ordered by key.
|
|
Entries() []ConfigEntry
|
|
}
|
|
|
|
// ConfigRegistry implements ConfigExtension. Declarations are additive; values are
|
|
// computed once by Resolve and reused by every later read.
|
|
type ConfigRegistry struct {
|
|
mu sync.RWMutex
|
|
src ConfigSource
|
|
decls map[string]*configDecl
|
|
order []string
|
|
values map[string]any
|
|
origins map[string]string
|
|
resolved bool
|
|
}
|
|
|
|
// NewConfigRegistry creates an empty configuration registry. A nil src is allowed so
|
|
// that the kernel can construct the registry before the composition root injects one.
|
|
func NewConfigRegistry(src ConfigSource) *ConfigRegistry {
|
|
return &ConfigRegistry{
|
|
src: src,
|
|
decls: make(map[string]*configDecl),
|
|
values: make(map[string]any),
|
|
origins: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// SetSource installs the raw value source. It is intended for the composition root,
|
|
// which builds the adapter after the kernel Context already exists.
|
|
func (r *ConfigRegistry) SetSource(src ConfigSource) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.src = src
|
|
}
|
|
|
|
// Declare registers every `config` tagged leaf of each binding's target struct.
|
|
// Repeated declarations of the same key are accepted only when their env, default,
|
|
// auto-enable and secret metadata agree; disagreement is ErrConfigConflict.
|
|
func (r *ConfigRegistry) Declare(pluginID string, bindings ...ConfigBinding) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
for _, b := range bindings {
|
|
if err := r.declareBinding(pluginID, b); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ConfigRegistry) declareBinding(pluginID string, b ConfigBinding) error {
|
|
target, err := bindingStruct(b.Target, b.Prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields, err := walkConfigFields(target.Type(), b.Prefix)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range fields {
|
|
if err := r.addDecl(pluginID, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bindingStruct validates that a binding or bind target is a usable struct pointer.
|
|
func bindingStruct(target any, prefix string) (reflect.Value, error) {
|
|
rv := reflect.ValueOf(target)
|
|
if !rv.IsValid() || rv.Kind() != reflect.Pointer || rv.IsNil() || rv.Elem().Kind() != reflect.Struct {
|
|
return reflect.Value{}, fmt.Errorf("%w: prefix %q received %T", ErrConfigTarget, prefix, target)
|
|
}
|
|
return rv.Elem(), nil
|
|
}
|
|
|
|
// walkConfigFields collects leaf configuration declarations from `config` tagged fields.
|
|
// A field without a `config` tag is skipped, except for embedded structs which are
|
|
// recursed into so their own tags resolve under the same prefix.
|
|
func walkConfigFields(t reflect.Type, prefix string) ([]configField, error) {
|
|
var out []configField
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
sf := t.Field(i)
|
|
if sf.PkgPath != "" {
|
|
continue
|
|
}
|
|
|
|
path := sf.Tag.Get("config")
|
|
if path == "-" {
|
|
continue
|
|
}
|
|
if path == "" {
|
|
if sf.Type.Kind() == reflect.Struct && sf.Type != durationType {
|
|
nested, err := walkConfigFields(sf.Type, prefix)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, nested...)
|
|
}
|
|
continue
|
|
}
|
|
|
|
out = append(out, configField{
|
|
key: joinKey(prefix, path),
|
|
path: path,
|
|
env: sf.Tag.Get("env"),
|
|
autoEnable: sf.Tag.Get("autoEnable"),
|
|
def: sf.Tag.Get("default"),
|
|
secret: strings.EqualFold(sf.Tag.Get("secret"), "true"),
|
|
typ: sf.Type,
|
|
})
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func joinKey(prefix, path string) string {
|
|
if prefix == "" {
|
|
return path
|
|
}
|
|
return prefix + "." + path
|
|
}
|
|
|
|
// addDecl records one leaf, enforcing the shared-declaration consistency rule.
|
|
func (r *ConfigRegistry) addDecl(pluginID string, f configField) error {
|
|
if existing, ok := r.decls[f.key]; ok {
|
|
if existing.env != f.env || existing.def != f.def ||
|
|
existing.autoEnable != f.autoEnable || existing.secret != f.secret {
|
|
return fmt.Errorf(
|
|
"%w: key %q declared by plugin %q and plugin %q with disagreeing env/default/autoEnable/secret metadata",
|
|
ErrConfigConflict, f.key, existing.pluginID, pluginID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
r.decls[f.key] = &configDecl{
|
|
key: f.key, pluginID: pluginID, env: f.env,
|
|
autoEnable: f.autoEnable, def: f.def, secret: f.secret, typ: f.typ,
|
|
}
|
|
r.order = append(r.order, f.key)
|
|
return nil
|
|
}
|