mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
187 lines
4.1 KiB
Go
187 lines
4.1 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
type CfgSecKey struct {
|
|
Sec, Key string
|
|
}
|
|
|
|
// OptionInterface is used to overcome Golang's generic interface limitation
|
|
type OptionInterface interface {
|
|
GetDefaultValue() any
|
|
}
|
|
|
|
type Option[T any] struct {
|
|
mu sync.RWMutex
|
|
|
|
cfgSecKey CfgSecKey
|
|
dynKey string
|
|
|
|
value T
|
|
defSimple T
|
|
defFunc func() T
|
|
emptyAsDef bool
|
|
has bool
|
|
revision int
|
|
}
|
|
|
|
func (opt *Option[T]) GetDefaultValue() any {
|
|
return opt.DefaultValue()
|
|
}
|
|
|
|
func (opt *Option[T]) parse(key, valStr string) (v T) {
|
|
v = opt.DefaultValue()
|
|
if valStr != "" {
|
|
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
|
|
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (opt *Option[T]) HasValue(ctx context.Context) bool {
|
|
_, _, has := opt.ValueRevision(ctx)
|
|
return has
|
|
}
|
|
|
|
func (opt *Option[T]) Value(ctx context.Context) (v T) {
|
|
v, _, _ = opt.ValueRevision(ctx)
|
|
return v
|
|
}
|
|
|
|
func isZeroOrEmpty(v any) bool {
|
|
if v == nil {
|
|
return true // interface itself is nil
|
|
}
|
|
r := reflect.ValueOf(v)
|
|
if r.IsZero() {
|
|
return true
|
|
}
|
|
|
|
if r.Kind() == reflect.Slice || r.Kind() == reflect.Map {
|
|
if r.IsNil() {
|
|
return true
|
|
}
|
|
return r.Len() == 0
|
|
}
|
|
return false
|
|
}
|
|
|
|
var SkipDatabaseConfig bool
|
|
|
|
func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) {
|
|
dg := GetDynGetter()
|
|
if dg == nil {
|
|
// this is an edge case: the database is not initialized but the system setting is going to be used
|
|
// it should panic to avoid inconsistent config values (from config / system setting) and fix the code
|
|
if SkipDatabaseConfig {
|
|
return opt.DefaultValue(), 0, false
|
|
}
|
|
panic("no config dyn value getter")
|
|
}
|
|
|
|
rev = dg.GetRevision(ctx)
|
|
|
|
// if the revision in the database doesn't change, use the last value
|
|
opt.mu.RLock()
|
|
if rev == opt.revision {
|
|
v = opt.value
|
|
has = opt.has
|
|
opt.mu.RUnlock()
|
|
return v, rev, has
|
|
}
|
|
opt.mu.RUnlock()
|
|
|
|
// try to parse the config and cache it
|
|
var valStr *string
|
|
if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue {
|
|
valStr = &dynVal
|
|
} else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue {
|
|
valStr = &cfgVal
|
|
}
|
|
if valStr == nil {
|
|
v = opt.DefaultValue()
|
|
has = false
|
|
} else {
|
|
v = opt.parse(opt.dynKey, *valStr)
|
|
if opt.emptyAsDef && isZeroOrEmpty(v) {
|
|
v = opt.DefaultValue()
|
|
} else {
|
|
has = true
|
|
}
|
|
}
|
|
|
|
opt.mu.Lock()
|
|
opt.value = v
|
|
opt.revision = rev
|
|
opt.has = has
|
|
opt.mu.Unlock()
|
|
return v, rev, has
|
|
}
|
|
|
|
func (opt *Option[T]) DynKey() string {
|
|
return opt.dynKey
|
|
}
|
|
|
|
// WithDefaultFunc sets the default value with a function
|
|
// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice
|
|
func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] {
|
|
opt.defFunc = f
|
|
return opt
|
|
}
|
|
|
|
func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] {
|
|
v := any(def)
|
|
switch v.(type) {
|
|
case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
|
default:
|
|
// TODO: use reflect to support convertible basic types like `type State string`
|
|
r := reflect.ValueOf(v)
|
|
if r.Kind() != reflect.Struct {
|
|
panic("invalid type for default value, use WithDefaultFunc instead")
|
|
}
|
|
}
|
|
opt.defSimple = def
|
|
return opt
|
|
}
|
|
|
|
func (opt *Option[T]) WithEmptyAsDefault() *Option[T] {
|
|
opt.emptyAsDef = true
|
|
return opt
|
|
}
|
|
|
|
func (opt *Option[T]) DefaultValue() T {
|
|
if opt.defFunc != nil {
|
|
return opt.defFunc()
|
|
}
|
|
return opt.defSimple
|
|
}
|
|
|
|
func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] {
|
|
opt.cfgSecKey = cfgSecKey
|
|
return opt
|
|
}
|
|
|
|
var allConfigOptions = map[string]OptionInterface{}
|
|
|
|
func NewOption[T any](dynKey string) *Option[T] {
|
|
v := &Option[T]{dynKey: dynKey}
|
|
allConfigOptions[dynKey] = v
|
|
return v
|
|
}
|
|
|
|
func GetConfigOption(dynKey string) OptionInterface {
|
|
return allConfigOptions[dynKey]
|
|
}
|