diff --git a/README.md b/README.md index 23f43907..f50051a8 100644 --- a/README.md +++ b/README.md @@ -211,8 +211,6 @@ Go-Spring provides multiple ways to register Beans: - **`gs.Object(obj)`** - Registers an existing object as a Bean - **`gs.Provide(ctor, args...)`** - Uses a constructor to generate and register a Bean - **`gs.Register(bd)`** - Registers a complete Bean definition (suitable for low-level encapsulation or advanced usage) -- **`gs.GroupRegister(fn)`** - Batch registers multiple Beans (commonly used for module initialization and other - scenarios) Example: @@ -221,14 +219,6 @@ gs.Object(&Service{}) // Register a struct instance gs.Provide(NewService) // Register using a constructor gs.Provide(NewRepo, gs.ValueArg("db")) // Constructor with parameters gs.Register(gs.NewBean(NewService)) // Complete definition registration - -// Batch register multiple Beans -gs.GroupRegister(func (p conf.Properties) []*gs.BeanDefinition { - return []*gs.BeanDefinition{ - gs.NewBean(NewUserService), - gs.NewBean(NewOrderService), - } -}) ``` ### 2️⃣ Injection Methods @@ -317,11 +307,10 @@ feature toggles, and gray release scenarios. ### 🎯 Common Condition Types - **`OnProperty("key")`**: Activates when the specified configuration key exists -- **`OnMissingProperty("key")`**: Activates when the specified configuration key does not exist - **`OnBean[Type]("name")`**: Activates when a Bean of the specified type/name exists - **`OnMissingBean[Type]("name")`**: Activates when a Bean of the specified type/name does not exist - **`OnSingleBean[Type]("name")`**: Activates when a Bean of the specified type/name is the only instance -- **`OnFunc(func(ctx CondContext) bool)`**: Uses custom condition logic to determine activation +- **`OnFunc(func(ctx ConditionContext) bool)`**: Uses custom condition logic to determine activation Example: diff --git a/README_CN.md b/README_CN.md index 3d41ddfa..aa26cb28 100644 --- a/README_CN.md +++ b/README_CN.md @@ -189,7 +189,6 @@ Go-Spring 提供多种方式注册 Bean: - **`gs.Object(obj)`** - 将已有对象注册为 Bean - **`gs.Provide(ctor, args...)`** - 使用构造函数生成并注册 Bean - **`gs.Register(bd)`** - 注册完整 Bean 定义(适合底层封装或高级用法) -- **`gs.GroupRegister(fn)`** - 批量注册多个 Bean(常用于模块初始化等场景) 示例: @@ -198,14 +197,6 @@ gs.Object(&Service{}) // 注册结构体实例 gs.Provide(NewService) // 使用构造函数注册 gs.Provide(NewRepo, gs.ValueArg("db")) // 构造函数带参数 gs.Register(gs.NewBean(NewService)) // 完整定义注册 - -// 批量注册多个 Bean -gs.GroupRegister(func (p conf.Properties) []*gs.BeanDefinition { - return []*gs.BeanDefinition{ - gs.NewBean(NewUserService), - gs.NewBean(NewOrderService), - } -}) ``` ### 2️⃣ 注入方式 @@ -291,11 +282,10 @@ Go-Spring 借鉴 Spring 的 `@Conditional` 思想,实现了灵活强大的条 ### 🎯 常用条件类型 - **`OnProperty("key")`**:当指定配置 key 存在时激活 -- **`OnMissingProperty("key")`**:当指定配置 key 不存在时激活 - **`OnBean[Type]("name")`**:当指定类型/名称的 Bean 存在时激活 - **`OnMissingBean[Type]("name")`**:当指定类型/名称的 Bean 不存在时激活 - **`OnSingleBean[Type]("name")`**:当指定类型/名称的 Bean 是唯一实例时激活 -- **`OnFunc(func(ctx CondContext) bool)`**:使用自定义条件逻辑判断是否激活 +- **`OnFunc(func(ctx ConditionContext) bool)`**:使用自定义条件逻辑判断是否激活 示例: diff --git a/conf/bind.go b/conf/bind.go index 624534b9..6cc20dbd 100644 --- a/conf/bind.go +++ b/conf/bind.go @@ -301,7 +301,7 @@ func getSlice(p Properties, et reflect.Type, param BindParam) (Properties, error r := New() for i, s := range arrVal { k := fmt.Sprintf("%s[%d]", param.Key, i) - _ = r.Set(k, s) // always no error + _ = r.Set(k, s, 0) // always no error } return r, nil } diff --git a/conf/bind_test.go b/conf/bind_test.go index 91f4b860..e12cc9bb 100644 --- a/conf/bind_test.go +++ b/conf/bind_test.go @@ -537,20 +537,22 @@ func TestProperties_Bind(t *testing.T) { p, err := conf.Load("./testdata/config/app.yaml") assert.That(t, err).Nil() - err = p.Set("extra.intsV0", "") + fileID := p.AddFile("bind_test.go") + + err = p.Set("extra.intsV0", "", fileID) assert.That(t, err).Nil() - err = p.Set("extra.intsV2", "1,2,3") + err = p.Set("extra.intsV2", "1,2,3", fileID) assert.That(t, err).Nil() - err = p.Set("prefix.extra.intsV2", "1,2,3") + err = p.Set("prefix.extra.intsV2", "1,2,3", fileID) assert.That(t, err).Nil() - err = p.Set("extra.mapV2.a", "1") + err = p.Set("extra.mapV2.a", "1", fileID) assert.That(t, err).Nil() - err = p.Set("extra.mapV2.b", "2") + err = p.Set("extra.mapV2.b", "2", fileID) assert.That(t, err).Nil() - err = p.Set("prefix.extra.mapV2.a", "1") + err = p.Set("prefix.extra.mapV2.a", "1", fileID) assert.That(t, err).Nil() - err = p.Set("prefix.extra.mapV2.b", "2") + err = p.Set("prefix.extra.mapV2.b", "2", fileID) assert.That(t, err).Nil() var c DBConfig diff --git a/conf/conf.go b/conf/conf.go index 0492821e..329665d2 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -121,19 +121,18 @@ package conf import ( "errors" "fmt" - "maps" "os" "path/filepath" "reflect" + "runtime" "strings" "time" + "github.com/go-spring/barky" "github.com/go-spring/spring-core/conf/reader/json" "github.com/go-spring/spring-core/conf/reader/prop" "github.com/go-spring/spring-core/conf/reader/toml" "github.com/go-spring/spring-core/conf/reader/yaml" - "github.com/go-spring/spring-core/conf/storage" - "github.com/go-spring/spring-core/util" "github.com/spf13/cast" ) @@ -222,13 +221,13 @@ var _ Properties = (*MutableProperties)(nil) // but it costs more CPU time when getting properties because it reads property node // by node. So `conf` uses a tree to strictly verify and a flat map to store. type MutableProperties struct { - *storage.Storage + *barky.Storage } // New creates empty *MutableProperties. func New() *MutableProperties { return &MutableProperties{ - Storage: storage.NewStorage(), + Storage: barky.NewStorage(), } } @@ -247,47 +246,30 @@ func Load(file string) (*MutableProperties, error) { if err != nil { return nil, err } - return Map(m), nil + p := New() + _ = p.merge(barky.FlattenMap(m), file) + return p, nil } // Map creates *MutableProperties from map. func Map(m map[string]any) *MutableProperties { p := New() - _ = p.merge(util.FlattenMap(m)) + _, file, _, _ := runtime.Caller(1) + _ = p.merge(barky.FlattenMap(m), file) return p } // merge flattens the map and sets all keys and values. -func (p *MutableProperties) merge(m map[string]string) error { +func (p *MutableProperties) merge(m map[string]string, file string) error { + fileID := p.AddFile(file) for key, val := range m { - if err := p.Set(key, val); err != nil { + if err := p.Set(key, val, fileID); err != nil { return err } } return nil } -// Data returns key-value pairs of the properties. -func (p *MutableProperties) Data() map[string]string { - m := make(map[string]string) - maps.Copy(m, p.RawData()) - return m -} - -// Keys returns keys of the properties. -func (p *MutableProperties) Keys() []string { - return util.OrderedMapKeys(p.RawData()) -} - -// Get returns key's value, using Def to return a default value. -func (p *MutableProperties) Get(key string, def ...string) string { - val, ok := p.RawData()[key] - if !ok && len(def) > 0 { - return def[0] - } - return val -} - // Resolve resolves string value that contains references to other // properties, the references are defined by ${key:=def}. func (p *MutableProperties) Resolve(s string) (string, error) { @@ -338,5 +320,18 @@ func (p *MutableProperties) Bind(i any, tag ...string) error { // CopyTo copies properties into another by override. func (p *MutableProperties) CopyTo(out *MutableProperties) error { - return out.merge(p.RawData()) + rawFile := p.RawFile() + newfile := make(map[string]int8) + oldFile := make([]string, len(rawFile)) + for k, v := range rawFile { + oldFile[v] = k + newfile[k] = out.AddFile(k) + } + for key, v := range p.RawData() { + fileID := newfile[oldFile[v.File]] + if err := out.Set(key, v.Value, fileID); err != nil { + return err + } + } + return nil } diff --git a/conf/reader/prop/prop.go b/conf/reader/prop/prop.go index c7bc5cbf..98356c15 100644 --- a/conf/reader/prop/prop.go +++ b/conf/reader/prop/prop.go @@ -23,7 +23,9 @@ func Read(b []byte) (map[string]any, error) { p := properties.NewProperties() p.DisableExpansion = true - _ = p.Load(b, properties.UTF8) // always no error + if err := p.Load(b, properties.UTF8); err != nil { + return nil, err + } ret := make(map[string]any) for k, v := range p.Map() { diff --git a/conf/reader/prop/prop_test.go b/conf/reader/prop/prop_test.go index a78221e8..6eaf781b 100644 --- a/conf/reader/prop/prop_test.go +++ b/conf/reader/prop/prop_test.go @@ -24,6 +24,11 @@ import ( func TestRead(t *testing.T) { + t.Run("error", func(t *testing.T) { + _, err := Read([]byte(`=1`)) + assert.ThatError(t, err).Matches(`properties: Line 1: "1"`) + }) + t.Run("basic type", func(t *testing.T) { r, err := Read([]byte(` empty= diff --git a/conf/storage/path.go b/conf/storage/path.go deleted file mode 100644 index 3450939b..00000000 --- a/conf/storage/path.go +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package storage - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -// PathType represents the type of a path segment. -type PathType int - -const ( - PathTypeKey PathType = iota // PathTypeKey indicates a named key in a map. - PathTypeIndex // PathTypeIndex indicates a numeric index in a list. -) - -// Path represents a segment of a hierarchical path. -// Each segment is either a key (e.g., "user") or an index (e.g., "0"). -type Path struct { - Type PathType // Type determines whether the segment is a key or index. - Elem string // Elem holds the actual key or index value as a string. -} - -// JoinPath constructs a string representation from a slice of Path segments. -// Keys are joined with '.', and indices are represented as '[i]'. -func JoinPath(path []Path) string { - var sb strings.Builder - for i, p := range path { - switch p.Type { - case PathTypeKey: - if i > 0 { - sb.WriteString(".") - } - sb.WriteString(p.Elem) - case PathTypeIndex: - sb.WriteString("[") - sb.WriteString(p.Elem) - sb.WriteString("]") - } - } - return sb.String() -} - -// SplitPath parses a string path into a slice of Path segments. -// It supports keys separated by '.' and indices enclosed in brackets (e.g., "users[0].name"). -func SplitPath(key string) (_ []Path, err error) { - if key == "" { - return nil, fmt.Errorf("invalid key '%s'", key) - } - var ( - path []Path - lastPos int - lastChar int32 - openBracket bool - ) - for i, c := range key { - switch c { - case ' ': - return nil, fmt.Errorf("invalid key '%s'", key) - case '.': - if openBracket || lastChar == '.' { - return nil, fmt.Errorf("invalid key '%s'", key) - } - if lastChar != ']' { - path = appendKey(path, key[lastPos:i]) - } - lastPos = i + 1 - lastChar = c - case '[': - if openBracket || lastChar == '.' { - return nil, fmt.Errorf("invalid key '%s'", key) - } - if i > 0 && lastChar != ']' { - path = appendKey(path, key[lastPos:i]) - } - openBracket = true - lastPos = i + 1 - lastChar = c - case ']': - if !openBracket { - return nil, fmt.Errorf("invalid key '%s'", key) - } - path, err = appendIndex(path, key[lastPos:i]) - if err != nil { - return nil, fmt.Errorf("invalid key '%s'", key) - } - openBracket = false - lastPos = i + 1 - lastChar = c - default: - if lastChar == ']' { - return nil, fmt.Errorf("invalid key '%s'", key) - } - lastChar = c - } - } - if openBracket || lastChar == '.' { - return nil, fmt.Errorf("invalid key '%s'", key) - } - if lastChar != ']' { - path = appendKey(path, key[lastPos:]) - } - return path, nil -} - -// appendKey appends a key segment to the path. -func appendKey(path []Path, s string) []Path { - return append(path, Path{PathTypeKey, s}) -} - -// appendIndex appends an index segment to the path. -func appendIndex(path []Path, s string) ([]Path, error) { - _, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return nil, errors.New("invalid key") - } - path = append(path, Path{PathTypeIndex, s}) - return path, nil -} diff --git a/conf/storage/path_test.go b/conf/storage/path_test.go deleted file mode 100644 index 5436630c..00000000 --- a/conf/storage/path_test.go +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package storage - -import ( - "errors" - "fmt" - "testing" - - "github.com/go-spring/gs-assert/assert" -) - -func TestSplitPath(t *testing.T) { - var testcases = []struct { - Key string - Err error - Path []Path - }{ - { - Key: "", - Err: errors.New("invalid key ''"), - }, - { - Key: " ", - Err: errors.New("invalid key ' '"), - }, - { - Key: ".", - Err: errors.New("invalid key '.'"), - }, - { - Key: "..", - Err: errors.New("invalid key '..'"), - }, - { - Key: "[", - Err: errors.New("invalid key '['"), - }, - { - Key: "[[", - Err: errors.New("invalid key '[['"), - }, - { - Key: "]", - Err: errors.New("invalid key ']'"), - }, - { - Key: "]]", - Err: errors.New("invalid key ']]'"), - }, - { - Key: "[]", - Err: errors.New("invalid key '[]'"), - }, - { - Key: "[0]", - Path: []Path{ - {PathTypeIndex, "0"}, - }, - }, - { - Key: "[0][", - Err: errors.New("invalid key '[0]['"), - }, - { - Key: "[0]]", - Err: errors.New("invalid key '[0]]'"), - }, - { - Key: "[[0]]", - Err: errors.New("invalid key '[[0]]'"), - }, - { - Key: "[.]", - Err: errors.New("invalid key '[.]'"), - }, - { - Key: "[a]", - Err: errors.New("invalid key '[a]'"), - }, - { - Key: "[a.b]", - Err: errors.New("invalid key '[a.b]'"), - }, - { - Key: "a", - Path: []Path{ - {PathTypeKey, "a"}, - }, - }, - { - Key: "a.", - Err: errors.New("invalid key 'a.'"), - }, - { - Key: "a.b", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeKey, "b"}, - }, - }, - { - Key: "a..b", - Err: errors.New("invalid key 'a..b'"), - }, - { - Key: "a[", - Err: errors.New("invalid key 'a['"), - }, - { - Key: "a]", - Err: errors.New("invalid key 'a]'"), - }, - { - Key: "a[0]", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeIndex, "0"}, - }, - }, - { - Key: "0[0]", - Path: []Path{ - {PathTypeKey, "0"}, - {PathTypeIndex, "0"}, - }, - }, - { - Key: "a.[0]", - Err: errors.New("invalid key 'a.[0]'"), - }, - { - Key: "a.0.b", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeKey, "0"}, - {PathTypeKey, "b"}, - }, - }, - { - Key: "a[0].b", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeIndex, "0"}, - {PathTypeKey, "b"}, - }, - }, - { - Key: "a.[0].b", - Err: errors.New("invalid key 'a.[0].b'"), - }, - { - Key: "a[0]..b", - Err: errors.New("invalid key 'a[0]..b'"), - }, - { - Key: "a[0][0]", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeIndex, "0"}, - {PathTypeIndex, "0"}, - }, - }, - { - Key: "a.[0].[0]", - Err: errors.New("invalid key 'a.[0].[0]'"), - }, - { - Key: "a[0]b", - Err: errors.New("invalid key 'a[0]b'"), - }, - { - Key: "a[0].b", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeIndex, "0"}, - {PathTypeKey, "b"}, - }, - }, - { - Key: "a[0].b.0", - Path: []Path{ - {PathTypeKey, "a"}, - {PathTypeIndex, "0"}, - {PathTypeKey, "b"}, - {PathTypeKey, "0"}, - }, - }, - } - for _, c := range testcases { - p, err := SplitPath(c.Key) - if err != nil { - assert.That(t, err).Equal(c.Err) - continue - } - assert.That(t, p).Equal(c.Path, fmt.Sprintf("key=%s", c.Key)) - assert.That(t, JoinPath(p)).Equal(c.Key) - } -} diff --git a/conf/storage/store.go b/conf/storage/store.go deleted file mode 100644 index 22bc6577..00000000 --- a/conf/storage/store.go +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -Package storage provides hierarchical configuration storage and path parsing utilities. - -Features: -- Storage manages key-value pairs with support for nested paths, subkey lookup, and conflict detection. -- Path represents structured access paths with support for parsing (SplitPath) and construction (JoinPath). -- Supports two path types: - - Key (e.g., "user.name") for map access - - Index (e.g., "[0]") for array access - -- Maintains a tree structure (treeNode) for consistent and type-safe hierarchy management. - -Use cases: -- Accessing values in JSON/YAML/TOML-like configs -- Managing nested config data (CRUD) -- Validating structure and detecting conflicts - -Notes: -- Path syntax follows common config patterns (e.g., "users[0].profile.age") -- Type-safe path handling (keys vs. indices) -*/ -package storage - -import ( - "errors" - "fmt" - - "github.com/go-spring/spring-core/util" -) - -// treeNode represents a node in the hierarchical key path tree. -// Each node tracks the type of its path segment and its child nodes. -type treeNode struct { - Type PathType - Data map[string]*treeNode -} - -// Storage stores hierarchical key-value pairs and tracks their structure using a tree. -// It supports nested paths and detects structural conflicts when paths differ in type. -type Storage struct { - root *treeNode // Root of the hierarchical key path tree - data map[string]string // Flat key-value storage for exact key matches -} - -// NewStorage creates and initializes a new Storage instance. -func NewStorage() *Storage { - return &Storage{ - data: make(map[string]string), - } -} - -// RawData returns the underlying flat key-value map. -// Note: This exposes internal state; use with caution. -func (s *Storage) RawData() map[string]string { - return s.data -} - -// SubKeys returns the immediate subkeys under the given key path. -// It walks the tree structure and returns child elements if the path exists. -// Returns an error if there's a type conflict along the path. -func (s *Storage) SubKeys(key string) (_ []string, err error) { - var path []Path - if key != "" { - if path, err = SplitPath(key); err != nil { - return nil, err - } - } - - if s.root == nil { - return nil, nil - } - - n := s.root - for i, pathNode := range path { - if n == nil || pathNode.Type != n.Type { - return nil, fmt.Errorf("property conflict at path %s", JoinPath(path[:i+1])) - } - v, ok := n.Data[pathNode.Elem] - if !ok { - return nil, nil - } - n = v - } - - if n == nil { - return nil, fmt.Errorf("property conflict at path %s", key) - } - return util.OrderedMapKeys(n.Data), nil -} - -// Has returns true if the given key exists in the storage, -// either as a direct value or as a valid path in the hierarchical tree structure. -func (s *Storage) Has(key string) bool { - if key == "" || s.root == nil { - return false - } - - if _, ok := s.data[key]; ok { - return true - } - - path, err := SplitPath(key) - if err != nil { - return false - } - - n := s.root - for _, node := range path { - if n == nil || node.Type != n.Type { - return false - } - v, ok := n.Data[node.Elem] - if !ok { - return false - } - n = v - } - return true -} - -// Set inserts a key-value pair into the storage. -// It also constructs or extends the corresponding hierarchical path in the tree. -// Returns an error if there is a type conflict or if the key is empty. -func (s *Storage) Set(key, val string) error { - if key == "" { - return errors.New("key is empty") - } - - path, err := SplitPath(key) - if err != nil { - return err - } - - // Initialize tree root if empty - if s.root == nil { - s.root = &treeNode{ - Type: path[0].Type, - Data: make(map[string]*treeNode), - } - } - - n := s.root - for i, pathNode := range path { - if n == nil || pathNode.Type != n.Type { - return fmt.Errorf("property conflict at path %s", JoinPath(path[:i+1])) - } - v, ok := n.Data[pathNode.Elem] - if !ok { - if i < len(path)-1 { - v = &treeNode{ - Type: path[i+1].Type, - Data: make(map[string]*treeNode), - } - } - n.Data[pathNode.Elem] = v - } - n = v - } - if n != nil { - return fmt.Errorf("property conflict at path %s", key) - } - - s.data[key] = val - return nil -} diff --git a/conf/storage/store_test.go b/conf/storage/store_test.go deleted file mode 100644 index a177ee06..00000000 --- a/conf/storage/store_test.go +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package storage - -import ( - "testing" - - "github.com/go-spring/gs-assert/assert" -) - -func TestStorage(t *testing.T) { - - t.Run("empty", func(t *testing.T) { - s := NewStorage() - assert.That(t, s.RawData()).Equal(map[string]string{}) - - subKeys, err := s.SubKeys("a") - assert.That(t, err).Nil() - assert.That(t, subKeys).Nil() - - subKeys, err = s.SubKeys("a.b") - assert.That(t, err).Nil() - assert.That(t, subKeys).Nil() - - subKeys, err = s.SubKeys("a[0]") - assert.That(t, err).Nil() - assert.That(t, subKeys).Nil() - - assert.That(t, s.Has("a")).False() - assert.That(t, s.Has("a.b")).False() - assert.That(t, s.Has("a[0]")).False() - - err = s.Set("", "abc") - assert.ThatError(t, err).Matches("key is empty") - }) - - t.Run("map-0", func(t *testing.T) { - s := NewStorage() - - err := s.Set("a", "b") - assert.That(t, err).Nil() - assert.That(t, s.Has("a")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "a": "b", - }) - - err = s.Set("a.y", "x") - assert.ThatError(t, err).Matches("property conflict at path a.y") - err = s.Set("a[0]", "x") - assert.ThatError(t, err).Matches("property conflict at path a\\[0]") - - assert.That(t, s.Has("")).False() - assert.That(t, s.Has("a[")).False() - assert.That(t, s.Has("a.y")).False() - assert.That(t, s.Has("a[0]")).False() - - subKeys, err := s.SubKeys("") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"a"}) - - _, err = s.SubKeys("a") - assert.ThatError(t, err).Matches("property conflict at path a") - _, err = s.SubKeys("a[") - assert.ThatError(t, err).Matches("invalid key 'a\\['") - - err = s.Set("a", "c") - assert.That(t, err).Nil() - assert.That(t, s.Has("a")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "a": "c", - }) - }) - - t.Run("map-1", func(t *testing.T) { - s := NewStorage() - - err := s.Set("m.x", "y") - assert.That(t, err).Nil() - assert.That(t, s.Has("m")).True() - assert.That(t, s.Has("m.x")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "m.x": "y", - }) - - assert.That(t, s.Has("")).False() - assert.That(t, s.Has("m.t")).False() - assert.That(t, s.Has("m.x.y")).False() - assert.That(t, s.Has("m[0]")).False() - assert.That(t, s.Has("m.x[0]")).False() - - err = s.Set("m", "a") - assert.ThatError(t, err).Matches("property conflict at path m") - err = s.Set("m.x.z", "w") - assert.ThatError(t, err).Matches("property conflict at path m") - err = s.Set("m[0]", "f") - assert.ThatError(t, err).Matches("property conflict at path m\\[0]") - - _, err = s.SubKeys("m.t") - assert.That(t, err).Nil() - subKeys, err := s.SubKeys("m") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"x"}) - - _, err = s.SubKeys("m.x") - assert.ThatError(t, err).Matches("property conflict at path m.x") - _, err = s.SubKeys("m[0]") - assert.ThatError(t, err).Matches("property conflict at path m\\[0]") - - err = s.Set("m.x", "z") - assert.That(t, err).Nil() - assert.That(t, s.Has("m")).True() - assert.That(t, s.Has("m.x")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "m.x": "z", - }) - - err = s.Set("m.t", "q") - assert.That(t, err).Nil() - assert.That(t, s.Has("m")).True() - assert.That(t, s.Has("m.x")).True() - assert.That(t, s.Has("m.t")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "m.x": "z", - "m.t": "q", - }) - - subKeys, err = s.SubKeys("m") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"t", "x"}) - }) - - t.Run("arr-0", func(t *testing.T) { - s := NewStorage() - - err := s.Set("[0]", "p") - assert.That(t, err).Nil() - assert.That(t, s.Has("[0]")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "[0]": "p", - }) - - err = s.Set("[0]x", "f") - assert.ThatError(t, err).Matches("invalid key '\\[0]x'") - err = s.Set("[0].x", "f") - assert.ThatError(t, err).Matches("property conflict at path \\[0].x") - - err = s.Set("[0]", "w") - assert.That(t, err).Nil() - assert.That(t, s.RawData()).Equal(map[string]string{ - "[0]": "w", - }) - - subKeys, err := s.SubKeys("") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"0"}) - - err = s.Set("[1]", "p") - assert.That(t, err).Nil() - assert.That(t, s.Has("[0]")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "[0]": "w", - "[1]": "p", - }) - - subKeys, err = s.SubKeys("") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"0", "1"}) - }) - - t.Run("arr-1", func(t *testing.T) { - s := NewStorage() - - err := s.Set("s[0]", "p") - assert.That(t, err).Nil() - assert.That(t, s.Has("s")).True() - assert.That(t, s.Has("s[0]")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "s[0]": "p", - }) - - err = s.Set("s[1]", "o") - assert.That(t, err).Nil() - assert.That(t, s.Has("s")).True() - assert.That(t, s.Has("s[0]")).True() - assert.That(t, s.Has("s[1]")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "s[0]": "p", - "s[1]": "o", - }) - - subKeys, err := s.SubKeys("s") - assert.That(t, err).Nil() - assert.That(t, subKeys).Equal([]string{"0", "1"}) - - err = s.Set("s", "w") - assert.ThatError(t, err).Matches("property conflict at path s") - err = s.Set("s.x", "f") - assert.ThatError(t, err).Matches("property conflict at path s.x") - }) - - t.Run("map && array", func(t *testing.T) { - s := NewStorage() - - err := s.Set("a.b[0].c", "123") - assert.That(t, err).Nil() - assert.That(t, s.Has("a")).True() - assert.That(t, s.Has("a.b")).True() - assert.That(t, s.Has("a.b[0]")).True() - assert.That(t, s.Has("a.b[0].c")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "a.b[0].c": "123", - }) - - err = s.Set("a.b[0].d[0]", "123") - assert.That(t, err).Nil() - assert.That(t, s.Has("a")).True() - assert.That(t, s.Has("a.b")).True() - assert.That(t, s.Has("a.b[0]")).True() - assert.That(t, s.Has("a.b[0].d")).True() - assert.That(t, s.Has("a.b[0].d[0]")).True() - assert.That(t, s.RawData()).Equal(map[string]string{ - "a.b[0].c": "123", - "a.b[0].d[0]": "123", - }) - }) -} diff --git a/docs/4. examples/bookman/go.mod b/docs/4. examples/bookman/go.mod index 3fb3dbd9..e415794f 100644 --- a/docs/4. examples/bookman/go.mod +++ b/docs/4. examples/bookman/go.mod @@ -9,12 +9,13 @@ require ( ) require ( - github.com/expr-lang/expr v1.17.2 // indirect + github.com/expr-lang/expr v1.17.5 // indirect + github.com/go-spring/barky v1.0.3 // indirect github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // indirect + github.com/go-spring/log v0.0.5 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cast v1.9.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/docs/4. examples/bookman/go.sum b/docs/4. examples/bookman/go.sum index c871f33f..213c55c3 100644 --- a/docs/4. examples/bookman/go.sum +++ b/docs/4. examples/bookman/go.sum @@ -1,13 +1,15 @@ -github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso= -github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k= +github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-spring/barky v1.0.3 h1:24U2IX47es7JfuAx7WkkOqBEV0bOy49/ZW4GCkVvD7Q= +github.com/go-spring/barky v1.0.3/go.mod h1:IlEMJj9d//EQs2oin0tuGKGACslZe73khbHcDPzF9KE= github.com/go-spring/gs-assert v1.0.2 h1:9vDppl7ZwMvQE4c83ac7GzN0VxZC5BrQue7dND7NclQ= github.com/go-spring/gs-assert v1.0.2/go.mod h1:FfibkqWz4HUBpbig1cKMlzW8Ha7RywTB93f1Q/NuF9I= github.com/go-spring/gs-mock v0.0.4 h1:f34YN+ntXflfn13aLa3ZVCB78IG7wWZGK4y5tB+OGpI= github.com/go-spring/gs-mock v0.0.4/go.mod h1:QK0PqZ+Vu9F+BU97zl8fip5XKibvDSoN+ofky413Z6Q= -github.com/go-spring/log v0.0.3 h1:hse6P3RpbQ6GKOB0nnQAvtEusFC1kdkfebdjv3p6O+g= -github.com/go-spring/log v0.0.3/go.mod h1:9SWgPEVWSGgloRTGR7niBliqfwC5UCjPUOl2jyJOimM= +github.com/go-spring/log v0.0.5 h1:a8yiGmZTS7MPYvYvePXtc0hIdaQ76pLdsXt8iJwgQBQ= +github.com/go-spring/log v0.0.5/go.mod h1:9SWgPEVWSGgloRTGR7niBliqfwC5UCjPUOl2jyJOimM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -22,8 +24,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/docs/4. examples/bookman/src/app/common/handlers/log/log.go b/docs/4. examples/bookman/src/app/common/handlers/log/log.go index 927fb1f3..9cb7225a 100644 --- a/docs/4. examples/bookman/src/app/common/handlers/log/log.go +++ b/docs/4. examples/bookman/src/app/common/handlers/log/log.go @@ -26,10 +26,7 @@ import ( ) func init() { - // Register a group of Bean definitions during application initialization. - // GroupRegister dynamically creates multiple Beans based on the configuration (conf.Properties), - // making it ideal for scenarios like setting up multiple loggers, clients, or resources from config. - gs.GroupRegister(func(p conf.Properties) ([]*gs.BeanDefinition, error) { + gs.Module(nil, func(p conf.Properties) error { var loggers map[string]struct { Name string `value:"${name}"` // Log file name @@ -39,10 +36,9 @@ func init() { // Bind configuration from the "${log}" node into the 'loggers' map. err := p.Bind(&loggers, "${log}") if err != nil { - return nil, err + return err } - var ret []*gs.BeanDefinition for k, l := range loggers { var ( f *os.File @@ -52,19 +48,17 @@ func init() { // Open (or create) the log file f, err = os.OpenFile(filepath.Join(l.Dir, l.Name), flag, os.ModePerm) if err != nil { - return nil, err + return err } // Create a new slog.Logger instance with a text handler writing to the file o := slog.New(slog.NewTextHandler(f, nil)) // Wrap the logger into a Bean with a destroy hook to close the file - b := gs.NewBean(o).Name(k).Destroy(func(_ *slog.Logger) { + gs.Object(o).Name(k).Destroy(func(_ *slog.Logger) { _ = f.Close() }) - - ret = append(ret, b) } - return ret, nil + return nil }) } diff --git a/docs/4. examples/miniapi/go.mod b/docs/4. examples/miniapi/go.mod index faaf9444..1610b58c 100644 --- a/docs/4. examples/miniapi/go.mod +++ b/docs/4. examples/miniapi/go.mod @@ -3,16 +3,17 @@ module miniapi go 1.24 require ( - github.com/go-spring/log v0.0.3 + github.com/go-spring/log v0.0.5 github.com/go-spring/spring-core v0.0.0 ) require ( - github.com/expr-lang/expr v1.17.2 // indirect + github.com/expr-lang/expr v1.17.5 // indirect + github.com/go-spring/barky v1.0.3 // indirect github.com/go-spring/gs-mock v0.0.4 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cast v1.9.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/docs/4. examples/miniapi/go.sum b/docs/4. examples/miniapi/go.sum index 8b941181..7e534aa4 100644 --- a/docs/4. examples/miniapi/go.sum +++ b/docs/4. examples/miniapi/go.sum @@ -1,13 +1,15 @@ -github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso= -github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k= +github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-spring/barky v1.0.3 h1:24U2IX47es7JfuAx7WkkOqBEV0bOy49/ZW4GCkVvD7Q= +github.com/go-spring/barky v1.0.3/go.mod h1:IlEMJj9d//EQs2oin0tuGKGACslZe73khbHcDPzF9KE= github.com/go-spring/gs-assert v1.0.2 h1:9vDppl7ZwMvQE4c83ac7GzN0VxZC5BrQue7dND7NclQ= github.com/go-spring/gs-assert v1.0.2/go.mod h1:FfibkqWz4HUBpbig1cKMlzW8Ha7RywTB93f1Q/NuF9I= github.com/go-spring/gs-mock v0.0.4 h1:f34YN+ntXflfn13aLa3ZVCB78IG7wWZGK4y5tB+OGpI= github.com/go-spring/gs-mock v0.0.4/go.mod h1:QK0PqZ+Vu9F+BU97zl8fip5XKibvDSoN+ofky413Z6Q= -github.com/go-spring/log v0.0.3 h1:hse6P3RpbQ6GKOB0nnQAvtEusFC1kdkfebdjv3p6O+g= -github.com/go-spring/log v0.0.3/go.mod h1:9SWgPEVWSGgloRTGR7niBliqfwC5UCjPUOl2jyJOimM= +github.com/go-spring/log v0.0.5 h1:a8yiGmZTS7MPYvYvePXtc0hIdaQ76pLdsXt8iJwgQBQ= +github.com/go-spring/log v0.0.5/go.mod h1:9SWgPEVWSGgloRTGR7niBliqfwC5UCjPUOl2jyJOimM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -20,8 +22,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/docs/4. examples/miniapi/main.go b/docs/4. examples/miniapi/main.go index 6ed40eca..bce928a4 100644 --- a/docs/4. examples/miniapi/main.go +++ b/docs/4. examples/miniapi/main.go @@ -37,7 +37,7 @@ func main() { // - Dependency Injection: Wires beans automatically. // - Dynamic Refresh: Updates configs at runtime without restart. gs.RunWith(func(ctx context.Context) error { - log.Infof(ctx, log.TagApp, "app started") + log.Infof(ctx, log.TagAppDef, "app started") return nil }) } diff --git a/go.mod b/go.mod index b33743fc..9435a68b 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.24 require ( github.com/expr-lang/expr v1.17.5 + github.com/go-spring/barky v1.0.3 github.com/go-spring/gs-assert v1.0.2 github.com/go-spring/gs-mock v0.0.4 - github.com/go-spring/log v0.0.5 + github.com/go-spring/log v0.0.6 github.com/magiconair/properties v1.8.10 github.com/pelletier/go-toml v1.9.5 github.com/spf13/cast v1.9.2 diff --git a/go.sum b/go.sum index a93d1144..0673ca22 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,14 @@ github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-spring/barky v1.0.3 h1:24U2IX47es7JfuAx7WkkOqBEV0bOy49/ZW4GCkVvD7Q= +github.com/go-spring/barky v1.0.3/go.mod h1:IlEMJj9d//EQs2oin0tuGKGACslZe73khbHcDPzF9KE= github.com/go-spring/gs-assert v1.0.2 h1:9vDppl7ZwMvQE4c83ac7GzN0VxZC5BrQue7dND7NclQ= github.com/go-spring/gs-assert v1.0.2/go.mod h1:FfibkqWz4HUBpbig1cKMlzW8Ha7RywTB93f1Q/NuF9I= github.com/go-spring/gs-mock v0.0.4 h1:f34YN+ntXflfn13aLa3ZVCB78IG7wWZGK4y5tB+OGpI= github.com/go-spring/gs-mock v0.0.4/go.mod h1:QK0PqZ+Vu9F+BU97zl8fip5XKibvDSoN+ofky413Z6Q= -github.com/go-spring/log v0.0.5 h1:a8yiGmZTS7MPYvYvePXtc0hIdaQ76pLdsXt8iJwgQBQ= -github.com/go-spring/log v0.0.5/go.mod h1:9SWgPEVWSGgloRTGR7niBliqfwC5UCjPUOl2jyJOimM= +github.com/go-spring/log v0.0.6 h1:R+0mKWCNzaEIZtqdfCc4e/Ha4FBhr01If5oJXjEwRO0= +github.com/go-spring/log v0.0.6/go.mod h1:WrLbwbjmU8Vk5ampzBXjG4rTv+BoxmmTXxX82L2hugg= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/gs/app.go b/gs/app.go index 5f959300..9ccf7b06 100644 --- a/gs/app.go +++ b/gs/app.go @@ -54,7 +54,7 @@ func (s *AppStarter) RunWith(fn func(ctx context.Context) error) { if err = s.initApp(); err != nil { return } - if err = gs_app.GS.RunWith(fn); err != nil { + if err = app.RunWith(fn); err != nil { return } log.Destroy() @@ -65,11 +65,11 @@ func (s *AppStarter) RunAsync() (func(), error) { if err := s.initApp(); err != nil { return nil, err } - if err := gs_app.GS.Start(); err != nil { + if err := app.Start(); err != nil { return nil, err } return func() { - gs_app.GS.Stop() + app.Stop() log.Destroy() }, nil } diff --git a/gs/gs.go b/gs/gs.go index 86a09a8e..4693abcb 100644 --- a/gs/gs.go +++ b/gs/gs.go @@ -19,6 +19,7 @@ package gs import ( "context" "reflect" + "runtime" "github.com/go-spring/log" "github.com/go-spring/spring-core/conf" @@ -72,25 +73,39 @@ func BindArg(fn any, args ...Arg) *gs_arg.BindArg { /************************************ cond ***********************************/ type ( - Condition = gs.Condition - CondContext = gs.CondContext + Condition = gs.Condition + ConditionContext = gs.ConditionContext + ConditionOnProperty = gs_cond.ConditionOnProperty ) +// OnOnce creates a Condition that wraps another Condition and ensures +// its Matches method is called only once. Subsequent calls will return +// the same result as the first call without re-evaluating the condition. +func OnOnce(conditions ...Condition) Condition { + var ( + done bool + result bool + ) + return OnFunc(func(ctx ConditionContext) (_ bool, err error) { + if done { + return result, nil + } + done = true + result, err = gs_cond.And(conditions...).Matches(ctx) + return result, err + }) +} + // OnFunc creates a Condition based on the provided function. -func OnFunc(fn func(ctx CondContext) (bool, error)) Condition { +func OnFunc(fn func(ctx ConditionContext) (bool, error)) Condition { return gs_cond.OnFunc(fn) } // OnProperty creates a Condition based on a property name and options. -func OnProperty(name string) gs_cond.OnPropertyInterface { +func OnProperty(name string) ConditionOnProperty { return gs_cond.OnProperty(name) } -// OnMissingProperty creates a Condition that checks for a missing property. -func OnMissingProperty(name string) Condition { - return gs_cond.OnMissingProperty(name) -} - // OnBean creates a Condition for when a specific bean exists. func OnBean[T any](name ...string) Condition { return gs_cond.OnBean[T](name...) @@ -136,11 +151,20 @@ func None(conditions ...Condition) Condition { return gs_cond.None(conditions...) } +// OnEnableJobs creates a Condition that checks whether the EnableJobsProp property is true. +func OnEnableJobs() ConditionOnProperty { + return OnProperty(EnableJobsProp).HavingValue("true").MatchIfMissing() +} + +// OnEnableServers creates a Condition that checks whether the EnableServersProp property is true. +func OnEnableServers() ConditionOnProperty { + return OnProperty(EnableServersProp).HavingValue("true").MatchIfMissing() +} + /************************************ ioc ************************************/ type ( - BeanID = gs.BeanID - BeanMock = gs.BeanMock + BeanID = gs.BeanID ) type ( @@ -172,7 +196,9 @@ func BeanSelectorFor[T any](name ...string) BeanSelector { // Property sets a system property. func Property(key string, val string) { - if err := gs_conf.SysConf.Set(key, val); err != nil { + _, file, _, _ := runtime.Caller(1) + fileID := gs_conf.SysConf.AddFile(file) + if err := gs_conf.SysConf.Set(key, val, fileID); err != nil { log.Errorf(context.Background(), log.TagAppDef, "failed to set property key=%s, err=%v", key, err) } } @@ -185,6 +211,7 @@ type ( ) var B = gs_app.NewBoot() +var app = gs_app.NewApp() // funcRunner is a function type that implements the Runner interface. type funcRunner func() error @@ -233,53 +260,77 @@ func RunAsync() (func(), error) { // Exiting returns a boolean indicating whether the application is exiting. func Exiting() bool { - return gs_app.GS.Exiting() + return app.Exiting() } // ShutDown shuts down the app with an optional message. func ShutDown() { - gs_app.GS.ShutDown() + app.ShutDown() } // Config returns the app configuration. func Config() *gs_conf.AppConfig { - return gs_app.GS.P + return app.P } // Component registers a bean definition for a given object. func Component[T any](i T) T { b := gs_bean.NewBean(reflect.ValueOf(i)) - gs_app.GS.C.Register(b).Caller(1) + app.C.Register(b).Caller(1) return i } +// RootBean registers a root bean definition. +func RootBean(b *RegisteredBean) { + app.C.RootBean(b) +} + // Object registers a bean definition for a given object. func Object(i any) *RegisteredBean { b := gs_bean.NewBean(reflect.ValueOf(i)) - return gs_app.GS.C.Register(b).Caller(1) + return app.C.Register(b).Caller(1) } // Provide registers a bean definition for a given constructor. func Provide(ctor any, args ...Arg) *RegisteredBean { b := gs_bean.NewBean(ctor, args...) - return gs_app.GS.C.Register(b).Caller(1) + return app.C.Register(b).Caller(1) } // Register registers a bean definition. func Register(b *BeanDefinition) *RegisteredBean { - return gs_app.GS.C.Register(b) + return app.C.Register(b) +} + +// Module registers a module. +func Module(conditions []ConditionOnProperty, fn func(p conf.Properties) error) { + app.C.Module(conditions, fn) } -// GroupRegister registers a group of bean definitions. -func GroupRegister(fn func(p conf.Properties) ([]*BeanDefinition, error)) { - gs_app.GS.C.GroupRegister(fn) +// Group registers a module for a group of beans. +func Group[T any, R any](key string, fn func(c T) (R, error), d func(R) error) { + app.C.Module([]ConditionOnProperty{ + OnProperty(key), + }, func(p conf.Properties) error { + var m map[string]T + if err := p.Bind(&m, "${"+key+"}"); err != nil { + return err + } + for name, c := range m { + b := Provide(fn, ValueArg(c)).Name(name) + if d != nil { + b.Destroy(d) + } + } + return nil + }) } // RefreshProperties refreshes the app configuration. func RefreshProperties() error { - p, err := gs_app.GS.P.Refresh() + p, err := app.P.Refresh() if err != nil { return err } - return gs_app.GS.C.RefreshProperties(p) + return app.C.RefreshProperties(p) } diff --git a/gs/gstest/testdata/biz/biz.go b/gs/gs_test.go similarity index 67% rename from gs/gstest/testdata/biz/biz.go rename to gs/gs_test.go index e3c08dbd..86b28501 100644 --- a/gs/gstest/testdata/biz/biz.go +++ b/gs/gs_test.go @@ -14,23 +14,33 @@ * limitations under the License. */ -package biz +package gs_test import ( "fmt" + "testing" "github.com/go-spring/spring-core/gs" - "github.com/go-spring/spring-core/gs/gstest/testdata/dao" ) -func init() { - gs.Object(&Service{}) +func TestMain(m *testing.M) { + gs.AddTester(&Tester{}) + gs.Object(&Dep{}) + gs.TestMain(m) } -type Service struct { - Dao *dao.Dao `autowire:""` +func TestAA(t *testing.T) { + +} + +type Dep struct { + Name string `value:"${name:=TestAA}"` +} + +type Tester struct { + Dep *Dep `autowire:""` } -func (s *Service) Hello(name string) string { - return fmt.Sprintf("hello %s", name) +func (o *Tester) TestAA(t *testing.T) { + fmt.Println(o.Dep.Name) } diff --git a/gs/gstest/gstest.go b/gs/gstest/gstest.go deleted file mode 100644 index 021680aa..00000000 --- a/gs/gstest/gstest.go +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* -Package gstest provides unit testing utilities for dependency injection in Go-Spring framework. - -Key Features: - - Test environment configuration: jobs and servers are disabled, and the "test" profile is automatically activated - - Autowire failure tolerance: non-critical autowiring errors are tolerated so that missing beans do not break tests - - Type-safe mocking: compile-time checked MockFor/With methods for registering mock beans - - Context lifecycle management: TestMain starts and stops the Go-Spring context automatically - - Injection helpers: Get[T](t) and Wire(t, obj) simplify bean retrieval and dependency injection - -Usage Pattern: - - // Step 1: Register your mock beans before tests run - // by calling `MockFor[T]().With(obj)` inside an `init()` function. - func init() { - gstest.MockFor[*Dao]().With(&MockDao{}) - } - - // Step 2: Implement TestMain and invoke `gstest.TestMain(m, opts...)` - // to bootstrap the application context, execute all tests, and then shut it down. - // You can supply `BeforeRun` and `AfterRun` hooks to run code immediately before or after your test suite. - func TestMain(m *testing.M) { - gstest.TestMain(m) - } - - // Step 3: Write your test cases and use Get[T](t) or Wire(t, obj) to retrieve beans and inject dependencies. - func TestService(t *testing.T) { - // Retrieve autowired test target - service := gstest.Get[*Service](t) - - // Verify business logic - result := service.Process() - assert.Equal(t, expect, result) - } -*/ -package gstest - -import ( - "testing" - - "github.com/go-spring/spring-core/gs" - "github.com/go-spring/spring-core/gs/internal/gs_app" -) - -func init() { - gs.EnableJobs(false) - gs.EnableServers(false) - gs.SetActiveProfiles("test") - gs.ForceAutowireIsNullable(true) -} - -// BeanMock is a mock for bean. -type BeanMock[T any] struct { - selector gs.BeanSelector -} - -// MockFor creates a mock for bean. -func MockFor[T any](name ...string) BeanMock[T] { - return BeanMock[T]{ - selector: gs.BeanSelectorFor[T](name...), - } -} - -// With registers a mock bean. -func (m BeanMock[T]) With(obj T) { - gs_app.GS.C.AddMock(gs.BeanMock{ - Object: obj, - Target: m.selector, - }) -} - -type runArg struct { - beforeRun func() - afterRun func() -} - -type RunOption func(arg *runArg) - -// BeforeRun specifies a function to be executed before all testcases. -func BeforeRun(fn func()) RunOption { - return func(arg *runArg) { - arg.beforeRun = fn - } -} - -// AfterRun specifies a function to be executed after all testcases. -func AfterRun(fn func()) RunOption { - return func(arg *runArg) { - arg.afterRun = fn - } -} - -// TestMain executes test cases and ensures shutdown of the app context. -func TestMain(m *testing.M, opts ...RunOption) { - arg := &runArg{} - for _, opt := range opts { - opt(arg) - } - - err := gs_app.GS.Start() - if err != nil { - panic(err) - } - - if arg.beforeRun != nil { - arg.beforeRun() - } - - m.Run() - - if arg.afterRun != nil { - arg.afterRun() - } - - gs_app.GS.Stop() -} - -// Get gets the bean from the app context. -func Get[T any](t *testing.T) T { - var s struct { - Value T `autowire:""` - } - return Wire(t, &s).Value -} - -// Wire injects dependencies into the object. -func Wire[T any](t *testing.T, obj T) T { - err := gs_app.GS.C.Wire(obj) - if err != nil { - t.Fatal(err) - } - return obj -} diff --git a/gs/gstest/gstest_test.go b/gs/gstest/gstest_test.go deleted file mode 100644 index 170f1698..00000000 --- a/gs/gstest/gstest_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2025 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package gstest_test - -import ( - "fmt" - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/gs/gstest" - "github.com/go-spring/spring-core/gs/gstest/testdata/app" - "github.com/go-spring/spring-core/gs/gstest/testdata/biz" -) - -func init() { - gstest.MockFor[*app.App]().With(&app.App{Name: "test"}) -} - -func TestMain(m *testing.M) { - var opts []gstest.RunOption - opts = append(opts, gstest.BeforeRun(func() { - fmt.Println("before run") - })) - opts = append(opts, gstest.AfterRun(func() { - fmt.Println("after run") - })) - gstest.TestMain(m, opts...) -} - -func TestGSTest(t *testing.T) { - // The dao.Dao object was not successfully created, - // and the corresponding injection will also fail. - // The following log will be printed on the console: - // autowire error: TagArg::GetArgValue error << bind path=string type=string error << property dao.addr not exist - - a := gstest.Get[*app.App](t) - assert.That(t, a.Name).Equal("test") - - s := gstest.Wire(t, new(struct { - App *app.App `autowire:""` - Service *biz.Service `autowire:""` - })) - assert.That(t, s.Service.Dao).Nil() - assert.That(t, s.App.Name).Equal("test") - assert.That(t, s.Service.Hello("xyz")).Equal("hello xyz") -} diff --git a/gs/gstest/testdata/app/app.go b/gs/gstest/testdata/app/app.go deleted file mode 100644 index a01fabe0..00000000 --- a/gs/gstest/testdata/app/app.go +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2025 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package app - -import ( - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&App{}) -} - -type App struct { - Name string `value:"${spring.app.name}"` -} diff --git a/gs/gstest/testdata/dao/dao.go b/gs/gstest/testdata/dao/dao.go deleted file mode 100644 index dd541190..00000000 --- a/gs/gstest/testdata/dao/dao.go +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2025 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dao - -import ( - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Provide(New, gs.TagArg("${dao.addr}")) -} - -type Dao struct { - addr string -} - -func New(addr string) (*Dao, error) { - return &Dao{addr: addr}, nil -} diff --git a/gs/http.go b/gs/http.go index d9f085cf..64ecddf6 100644 --- a/gs/http.go +++ b/gs/http.go @@ -21,28 +21,36 @@ import ( "net" "net/http" "time" + + "github.com/go-spring/spring-core/conf" + "github.com/go-spring/spring-core/gs/internal/gs" ) func init() { - // Register the default ServeMux as a bean if no other ServeMux instance exists - Object(http.DefaultServeMux).Condition( - OnMissingBean[*http.ServeMux](), - OnProperty(EnableSimpleHttpServerProp).HavingValue("true").MatchIfMissing(), - ) - - // Provide a new SimpleHttpServer instance with configuration bindings. - Provide( - NewSimpleHttpServer, - IndexArg(1, BindArg(SetHttpServerAddr, TagArg("${http.server.addr:=0.0.0.0:9090}"))), - IndexArg(1, BindArg(SetHttpServerReadTimeout, TagArg("${http.server.readTimeout:=5s}"))), - IndexArg(1, BindArg(SetHttpServerHeaderTimeout, TagArg("${http.server.headerTimeout:=1s}"))), - IndexArg(1, BindArg(SetHttpServerWriteTimeout, TagArg("${http.server.writeTimeout:=5s}"))), - IndexArg(1, BindArg(SetHttpServerIdleTimeout, TagArg("${http.server.idleTimeout:=60s}"))), - ).Condition( - OnBean[*http.ServeMux](), - OnProperty(EnableServersProp).HavingValue("true").MatchIfMissing(), - OnProperty(EnableSimpleHttpServerProp).HavingValue("true").MatchIfMissing(), - ).AsServer() + Module( + []ConditionOnProperty{ + OnEnableServers(), + OnProperty(EnableSimpleHttpServerProp).HavingValue("true").MatchIfMissing(), + }, + func(p conf.Properties) error { + + // Register the default ServeMux as a bean if no other ServeMux instance exists + Object(http.DefaultServeMux).Export(gs.As[http.Handler]()).Condition( + OnMissingBean[http.Handler](), + ) + + // Provide a new SimpleHttpServer instance with configuration bindings. + Provide( + NewSimpleHttpServer, + IndexArg(1, BindArg(SetHttpServerAddr, TagArg("${http.server.addr:=0.0.0.0:9090}"))), + IndexArg(1, BindArg(SetHttpServerReadTimeout, TagArg("${http.server.readTimeout:=5s}"))), + IndexArg(1, BindArg(SetHttpServerHeaderTimeout, TagArg("${http.server.headerTimeout:=1s}"))), + IndexArg(1, BindArg(SetHttpServerWriteTimeout, TagArg("${http.server.writeTimeout:=5s}"))), + IndexArg(1, BindArg(SetHttpServerIdleTimeout, TagArg("${http.server.idleTimeout:=60s}"))), + ).AsServer() + + return nil + }) } // HttpServerConfig holds configuration options for the HTTP server. @@ -98,7 +106,7 @@ type SimpleHttpServer struct { } // NewSimpleHttpServer creates a new instance of SimpleHttpServer. -func NewSimpleHttpServer(mux *http.ServeMux, opts ...HttpServerOption) *SimpleHttpServer { +func NewSimpleHttpServer(h http.Handler, opts ...HttpServerOption) *SimpleHttpServer { arg := &HttpServerConfig{ Address: "0.0.0.0:9090", ReadTimeout: time.Second * 5, @@ -111,7 +119,7 @@ func NewSimpleHttpServer(mux *http.ServeMux, opts ...HttpServerOption) *SimpleHt } return &SimpleHttpServer{svr: &http.Server{ Addr: arg.Address, - Handler: mux, + Handler: h, ReadTimeout: arg.ReadTimeout, WriteTimeout: arg.WriteTimeout, }} diff --git a/gs/internal/gs/gs.go b/gs/internal/gs/gs.go index 2fd71762..ff9b6300 100644 --- a/gs/internal/gs/gs.go +++ b/gs/internal/gs/gs.go @@ -14,7 +14,7 @@ * limitations under the License. */ -//go:generate gs mock -o=gs_mock.go -i=CondContext,ArgContext,Runner,Job,Server +//go:generate gs mock -o=gs_mock.go -i=ConditionContext,ArgContext,Runner,Job,Server package gs @@ -89,23 +89,23 @@ func (s BeanSelectorImpl) String() string { // when registering beans in the IoC container. type Condition interface { // Matches checks whether the condition is satisfied. - Matches(ctx CondContext) (bool, error) + Matches(ctx ConditionContext) (bool, error) } -// CondBean represents a bean with Name and Type. -type CondBean interface { +// ConditionBean represents a bean with Name and Type. +type ConditionBean interface { Name() string // Name of the bean Type() reflect.Type // Type of the bean } -// CondContext defines methods for the IoC container used by conditions. -type CondContext interface { +// ConditionContext defines methods for the IoC container used by conditions. +type ConditionContext interface { // Has checks whether the IoC container has a property with the given key. Has(key string) bool // Prop retrieves the value of a property from the IoC container. Prop(key string, def ...string) string // Find searches for bean definitions matching the given BeanSelector. - Find(s BeanSelector) ([]CondBean, error) + Find(s BeanSelector) ([]ConditionBean, error) } /************************************* arg ***********************************/ @@ -317,6 +317,7 @@ func NewRegisteredBean(d BeanRegistration) *RegisteredBean { } // BeanDefinition represents a bean that has not yet been registered. +// todo 等 Group 函数确定下来之后,BD 定义应该就不需要了。 type BeanDefinition struct { beanBuilder[BeanDefinition] } diff --git a/gs/internal/gs/gs_mock.go b/gs/internal/gs/gs_mock.go index e87c9fae..3327a360 100755 --- a/gs/internal/gs/gs_mock.go +++ b/gs/internal/gs/gs_mock.go @@ -1,6 +1,6 @@ -// Code generated by gs-mock v0.0.3. DO NOT EDIT. +// Code generated by gs-mock v0.0.4. DO NOT EDIT. // Source: https://github.com/go-spring/gs-mock -// gs mock -o gs_mock.go -i 'CondContext,ArgContext,Runner,Job,Server' +// gs mock -o gs_mock.go -i 'ConditionContext,ArgContext,Runner,Job,Server' package gs @@ -10,51 +10,51 @@ import ( "reflect" ) -type CondContextMockImpl struct { +type ConditionContextMockImpl struct { r *gsmock.Manager } -func NewCondContextMockImpl(r *gsmock.Manager) *CondContextMockImpl { - return &CondContextMockImpl{r: r} +func NewConditionContextMockImpl(r *gsmock.Manager) *ConditionContextMockImpl { + return &ConditionContextMockImpl{r: r} } -func (impl *CondContextMockImpl) Has(key string) bool { - t := reflect.TypeFor[CondContextMockImpl]() +func (impl *ConditionContextMockImpl) Has(key string) bool { + t := reflect.TypeFor[ConditionContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Has", key); ok { return gsmock.Unbox1[bool](ret) } panic("no mock code matched") } -func (impl *CondContextMockImpl) MockHas() *gsmock.Mocker11[string, bool] { - t := reflect.TypeFor[CondContextMockImpl]() +func (impl *ConditionContextMockImpl) MockHas() *gsmock.Mocker11[string, bool] { + t := reflect.TypeFor[ConditionContextMockImpl]() return gsmock.NewMocker11[string, bool](impl.r, t, "Has") } -func (impl *CondContextMockImpl) Prop(key string, def ...string) string { - t := reflect.TypeFor[CondContextMockImpl]() +func (impl *ConditionContextMockImpl) Prop(key string, def ...string) string { + t := reflect.TypeFor[ConditionContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Prop", key, def); ok { return gsmock.Unbox1[string](ret) } panic("no mock code matched") } -func (impl *CondContextMockImpl) MockProp() *gsmock.Mocker21[string, []string, string] { - t := reflect.TypeFor[CondContextMockImpl]() +func (impl *ConditionContextMockImpl) MockProp() *gsmock.Mocker21[string, []string, string] { + t := reflect.TypeFor[ConditionContextMockImpl]() return gsmock.NewMocker21[string, []string, string](impl.r, t, "Prop") } -func (impl *CondContextMockImpl) Find(s BeanSelector) ([]CondBean, error) { - t := reflect.TypeFor[CondContextMockImpl]() +func (impl *ConditionContextMockImpl) Find(s BeanSelector) ([]ConditionBean, error) { + t := reflect.TypeFor[ConditionContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Find", s); ok { - return gsmock.Unbox2[[]CondBean, error](ret) + return gsmock.Unbox2[[]ConditionBean, error](ret) } panic("no mock code matched") } -func (impl *CondContextMockImpl) MockFind() *gsmock.Mocker12[BeanSelector, []CondBean, error] { - t := reflect.TypeFor[CondContextMockImpl]() - return gsmock.NewMocker12[BeanSelector, []CondBean, error](impl.r, t, "Find") +func (impl *ConditionContextMockImpl) MockFind() *gsmock.Mocker12[BeanSelector, []ConditionBean, error] { + t := reflect.TypeFor[ConditionContextMockImpl]() + return gsmock.NewMocker12[BeanSelector, []ConditionBean, error](impl.r, t, "Find") } type ArgContextMockImpl struct { diff --git a/gs/internal/gs_app/app.go b/gs/internal/gs_app/app.go index 25f3a0a1..dac1a2c0 100644 --- a/gs/internal/gs_app/app.go +++ b/gs/internal/gs_app/app.go @@ -34,9 +34,6 @@ import ( "github.com/go-spring/spring-core/util/goutil" ) -// GS is the global application instance. -var GS = NewApp() - // App represents the core application, managing its lifecycle, // configuration, and dependency injection. type App struct { @@ -108,7 +105,7 @@ func (app *App) RunWith(fn func(ctx context.Context) error) error { // loading, IoC container refreshing, dependency injection, and runs // runners, jobs and servers. func (app *App) Start() error { - app.C.Object(app) + app.C.RootBean(app.C.Object(app)) // loads the layered app properties var p conf.Properties diff --git a/gs/internal/gs_app/app_test.go b/gs/internal/gs_app/app_test.go index 3edffb95..dc9055ec 100644 --- a/gs/internal/gs_app/app_test.go +++ b/gs/internal/gs_app/app_test.go @@ -78,7 +78,8 @@ func TestApp(t *testing.T) { Reset() t.Cleanup(Reset) - _ = gs_conf.SysConf.Set("a", "123") + fileID := gs_conf.SysConf.AddFile("app_test.go") + _ = gs_conf.SysConf.Set("a", "123", fileID) _ = os.Setenv("GS_A_B", "456") app := NewApp() err := app.Run() @@ -90,9 +91,9 @@ func TestApp(t *testing.T) { t.Cleanup(Reset) app := NewApp() - app.C.Provide(func() (*http.Server, error) { + app.C.RootBean(app.C.Provide(func() (*http.Server, error) { return nil, errors.New("fail to create bean") - }) + })) err := app.Run() assert.ThatError(t, err).Matches("fail to create bean") }) @@ -115,8 +116,9 @@ func TestApp(t *testing.T) { Reset() t.Cleanup(Reset) - _ = gs_conf.SysConf.Set("spring.app.enable-jobs", "false") - _ = gs_conf.SysConf.Set("spring.app.enable-servers", "false") + fileID := gs_conf.SysConf.AddFile("app_test.go") + _ = gs_conf.SysConf.Set("spring.app.enable-jobs", "false", fileID) + _ = gs_conf.SysConf.Set("spring.app.enable-servers", "false", fileID) app := NewApp() go func() { time.Sleep(50 * time.Millisecond) diff --git a/gs/internal/gs_app/boot.go b/gs/internal/gs_app/boot.go index 13c6c780..862484c8 100644 --- a/gs/internal/gs_app/boot.go +++ b/gs/internal/gs_app/boot.go @@ -71,6 +71,11 @@ func (b *BootImpl) Config() *gs_conf.BootConfig { return b.p } +// RootBean registers a root bean definition. +func (b *BootImpl) RootBean(x *gs.RegisteredBean) { + b.c.RootBean(x) +} + // Object registers an object bean. func (b *BootImpl) Object(i any) *gs.RegisteredBean { b.flag = true @@ -103,7 +108,7 @@ func (b *BootImpl) Run() error { if !b.flag { return nil } - b.c.Object(b) + b.c.RootBean(b.c.Object(b)) var p conf.Properties diff --git a/gs/internal/gs_app/boot_test.go b/gs/internal/gs_app/boot_test.go index a3573ce7..3edaa30a 100644 --- a/gs/internal/gs_app/boot_test.go +++ b/gs/internal/gs_app/boot_test.go @@ -34,7 +34,8 @@ func TestBoot(t *testing.T) { Reset() t.Cleanup(Reset) - _ = gs_conf.SysConf.Set("a", "123") + fileID := gs_conf.SysConf.AddFile("boot_test.go") + _ = gs_conf.SysConf.Set("a", "123", fileID) _ = os.Setenv("GS_A_B", "456") boot := NewBoot().(*BootImpl) err := boot.Run() @@ -45,7 +46,8 @@ func TestBoot(t *testing.T) { Reset() t.Cleanup(Reset) - _ = gs_conf.SysConf.Set("a", "123") + fileID := gs_conf.SysConf.AddFile("boot_test.go") + _ = gs_conf.SysConf.Set("a", "123", fileID) _ = os.Setenv("GS_A_B", "456") boot := NewBoot().(*BootImpl) boot.Object(bytes.NewBuffer(nil)) @@ -58,9 +60,9 @@ func TestBoot(t *testing.T) { t.Cleanup(Reset) boot := NewBoot().(*BootImpl) - boot.Provide(func() (*bytes.Buffer, error) { + boot.RootBean(boot.Provide(func() (*bytes.Buffer, error) { return nil, errors.New("fail to create bean") - }) + })) err := boot.Run() assert.ThatError(t, err).Matches("fail to create bean") }) diff --git a/gs/internal/gs_arg/arg_test.go b/gs/internal/gs_arg/arg_test.go index 3262d4cc..fea22374 100644 --- a/gs/internal/gs_arg/arg_test.go +++ b/gs/internal/gs_arg/arg_test.go @@ -590,7 +590,7 @@ func TestBindArg_GetArgValue(t *testing.T) { Value("test"), } arg := Bind(fn, args...) - arg.Condition(gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("condition error") })) @@ -614,7 +614,7 @@ func TestBindArg_GetArgValue(t *testing.T) { Value("test"), } arg := Bind(fn, args...) - arg.Condition(gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, nil })) @@ -639,7 +639,7 @@ func TestBindArg_GetArgValue(t *testing.T) { Value("test"), } arg := Bind(fn, args...) - arg.Condition(gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return true, nil })) diff --git a/gs/internal/gs_bean/bean.go b/gs/internal/gs_bean/bean.go index 40136a81..93c298ff 100644 --- a/gs/internal/gs_bean/bean.go +++ b/gs/internal/gs_bean/bean.go @@ -327,7 +327,7 @@ func (d *BeanDefinition) SetExport(exports ...reflect.Type) { // OnProfiles sets the conditions for the bean based on the active profiles. func (d *BeanDefinition) OnProfiles(profiles string) { - d.SetCondition(gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + d.SetCondition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { val := strings.TrimSpace(ctx.Prop("spring.profiles.active")) if val == "" { return false, nil diff --git a/gs/internal/gs_bean/bean_test.go b/gs/internal/gs_bean/bean_test.go index 310a9674..15e603c6 100644 --- a/gs/internal/gs_bean/bean_test.go +++ b/gs/internal/gs_bean/bean_test.go @@ -201,7 +201,7 @@ func TestBeanDefinition(t *testing.T) { t.Run("no profile property", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockProp().ReturnValue("") for _, c := range bean.Conditions() { @@ -213,7 +213,7 @@ func TestBeanDefinition(t *testing.T) { t.Run("profile property not match", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockProp().ReturnValue("prod") for _, c := range bean.Conditions() { @@ -225,7 +225,7 @@ func TestBeanDefinition(t *testing.T) { t.Run("profile property is dev", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockProp().ReturnValue("dev") for _, c := range bean.Conditions() { @@ -237,7 +237,7 @@ func TestBeanDefinition(t *testing.T) { t.Run("profile property is test", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockProp().ReturnValue("test") for _, c := range bean.Conditions() { @@ -249,7 +249,7 @@ func TestBeanDefinition(t *testing.T) { t.Run("profile property is dev&test", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockProp().ReturnValue("dev,test") for _, c := range bean.Conditions() { diff --git a/gs/internal/gs_cond/cond.go b/gs/internal/gs_cond/cond.go index eb834c67..8ae8dea5 100755 --- a/gs/internal/gs_cond/cond.go +++ b/gs/internal/gs_cond/cond.go @@ -60,7 +60,7 @@ offering runtime decision-making through various condition types. 4. Custom Conditions - Functional condition: - cond := OnFunc(func(ctx gs.CondContext) (bool, error) { + cond := OnFunc(func(ctx gs.ConditionContext) (bool, error) { return time.Now().Hour() > 9, nil }) @@ -83,16 +83,16 @@ import ( // onFunc is an implementation of [gs.Condition] that wraps a function. // It allows a condition to be evaluated based on the result of a function. type onFunc struct { - fn func(ctx gs.CondContext) (bool, error) + fn func(ctx gs.ConditionContext) (bool, error) } // OnFunc creates a Conditional that evaluates using a custom function. -func OnFunc(fn func(ctx gs.CondContext) (bool, error)) gs.Condition { +func OnFunc(fn func(ctx gs.ConditionContext) (bool, error)) gs.Condition { return &onFunc{fn: fn} } // Matches checks if the condition is met according to the provided context. -func (c *onFunc) Matches(ctx gs.CondContext) (bool, error) { +func (c *onFunc) Matches(ctx gs.ConditionContext) (bool, error) { ok, err := c.fn(ctx) if err != nil { return false, errutil.WrapError(err, "condition matches error: %s", c) @@ -107,12 +107,12 @@ func (c *onFunc) String() string { /******************************* OnProperty **********************************/ -// OnPropertyInterface defines the methods for evaluating a condition based on a property. +// ConditionOnProperty defines the methods for evaluating a condition based on a property. // This interface provides flexibility for matching missing properties and checking their values. -type OnPropertyInterface interface { +type ConditionOnProperty interface { gs.Condition - MatchIfMissing() OnPropertyInterface - HavingValue(s string) OnPropertyInterface + MatchIfMissing() ConditionOnProperty + HavingValue(s string) ConditionOnProperty } // onProperty evaluates a condition based on the existence and value of a property @@ -125,24 +125,24 @@ type onProperty struct { } // OnProperty creates a condition based on the presence and value of a specified property. -func OnProperty(name string) OnPropertyInterface { +func OnProperty(name string) ConditionOnProperty { return &onProperty{name: name} } // MatchIfMissing sets the condition to match if the property is missing. -func (c *onProperty) MatchIfMissing() OnPropertyInterface { +func (c *onProperty) MatchIfMissing() ConditionOnProperty { c.matchIfMissing = true return c } // HavingValue sets the expected value or expression to match. -func (c *onProperty) HavingValue(s string) OnPropertyInterface { +func (c *onProperty) HavingValue(s string) ConditionOnProperty { c.havingValue = s return c } // Matches checks if the condition is met according to the provided context. -func (c *onProperty) Matches(ctx gs.CondContext) (bool, error) { +func (c *onProperty) Matches(ctx gs.ConditionContext) (bool, error) { // If the context doesn't have the property, handle accordingly. if !ctx.Has(c.name) { @@ -185,28 +185,6 @@ func (c *onProperty) String() string { return sb.String() } -/*************************** OnMissingProperty *******************************/ - -// onMissingProperty is a condition that matches when a specified property is -// absent from the context. -type onMissingProperty struct { - name string // The name of the property to check for absence. -} - -// OnMissingProperty creates a condition that matches if the specified property is missing. -func OnMissingProperty(name string) gs.Condition { - return &onMissingProperty{name: name} -} - -// Matches checks if the condition is met according to the provided context. -func (c *onMissingProperty) Matches(ctx gs.CondContext) (bool, error) { - return !ctx.Has(c.name), nil -} - -func (c *onMissingProperty) String() string { - return fmt.Sprintf("OnMissingProperty(name=%s)", c.name) -} - /********************************* OnBean ************************************/ // onBean checks for the existence of beans that match a selector. @@ -228,7 +206,7 @@ func OnBeanSelector(s gs.BeanSelector) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (c *onBean) Matches(ctx gs.CondContext) (bool, error) { +func (c *onBean) Matches(ctx gs.ConditionContext) (bool, error) { beans, err := ctx.Find(c.s) if err != nil { return false, errutil.WrapError(err, "condition matches error: %s", c) @@ -261,7 +239,7 @@ func OnMissingBeanSelector(s gs.BeanSelector) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (c *onMissingBean) Matches(ctx gs.CondContext) (bool, error) { +func (c *onMissingBean) Matches(ctx gs.ConditionContext) (bool, error) { beans, err := ctx.Find(c.s) if err != nil { return false, errutil.WrapError(err, "condition matches error: %s", c) @@ -294,7 +272,7 @@ func OnSingleBeanSelector(s gs.BeanSelector) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (c *onSingleBean) Matches(ctx gs.CondContext) (bool, error) { +func (c *onSingleBean) Matches(ctx gs.ConditionContext) (bool, error) { beans, err := ctx.Find(c.s) if err != nil { return false, errutil.WrapError(err, "condition matches error: %s", c) @@ -321,7 +299,7 @@ func OnExpression(expression string) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (c *onExpression) Matches(ctx gs.CondContext) (bool, error) { +func (c *onExpression) Matches(ctx gs.ConditionContext) (bool, error) { err := util.ErrUnimplementedMethod return false, errutil.WrapError(err, "condition matches error: %s", c) } @@ -344,7 +322,7 @@ func Not(c gs.Condition) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (c *onNot) Matches(ctx gs.CondContext) (bool, error) { +func (c *onNot) Matches(ctx gs.ConditionContext) (bool, error) { ok, err := c.c.Matches(ctx) if err != nil { return false, errutil.WrapError(err, "condition matches error: %s", c) @@ -376,7 +354,7 @@ func Or(conditions ...gs.Condition) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (g *onOr) Matches(ctx gs.CondContext) (bool, error) { +func (g *onOr) Matches(ctx gs.ConditionContext) (bool, error) { for _, c := range g.conditions { if ok, err := c.Matches(ctx); err != nil { return false, errutil.WrapError(err, "condition matches error: %s", g) @@ -411,7 +389,7 @@ func And(conditions ...gs.Condition) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (g *onAnd) Matches(ctx gs.CondContext) (bool, error) { +func (g *onAnd) Matches(ctx gs.ConditionContext) (bool, error) { for _, c := range g.conditions { ok, err := c.Matches(ctx) if err != nil { @@ -447,7 +425,7 @@ func None(conditions ...gs.Condition) gs.Condition { } // Matches checks if the condition is met according to the provided context. -func (g *onNone) Matches(ctx gs.CondContext) (bool, error) { +func (g *onNone) Matches(ctx gs.ConditionContext) (bool, error) { for _, c := range g.conditions { if ok, err := c.Matches(ctx); err != nil { return false, errutil.WrapError(err, "condition matches error: %s", g) diff --git a/gs/internal/gs_cond/cond_test.go b/gs/internal/gs_cond/cond_test.go index 41a5d348..996eaf7d 100644 --- a/gs/internal/gs_cond/cond_test.go +++ b/gs/internal/gs_cond/cond_test.go @@ -28,13 +28,13 @@ import ( ) var ( - trueCond = OnFunc(func(ctx gs.CondContext) (bool, error) { return true, nil }) - falseCond = OnFunc(func(ctx gs.CondContext) (bool, error) { return false, nil }) + trueCond = OnFunc(func(ctx gs.ConditionContext) (bool, error) { return true, nil }) + falseCond = OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, nil }) ) func TestConditionString(t *testing.T) { - c := OnFunc(func(ctx gs.CondContext) (bool, error) { return false, nil }) + c := OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, nil }) assert.That(t, fmt.Sprint(c)).Equal(`OnFunc(fn=gs_cond.TestConditionString.func1)`) c = OnProperty("a").HavingValue("123") @@ -43,9 +43,6 @@ func TestConditionString(t *testing.T) { c = OnProperty("a").HavingValue("123").MatchIfMissing() assert.That(t, fmt.Sprint(c)).Equal(`OnProperty(name=a, havingValue=123, matchIfMissing)`) - c = OnMissingProperty("a") - assert.That(t, fmt.Sprint(c)).Equal(`OnMissingProperty(name=a)`) - c = OnBean[any]("a") assert.That(t, fmt.Sprint(c)).Equal(`OnBean(selector={Name:a})`) @@ -104,7 +101,7 @@ func TestConditionString(t *testing.T) { func TestOnFunc(t *testing.T) { t.Run("success", func(t *testing.T) { - fn := func(ctx gs.CondContext) (bool, error) { return true, nil } + fn := func(ctx gs.ConditionContext) (bool, error) { return true, nil } cond := OnFunc(fn) ok, err := cond.Matches(nil) assert.That(t, ok).True() @@ -112,7 +109,7 @@ func TestOnFunc(t *testing.T) { }) t.Run("error", func(t *testing.T) { - fn := func(ctx gs.CondContext) (bool, error) { return false, errors.New("test error") } + fn := func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("test error") } cond := OnFunc(fn) _, err := cond.Matches(nil) assert.ThatError(t, err).Matches("test error") @@ -123,7 +120,7 @@ func TestOnProperty(t *testing.T) { t.Run("property exist", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) cond := OnProperty("test.prop") @@ -134,7 +131,7 @@ func TestOnProperty(t *testing.T) { t.Run("property exist and match", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) ctx.MockProp().ReturnValue("42") @@ -146,7 +143,7 @@ func TestOnProperty(t *testing.T) { t.Run("property exist but not match", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) ctx.MockProp().ReturnValue("42") @@ -157,7 +154,7 @@ func TestOnProperty(t *testing.T) { t.Run("property not exist but MatchIfMissing", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(false) cond := OnProperty("missing.prop").MatchIfMissing() @@ -169,7 +166,7 @@ func TestOnProperty(t *testing.T) { t.Run("number expression", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) ctx.MockProp().ReturnValue("42") @@ -180,7 +177,7 @@ func TestOnProperty(t *testing.T) { t.Run("string expression", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) ctx.MockProp().ReturnValue("42") @@ -191,7 +188,7 @@ func TestOnProperty(t *testing.T) { t.Run("invalid expression", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockHas().ReturnValue(true) ctx.MockProp().ReturnValue("42") @@ -202,35 +199,12 @@ func TestOnProperty(t *testing.T) { }) } -func TestOnMissingProperty(t *testing.T) { - - t.Run("property exist", func(t *testing.T) { - m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockHas().ReturnValue(true) - - cond := OnMissingProperty("existing") - ok, _ := cond.Matches(ctx) - assert.That(t, ok).False() - }) - - t.Run("property not exist", func(t *testing.T) { - m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockHas().ReturnValue(false) - - cond := OnMissingProperty("missing") - ok, _ := cond.Matches(ctx) - assert.That(t, ok).True() - }) -} - func TestOnBean(t *testing.T) { t.Run("found bean", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockFind().ReturnValue([]gs.CondBean{nil}, nil) + ctx := gs.NewConditionContextMockImpl(m) + ctx.MockFind().ReturnValue([]gs.ConditionBean{nil}, nil) cond := OnBean[any]("b") ok, err := cond.Matches(ctx) @@ -240,7 +214,7 @@ func TestOnBean(t *testing.T) { t.Run("not found bean", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, nil) cond := OnBean[any]("b") @@ -251,7 +225,7 @@ func TestOnBean(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnBean[any]("b") @@ -265,7 +239,7 @@ func TestOnMissingBean(t *testing.T) { t.Run("not found bean", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, nil) cond := OnMissingBean[any]("bean1") @@ -276,8 +250,8 @@ func TestOnMissingBean(t *testing.T) { t.Run("found bean", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockFind().ReturnValue([]gs.CondBean{nil}, nil) + ctx := gs.NewConditionContextMockImpl(m) + ctx.MockFind().ReturnValue([]gs.ConditionBean{nil}, nil) cond := OnMissingBean[any]("bean1") ok, err := cond.Matches(ctx) @@ -287,7 +261,7 @@ func TestOnMissingBean(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnMissingBean[any]("b") @@ -301,8 +275,8 @@ func TestOnSingleBean(t *testing.T) { t.Run("found only one bean", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockFind().ReturnValue([]gs.CondBean{nil}, nil) + ctx := gs.NewConditionContextMockImpl(m) + ctx.MockFind().ReturnValue([]gs.ConditionBean{nil}, nil) cond := OnSingleBean[any]("b") ok, _ := cond.Matches(ctx) @@ -311,8 +285,8 @@ func TestOnSingleBean(t *testing.T) { t.Run("found two beans", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) - ctx.MockFind().ReturnValue([]gs.CondBean{nil, nil}, nil) + ctx := gs.NewConditionContextMockImpl(m) + ctx.MockFind().ReturnValue([]gs.ConditionBean{nil, nil}, nil) cond := OnSingleBean[any]("b") ok, _ := cond.Matches(ctx) @@ -321,7 +295,7 @@ func TestOnSingleBean(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") @@ -333,7 +307,7 @@ func TestOnSingleBean(t *testing.T) { func TestOnExpression(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) cond := OnExpression("1+1==2") _, err := cond.Matches(ctx) @@ -358,7 +332,7 @@ func TestNot(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") @@ -396,7 +370,7 @@ func TestAnd(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") @@ -434,7 +408,7 @@ func TestOr(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") @@ -474,7 +448,7 @@ func TestNone(t *testing.T) { t.Run("return error", func(t *testing.T) { m := gsmock.NewManager() - ctx := gs.NewCondContextMockImpl(m) + ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") diff --git a/gs/internal/gs_conf/cmd.go b/gs/internal/gs_conf/cmd.go index 1393a72b..b2332774 100644 --- a/gs/internal/gs_conf/cmd.go +++ b/gs/internal/gs_conf/cmd.go @@ -40,11 +40,13 @@ func NewCommandArgs() *CommandArgs { // CopyTo processes command-line parameters and sets them as key-value pairs // in the provided conf.Properties. Parameters should be passed in the form // of `-D key[=value/true]`. -func (c *CommandArgs) CopyTo(out *conf.MutableProperties) error { +func (c *CommandArgs) CopyTo(p *conf.MutableProperties) error { if len(os.Args) == 0 { return nil } + fileID := p.AddFile("Args") + // Default option prefix is "-D", but it can be overridden by the // environment variable `GS_ARGS_PREFIX`. option := "-D" @@ -64,7 +66,7 @@ func (c *CommandArgs) CopyTo(out *conf.MutableProperties) error { if len(ss) == 1 { ss = append(ss, "true") } - if err := out.Set(ss[0], ss[1]); err != nil { + if err := p.Set(ss[0], ss[1], fileID); err != nil { return err } } diff --git a/gs/internal/gs_conf/conf_test.go b/gs/internal/gs_conf/conf_test.go index 38369264..6837b60b 100644 --- a/gs/internal/gs_conf/conf_test.go +++ b/gs/internal/gs_conf/conf_test.go @@ -73,7 +73,8 @@ func TestAppConfig(t *testing.T) { t.Run("merge error - 2", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf") - _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080") + fileID := SysConf.AddFile("conf_test.go") + _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080", fileID) _, err := NewAppConfig().Refresh() assert.ThatError(t, err).Matches("property conflict at path http.server.addr") }) @@ -112,7 +113,8 @@ func TestBootConfig(t *testing.T) { t.Run("merge error - 2", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf") - _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080") + fileID := SysConf.AddFile("conf_test.go") + _ = SysConf.Set("http.server[0].addr", "0.0.0.0:8080", fileID) _, err := NewBootConfig().Refresh() assert.ThatError(t, err).Matches("property conflict at path http.server.addr") }) diff --git a/gs/internal/gs_conf/env.go b/gs/internal/gs_conf/env.go index ccd8ccb8..b1200d51 100644 --- a/gs/internal/gs_conf/env.go +++ b/gs/internal/gs_conf/env.go @@ -39,6 +39,7 @@ func (c *Environment) CopyTo(p *conf.MutableProperties) error { return nil } const prefix = "GS_" + fileID := p.AddFile("Environment") for _, env := range environ { ss := strings.SplitN(env, "=", 2) k, v := ss[0], "" @@ -56,7 +57,7 @@ func (c *Environment) CopyTo(p *conf.MutableProperties) error { } else { propKey = k } - if err := p.Set(propKey, v); err != nil { + if err := p.Set(propKey, v, fileID); err != nil { return err } } diff --git a/gs/internal/gs_core/core.go b/gs/internal/gs_core/core.go index bf530cc0..3dee7bcd 100755 --- a/gs/internal/gs_core/core.go +++ b/gs/internal/gs_core/core.go @@ -43,7 +43,7 @@ func (c *Container) Refresh(p conf.Properties) error { return err } c.Injecting = injecting.New(p) - if err := c.Injecting.Refresh(c.Beans()); err != nil { + if err := c.Injecting.Refresh(c.Roots(), c.Beans()); err != nil { return err } c.Resolving = nil diff --git a/gs/internal/gs_core/core_test.go b/gs/internal/gs_core/core_test.go index 151a28e7..9f0ac17f 100644 --- a/gs/internal/gs_core/core_test.go +++ b/gs/internal/gs_core/core_test.go @@ -40,7 +40,7 @@ func TestContainer(t *testing.T) { t.Run("resolve error", func(t *testing.T) { c := New() c.Object(&http.Server{}).Condition( - gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("condition error") }), ) @@ -50,7 +50,7 @@ func TestContainer(t *testing.T) { t.Run("inject error", func(t *testing.T) { c := New() - c.Provide(func(addr string) *http.Server { return nil }) + c.RootBean(c.Provide(func(addr string) *http.Server { return nil })) err := c.Refresh(conf.New()) assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") }) diff --git a/gs/internal/gs_core/injecting/injecting.go b/gs/internal/gs_core/injecting/injecting.go index e7114069..669c0355 100644 --- a/gs/internal/gs_core/injecting/injecting.go +++ b/gs/internal/gs_core/injecting/injecting.go @@ -80,7 +80,7 @@ func (c *Injecting) RefreshProperties(p conf.Properties) error { } // Refresh loads and wires all provided bean definitions. -func (c *Injecting) Refresh(beans []*gs_bean.BeanDefinition) (err error) { +func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) { allowCircularReferences := cast.ToBool(c.p.Data().Get("spring.allow-circular-references")) forceAutowireIsNullable := cast.ToBool(c.p.Data().Get("spring.force-autowire-is-nullable")) @@ -113,7 +113,7 @@ func (c *Injecting) Refresh(beans []*gs_bean.BeanDefinition) (err error) { // Injects all beans r.state = Refreshing - for _, b := range beans { + for _, b := range roots { if err = r.wireBean(b, stack); err != nil { return err } @@ -853,9 +853,9 @@ func (a *ArgContext) Prop(key string, def ...string) string { } // Find returns beans satisfying a selector, as conditional beans. -func (a *ArgContext) Find(s gs.BeanSelector) ([]gs.CondBean, error) { +func (a *ArgContext) Find(s gs.BeanSelector) ([]gs.ConditionBean, error) { beans := a.c.findBeans(s) - var ret []gs.CondBean + var ret []gs.ConditionBean for _, bean := range beans { ret = append(ret, bean) } diff --git a/gs/internal/gs_core/injecting/injecting_test.go b/gs/internal/gs_core/injecting/injecting_test.go index 93283367..8bdcfc89 100644 --- a/gs/internal/gs_core/injecting/injecting_test.go +++ b/gs/internal/gs_core/injecting/injecting_test.go @@ -198,12 +198,12 @@ func provideBean(ctor any, args ...gs.Arg) *gs.BeanDefinition { return gs_bean.NewBean(ctor, args...) } -func extractBeans(beans []*gs.BeanDefinition) []*gs_bean.BeanDefinition { +func extractBeans(beans []*gs.BeanDefinition) (_, _ []*gs_bean.BeanDefinition) { var ret []*gs_bean.BeanDefinition for _, b := range beans { ret = append(ret, b.BeanRegistration().(*gs_bean.BeanDefinition)) } - return ret + return ret, ret } type LazyA struct { diff --git a/gs/internal/gs_core/resolving/resolving.go b/gs/internal/gs_core/resolving/resolving.go index fa1117aa..55b85d9a 100644 --- a/gs/internal/gs_core/resolving/resolving.go +++ b/gs/internal/gs_core/resolving/resolving.go @@ -35,20 +35,24 @@ type RefreshState int const ( RefreshDefault = RefreshState(iota) + RefreshPrepare Refreshing Refreshed ) -// BeanGroupFunc defines a function that dynamically registers beans -// based on configuration properties. -type BeanGroupFunc func(p conf.Properties) ([]*gs.BeanDefinition, error) +// Module represents a module registered in the container. +type Module struct { + f func(p conf.Properties) error + c gs.Condition +} // Resolving manages bean definitions, mocks, and dynamic bean registration functions. type Resolving struct { - state RefreshState // Current refresh state - mocks []gs.BeanMock // Registered mock beans - beans []*gs_bean.BeanDefinition // Managed bean definitions - funcs []BeanGroupFunc // Dynamic bean registration functions + state RefreshState // Current refresh state + mocks []gs.BeanMock // Registered mock beans + beans []*gs_bean.BeanDefinition // Managed bean definitions + roots []*gs_bean.BeanDefinition // Root beans + modules []Module } // New creates an empty Resolving instance. @@ -56,6 +60,11 @@ func New() *Resolving { return &Resolving{} } +// Roots returns all root beans. +func (c *Resolving) Roots() []*gs_bean.BeanDefinition { + return c.roots +} + // Beans returns all active bean definitions, excluding deleted ones. func (c *Resolving) Beans() []*gs_bean.BeanDefinition { var beans []*gs_bean.BeanDefinition @@ -95,9 +104,22 @@ func (c *Resolving) Register(b *gs.BeanDefinition) *gs.RegisteredBean { return gs.NewRegisteredBean(bd) } -// GroupRegister adds a function to dynamically register beans. -func (c *Resolving) GroupRegister(fn BeanGroupFunc) { - c.funcs = append(c.funcs, fn) +// Module registers a module into the container. +func (c *Resolving) Module(conditions []gs_cond.ConditionOnProperty, fn func(p conf.Properties) error) { + var arr []gs.Condition + for _, cond := range conditions { + arr = append(arr, cond) + } + c.modules = append(c.modules, Module{ + f: fn, + c: gs_cond.And(arr...), + }) +} + +// RootBean adds a root bean to the container. +func (c *Resolving) RootBean(b *gs.RegisteredBean) { + bd := b.BeanRegistration().(*gs_bean.BeanDefinition) + c.roots = append(c.roots, bd) } // Refresh performs the full initialization process of the container. @@ -111,12 +133,14 @@ func (c *Resolving) Refresh(p conf.Properties) error { if c.state != RefreshDefault { return errors.New("container is already refreshing or refreshed") } - c.state = Refreshing + c.state = RefreshPrepare - if err := c.applyGroupFuncs(p); err != nil { + if err := c.applyModules(p); err != nil { return err } + c.state = Refreshing + if err := c.scanConfigurations(); err != nil { return err } @@ -133,20 +157,32 @@ func (c *Resolving) Refresh(p conf.Properties) error { return err } + for _, b := range c.roots { + if b.Status() == gs_bean.StatusDeleted { + continue + } + if b.Status() != gs_bean.StatusResolved { + return fmt.Errorf("bean %q status is invalid for wiring", b) + } + } + c.state = Refreshed return nil } -// applyGroupFuncs executes registered group functions to add dynamic beans. -func (c *Resolving) applyGroupFuncs(p conf.Properties) error { - for _, fn := range c.funcs { - beans, err := fn(p) - if err != nil { - return err +// applyModules executes registered modules to add beans. +func (c *Resolving) applyModules(p conf.Properties) error { + ctx := &ConditionContext{p: p, c: c} + for _, m := range c.modules { + if m.c != nil { + if ok, err := m.c.Matches(ctx); err != nil { + return err + } else if !ok { + continue + } } - for _, b := range beans { - d := b.BeanRegistration().(*gs_bean.BeanDefinition) - c.beans = append(c.beans, d) + if err := m.f(p); err != nil { + return err } } return nil @@ -305,7 +341,7 @@ func (c *Resolving) applyMock(mock gs.BeanMock) error { // resolveBeans evaluates conditions for all beans and marks inactive ones. func (c *Resolving) resolveBeans(p conf.Properties) error { - ctx := &CondContext{p: p, c: c} + ctx := &ConditionContext{p: p, c: c} for _, b := range c.beans { if err := ctx.resolveBean(b); err != nil { return err @@ -332,15 +368,15 @@ func (c *Resolving) checkDuplicateBeans() error { return nil } -// CondContext provides condition evaluation context during resolution. -type CondContext struct { +// ConditionContext provides condition evaluation context during resolution. +type ConditionContext struct { c *Resolving p conf.Properties } // resolveBean evaluates a bean's conditions, updating its status accordingly. // If any condition fails, the bean is marked as deleted. -func (c *CondContext) resolveBean(b *gs_bean.BeanDefinition) error { +func (c *ConditionContext) resolveBean(b *gs_bean.BeanDefinition) error { if b.Status() >= gs_bean.StatusResolving { return nil } @@ -358,18 +394,18 @@ func (c *CondContext) resolveBean(b *gs_bean.BeanDefinition) error { } // Has checks if a configuration property exists. -func (c *CondContext) Has(key string) bool { +func (c *ConditionContext) Has(key string) bool { return c.p.Has(key) } // Prop retrieves a configuration property with optional default value. -func (c *CondContext) Prop(key string, def ...string) string { +func (c *ConditionContext) Prop(key string, def ...string) string { return c.p.Get(key, def...) } // Find returns beans matching the selector after resolving their conditions. -func (c *CondContext) Find(s gs.BeanSelector) ([]gs.CondBean, error) { - var found []gs.CondBean +func (c *ConditionContext) Find(s gs.BeanSelector) ([]gs.ConditionBean, error) { + var found []gs.ConditionBean t, name := s.TypeAndName() for _, b := range c.c.beans { if b.Status() == gs_bean.StatusResolving || b.Status() == gs_bean.StatusDeleted { diff --git a/gs/internal/gs_core/resolving/resolving_test.go b/gs/internal/gs_core/resolving/resolving_test.go index 7e17a565..ca2ac709 100644 --- a/gs/internal/gs_core/resolving/resolving_test.go +++ b/gs/internal/gs_core/resolving/resolving_test.go @@ -28,7 +28,6 @@ import ( "github.com/go-spring/spring-core/conf" "github.com/go-spring/spring-core/gs/internal/gs" "github.com/go-spring/spring-core/gs/internal/gs_arg" - "github.com/go-spring/spring-core/gs/internal/gs_bean" "github.com/go-spring/spring-core/gs/internal/gs_cond" ) @@ -91,15 +90,6 @@ func TestResolving(t *testing.T) { }, "container is refreshing or already refreshed") }) - t.Run("group error", func(t *testing.T) { - r := New() - r.GroupRegister(func(p conf.Properties) ([]*gs.BeanDefinition, error) { - return nil, fmt.Errorf("group error") - }) - err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("group error") - }) - t.Run("configuration error - 1", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Configuration() @@ -164,7 +154,7 @@ func TestResolving(t *testing.T) { t.Run("resolve error - 1", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Condition( - gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("condition error") }), ) @@ -178,7 +168,7 @@ func TestResolving(t *testing.T) { gs_cond.OnBean[*TestBean](), ) r.Object(&TestBean{Value: 1}).Condition( - gs_cond.OnFunc(func(ctx gs.CondContext) (bool, error) { + gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("condition error") }), ) @@ -213,19 +203,18 @@ func TestResolving(t *testing.T) { t.Run("success", func(t *testing.T) { r := New() { - r.GroupRegister(func(p conf.Properties) (beans []*gs.BeanDefinition, err error) { + r.Module(nil, func(p conf.Properties) error { keys, err := p.SubKeys("logger") if err != nil { - return nil, err + return err } for _, name := range keys { arg := gs_arg.Tag(fmt.Sprintf("logger.%s", name)) - l := gs_bean.NewBean(NewZeroLogger, arg). + r.Provide(NewZeroLogger, arg). Export(gs.As[Logger](), gs.As[CtxLogger]()). Name(name) - beans = append(beans, l) } - return + return nil }) r.Provide(NewLogger, gs_arg.Value("c")).Name("c") r.AddMock(gs.BeanMock{ diff --git a/gs/internal/gs_dync/dync.go b/gs/internal/gs_dync/dync.go index 5b1ab8bd..40870638 100644 --- a/gs/internal/gs_dync/dync.go +++ b/gs/internal/gs_dync/dync.go @@ -67,8 +67,8 @@ import ( "sync" "sync/atomic" + "github.com/go-spring/barky" "github.com/go-spring/spring-core/conf" - "github.com/go-spring/spring-core/util" ) // refreshable represents an object that can be dynamically refreshed. @@ -209,7 +209,7 @@ func (p *Properties) Refresh(prop conf.Properties) (err error) { changes[k] = struct{}{} } - keys := util.OrderedMapKeys(changes) + keys := barky.OrderedMapKeys(changes) return p.refreshKeys(keys) } @@ -233,7 +233,7 @@ func (p *Properties) refreshKeys(keys []string) (err error) { // Sort and collect objects that need updating. updateObjects := make([]*refreshObject, 0, len(updateIndexes)) { - ints := util.OrderedMapKeys(updateIndexes) + ints := barky.OrderedMapKeys(updateIndexes) for _, k := range ints { updateObjects = append(updateObjects, updateIndexes[k]) } diff --git a/gs/log.go b/gs/log.go index 1ab2ec6f..bf67c302 100644 --- a/gs/log.go +++ b/gs/log.go @@ -61,5 +61,8 @@ func initLog() error { if logFile == "" { // no log file exists return nil } - return log.RefreshFile(logFile) + if err = log.RefreshFile(logFile); err != nil { + return err + } + return nil } diff --git a/gs/pprof.go b/gs/pprof.go index edc84df4..74726034 100644 --- a/gs/pprof.go +++ b/gs/pprof.go @@ -27,7 +27,7 @@ func init() { NewSimplePProfServer, TagArg("${pprof.server.addr:=0.0.0.0:9981}"), ).Condition( - OnProperty(EnableServersProp).HavingValue("true").MatchIfMissing(), + OnEnableServers(), OnProperty(EnableSimplePProfServerProp).HavingValue("true").MatchIfMissing(), ).AsServer() } diff --git a/gs/test.go b/gs/test.go new file mode 100644 index 00000000..1c6491b9 --- /dev/null +++ b/gs/test.go @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Go-Spring Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gs + +import ( + "reflect" + "strings" + "testing" + + "github.com/go-spring/spring-core/gs/internal/gs" + "github.com/go-spring/spring-core/util" +) + +// BeanMock is a mock for bean. +type BeanMock[T any] struct { + selector gs.BeanSelector +} + +// MockFor creates a mock for bean. +func MockFor[T any](name ...string) BeanMock[T] { + return BeanMock[T]{ + selector: gs.BeanSelectorFor[T](name...), + } +} + +// With registers a mock bean. +func (m BeanMock[T]) With(obj T) { + app.C.AddMock(gs.BeanMock{ + Object: obj, + Target: m.selector, + }) +} + +var testers []any + +// AddTester adds a tester to the test suite. +func AddTester(t any) { + testers = append(testers, t) + app.C.RootBean(app.C.Object(t)) +} + +// TestMain is the entry point for testing. +func TestMain(m *testing.M) { + + // patch m.tests + mValue := util.PatchValue(reflect.ValueOf(m)) + fValue := util.PatchValue(mValue.Elem().FieldByName("tests")) + tests := fValue.Interface().([]testing.InternalTest) + for _, tester := range testers { + tt := reflect.TypeOf(tester) + typeName := tt.Elem().String() + for i := range tt.NumMethod() { + methodType := tt.Method(i) + if strings.HasPrefix(methodType.Name, "Test") { + tests = append(tests, testing.InternalTest{ + Name: typeName + "." + methodType.Name, + F: func(t *testing.T) { + testMethod := reflect.ValueOf(tester).Method(i) + testMethod.Call([]reflect.Value{reflect.ValueOf(t)}) + }, + }) + } + } + } + fValue.Set(reflect.ValueOf(tests)) + + // run app + stop, err := RunAsync() + if err != nil { + panic(err) + } + + // run test + m.Run() + + // stop app + stop() +} diff --git a/util/flat.go b/util/flat.go deleted file mode 100644 index 3aa7302c..00000000 --- a/util/flat.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package util - -import ( - "fmt" - "reflect" - - "github.com/spf13/cast" -) - -// FlattenMap flattens a nested map, array, or slice into a single-level map -// with string keys and string values. It recursively processes each element -// of the input map and adds its flattened representation to the result map. -func FlattenMap(m map[string]any) map[string]string { - result := make(map[string]string) - for key, val := range m { - FlattenValue(key, val, result) - } - return result -} - -// FlattenValue flattens a single value (which can be a map, array, slice, -// or other types) into the result map. -func FlattenValue(key string, val any, result map[string]string) { - if val == nil { - return - } - switch v := reflect.ValueOf(val); v.Kind() { - case reflect.Map: - if v.Len() == 0 { - result[key] = "" - return - } - iter := v.MapRange() - for iter.Next() { - mapKey := cast.ToString(iter.Key().Interface()) - mapValue := iter.Value().Interface() - FlattenValue(key+"."+mapKey, mapValue, result) - } - case reflect.Array, reflect.Slice: - if v.Len() == 0 { - result[key] = "" - return - } - for i := range v.Len() { - subKey := fmt.Sprintf("%s[%d]", key, i) - subValue := v.Index(i).Interface() - // If an element is nil, treat it as an empty value and assign an empty string. - // Note: We do not remove the nil element to avoid changing the array's size. - if subValue == nil { - result[subKey] = "" - continue - } - FlattenValue(subKey, subValue, result) - } - default: - result[key] = cast.ToString(val) - } -} diff --git a/util/flat_test.go b/util/flat_test.go deleted file mode 100644 index 90aa5f02..00000000 --- a/util/flat_test.go +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2024 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package util_test - -import ( - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func TestFlatten(t *testing.T) { - m := util.FlattenMap(map[string]any{ - "int": 123, - "str": "abc", - "arr": []any{ - "abc", - "def", - map[string]any{ - "a": "123", - "b": "456", - }, - nil, - ([]any)(nil), // it doesn't equal to nil - (map[string]string)(nil), // it doesn't equal to nil - []any{}, - map[string]string{}, - }, - "map": map[string]any{ - "a": "123", - "b": "456", - "arr": []string{ - "abc", - "def", - }, - "nil": nil, - "nil_arr": []any(nil), // it doesn't equal to nil - "nil_map": map[string]string(nil), // it doesn't equal to nil - "empty_arr": []any{}, - "empty_map": map[string]string{}, - }, - "nil": nil, - "nil_arr": []any(nil), // it doesn't equal to nil - "nil_map": map[string]string(nil), // it doesn't equal to nil - "empty_arr": []any{}, - "empty_map": map[string]string{}, - }) - expect := map[string]string{ - "int": "123", - "str": "abc", - "nil_arr": "", - "nil_map": "", - "empty_arr": "", - "empty_map": "", - "map.a": "123", - "map.b": "456", - "map.arr[0]": "abc", - "map.arr[1]": "def", - "map.nil_arr": "", - "map.nil_map": "", - "map.empty_arr": "", - "map.empty_map": "", - "arr[0]": "abc", - "arr[1]": "def", - "arr[2].a": "123", - "arr[2].b": "456", - "arr[3]": "", - "arr[4]": "", - "arr[5]": "", - "arr[6]": "", - "arr[7]": "", - } - assert.That(t, m).Equal(expect) -} diff --git a/util/map.go b/util/map.go deleted file mode 100644 index d32c25c2..00000000 --- a/util/map.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2025 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package util - -import ( - "cmp" - "slices" -) - -// MapKeys returns the keys of the map m. -func MapKeys[M ~map[K]V, K comparable, V any](m M) []K { - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -} - -// OrderedMapKeys returns the keys of the map m in sorted order. -func OrderedMapKeys[M ~map[K]V, K cmp.Ordered, V any](m M) []K { - r := MapKeys(m) - slices.Sort(r) - return r -} diff --git a/util/map_test.go b/util/map_test.go deleted file mode 100644 index bcad73e9..00000000 --- a/util/map_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2025 The Go-Spring Authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package util_test - -import ( - "maps" - "slices" - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func BenchmarkOrderedMapKeys(b *testing.B) { - m := map[string]string{ - "a": "1", - "b": "2", - "c": "3", - "d": "4", - "e": "5", - "f": "6", - "g": "7", - "h": "8", - "i": "9", - "j": "10", - "k": "11", - "l": "12", - "m": "13", - "n": "14", - "o": "15", - "p": "16", - "q": "17", - "r": "18", - "s": "19", - "t": "20", - "u": "21", - "v": "22", - "w": "23", - "x": "24", - "y": "25", - "z": "26", - } - b.Run("std", func(b *testing.B) { - for b.Loop() { - slices.Sorted(maps.Keys(m)) - } - }) - b.Run("util", func(b *testing.B) { - for b.Loop() { - util.OrderedMapKeys(m) - } - }) -} - -func TestOrderedMapKeys(t *testing.T) { - assert.That(t, util.OrderedMapKeys(map[string]int{})).Equal([]string{}) - assert.That(t, util.OrderedMapKeys(map[string]int{"a": 1, "b": 2})).Equal([]string{"a", "b"}) - assert.That(t, util.OrderedMapKeys(map[int]string{})).Equal([]int{}) - assert.That(t, util.OrderedMapKeys(map[int]string{1: "a", 2: "b"})).Equal([]int{1, 2}) -}