diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a9ce3811..f5c01c89 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -3,8 +3,17 @@ Thank you for your interest in and support of the Go-Spring project! To cultivate an open, respectful, inclusive, and professional community, we ask that all participants follow this Code -of Conduct in every interaction—whether reporting issues, submitting code, engaging in discussions, or contributing in -any other way. +of Conduct in every interaction—whether reporting issues, submitting code, engaging in discussions, or participating in +any official project activity. + +## Scope + +This Code of Conduct applies to all interactions related to the Go-Spring project, including but not limited to: + +* Issues and pull requests on the repository +* Discussions in forums, chat channels, or mailing lists +* Contributions to documentation, wiki, or website +* Participation in online or offline project events ## Our Commitment @@ -13,30 +22,58 @@ experience level. We value and support diversity and inclusivity within our comm ## Encouraged Behavior -- Communicate with respect and courtesy. -- Welcome and consider constructive feedback. -- Appreciate diverse perspectives and technical choices. -- Demonstrate patience and empathy in collaborations. -- Contribute positively to a welcoming and supportive environment. +* Communicate with respect and courtesy +* Welcome and consider constructive feedback +* Appreciate diverse perspectives and technical choices +* Demonstrate patience and empathy in collaborations +* Contribute positively to a welcoming and supportive environment + +**Examples of encouraged behavior:** + +* Providing constructive code reviews +* Welcoming newcomers with guidance +* Sharing knowledge in a respectful manner ## Unacceptable Behavior -- Use of discriminatory, abusive, or offensive language or conduct. -- Harassment, threats, or personal attacks. -- Sharing explicit content or inappropriate links. -- Deliberate disruption of constructive efforts. -- Impersonation or violation of personal privacy. +* Use of discriminatory, abusive, or offensive language or conduct +* Harassment, threats, or personal attacks +* Sharing explicit content or inappropriate links +* Deliberate disruption of constructive efforts +* Impersonation or violation of personal privacy + +**Examples of unacceptable behavior:** + +* Using slurs or offensive language +* Harassing contributors for opinions or technical choices +* Posting inappropriate content or links -## Enforcement +## Reporting Violations + +If you witness or experience a violation of this Code of Conduct, please report it to the project maintainers via email +or by submitting an anonymous issue. + +When reporting a violation, please provide: + +* Description of the incident +* Links to relevant content or screenshots +* Names of involved participants (if known) + +Maintainers will respond within 48 hours and handle all reports confidentially. + +## Enforcement and Consequences Project maintainers are responsible for upholding this Code of Conduct. They have the authority to remove, edit, or reject comments, commits, code, wiki edits, issues, or other contributions that violate these guidelines, and to take further action as necessary. -## Reporting Violations +Possible consequences for violations include: -If you witness or experience a violation of this Code of Conduct, please contact the maintainers via email or submit an -anonymous issue. All reports will be handled with discretion and taken seriously. +* Removal of contributions or comments +* Temporary or permanent suspension of project access +* Reporting to relevant platform authorities + +Appeals can be submitted via email to the maintainers. --- @@ -46,30 +83,67 @@ anonymous issue. All reports will be handled with discretion and taken seriously 为营造一个开放、友善、包容和专业的社区氛围,我们制定了本行为准则。无论你是报告问题、提交代码、参与讨论,还是以其他形式参与项目,都请遵循以下准则。 +## 适用范围 + +本行为准则适用于与 Go-Spring 项目相关的所有活动,包括但不限于: + +* 仓库中的 Issue 和 Pull Request +* 论坛、聊天群或邮件列表中的讨论 +* 文档、Wiki 或网站的贡献 +* 线上或线下的官方项目活动 + ## 我们的承诺 -我们承诺为每一位参与者提供一个免受骚扰、歧视和攻击性行为干扰的环境。我们欢迎来自不同背景、经验和身份的贡献者,共同建设一个多元、包容的社区。 +我们承诺为每一位参与者提供一个免受骚扰干扰的环境,无论年龄、背景、身份或经验水平。我们欢迎来自不同背景的贡献者,共同建设一个多元、包容的社区。 ## 倡导的行为 -- 保持尊重、礼貌的沟通方式; -- 乐于接受建设性的意见与反馈; -- 尊重不同的观点与技术选择; -- 在协作中体现耐心与包容; -- 积极参与,共建积极向上的社区氛围。 +* 保持尊重、礼貌的沟通方式 +* 乐于接受建设性的意见与反馈 +* 尊重不同的观点与技术选择 +* 在协作中体现耐心与包容 +* 积极参与,共建积极向上的社区氛围 + +**正面行为示例:** + +* 提供建设性的代码审查 +* 欢迎新人并提供指导 +* 尊重他人分享知识 ## 不被接受的行为 -- 使用歧视性、侮辱性或攻击性的言语与行为; -- 进行人身攻击、骚扰或威胁; -- 发布淫秽内容或不当链接; -- 蓄意干扰他人正常贡献; -- 冒充他人或侵犯他人隐私。 +* 使用歧视性、侮辱性或攻击性的言语与行为 +* 人身攻击、骚扰或威胁他人 +* 发布淫秽内容或不当链接 +* 蓄意干扰他人正常贡献 +* 冒充他人或侵犯他人隐私 + +**违规行为示例:** + +* 使用侮辱性词语或攻击性语言 +* 因观点或技术选择骚扰贡献者 +* 发布不当内容或链接 + +## 举报违规行为 + +若你遇到或目击违反本行为准则的情况,请通过电子邮件联系项目维护者,或通过 Issue 匿名举报。 + +提交举报时,请尽量提供以下信息: + +* 事件描述 +* 相关内容链接或截图 +* 相关人员姓名(如已知) + +维护者将在 48 小时内回复,并确保信息保密。 + +## 执行与后果 -## 执行与维护 +项目维护者有权也有责任删除、修改或拒绝任何违反行为准则的评论、提交、代码、Wiki 编辑、Issue 或其他形式的贡献内容,并在必要时采取进一步措施。 -项目维护者有权也有责任删除、修改或拒绝任何违反行为准则的评论、提交、代码、Wiki 编辑、Issue 或其他形式的贡献内容,必要时可采取进一步措施。 +违规可能导致的后果包括: -## 如何报告问题 +* 删除贡献内容或评论 +* 暂停或永久禁用项目访问权限 +* 向相关平台报告 -若你遇到违反行为准则的情况,请通过电子邮件联系项目维护者,或通过 Issue 匿名举报。我们承诺认真对待每一份举报,并确保信息保密。 +如对处理结果有异议,可通过电子邮件向维护者提交申诉。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3deaec3..cd653b91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,98 +1,188 @@ # Contributing to Go-Spring -First of all, thank you for your interest in and support of the Go-Spring project! +First of all, thank you for your interest in and support of the Go-Spring project! +Before contributing, please read our [Contributor Code of Conduct](CODE_OF_CONDUCT.md). We welcome all kinds of contributions, including reporting issues, improving documentation, fixing bugs, and developing -new features. Please follow the guidelines below to contribute: +new features. Please follow the guidelines below to contribute. + +## Table of Contents + +- [Submitting Issues](#submitting-issues) +- [Submitting Pull Requests](#submitting-pull-requests) +- [Branch Naming Guidelines](#branch-naming-guidelines) +- [Local Development Environment](#local-development-environment) +- [Testing](#testing) +- [Contact Us](#contact-us) ## Submitting Issues -- Before submitting, please search existing issues to avoid duplicates. +- Search existing issues before submitting to avoid duplicates. - Provide clear reproduction steps, expected behavior, and actual results. -- If available, include error logs and relevant environment information. +- Include error logs and environment information if applicable. ## Submitting Pull Requests 1. **Fork the repository and create a new branch** + ```bash git checkout -b feature/your-feature-name - ``` + ```` 2. **Maintain consistent coding style** - - Follow Go’s official style guidelines (use `gofmt`, `golint`, `go vet`) - - It’s recommended to use [`golangci-lint`](https://github.com/golangci/golangci-lint) for local linting + + * Follow Go’s official style guidelines (`gofmt`, `golint`, `go vet`). + * Recommended: [`golangci-lint`](https://github.com/golangci/golangci-lint) for local linting. 3. **Write tests** - - All new features must include unit tests - - Use Go’s built-in `testing` package, and name test files as `xxx_test.go` -4. **Update documentation (if applicable)** + * All new features or bug fixes must include unit tests. + * Use Go’s `testing` package; test files should be named `xxx_test.go`. + * Example: + + ```go + func TestAdd(t *testing.T) { + result := Add(1, 2) + if result != 3 { + t.Errorf("expected 3, got %d", result) + } + } + ``` + +4. **Update documentation** + + * If your changes affect usage or APIs, update README or code comments. 5. **Submit and create a Pull Request** - - Clearly describe the purpose, changes made, and testing results - - Link relevant issues (if any) + + * Clearly describe: + + * **What**: What changes are made + * **Why**: Why the changes are needed + * **How**: How it was implemented + * **Testing**: How it was tested + * Link related issues if applicable. + +## Branch Naming Guidelines + +* `feature/xxx` – New feature +* `fix/xxx` – Bug fix +* `doc/xxx` – Documentation updates +* `refactor/xxx` – Code refactoring ## Local Development Environment -- Go version: Latest stable release is recommended (e.g., `go1.21+`) -- Use Go Modules for dependency management -- Make sure all tests pass: +* Recommended Go version: latest stable release (e.g., `go1.21+`) +* Use Go Modules for dependency management. +* Make sure all tests pass before submitting: + ```bash go test ./... ``` +## Testing + +* Run `go test ./...` to ensure all tests pass. +* For examples or integration tests, provide instructions if needed. + ## Contact Us -If you have any questions, feel free to open an issue or join the discussion forum. +* Open an issue on GitHub for questions or feedback. +* Join project discussions via the community forum or chat. -Thank you for contributing! +Thank you for contributing to Go-Spring! ---- +--- -# Contributing to Go-Spring +# 贡献 Go-Spring 的指南 首先,感谢你关注并支持 Go-Spring 项目! +在贡献之前,请先阅读我们的 [贡献者行为准则](CODE_OF_CONDUCT.md)。 + +我们欢迎各种形式的贡献,包括提交 Issue、完善文档、修复 Bug、开发新功能等。请按照以下指引参与贡献。 -我们欢迎各种形式的贡献,包括但不限于 Issue 提交、文档完善、Bug 修复、功能开发等。请按照以下指引参与贡献: +## 目录 + +* [提交 Issue](#提交-issue) +* [提交 Pull Request](#提交-pull-request) +* [分支命名规范](#分支命名规范) +* [本地开发环境要求](#本地开发环境要求) +* [测试](#测试) +* [联系我们](#联系我们) ## 提交 Issue -- 在提交前,请先搜索现有的 Issue,避免重复提交。 -- 请提供清晰的复现步骤、预期行为以及实际结果。 -- 如有错误日志或运行环境信息,请一并附上。 +* 在提交前,请先搜索现有 Issue,避免重复。 +* 提供清晰的复现步骤、预期行为以及实际结果。 +* 如有错误日志或运行环境信息,请一并附上。 ## 提交 Pull Request 1. **Fork 仓库并创建新分支** + ```bash git checkout -b feature/your-feature-name ``` 2. **保持一致的代码风格** - - 遵循 Go 官方代码规范(使用 `gofmt`、`golint`、`go vet`) - - 推荐使用 [`golangci-lint`](https://github.com/golangci/golangci-lint) 进行本地代码检查 + + * 遵循 Go 官方代码规范(使用 `gofmt`、`golint`、`go vet`)。 + * 推荐使用 [`golangci-lint`](https://github.com/golangci/golangci-lint) 进行本地检查。 3. **编写测试用例** - - 所有新功能必须配备单元测试 - - 使用 Go 内置的 `testing` 包,测试文件应命名为 `xxx_test.go` -4. **更新相关文档(如有变更)** + * 所有新功能或 Bug 修复必须配备单元测试。 + * 使用 Go 内置 `testing` 包,测试文件命名为 `xxx_test.go`。 + * 示例: + + ```go + func TestAdd(t *testing.T) { + result := Add(1, 2) + if result != 3 { + t.Errorf("expected 3, got %d", result) + } + } + ``` + +4. **更新文档** + + * 如果变更影响使用或接口,请同步更新 README 或代码注释。 5. **提交并创建 Pull Request** - - 说明 PR 的目的、变更内容、测试情况等 - - 关联相关 Issue(如有) + + * 清晰说明: + + * **What**:本次修改的内容 + * **Why**:修改原因 + * **How**:实现方式 + * **Testing**:测试情况 + * 关联相关 Issue(如有)。 + +## 分支命名规范 + +* `feature/xxx` – 新功能 +* `fix/xxx` – Bug 修复 +* `doc/xxx` – 文档更新 +* `refactor/xxx` – 代码重构 ## 本地开发环境要求 -- Go 版本:推荐使用最新版稳定版(如 `go1.21+`) -- 使用 Go Modules 进行依赖管理 -- 确保测试全部通过: +* Go 版本:推荐使用最新版稳定版(如 `go1.21+`) +* 使用 Go Modules 管理依赖 +* 提交前确保所有测试通过: + ```bash go test ./... ``` +## 测试 + +* 运行 `go test ./...` 确保所有测试通过 +* 对于示例或集成测试,请提供使用说明(如适用) + ## 联系我们 -如有疑问,欢迎通过 Issue 与我们联系,或参与项目的讨论区。 +* 可通过 GitHub Issue 提问或反馈 +* 参与项目讨论区交流 -感谢你的贡献! +感谢你为 Go-Spring 做出的贡献! diff --git a/README.md b/README.md index f50051a8..5870b128 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,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) Example: @@ -218,7 +217,6 @@ Example: 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 ``` ### 2️⃣ Injection Methods diff --git a/README_CN.md b/README_CN.md index aa26cb28..dc9d51d5 100644 --- a/README_CN.md +++ b/README_CN.md @@ -188,7 +188,6 @@ Go-Spring 提供多种方式注册 Bean: - **`gs.Object(obj)`** - 将已有对象注册为 Bean - **`gs.Provide(ctor, args...)`** - 使用构造函数生成并注册 Bean -- **`gs.Register(bd)`** - 注册完整 Bean 定义(适合底层封装或高级用法) 示例: @@ -196,7 +195,6 @@ Go-Spring 提供多种方式注册 Bean: gs.Object(&Service{}) // 注册结构体实例 gs.Provide(NewService) // 使用构造函数注册 gs.Provide(NewRepo, gs.ValueArg("db")) // 构造函数带参数 -gs.Register(gs.NewBean(NewService)) // 完整定义注册 ``` ### 2️⃣ 注入方式 diff --git a/conf/bind.go b/conf/bind.go index 6cc20dbd..a50654d4 100644 --- a/conf/bind.go +++ b/conf/bind.go @@ -23,22 +23,37 @@ import ( "strconv" "strings" - "github.com/go-spring/spring-core/util" - "github.com/go-spring/spring-core/util/errutil" + "github.com/go-spring/spring-base/util" ) var ( - ErrNotExist = errors.New("not exist") - ErrInvalidSyntax = errors.New("invalid syntax") + ErrNotExist = util.FormatError(nil, "not exist") + ErrInvalidSyntax = util.FormatError(nil, "invalid syntax") ) -// ParsedTag represents a parsed configuration tag, including key, -// default value, and optional custom splitter for list parsing. +// ParsedTag represents a parsed configuration tag that encodes +// metadata for binding configuration values from property sources. +// +// A tag string generally follows the pattern: +// +// ${key:=default}>>splitter +// +// - "key": the property key used to look up a value. +// - "default": optional fallback value if the key does not exist. +// - "splitter": optional custom function name to split strings into slices. +// +// Examples: +// +// "${db.host:=localhost}" -> key=db.host, default=localhost +// "${ports:=8080,9090}>>csv" -> key=ports, default=8080,9090, splitter=csv +// "${:=foo}" -> empty key, only default value "foo" +// +// The parsing logic is strict; malformed tags will result in ErrInvalidSyntax. type ParsedTag struct { Key string // short property key - Def string // default value - HasDef bool // has default value - Splitter string // splitter's name + Def string // default value string + HasDef bool // indicates whether a default value exists + Splitter string // optional splitter function name for slice parsing } func (tag ParsedTag) String() string { @@ -57,46 +72,67 @@ func (tag ParsedTag) String() string { return sb.String() } -// ParseTag parses a tag string in the format `${key:=default}>>splitter` -// into a ParsedTag struct. Returns an error if the format is invalid. +// ParseTag parses a tag string into a ParsedTag struct. +// +// Supported syntax: `${key:=default}>>splitter` +// +// - The `${...}` block is mandatory. +// - ":=" introduces an optional default value. +// - ">>splitter" is optional and specifies a custom splitter. +// +// Example parses: +// +// "${foo}" -> Key="foo" +// "${foo:=bar}" -> Key="foo", HasDef=true, Def="bar" +// "${foo:=bar}>>csv" -> Key="foo", HasDef=true, Def="bar", Splitter="csv" +// "${:=fallback}" -> Key="", HasDef=true, Def="fallback" +// +// Errors: +// - Returns ErrInvalidSyntax if the string does not follow the pattern. func ParseTag(tag string) (ret ParsedTag, err error) { - i := strings.LastIndex(tag, ">>") - if i == 0 { - err = fmt.Errorf("parse tag '%s' error: %w", tag, ErrInvalidSyntax) - return - } j := strings.LastIndex(tag, "}") if j <= 0 { - err = fmt.Errorf("parse tag '%s' error: %w", tag, ErrInvalidSyntax) + err = util.FormatError(ErrInvalidSyntax, "parse tag '%s' error", tag) return } k := strings.Index(tag, "${") if k < 0 { - err = fmt.Errorf("parse tag '%s' error: %w", tag, ErrInvalidSyntax) + err = util.FormatError(ErrInvalidSyntax, "parse tag '%s' error", tag) return } - if i > j { + if i := strings.LastIndex(tag, ">>"); i > j { ret.Splitter = strings.TrimSpace(tag[i+2:]) } ss := strings.SplitN(tag[k+2:j], ":=", 2) - ret.Key = ss[0] + ret.Key = strings.TrimSpace(ss[0]) if len(ss) > 1 { ret.HasDef = true - ret.Def = ss[1] + ret.Def = strings.TrimSpace(ss[1]) } return } -// BindParam holds binding metadata for a single configuration value. +// BindParam holds metadata needed to bind a single configuration value +// to a Go struct field, slice element, or map entry. type BindParam struct { - Key string // full key - Path string // full path + Key string // full property key + Path string // full property path Tag ParsedTag // parsed tag - Validate reflect.StructTag // full field tag + Validate reflect.StructTag // original struct field tag for validation } -// BindTag parses and binds the configuration tag to the BindParam. -// It handles default values and nested key expansion. +// BindTag parses the tag string, stores the ParsedTag in BindParam, +// and resolves nested key expansion. +// +// Special cases: +// - "${:=default}" -> Key is empty, only default is set. +// - "${ROOT}" -> explicitly resets Key to an empty string. +// +// If a BindParam already has a Key, new keys are appended hierarchically, +// e.g. parent Key="db", tag="${host}" -> final Key="db.host". +// +// Errors: +// - ErrInvalidSyntax if the tag string is malformed or empty without default. func (param *BindParam) BindTag(tag string, validate reflect.StructTag) error { param.Validate = validate parsedTag, err := ParseTag(tag) @@ -108,7 +144,7 @@ func (param *BindParam) BindTag(tag string, validate reflect.StructTag) error { param.Tag = parsedTag return nil } - return fmt.Errorf("parse tag '%s' error: %w", tag, ErrInvalidSyntax) + return util.FormatError(ErrInvalidSyntax, "parse tag '%s' error", tag) } if parsedTag.Key == "ROOT" { parsedTag.Key = "" @@ -127,20 +163,34 @@ type Filter interface { Do(i any, param BindParam) (bool, error) } -// BindValue binds a value from properties `p` to the reflect.Value `v` of type `t` -// using metadata in `param`. It supports primitives, structs, maps, and slices. +// BindValue attempts to bind a property value from the property source `p` +// into the given reflect.Value `v`, based on metadata in `param`. +// +// Supported binding targets: +// - Primitive types (string, int, float, bool, etc.). +// - Structs (recursively bound field by field). +// - Maps (bound by iterating subkeys). +// - Slices (bound by either indexed keys or split strings). +// +// Errors: +// - Returns ErrNotExist if the property is missing without a default. +// - Returns type conversion errors if parsing fails. +// - Returns wrapped errors with context (path, type). func BindValue(p Properties, v reflect.Value, t reflect.Type, param BindParam, filter Filter) (RetErr error) { if !util.IsPropBindingTarget(t) { - err := errors.New("target should be value type") - return fmt.Errorf("bind path=%s type=%s error: %w", param.Path, v.Type().String(), err) + err := util.FormatError(nil, "target should be value type") + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } + // run validation if "expr" tag is defined and no prior error defer func() { if RetErr == nil { tag, ok := param.Validate.Lookup("expr") if ok && len(tag) > 0 { - RetErr = validateField(tag, v.Interface()) + if RetErr = validateField(tag, v.Interface()); RetErr != nil { + RetErr = util.FormatError(RetErr, "validate path=%s type=%s error", param.Path, v.Type().String()) + } } } }() @@ -151,8 +201,8 @@ func BindValue(p Properties, v reflect.Value, t reflect.Type, param BindParam, f case reflect.Slice: return bindSlice(p, v, t, param, filter) case reflect.Array: - err := errors.New("use slice instead of array") - return fmt.Errorf("bind path=%s type=%s error: %w", param.Path, v.Type().String(), err) + err := util.FormatError(nil, "use slice instead of array") + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) default: // for linter } @@ -164,22 +214,25 @@ func BindValue(p Properties, v reflect.Value, t reflect.Type, param BindParam, f return nil } + // resolve property value (with default and references) val, err := resolve(p, param) if err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } + // try converter function first if fn != nil { fnValue := reflect.ValueOf(fn) out := fnValue.Call([]reflect.Value{reflect.ValueOf(val)}) if !out[1].IsNil() { err = out[1].Interface().(error) - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } v.Set(out[0]) return nil } + // fallback: parse string into basic types switch v.Kind() { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: var u uint64 @@ -187,41 +240,51 @@ func BindValue(p Properties, v reflect.Value, t reflect.Type, param BindParam, f v.SetUint(u) return nil } - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: var i int64 if i, err = strconv.ParseInt(val, 0, 0); err == nil { v.SetInt(i) return nil } - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) case reflect.Float32, reflect.Float64: var f float64 if f, err = strconv.ParseFloat(val, 64); err == nil { v.SetFloat(f) return nil } - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) case reflect.Bool: var b bool if b, err = strconv.ParseBool(val); err == nil { v.SetBool(b) return nil } - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) default: + // treat everything else as string v.SetString(val) return nil } } -// bindSlice binds properties to a slice value. +// bindSlice binds configuration values into a slice of type []T. +// +// Supported input formats: +// 1. Indexed keys in the property source: +// e.g. "list[0]=a", "list[1]=b" +// 2. A single delimited string: +// e.g. "list=a,b,c" (split by "," or custom splitter) +// +// The slice is always reset (v.Set(slice)) before return, +// even if binding fails midway. func bindSlice(p Properties, v reflect.Value, t reflect.Type, param BindParam, filter Filter) error { - et := t.Elem() - p, err := getSlice(p, et, param) + elemType := t.Elem() + p, err := getSlice(p, elemType, param) if err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } slice := reflect.MakeSlice(t, 0, 0) @@ -232,46 +295,55 @@ func bindSlice(p Properties, v reflect.Value, t reflect.Type, param BindParam, f } for i := 0; ; i++ { - ev := reflect.New(et).Elem() + subValue := reflect.New(elemType).Elem() subParam := BindParam{ Key: fmt.Sprintf("%s[%d]", param.Key, i), Path: fmt.Sprintf("%s[%d]", param.Path, i), } - err = BindValue(p, ev, et, subParam, filter) + err = BindValue(p, subValue, elemType, subParam, filter) if errors.Is(err, ErrNotExist) { + // stop when no more indexed elements break } if err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } - slice = reflect.Append(slice, ev) + slice = reflect.Append(slice, subValue) } return nil } -// getSlice retrieves and splits a string into slice elements, -// creating a new Properties instance if necessary. +// getSlice prepares a Properties object representing slice elements +// derived from either: +// +// - Explicit indexed properties (preferred). +// - A single delimited string property, split into multiple elements. +// +// Errors: +// - ErrNotExist if property is missing and no default is provided. +// - Unknown splitter name if specified splitter is not registered. +// - Converter missing for non-primitive element types. func getSlice(p Properties, et reflect.Type, param BindParam) (Properties, error) { - // properties that defined as list. + // case 1: properties already defined as list (e.g. key[0], key[1]...) if p.Has(param.Key + "[0]") { return p, nil } - // properties that defined as string and needs to split into []string. + // case 2: property is a single string -> split into slice var strVal string { if p.Has(param.Key) { strVal = p.Get(param.Key) } else { if !param.Tag.HasDef { - return nil, fmt.Errorf("property %q %w", param.Key, ErrNotExist) + return nil, util.FormatError(nil, "property %q %w", param.Key, ErrNotExist) } if param.Tag.Def == "" { return nil, nil } if !util.IsPrimitiveValueType(et) && converters[et] == nil { - return nil, fmt.Errorf("can't find converter for %s", et.String()) + return nil, util.FormatError(nil, "can't find converter for %s", et.String()) } strVal = param.Tag.Def } @@ -285,17 +357,19 @@ func getSlice(p Properties, et reflect.Type, param BindParam) (Properties, error arrVal []string ) + // split string into elements if s := param.Tag.Splitter; s == "" { arrVal = strings.Split(strVal, ",") for i := range arrVal { arrVal[i] = strings.TrimSpace(arrVal[i]) } } else if fn, ok := splitters[s]; ok && fn != nil { + // use custom splitter function if arrVal, err = fn(strVal); err != nil { - return nil, fmt.Errorf("split error: %w, value: %q", err, strVal) + return nil, util.FormatError(err, "split %q error", strVal) } } else { - return nil, fmt.Errorf("unknown splitter '%s'", s) + return nil, util.FormatError(nil, "unknown splitter '%s'", s) } r := New() @@ -306,39 +380,54 @@ func getSlice(p Properties, et reflect.Type, param BindParam) (Properties, error return r, nil } -// bindMap binds properties to a map value. +// bindMap binds configuration properties into a Go map[K]V. +// +// Example: +// +// Properties: +// "users.alice.age" = 20 +// "users.bob.age" = 30 +// +// Binding into map[string]User produces: +// {"alice": User{Age:20}, "bob": User{Age:30}} +// +// Errors: +// - Returns error if property is missing without default. +// - Propagates binding errors from element binding. func bindMap(p Properties, v reflect.Value, t reflect.Type, param BindParam, filter Filter) error { if param.Tag.HasDef && param.Tag.Def != "" { - err := errors.New("map can't have a non-empty default value") - return fmt.Errorf("bind path=%s type=%s error: %w", param.Path, v.Type().String(), err) + err := util.FormatError(nil, "map can't have a non-empty default value") + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } - et := t.Elem() + elemType := t.Elem() ret := reflect.MakeMap(t) defer func() { v.Set(ret) }() - // 当成默认值处理 + // handle empty key as default value placeholder if param.Tag.Key == "" { if param.Tag.HasDef { return nil } } + // ensure property exists if !p.Has(param.Key) { if param.Tag.HasDef { return nil } - return fmt.Errorf("property %q %w", param.Key, ErrNotExist) + return util.FormatError(nil, "property %q %w", param.Key, ErrNotExist) } + // fetch subkeys under the current key prefix keys, err := p.SubKeys(param.Key) if err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } for _, key := range keys { - e := reflect.New(et).Elem() + subValue := reflect.New(elemType).Elem() subKey := key if param.Key != "" { subKey = param.Key + "." + key @@ -347,26 +436,44 @@ func bindMap(p Properties, v reflect.Value, t reflect.Type, param BindParam, fil Key: subKey, Path: param.Path, } - if err = BindValue(p, e, et, subParam, filter); err != nil { + if err = BindValue(p, subValue, elemType, subParam, filter); err != nil { return err // no wrap } - ret.SetMapIndex(reflect.ValueOf(key), e) + ret.SetMapIndex(reflect.ValueOf(key), subValue) } return nil } -// bindStruct binds properties to a struct value. +// bindStruct binds configuration properties into a struct. +// +// Example: +// +// type Config struct { +// Host string `value:"${db.host:=localhost}"` +// Port int `value:"${db.port:=3306}"` +// } +// +// With properties: +// db.host=127.0.0.1 +// Result: +// Config{Host:"127.0.0.1", Port:3306} +// +// Errors: +// - Invalid syntax in tag. +// - Binding or conversion failures in nested fields. +// - Infinite recursion is avoided for embedded pointer structs. func bindStruct(p Properties, v reflect.Value, t reflect.Type, param BindParam, filter Filter) error { if param.Tag.HasDef && param.Tag.Def != "" { - err := errors.New("struct can't have a non-empty default value") - return fmt.Errorf("bind path=%s type=%s error: %w", param.Path, v.Type().String(), err) + err := util.FormatError(nil, "struct can't have a non-empty default value") + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } for i := range t.NumField() { ft := t.Field(i) fv := v.Field(i) + // skip unexported fields if !fv.CanInterface() { continue } @@ -378,12 +485,12 @@ func bindStruct(p Properties, v reflect.Value, t reflect.Type, param BindParam, if tag, ok := ft.Tag.Lookup("value"); ok { if err := subParam.BindTag(tag, ft.Tag); err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } if filter != nil { ret, err := filter.Do(fv.Addr().Interface(), subParam) if err != nil { - return errutil.WrapError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) + return util.FormatError(err, "bind path=%s type=%s error", param.Path, v.Type().String()) } if ret { continue @@ -408,7 +515,16 @@ func bindStruct(p Properties, v reflect.Value, t reflect.Type, param BindParam, return nil } -// resolve returns property references processed property value. +// resolve fetches the final string value of a property key, +// applying default values and resolving references recursively. +// +// Example: +// +// Properties: +// "host" = "localhost" +// "url" = "http://${host}:8080" +// +// resolve(url) -> "http://localhost:8080" func resolve(p Properties, param BindParam) (string, error) { const defVal = "@@def@@" val := p.Get(param.Key, defVal) @@ -416,15 +532,35 @@ func resolve(p Properties, param BindParam) (string, error) { return resolveString(p, val) } if p.Has(param.Key) { - return "", fmt.Errorf("property %s isn't simple value", param.Key) + return "", util.FormatError(nil, "property %q isn't simple value", param.Key) } if param.Tag.HasDef { return resolveString(p, param.Tag.Def) } - return "", fmt.Errorf("property %s %w", param.Key, ErrNotExist) + return "", util.FormatError(nil, "property %q %w", param.Key, ErrNotExist) } -// resolveString returns property references processed string. +// resolveString expands property references of the form ${key} +// inside a string, recursively resolving nested expressions. +// +// Supported features: +// - Nested references: e.g. "${outer${inner}}" +// - Default values: "${key:=fallback}" +// - Arbitrary string concatenation around references. +// +// Example: +// +// Properties: +// "host" = "localhost" +// "port" = "8080" +// Input: +// "http://${host}:${port}" +// Output: +// "http://localhost:8080" +// +// Errors: +// - ErrInvalidSyntax if braces are unbalanced. +// - Propagates errors from resolve(). func resolveString(p Properties, s string) (string, error) { // If there is no property reference, return the original string. @@ -434,19 +570,19 @@ func resolveString(p Properties, s string) (string, error) { } var ( - length = len(s) - count = 1 - end = -1 + level = 1 + end = -1 ) - for i := start + 2; i < length; i++ { + // scan for matching closing brace, handling nested references + for i := start + 2; i < len(s); i++ { if s[i] == '$' { - if i+1 < length && s[i+1] == '{' { - count++ + if i+1 < len(s) && s[i+1] == '{' { + level++ } } else if s[i] == '}' { - count-- - if count == 0 { + level-- + if level == 0 { end = i break } @@ -455,21 +591,24 @@ func resolveString(p Properties, s string) (string, error) { if end < 0 { err := ErrInvalidSyntax - return "", fmt.Errorf("resolve string %q error: %w", s, err) + return "", util.FormatError(err, "resolve string %q error", s) } var param BindParam _ = param.BindTag(s[start:end+1], "") - s1, err := resolve(p, param) + // resolve the referenced property + resolved, err := resolve(p, param) if err != nil { - return "", errutil.WrapError(err, "resolve string %q error", s) + return "", util.FormatError(err, "resolve string %q error", s) } - s2, err := resolveString(p, s[end+1:]) + // resolve the remaining part of the string + suffix, err := resolveString(p, s[end+1:]) if err != nil { - return "", errutil.WrapError(err, "resolve string %q error", s) + return "", util.FormatError(err, "resolve string %q error", s) } - return s[:start] + s1 + s2, nil + // combine: prefix + resolved + suffix + return s[:start] + resolved + suffix, nil } diff --git a/conf/bind_test.go b/conf/bind_test.go index e12cc9bb..7481a5d3 100644 --- a/conf/bind_test.go +++ b/conf/bind_test.go @@ -25,7 +25,7 @@ import ( "testing" "time" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" "github.com/spf13/cast" ) @@ -69,77 +69,109 @@ func TestConverter(t *testing.T) { Duration time.Duration `value:"${duration:=10s}"` } - t.Run("success", func(t *testing.T) { + t.Run("built-in types", func(t *testing.T) { err := conf.New().Bind(&s) assert.That(t, err).Nil() assert.That(t, s.Time).Equal(time.Date(2025, 2, 1, 0, 0, 0, 0, time.UTC)) assert.That(t, s.Duration).Equal(10 * time.Second) }) - t.Run("error", func(t *testing.T) { + t.Run("invalid time format", func(t *testing.T) { p := conf.Map(map[string]any{ "time": "2025-02-01M00:00:00", }) err := p.Bind(&s) - assert.ThatError(t, err).Matches("unable to parse date: 2025-02-01M00:00:00") + assert.Error(t, err).Matches("unable to parse date: 2025-02-01M00:00:00") }) } func TestSplitter(t *testing.T) { - t.Run("success", func(t *testing.T) { + t.Run("split points success", func(t *testing.T) { var points []image.Point err := conf.New().Bind(&points, "${:=(1,2)(3,4)}>>PointSplitter") assert.That(t, err).Nil() assert.That(t, points).Equal([]image.Point{{X: 1, Y: 2}, {X: 3, Y: 4}}) }) - t.Run("split error", func(t *testing.T) { + t.Run("split points error", func(t *testing.T) { var points []image.Point err := conf.New().Bind(&points, "${:=(1}>>PointSplitter") - assert.ThatError(t, err).Matches("split error") + assert.Error(t, err).Matches("split error") }) t.Run("unknown splitter", func(t *testing.T) { var points []image.Point err := conf.New().Bind(&points, "${:=(1}>>UnknownSplitter") - assert.ThatError(t, err).Matches("unknown splitter 'UnknownSplitter'") + assert.Error(t, err).Matches("unknown splitter 'UnknownSplitter'") + }) +} + +func TestSplitterError(t *testing.T) { + conf.RegisterSplitter("ErrorSplitter", func(str string) ([]string, error) { + return nil, errors.New("splitter error") + }) + + t.Run("splitter returns error", func(t *testing.T) { + var strs []string + err := conf.New().Bind(&strs, "${strs:=a,b,c}>>ErrorSplitter") + assert.Error(t, err).Matches("splitter error") }) } func TestParseTag(t *testing.T) { - t.Run("normal", func(t *testing.T) { + t.Run("simple tag", func(t *testing.T) { tag, err := conf.ParseTag("${a}") assert.That(t, err).Nil() assert.That(t, tag.String()).Equal("${a}") }) - t.Run("default", func(t *testing.T) { + t.Run("with default", func(t *testing.T) { tag, err := conf.ParseTag("${a:=123}") assert.That(t, err).Nil() assert.That(t, tag.String()).Equal("${a:=123}") }) - t.Run("splitter", func(t *testing.T) { + t.Run("with splitter", func(t *testing.T) { tag, err := conf.ParseTag("${a:=1,2,3}>>splitter") assert.That(t, err).Nil() assert.That(t, tag.String()).Equal("${a:=1,2,3}>>splitter") }) - t.Run("error - 1", func(t *testing.T) { + t.Run("missing dollar brace", func(t *testing.T) { _, err := conf.ParseTag(">>splitter") - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") }) - t.Run("error - 2", func(t *testing.T) { + t.Run("unmatched braces", func(t *testing.T) { _, err := conf.ParseTag("${a:=1,2,3") - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") }) - t.Run("error - 3", func(t *testing.T) { + t.Run("missing dollar sign", func(t *testing.T) { _, err := conf.ParseTag("{a:=1,2,3}") - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") + }) + + t.Run("empty key with default", func(t *testing.T) { + tag, err := conf.ParseTag("${:=default}") + assert.That(t, err).Nil() + assert.That(t, tag).Equal(conf.ParsedTag{ + Key: "", + Def: "default", + HasDef: true, + }) + }) + + t.Run("key with special chars", func(t *testing.T) { + tag, err := conf.ParseTag("${key-with.dots_and_underscores:=value}") + assert.That(t, err).Nil() + assert.That(t, tag).Equal(conf.ParsedTag{ + Key: "key-with.dots_and_underscores", + Def: "value", + HasDef: true, + }) }) } @@ -206,16 +238,32 @@ func TestBindParam(t *testing.T) { }) }) - t.Run("error - 1", func(t *testing.T) { + t.Run("invalid format", func(t *testing.T) { var param conf.BindParam err := param.BindTag("a:=123", "") - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") }) - t.Run("error - 2", func(t *testing.T) { + t.Run("empty tag", func(t *testing.T) { var param conf.BindParam err := param.BindTag("${}", "") - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") + }) + + t.Run("empty tag with default", func(t *testing.T) { + var param conf.BindParam + err := param.BindTag("${:=}", "") + assert.Error(t, err).Nil() + }) + + t.Run("nested key", func(t *testing.T) { + var param = conf.BindParam{ + Key: "parent", + Path: "Parent", + } + err := param.BindTag("${child.key:=value}", "") + assert.That(t, err).Nil() + assert.That(t, param.Key).Equal("parent.child.key") }) } @@ -271,6 +319,19 @@ type UnnamedDefault struct { Map map[string]int `value:"${:=}"` } +type AdvancedTypes struct { + BoolSlice []bool `value:"${boolSlice:=true,false,true}"` + IntSlice []int `value:"${intSlice:=1,2,3}"` + StringSlice []string `value:"${stringSlice:=a,b,c}"` + NestedMap map[string]Data `value:"${nestedMap}"` + EmptyStruct Data `value:"${emptyStruct}"` +} + +type Data struct { + Name string `value:"${name}"` + Age int `value:"${age}"` +} + func TestProperties_Bind(t *testing.T) { t.Run("unnamed default", func(t *testing.T) { @@ -284,20 +345,20 @@ func TestProperties_Bind(t *testing.T) { }) }) - t.Run("BindTag error", func(t *testing.T) { + t.Run("invalid tag", func(t *testing.T) { var i int err := conf.New().Bind(&i, "$") - assert.ThatError(t, err).Matches("parse tag '\\$' error: invalid syntax") + assert.Error(t, err).Matches("parse tag '\\$' error: invalid syntax") }) - t.Run("target error - 1", func(t *testing.T) { + t.Run("non pointer target", func(t *testing.T) { err := conf.New().Bind(5) - assert.ThatError(t, err).Matches("should be a ptr") + assert.Error(t, err).Matches("should be a pointer but int") }) - t.Run("target error - 1", func(t *testing.T) { + t.Run("pointer to pointer target", func(t *testing.T) { err := conf.New().Bind(new(*int)) - assert.ThatError(t, err).Matches("target should be value type") + assert.Error(t, err).Matches("target should be value type") }) t.Run("validate error", func(t *testing.T) { @@ -307,57 +368,57 @@ func TestProperties_Bind(t *testing.T) { err := conf.Map(map[string]any{ "v": "1", }).Bind(&s) - assert.ThatError(t, err).Matches("validate failed on .* for value 1") + assert.Error(t, err).Matches("validate failed on .* for value 1") }) t.Run("array error", func(t *testing.T) { err := conf.New().Bind(new(struct { Arr [3]string `value:"${arr:=1,2,3}"` })) - assert.ThatError(t, err).Matches("use slice instead of array") + assert.Error(t, err).Matches("use slice instead of array") }) - t.Run("type error - 1", func(t *testing.T) { + t.Run("string to int error", func(t *testing.T) { var s struct { Value int `value:"${v}"` } err := conf.Map(map[string]any{ "v": "abc", }).Bind(&s) - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing .*: invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing .*: invalid syntax") }) - t.Run("type error - 2", func(t *testing.T) { + t.Run("string to uint error", func(t *testing.T) { var s struct { Value uint `value:"${v}"` } err := conf.Map(map[string]any{ "v": "abc", }).Bind(&s) - assert.ThatError(t, err).Matches("strconv.ParseUint: parsing .*: invalid syntax") + assert.Error(t, err).Matches("strconv.ParseUint: parsing .*: invalid syntax") }) - t.Run("type error - 3", func(t *testing.T) { + t.Run("string to float error", func(t *testing.T) { var s struct { Value float32 `value:"${v}"` } err := conf.Map(map[string]any{ "v": "abc", }).Bind(&s) - assert.ThatError(t, err).Matches("strconv.ParseFloat: parsing .*: invalid syntax") + assert.Error(t, err).Matches("strconv.ParseFloat: parsing .*: invalid syntax") }) - t.Run("type error - 4", func(t *testing.T) { + t.Run("string to bool error", func(t *testing.T) { var s struct { Value bool `value:"${v}"` } err := conf.Map(map[string]any{ "v": "abc", }).Bind(&s) - assert.ThatError(t, err).Matches("strconv.ParseBool: parsing .*: invalid syntax") + assert.Error(t, err).Matches("strconv.ParseBool: parsing .*: invalid syntax") }) - t.Run("slice error - 1", func(t *testing.T) { + t.Run("slice error", func(t *testing.T) { var s struct { Value []int `value:"${v}"` } @@ -366,34 +427,34 @@ func TestProperties_Bind(t *testing.T) { "1", "2", "a", }, }).Bind(&s) - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing .*: invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing .*: invalid syntax") }) - t.Run("slice error - 2", func(t *testing.T) { + t.Run("missing slice property", func(t *testing.T) { var s struct { Value []int `value:"${v}"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("property \"v\" not exist") + assert.Error(t, err).Matches("property \"v\" not exist") }) - t.Run("slice error - 3", func(t *testing.T) { + t.Run("missing converter for slice", func(t *testing.T) { var s struct { Value []image.Rectangle `value:"${v:={(1,2)(3,4)}"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("can't find converter for image.Rectangle") + assert.Error(t, err).Matches("can't find converter for image.Rectangle") }) - t.Run("map error - 1", func(t *testing.T) { + t.Run("map non empty default", func(t *testing.T) { var s struct { Value map[string]int `value:"${v:=a:b,1:2}"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("map can't have a non-empty default value") + assert.Error(t, err).Matches("map can't have a non-empty default value") }) - t.Run("map error - 2", func(t *testing.T) { + t.Run("map from slice", func(t *testing.T) { var s struct { Value map[string]int `value:"${v}"` } @@ -402,38 +463,38 @@ func TestProperties_Bind(t *testing.T) { "1", "2", "3", }, }).Bind(&s) - assert.ThatError(t, err).Matches("property v.0 not exist") + assert.Error(t, err).Matches("property \"v.0\" not exist") }) - t.Run("map error - 3", func(t *testing.T) { + t.Run("map type conflict", func(t *testing.T) { var s struct { Value map[string]int `value:"${v}"` } err := conf.Map(map[string]any{ "v": "a:b,1:2", }).Bind(&s) - assert.ThatError(t, err).Matches("property conflict at path v") + assert.Error(t, err).Matches("property conflict at path v") }) - t.Run("map error - 4", func(t *testing.T) { + t.Run("missing map property", func(t *testing.T) { var s struct { Value map[string]int `value:"${v}"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("property \"v\" not exist") + assert.Error(t, err).Matches("property \"v\" not exist") }) - t.Run("struct error - 1", func(t *testing.T) { + t.Run("struct non empty default", func(t *testing.T) { var s struct { Value struct { Int int } `value:"${v:={123}}"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("struct can't have a non-empty default value") + assert.Error(t, err).Matches("struct can't have a non-empty default value") }) - t.Run("struct error - 2", func(t *testing.T) { + t.Run("unexported field", func(t *testing.T) { var s struct { int `value:"${v}"` } @@ -444,15 +505,15 @@ func TestProperties_Bind(t *testing.T) { assert.That(t, s.int).Equal(0) }) - t.Run("struct error - 3", func(t *testing.T) { + t.Run("invalid struct tag", func(t *testing.T) { var s struct { Value int `value:"v"` } err := conf.New().Bind(&s) - assert.ThatError(t, err).Matches("parse tag 'v' error: invalid syntax") + assert.Error(t, err).Matches("parse tag 'v' error: invalid syntax") }) - t.Run("struct error - 4", func(t *testing.T) { + t.Run("embedded interface", func(t *testing.T) { var s struct { io.Reader } @@ -565,7 +626,59 @@ func TestProperties_Bind(t *testing.T) { assert.That(t, c).Equal(expect) }) - t.Run("filter false", func(t *testing.T) { + t.Run("advanced types", func(t *testing.T) { + p := conf.Map(map[string]any{ + "boolSlice": "true,false,true", + "intSlice": "1,2,3", + "stringSlice": "a,b,c", + "nestedMap": map[string]any{ + "user1": map[string]any{ + "name": "Alice", + "age": 25, + }, + "user2": map[string]any{ + "name": "Bob", + "age": 30, + }, + }, + "emptyStruct": map[string]any{ + "name": "Empty", + "age": 0, + }, + "pointerField": map[string]any{ + "name": "Pointer", + "age": 40, + }, + }) + + var s AdvancedTypes + err := p.Bind(&s) + assert.That(t, err).Nil() + + assert.That(t, s.BoolSlice).Equal([]bool{true, false, true}) + assert.That(t, s.IntSlice).Equal([]int{1, 2, 3}) + assert.That(t, s.StringSlice).Equal([]string{"a", "b", "c"}) + + assert.That(t, s.NestedMap).Equal(map[string]Data{ + "user1": {Name: "Alice", Age: 25}, + "user2": {Name: "Bob", Age: 30}, + }) + + assert.That(t, s.EmptyStruct).Equal(Data{Name: "Empty", Age: 0}) + }) + + t.Run("empty collections with defaults", func(t *testing.T) { + var s struct { + EmptySlice []string `value:"${emptySlice:=}"` + EmptyMap map[string]int `value:"${emptyMap:=}"` + } + err := conf.New().Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.EmptySlice).Equal([]string{}) + assert.That(t, s.EmptyMap).Equal(map[string]int{}) + }) + + t.Run("filter returns false", func(t *testing.T) { var param conf.BindParam err := param.BindTag("${ROOT}", "") assert.That(t, err).Nil() @@ -583,7 +696,7 @@ func TestProperties_Bind(t *testing.T) { assert.That(t, s.Value).Equal(3) }) - t.Run("filter true", func(t *testing.T) { + t.Run("filter returns true", func(t *testing.T) { var param conf.BindParam err := param.BindTag("${ROOT}", "") assert.That(t, err).Nil() @@ -601,7 +714,7 @@ func TestProperties_Bind(t *testing.T) { assert.That(t, s.Value).Equal(0) }) - t.Run("filter error", func(t *testing.T) { + t.Run("filter returns error", func(t *testing.T) { var param conf.BindParam err := param.BindTag("${ROOT}", "") assert.That(t, err).Nil() @@ -615,7 +728,193 @@ func TestProperties_Bind(t *testing.T) { funcFilter(func(i any, param conf.BindParam) (bool, error) { return false, errors.New("filter error") })) - assert.ThatError(t, err).Matches("filter error") + assert.Error(t, err).Matches("filter error") assert.That(t, s.Value).Equal(0) }) + + t.Run("property reference resolution", func(t *testing.T) { + p := conf.Map(map[string]any{ + "host": "localhost", + "port": "8080", + "url": "http://${host}:${port}", + }) + + var s struct { + URL string `value:"${url}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.URL).Equal("http://localhost:8080") + }) + + t.Run("nested property reference", func(t *testing.T) { + p := conf.Map(map[string]any{ + "protocol": "https", + "host": "example.com", + "port": "443", + "path": "api", + "url": "${protocol}://${host}:${port}/${path}", + }) + + var s struct { + URL string `value:"${url}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.URL).Equal("https://example.com:443/api") + }) +} + +func TestResolveString(t *testing.T) { + t.Run("unbalanced braces", func(t *testing.T) { + _, err := conf.ParseTag("${key") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") + }) + + t.Run("missing property", func(t *testing.T) { + p := conf.New() + + var s struct { + Value string `value:"${missing}"` + } + + err := p.Bind(&s) + assert.Error(t, err).Matches("property \"missing\" not exist") + }) + + t.Run("missing property with default", func(t *testing.T) { + p := conf.New() + + var s struct { + Value string `value:"${missing:=default}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Value).Equal("default") + }) +} + +func TestMapBinding(t *testing.T) { + t.Run("map success", func(t *testing.T) { + p := conf.Map(map[string]any{ + "config": map[string]any{ + "a": 1, + "b": 2, + "c": 3, + }, + }) + + var s struct { + Config map[string]int `value:"${config}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Config).Equal(map[string]int{ + "a": 1, + "b": 2, + "c": 3, + }) + }) + + t.Run("empty map", func(t *testing.T) { + p := conf.Map(map[string]any{ + "config": map[string]any{}, + }) + + var s struct { + Config map[string]int `value:"${config}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Config).Equal(map[string]int{}) + }) +} + +func TestSliceBinding(t *testing.T) { + t.Run("int slice from comma separated string", func(t *testing.T) { + p := conf.Map(map[string]any{ + "numbers": "1,2,3,4,5", + }) + + var s struct { + Numbers []int `value:"${numbers}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Numbers).Equal([]int{1, 2, 3, 4, 5}) + }) + + t.Run("string slice with whitespace", func(t *testing.T) { + p := conf.Map(map[string]any{ + "values": " a , b , c ", + }) + + var s struct { + Values []string `value:"${values}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Values).Equal([]string{"a", "b", "c"}) + }) + + t.Run("bool slice", func(t *testing.T) { + p := conf.Map(map[string]any{ + "flags": "true,false,true", + }) + + var s struct { + Flags []bool `value:"${flags}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Flags).Equal([]bool{true, false, true}) + }) +} + +func TestStructBinding(t *testing.T) { + t.Run("nested struct", func(t *testing.T) { + p := conf.Map(map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": 25, + }, + }) + + var s struct { + User Data `value:"${user}"` + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.User).Equal(Data{ + Name: "Alice", + Age: 25, + }) + }) + + t.Run("embedded struct", func(t *testing.T) { + p := conf.Map(map[string]any{ + "name": "Bob", + "age": 30, + }) + + var s struct { + Data + } + + err := p.Bind(&s) + assert.That(t, err).Nil() + assert.That(t, s.Data).Equal(Data{ + Name: "Bob", + Age: 30, + }) + }) } diff --git a/conf/conf.go b/conf/conf.go index 329665d2..b2e9c557 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -119,8 +119,6 @@ Validation: package conf import ( - "errors" - "fmt" "os" "path/filepath" "reflect" @@ -128,7 +126,8 @@ import ( "strings" "time" - "github.com/go-spring/barky" + "github.com/go-spring/spring-base/barky" + "github.com/go-spring/spring-base/util" "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" @@ -144,21 +143,32 @@ var ( func init() { + // built-in readers RegisterReader(json.Read, ".json") RegisterReader(prop.Read, ".properties") RegisterReader(yaml.Read, ".yaml", ".yml") RegisterReader(toml.Read, ".toml", ".tml") + // time.Time RegisterConverter(func(s string) (time.Time, error) { - return cast.ToTimeE(strings.TrimSpace(s)) + v, err := cast.ToTimeE(strings.TrimSpace(s)) + if err != nil { + return time.Time{}, util.FormatError(err, "invalid time format: %s", s) + } + return v, nil }) + // time.Duration RegisterConverter(func(s string) (time.Duration, error) { - return time.ParseDuration(strings.TrimSpace(s)) + v, err := time.ParseDuration(strings.TrimSpace(s)) + if err != nil { + return time.Duration(0), util.FormatError(err, "invalid duration format: %s", s) + } + return v, nil }) } -// Reader parses []byte into nested map[string]any. +// Reader parses raw bytes into a nested map[string]any. type Reader func(b []byte) (map[string]any, error) // RegisterReader registers its Reader for some kind of file extension. @@ -168,10 +178,10 @@ func RegisterReader(r Reader, ext ...string) { } } -// Splitter splits string into []string by some characters. +// Splitter splits a string into a slice of strings using custom logic. type Splitter func(string) ([]string, error) -// RegisterSplitter registers a Splitter and named it. +// RegisterSplitter registers a Splitter with a given name. func RegisterSplitter(name string, fn Splitter) { splitters[name] = fn } @@ -179,30 +189,30 @@ func RegisterSplitter(name string, fn Splitter) { // Converter converts a string to a target type T. type Converter[T any] func(string) (T, error) -// RegisterConverter registers its converter for non-primitive type such as -// time.Time, time.Duration, or other user-defined value type. +// RegisterConverter registers a Converter for a non-primitive type such as +// time.Time, time.Duration, or other user-defined value types. func RegisterConverter[T any](fn Converter[T]) { t := reflect.TypeFor[T]() converters[t] = fn } -// Properties is the interface for read-only properties. +// Properties defines the read-only interface for accessing configuration data. type Properties interface { - // Data returns key-value pairs of the properties. + // Data returns all key-value pairs as a flat map. Data() map[string]string - // Keys returns keys of the properties. + // Keys returns all keys. Keys() []string - // SubKeys returns the sorted sub keys of the key. + // SubKeys returns the sorted sub-keys of a given key. SubKeys(key string) ([]string, error) - // Has returns whether the key exists. + // Has checks whether a key exists. Has(key string) bool - // Get returns key's value. + // Get returns the value for a given key, with an optional default. Get(key string, def ...string) string - // Resolve resolves string that contains references. + // Resolve resolves placeholders inside a string (e.g. ${key:=default}). Resolve(s string) (string, error) - // Bind binds properties into a value. + // Bind binds property values into a target object (struct, map, slice, or primitive). Bind(i any, tag ...string) error - // CopyTo copies properties into another by override. + // CopyTo copies properties into another instance, overriding existing values. CopyTo(out *MutableProperties) error } @@ -210,11 +220,13 @@ var _ Properties = (*MutableProperties)(nil) // MutableProperties stores the data with map[string]string and the keys are case-sensitive, // you can get one of them by its key, or bind some of them to a value. +// // There are too many formats of configuration files, and too many conflicts between // them. Each format of configuration file provides its special characteristics, but // usually they are not all necessary, and complementary. For example, `conf` disabled // Java properties' expansion when reading file, but also provides similar function // when getting or binding properties. +// // A good rule of thumb is that treating application configuration as a tree, but not // all formats of configuration files designed as a tree or not ideal, for instance // Java properties isn't strictly verified. Although configuration can store as a tree, @@ -224,38 +236,40 @@ type MutableProperties struct { *barky.Storage } -// New creates empty *MutableProperties. +// New creates a new empty MutableProperties instance. func New() *MutableProperties { return &MutableProperties{ Storage: barky.NewStorage(), } } -// Load creates *MutableProperties from file. +// Load creates a MutableProperties instance from a configuration file. +// Returns an error if the file type is not supported or parsing fails. func Load(file string) (*MutableProperties, error) { b, err := os.ReadFile(file) if err != nil { - return nil, err + return nil, util.FormatError(err, "read file %s error", file) } ext := filepath.Ext(file) r, ok := readers[ext] if !ok { - return nil, fmt.Errorf("unsupported file type %s", ext) + err = util.FormatError(nil, "unsupported file type %s", ext) + return nil, util.FormatError(err, "read file %s error", file) } m, err := r(b) if err != nil { - return nil, err + return nil, util.FormatError(err, "read file %s error", file) } p := New() _ = p.merge(barky.FlattenMap(m), file) return p, nil } -// Map creates *MutableProperties from map. -func Map(m map[string]any) *MutableProperties { +// Map creates a MutableProperties instance directly from a map. +func Map(data map[string]any) *MutableProperties { p := New() _, file, _, _ := runtime.Caller(1) - _ = p.merge(barky.FlattenMap(m), file) + _ = p.merge(barky.FlattenMap(data), file) return p } @@ -270,29 +284,32 @@ func (p *MutableProperties) merge(m map[string]string, file string) error { return nil } -// Resolve resolves string value that contains references to other -// properties, the references are defined by ${key:=def}. +// Resolve resolves placeholders in a string, replacing references like +// ${key:=default} with their actual values from the properties. func (p *MutableProperties) Resolve(s string) (string, error) { return resolveString(p, s) } -// Bind binds properties to a value, the bind value can be primitive type, -// map, slice, struct. When binding to struct, the tag 'value' indicates -// which properties should be bind. The 'value' tag are defined by -// value:"${a:=b}>>splitter", 'a' is the key, 'b' is the default value, -// 'splitter' is the Splitter's name when you want split string value -// into []string value. +// Bind maps property values into the provided target object. +// Supported targets: primitive values, maps, slices, and structs. +// Struct binding uses the `value` tag in the form: +// +// value:"${key:=default}>>splitter" +// +// - key: property key +// - default: default value if key is missing +// - splitter: registered splitter name for splitting into []string func (p *MutableProperties) Bind(i any, tag ...string) error { var v reflect.Value { - switch e := i.(type) { + switch refVal := i.(type) { case reflect.Value: - v = e + v = refVal default: v = reflect.ValueOf(i) if v.Kind() != reflect.Ptr { - return errors.New("should be a ptr") + return util.FormatError(nil, "should be a pointer but %T", i) } v = v.Elem() } @@ -300,25 +317,26 @@ func (p *MutableProperties) Bind(i any, tag ...string) error { t := v.Type() typeName := t.Name() - if typeName == "" { // primitive type has no name + if typeName == "" { // primitive types have no name typeName = t.String() } s := "${ROOT}" - if len(tag) > 0 { + if len(tag) > 0 && tag[0] != "" { s = tag[0] } var param BindParam err := param.BindTag(s, "") if err != nil { - return err + return util.FormatError(err, "bind tag '%s' error", s) } param.Path = typeName return BindValue(p, v, t, param, nil) } -// CopyTo copies properties into another by override. +// CopyTo copies all properties into another MutableProperties instance, +// overriding values if keys already exist. func (p *MutableProperties) CopyTo(out *MutableProperties) error { rawFile := p.RawFile() newfile := make(map[string]int8) diff --git a/conf/conf_test.go b/conf/conf_test.go index ebd441ba..75917aed 100644 --- a/conf/conf_test.go +++ b/conf/conf_test.go @@ -21,7 +21,7 @@ import ( "strings" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) @@ -42,32 +42,33 @@ func TestProperties_Load(t *testing.T) { t.Run("file not exist", func(t *testing.T) { _, err := conf.Load("./testdata/config/app.tcl") - assert.ThatError(t, err).Matches("no such file or directory") + assert.Error(t, err).Matches("no such file or directory") }) t.Run("unsupported ext", func(t *testing.T) { _, err := conf.Load("./testdata/config/app.unknown") - assert.ThatError(t, err).Matches("unsupported file type .unknown") + assert.Error(t, err).Matches("unsupported file type .unknown") }) t.Run("syntax error", func(t *testing.T) { _, err := conf.Load("./testdata/config/err.yaml") - assert.ThatError(t, err).Matches("did not find expected node content") + assert.Error(t, err).Matches("did not find expected node content") }) } func TestProperties_Resolve(t *testing.T) { - t.Run("success - 1", func(t *testing.T) { + t.Run("success", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) + s, err := p.Resolve("${a.b.c[0]}") assert.That(t, err).Nil() assert.That(t, s).Equal("3") }) - t.Run("success - 2", func(t *testing.T) { + t.Run("success with default", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) @@ -76,7 +77,7 @@ func TestProperties_Resolve(t *testing.T) { assert.That(t, s).Equal("3") }) - t.Run("default", func(t *testing.T) { + t.Run("key with default", func(t *testing.T) { p := conf.New() s, err := p.Resolve("${a.b.c:=123}") assert.That(t, err).Nil() @@ -86,36 +87,35 @@ func TestProperties_Resolve(t *testing.T) { t.Run("key not exist", func(t *testing.T) { p := conf.New() _, err := p.Resolve("${a.b.c}") - assert.ThatError(t, err).Matches("property a.b.c not exist") + assert.Error(t, err).Matches("property \"a.b.c\" not exist") }) - t.Run("syntax error - 1", func(t *testing.T) { + t.Run("array property as string", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) _, err := p.Resolve("${a.b.c}") - assert.ThatError(t, err).Matches("property a.b.c isn't simple value") + assert.Error(t, err).Matches("property \"a.b.c\" isn't simple value") }) - t.Run("syntax error - 2", func(t *testing.T) { + t.Run("missing bracket", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) _, err := p.Resolve("${a.b.c") - assert.ThatError(t, err).Matches("resolve string .* error: invalid syntax") + assert.Error(t, err).Matches("resolve string .* error: invalid syntax") }) - t.Run("syntax error - 3", func(t *testing.T) { + t.Run("invalid expression", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) _, err := p.Resolve("${a.b.c[0]}==${a.b.c}") - assert.ThatError(t, err).Matches("property a.b.c isn't simple value") + assert.Error(t, err).Matches("property \"a.b.c\" isn't simple value") }) } func TestProperties_CopyTo(t *testing.T) { - t.Run("success", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, @@ -155,7 +155,7 @@ func TestProperties_CopyTo(t *testing.T) { }) }) - t.Run("error", func(t *testing.T) { + t.Run("type conflict", func(t *testing.T) { p := conf.Map(map[string]any{ "a.b.c": []string{"3"}, }) @@ -169,7 +169,7 @@ func TestProperties_CopyTo(t *testing.T) { assert.That(t, s.Get("a.b.c")).Equal("3") err := p.CopyTo(s) - assert.ThatError(t, err).Matches("property conflict at path a.b.c\\[0]") + assert.Error(t, err).Matches("property conflict at path a.b.c\\[0]") }) } diff --git a/conf/expr.go b/conf/expr.go index e71054cd..01a24d17 100644 --- a/conf/expr.go +++ b/conf/expr.go @@ -17,10 +17,10 @@ package conf import ( - "fmt" "maps" "github.com/expr-lang/expr" + "github.com/go-spring/spring-base/util" ) // ValidateFunc defines a type for validation functions, which accept @@ -44,14 +44,14 @@ func validateField(tag string, i any) error { maps.Copy(env, validateFuncs) r, err := expr.Eval(tag, env) if err != nil { - return fmt.Errorf("eval %q returns error, %w", tag, err) + return util.FormatError(err, "eval %q returns error", tag) } ret, ok := r.(bool) if !ok { - return fmt.Errorf("eval %q doesn't return bool value", tag) + return util.FormatError(nil, "eval %q doesn't return bool value", tag) } if !ret { - return fmt.Errorf("validate failed on %q for value %v", tag, i) + return util.FormatError(nil, "validate failed on %q for value %v", tag, i) } return nil } diff --git a/conf/expr_test.go b/conf/expr_test.go index 1aeb14ca..4c565453 100644 --- a/conf/expr_test.go +++ b/conf/expr_test.go @@ -19,7 +19,7 @@ package conf_test import ( "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) @@ -28,7 +28,7 @@ func TestExpr(t *testing.T) { return i < 5 }) - t.Run("success", func(t *testing.T) { + t.Run("basic function validation", func(t *testing.T) { var v struct { A int `value:"${a}" expr:"checkInt($)"` } @@ -40,7 +40,31 @@ func TestExpr(t *testing.T) { assert.That(t, 4).Equal(v.A) }) - t.Run("return false", func(t *testing.T) { + t.Run("constant expression", func(t *testing.T) { + var v struct { + A int `value:"${a}" expr:"$ < 10"` + } + p := conf.Map(map[string]any{ + "a": 5, + }) + err := p.Bind(&v) + assert.That(t, err).Nil() + assert.That(t, 5).Equal(v.A) + }) + + t.Run("complex expression", func(t *testing.T) { + var v struct { + A int `value:"${a}" expr:"$ >= 1 && $ <= 3"` + } + p := conf.Map(map[string]any{ + "a": 2, + }) + err := p.Bind(&v) + assert.That(t, err).Nil() + assert.That(t, 2).Equal(v.A) + }) + + t.Run("validation failure", func(t *testing.T) { var v struct { A int `value:"${a}" expr:"checkInt($)"` } @@ -48,7 +72,7 @@ func TestExpr(t *testing.T) { "a": 14, }) err := p.Bind(&v) - assert.ThatError(t, err).Matches("validate failed on .* for value 14") + assert.Error(t, err).Matches("validate failed on .* for value 14") }) t.Run("syntax error", func(t *testing.T) { @@ -59,7 +83,7 @@ func TestExpr(t *testing.T) { "a": 4, }) err := p.Bind(&v) - assert.ThatError(t, err).Matches("eval .* returns error") + assert.Error(t, err).Matches("eval .* returns error") }) t.Run("return not bool", func(t *testing.T) { @@ -70,6 +94,29 @@ func TestExpr(t *testing.T) { "a": 4, }) err := p.Bind(&v) - assert.ThatError(t, err).Matches("eval .* doesn't return bool value") + assert.Error(t, err).Matches("eval .* doesn't return bool value") + }) + + t.Run("unregistered function", func(t *testing.T) { + var v struct { + A int `value:"${a}" expr:"unknownFunc($)"` + } + p := conf.Map(map[string]any{ + "a": 5, + }) + err := p.Bind(&v) + assert.Error(t, err).Matches("eval .* returns error") + }) + + t.Run("empty expression", func(t *testing.T) { + var v struct { + A int `value:"${a}" expr:""` + } + p := conf.Map(map[string]any{ + "a": 5, + }) + err := p.Bind(&v) + assert.That(t, err).Nil() + assert.That(t, 5).Equal(v.A) }) } diff --git a/conf/reader/json/json.go b/conf/reader/json/json.go index 654613c4..2f6ef603 100644 --- a/conf/reader/json/json.go +++ b/conf/reader/json/json.go @@ -18,14 +18,15 @@ package json import ( "encoding/json" + + "github.com/go-spring/spring-base/util" ) // Read parses []byte in the json format into map. func Read(b []byte) (map[string]any, error) { var ret map[string]any - err := json.Unmarshal(b, &ret) - if err != nil { - return nil, err + if err := json.Unmarshal(b, &ret); err != nil { + return nil, util.FormatError(err, "read json error") } return ret, nil } diff --git a/conf/reader/json/json_test.go b/conf/reader/json/json_test.go index 1943e700..9b9f8aaf 100644 --- a/conf/reader/json/json_test.go +++ b/conf/reader/json/json_test.go @@ -19,17 +19,17 @@ package json import ( "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestRead(t *testing.T) { - t.Run("error", func(t *testing.T) { + t.Run("invalid json format", func(t *testing.T) { _, err := Read([]byte(`{`)) - assert.ThatError(t, err).Matches("unexpected end of JSON input") + assert.Error(t, err).Matches("unexpected end of JSON input") }) - t.Run("basic type", func(t *testing.T) { + t.Run("basic data types", func(t *testing.T) { r, err := Read([]byte(`{ "empty": "", "bool": false, @@ -50,4 +50,52 @@ func TestRead(t *testing.T) { "time": "2018-02-17T15:02:31+08:00", }) }) + + t.Run("nested objects", func(t *testing.T) { + r, err := Read([]byte(`{ + "user": { + "name": "Alice", + "age": 30 + }, + "active": true + }`)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{ + "user": map[string]any{ + "name": "Alice", + "age": float64(30), + }, + "active": true, + }) + }) + + t.Run("arrays", func(t *testing.T) { + r, err := Read([]byte(`{ + "tags": ["go", "spring"], + "numbers": [1, 2] + }`)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{ + "tags": []any{"go", "spring"}, + "numbers": []any{float64(1), float64(2)}, + }) + }) + + t.Run("null values", func(t *testing.T) { + r, err := Read([]byte(`{ + "value": null, + "name": "test" + }`)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{ + "value": nil, + "name": "test", + }) + }) + + t.Run("empty object", func(t *testing.T) { + r, err := Read([]byte(`{}`)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{}) + }) } diff --git a/conf/reader/prop/prop.go b/conf/reader/prop/prop.go index 98356c15..0f4e43c6 100644 --- a/conf/reader/prop/prop.go +++ b/conf/reader/prop/prop.go @@ -16,7 +16,10 @@ package prop -import "github.com/magiconair/properties" +import ( + "github.com/go-spring/spring-base/util" + "github.com/magiconair/properties" +) // Read parses []byte in the properties format into map. func Read(b []byte) (map[string]any, error) { @@ -24,7 +27,7 @@ func Read(b []byte) (map[string]any, error) { p := properties.NewProperties() p.DisableExpansion = true if err := p.Load(b, properties.UTF8); err != nil { - return nil, err + return nil, util.FormatError(err, "read properties error") } ret := make(map[string]any) diff --git a/conf/reader/prop/prop_test.go b/conf/reader/prop/prop_test.go index 6eaf781b..ebfaad0a 100644 --- a/conf/reader/prop/prop_test.go +++ b/conf/reader/prop/prop_test.go @@ -19,14 +19,14 @@ package prop import ( "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestRead(t *testing.T) { - t.Run("error", func(t *testing.T) { + t.Run("invalid properties format", func(t *testing.T) { _, err := Read([]byte(`=1`)) - assert.ThatError(t, err).Matches(`properties: Line 1: "1"`) + assert.Error(t, err).Matches(`properties: Line 1: "1"`) }) t.Run("basic type", func(t *testing.T) { @@ -92,7 +92,6 @@ func TestRead(t *testing.T) { }) t.Run("map with struct", func(t *testing.T) { - r, err := Read([]byte(` map.k1.bool=false map.k1.int=3 @@ -115,4 +114,32 @@ func TestRead(t *testing.T) { "map.k2.string": "hello", }) }) + + t.Run("escape sequences", func(t *testing.T) { + r, err := Read([]byte(` + key1=value\nwith\nnewlines + key2=value\twith\ttabs + key3=unicode\u0041 + `)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{ + "key1": "value\nwith\nnewlines", + "key2": "value\twith\ttabs", + "key3": "unicodeA", + }) + }) + + t.Run("special characters", func(t *testing.T) { + r, err := Read([]byte(` + key.with.dots=value + key-with-dashes=value + unicode_key=值 + `)) + assert.That(t, err).Nil() + assert.That(t, r).Equal(map[string]any{ + "key.with.dots": "value", + "key-with-dashes": "value", + "unicode_key": "值", + }) + }) } diff --git a/conf/reader/toml/toml.go b/conf/reader/toml/toml.go index e290dd06..3994035e 100644 --- a/conf/reader/toml/toml.go +++ b/conf/reader/toml/toml.go @@ -17,6 +17,7 @@ package toml import ( + "github.com/go-spring/spring-base/util" "github.com/pelletier/go-toml" ) @@ -24,7 +25,7 @@ import ( func Read(b []byte) (map[string]any, error) { tree, err := toml.LoadBytes(b) if err != nil { - return nil, err + return nil, util.FormatError(err, "read toml error") } return tree.ToMap(), nil } diff --git a/conf/reader/toml/toml_test.go b/conf/reader/toml/toml_test.go index 0d0267f0..58641f85 100644 --- a/conf/reader/toml/toml_test.go +++ b/conf/reader/toml/toml_test.go @@ -19,14 +19,14 @@ package toml import ( "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestRead(t *testing.T) { - t.Run("error", func(t *testing.T) { + t.Run("invalid toml format", func(t *testing.T) { _, err := Read([]byte(`{`)) - assert.ThatError(t, err).Matches("parsing error: keys cannot contain { character") + assert.Error(t, err).Matches("parsing error: keys cannot contain { character") }) t.Run("basic type", func(t *testing.T) { diff --git a/conf/reader/yaml/yaml.go b/conf/reader/yaml/yaml.go index 3a1507b0..8f61a212 100644 --- a/conf/reader/yaml/yaml.go +++ b/conf/reader/yaml/yaml.go @@ -17,15 +17,15 @@ package yaml import ( + "github.com/go-spring/spring-base/util" "gopkg.in/yaml.v2" ) // Read parses []byte in the yaml format into map. func Read(b []byte) (map[string]any, error) { ret := make(map[string]any) - err := yaml.Unmarshal(b, &ret) - if err != nil { - return nil, err + if err := yaml.Unmarshal(b, &ret); err != nil { + return nil, util.FormatError(err, "read yaml error") } return ret, nil } diff --git a/conf/reader/yaml/yaml_test.go b/conf/reader/yaml/yaml_test.go index 89296225..2eb4f7a6 100644 --- a/conf/reader/yaml/yaml_test.go +++ b/conf/reader/yaml/yaml_test.go @@ -20,14 +20,14 @@ import ( "strings" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestRead(t *testing.T) { - t.Run("error", func(t *testing.T) { + t.Run("invalid yaml format", func(t *testing.T) { _, err := Read([]byte(`{`)) - assert.ThatError(t, err).Matches("did not find expected node content") + assert.Error(t, err).Matches("did not find expected node content") }) t.Run("basic type", func(t *testing.T) { diff --git a/docs/0. overview/overview.md b/docs/0. overview/overview.md deleted file mode 100644 index 5a0e6a0d..00000000 --- a/docs/0. overview/overview.md +++ /dev/null @@ -1 +0,0 @@ -todo 项目概览、目标、特性,解决了什么问题 \ No newline at end of file diff --git a/docs/1. getting-started/getting-started.md b/docs/1. getting-started/getting-started.md deleted file mode 100644 index 66818101..00000000 --- a/docs/1. getting-started/getting-started.md +++ /dev/null @@ -1 +0,0 @@ -todo 快速开始、安装、HelloWorld、项目结构引导 \ No newline at end of file diff --git a/docs/2. concepts/concepts.md b/docs/2. concepts/concepts.md deleted file mode 100644 index 15bcb69a..00000000 --- a/docs/2. concepts/concepts.md +++ /dev/null @@ -1 +0,0 @@ -todo 核心架构理念、DI、配置系统、生命周期管理等 \ No newline at end of file diff --git a/docs/3. guides/guides.md b/docs/3. guides/guides.md deleted file mode 100644 index 2bbc33ce..00000000 --- a/docs/3. guides/guides.md +++ /dev/null @@ -1 +0,0 @@ -todo 常见操作,如创建服务、注入 Bean、读取配置、测试 \ No newline at end of file diff --git a/docs/4. examples/bookman/README.md b/docs/4. examples/bookman/README.md deleted file mode 100644 index e11d631e..00000000 --- a/docs/4. examples/bookman/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# BookMan - -[中文](README_CN.md) - -## 1. Directory Structure - -```text -conf/ Configuration files -log/ Log files -public/ Static files -src/ Source code - app/ Startup phase files - bootstrap/ Bootstrap files - common/ Common modules for startup - handlers/ Startup component handlers - log/ Logging component - httpsvr/ HTTP server module - controller/ Controller modules - biz/ Business logic modules - job/ Background job modules - service/ Business service modules - dao/ Data access layer - idl/ Interface definition files - http/ HTTP service interfaces - proto/ Generated protocol code - sdk/ Wrapped SDK modules -``` - -**Directory Structure Features**: - -- **Modular design** with clear separation of responsibilities. -- **Classic structure** for easy development, management, and scalability. -- **Maintainability** supporting continuous iteration for large-scale applications. - -## 2. Functionality Overview - -### 2.1 Bootstrap Phase Configuration Management - -- Fetch configuration files remotely and save them locally. -- Register configuration refresh beans during the startup phase. -- Related file: `src/app/bootstrap/bootstrap.go` - -### 2.2 Logging Component Initialization - -- Load and parse local configuration files during the startup phase. -- Create logging components based on the configuration. -- Related file: `src/app/common/handlers/log/log.go` - -### 2.3 HTTP Server Initialization - -- Create an HTTP server during the startup phase. -- Register HTTP service routes. -- Related file: `src/app/common/httpsvr/httpsvr.go` - -### 2.4 Controller Grouping and Management - -- Group controller methods based on functionality. -- Independently inject and manage each sub-controller. -- Related files: - - `src/app/controller/controller.go` - - `src/app/controller/controller-book.go` - -### 2.5 Dynamic Configuration Refresh - -- Support dynamic configuration refresh at runtime. -- Related file: `src/biz/service/book_service/book_service.go` - -### 2.6 Graceful Shutdown of Background Jobs - -- Ensure background tasks shut down gracefully, preserving data integrity and releasing resources properly. -- Related file: `src/biz/job/job.go` - -## 3. Summary - -This project follows modular, clear, maintainable, and extensible design principles, making it suitable for the -development needs of medium to large-scale systems. It implements a complete and robust architecture with modules for -bootstrapping, logging management, HTTP services, dynamic configuration refreshing, and background job handling. \ No newline at end of file diff --git a/docs/4. examples/bookman/README_CN.md b/docs/4. examples/bookman/README_CN.md deleted file mode 100644 index 474f8bf3..00000000 --- a/docs/4. examples/bookman/README_CN.md +++ /dev/null @@ -1,76 +0,0 @@ -# BookMan - -[English](README.md) - -## 一、目录结构 - -```text -conf/ 配置文件目录 -log/ 日志文件目录 -public/ 静态文件目录 -src/ 源文件目录 - app/ 启动阶段文件 - bootstrap/ 引导阶段文件 - common/ 启动阶段公共模块 - handlers/ 启动阶段组件 - log/ 日志组件 - httpsvr/ HTTP 服务模块 - controller/ 控制器模块 - biz/ 业务逻辑模块 - job/ 后台任务模块 - service/ 业务服务模块 - dao/ 数据访问层 - idl/ 接口描述文件 - http/ HTTP 服务接口 - proto/ 生成的协议代码 - sdk/ 封装的 SDK 文件 -``` - -**目录结构特点**: - -- **模块化设计**,清晰划分各层职责。 -- **经典结构**,便于开发、管理和扩展。 -- **易于维护**,支持大规模应用持续迭代。 - -## 二、功能描述 - -### 2.1 引导阶段配置管理 - -- 从远程拉取配置文件并保存至本地。 -- 向启动阶段注册配置刷新的 Bean。 -- 相关文件:`src/app/bootstrap/bootstrap.go` - -### 2.2 日志组件初始化 - -- 启动阶段读取并解析本地配置。 -- 根据配置创建日志组件。 -- 相关文件:`src/app/common/handlers/log/log.go` - -### 2.3 HTTP 服务器启动 - -- 启动阶段创建 HTTP 服务器。 -- 注册 HTTP 服务路由。 -- 相关文件:`src/app/common/httpsvr/httpsvr.go` - -### 2.4 控制器功能分组管理 - -- 根据功能对 Controller 方法进行分组。 -- 每个子 Controller 独立注入和管理。 -- 相关文件: - - `src/app/controller/controller.go` - - `src/app/controller/controller-book.go` - -### 2.5 动态配置刷新 - -- 支持运行时动态刷新配置。 -- 相关文件:`src/biz/service/book_service/book_service.go` - -### 2.6 后台任务优雅退出 - -- 后台任务支持优雅停止,保证数据安全和资源释放。 -- 相关文件:`src/biz/job/job.go` - -## 三、总结 - -本项目遵循模块化、清晰、可维护、可扩展的设计原则,适合中大型系统的开发需求。通过引导配置、日志管理、HTTP -服务、动态刷新、后台任务管理等功能模块,实现了完整、健壮的应用架构。 \ No newline at end of file diff --git a/docs/4. examples/bookman/conf/app-test.properties b/docs/4. examples/bookman/conf/app-test.properties deleted file mode 100755 index 0f7d5d27..00000000 --- a/docs/4. examples/bookman/conf/app-test.properties +++ /dev/null @@ -1,7 +0,0 @@ -server.addr=0.0.0.0:9090 - -log.biz.name=biz.log -log.biz.dir=./log - -log.dao.name=dao.log -log.dao.dir=./log \ No newline at end of file diff --git a/docs/4. examples/bookman/conf/app.properties b/docs/4. examples/bookman/conf/app.properties deleted file mode 100755 index 1d1906b2..00000000 --- a/docs/4. examples/bookman/conf/app.properties +++ /dev/null @@ -1 +0,0 @@ -server.addr=0.0.0.0:8080 \ No newline at end of file diff --git a/docs/4. examples/bookman/go.mod b/docs/4. examples/bookman/go.mod deleted file mode 100644 index e415794f..00000000 --- a/docs/4. examples/bookman/go.mod +++ /dev/null @@ -1,22 +0,0 @@ -module bookman - -go 1.24 - -require ( - github.com/go-spring/gs-assert v1.0.2 - github.com/go-spring/spring-core v0.0.0 - github.com/lvan100/go-loop v0.0.3 -) - -require ( - 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.5 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/cast v1.9.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../ diff --git a/docs/4. examples/bookman/go.sum b/docs/4. examples/bookman/go.sum deleted file mode 100644 index 213c55c3..00000000 --- a/docs/4. examples/bookman/go.sum +++ /dev/null @@ -1,32 +0,0 @@ -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/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lvan100/go-loop v0.0.3 h1:s9nlQpPalVITSsMwlxTR+PlaC4CqODnL2qJJYGHwKsU= -github.com/lvan100/go-loop v0.0.3/go.mod h1:YDtb+ZmMigBqRJqSwD4S/3mGFyY2cF/LCezXEWfwvG4= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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.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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/bookman/init.go b/docs/4. examples/bookman/init.go deleted file mode 100644 index 54932e4f..00000000 --- a/docs/4. examples/bookman/init.go +++ /dev/null @@ -1,59 +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 main - -import ( - "fmt" - "os" - "path/filepath" - "runtime" - - "github.com/go-spring/spring-core/gs" -) - -const banner = ` - ____ _ __ __ - | __ ) ___ ___ | | __| \/ | __ _ _ __ - | _ \ / _ \ / _ \ | |/ /| |\/| | / _' || '_ \ - | |_) || (_) || (_) || < | | | || (_| || | | | - |____/ \___/ \___/ |_|\_\|_| |_| \__,_||_| |_| -` - -func init() { - gs.Banner(banner) -} - -// init sets the working directory of the application to the directory -// where this source file resides. -// This ensures that any relative file operations are based on the source file location, -// not the process launch path. -func init() { - var execDir string - _, filename, _, ok := runtime.Caller(0) - if ok { - execDir = filepath.Dir(filename) - } - err := os.Chdir(execDir) - if err != nil { - panic(err) - } - workDir, err := os.Getwd() - if err != nil { - panic(err) - } - fmt.Println(workDir) -} diff --git a/docs/4. examples/bookman/main.go b/docs/4. examples/bookman/main.go deleted file mode 100644 index 7ee46a03..00000000 --- a/docs/4. examples/bookman/main.go +++ /dev/null @@ -1,68 +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 main - -import ( - "context" - "fmt" - "io" - "net/http" - "time" - - "github.com/go-spring/spring-core/gs" - "github.com/lvan100/go-loop" - - _ "bookman/src/app" - _ "bookman/src/biz" -) - -func init() { - gs.SetActiveProfiles("online") - gs.EnableSimplePProfServer(true) - gs.FuncJob(runTest).Name("#job") -} - -func main() { - gs.Run() -} - -// runTest performs a simple test. -func runTest(ctx context.Context) error { - time.Sleep(time.Millisecond * 500) - - loop.Times(5, func(_ int) { - url := "http://127.0.0.1:9090/books" - resp, err := http.Get(url) - if err != nil { - panic(err) - } - b, err := io.ReadAll(resp.Body) - if err != nil { - panic(err) - } - defer func() { - err = resp.Body.Close() - _ = err - }() - fmt.Print(string(b)) - time.Sleep(time.Millisecond * 400) - }) - - // Shut down the application gracefully - gs.ShutDown() - return nil -} diff --git a/docs/4. examples/bookman/public/index.html b/docs/4. examples/bookman/public/index.html deleted file mode 100644 index 615e8730..00000000 --- a/docs/4. examples/bookman/public/index.html +++ /dev/null @@ -1 +0,0 @@ -It Works! \ No newline at end of file diff --git a/docs/4. examples/bookman/src/app/app.go b/docs/4. examples/bookman/src/app/app.go deleted file mode 100644 index ae5a7abb..00000000 --- a/docs/4. examples/bookman/src/app/app.go +++ /dev/null @@ -1,24 +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 ( - _ "bookman/src/app/bootstrap" - _ "bookman/src/app/common/handlers/log" - _ "bookman/src/app/common/httpsvr" - _ "bookman/src/app/controller" -) diff --git a/docs/4. examples/bookman/src/app/bootstrap/bootstrap.go b/docs/4. examples/bookman/src/app/bootstrap/bootstrap.go deleted file mode 100644 index 5384a5f2..00000000 --- a/docs/4. examples/bookman/src/app/bootstrap/bootstrap.go +++ /dev/null @@ -1,98 +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 bootstrap - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - // Register a function runner to initialize the remote configuration setup. - gs.B.FuncRunner(initRemoteConfig).OnProfiles("online") -} - -// initRemoteConfig initializes the remote configuration setup. -// It first attempts to retrieve remote config, then starts a background job -// to periodically refresh the configuration. -func initRemoteConfig() error { - if err := getRemoteConfig(); err != nil { - return err - } - // Register a function job to refresh the configuration. - // A bean can be registered into the app during the bootstrap phase. - gs.FuncJob(refreshRemoteConfig) - return nil -} - -// getRemoteConfig fetches and writes the remote configuration to a local file. -// It creates necessary directories and generates a properties file containing. -func getRemoteConfig() error { - err := os.MkdirAll("./conf/remote", os.ModePerm) - if err != nil { - return err - } - - const data = ` - server.addr=0.0.0.0:9090 - - log.access.name=access.log - log.access.dir=./log - - log.biz.name=biz.log - log.biz.dir=./log - - log.dao.name=dao.log - log.dao.dir=./log - - refresh_time=%v - ` - - const file = "conf/remote/app-online.properties" - str := fmt.Sprintf(data, time.Now().UnixMilli()) - return os.WriteFile(file, []byte(str), os.ModePerm) -} - -// refreshRemoteConfig runs a continuous loop to periodically update configuration. -// It refreshes every 500ms until context cancellation. -func refreshRemoteConfig(ctx context.Context) error { - for { - select { - case <-ctx.Done(): // Gracefully exit when context is canceled. - // The context (ctx) is derived from the app instance. - // When the app exits, the context is canceled, - // allowing the loop to terminate gracefully. - fmt.Println("config updater exit") - return nil - case <-time.After(time.Millisecond * 500): - if err := getRemoteConfig(); err != nil { - fmt.Println("get remote config error:", err) - return err - } - // Refreshes the app configuration. - if err := gs.RefreshProperties(); err != nil { - fmt.Println("refresh properties error:", err) - return err - } - fmt.Println("refresh properties success") - } - } -} 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 deleted file mode 100644 index 9cb7225a..00000000 --- a/docs/4. examples/bookman/src/app/common/handlers/log/log.go +++ /dev/null @@ -1,64 +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 log - -import ( - "log/slog" - "os" - "path/filepath" - - "github.com/go-spring/spring-core/conf" - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Module(nil, func(p conf.Properties) error { - - var loggers map[string]struct { - Name string `value:"${name}"` // Log file name - Dir string `value:"${dir}"` // Directory where the log file will be stored - } - - // Bind configuration from the "${log}" node into the 'loggers' map. - err := p.Bind(&loggers, "${log}") - if err != nil { - return err - } - - for k, l := range loggers { - var ( - f *os.File - flag = os.O_WRONLY | os.O_CREATE | os.O_APPEND - ) - - // Open (or create) the log file - f, err = os.OpenFile(filepath.Join(l.Dir, l.Name), flag, os.ModePerm) - if err != nil { - 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 - gs.Object(o).Name(k).Destroy(func(_ *slog.Logger) { - _ = f.Close() - }) - } - return nil - }) -} diff --git a/docs/4. examples/bookman/src/app/common/httpsvr/httpsvr.go b/docs/4. examples/bookman/src/app/common/httpsvr/httpsvr.go deleted file mode 100644 index 30c80a75..00000000 --- a/docs/4. examples/bookman/src/app/common/httpsvr/httpsvr.go +++ /dev/null @@ -1,57 +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 httpsvr - -import ( - "fmt" - "log/slog" - "net/http" - - "bookman/src/app/controller" - "bookman/src/idl/http/proto" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - // Registers a custom ServeMux to replace the default implementation. - gs.Provide( - NewServeMux, - gs.IndexArg(1, gs.TagArg("access")), - ) -} - -// NewServeMux creates a new HTTP request multiplexer and registers -// routes with access logging middleware. -func NewServeMux(c *controller.Controller, logger *slog.Logger) *http.ServeMux { - mux := http.NewServeMux() - proto.RegisterRouter(mux, c, Access(logger)) - - // Users can customize routes by adding handlers to the mux - mux.Handle("GET /", http.FileServer(http.Dir("./public"))) - return mux -} - -// Access is a middleware to log incoming HTTP requests. -func Access(logger *slog.Logger) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger.Info(fmt.Sprintf("access %s %s", r.Method, r.URL.Path)) - next.ServeHTTP(w, r) - }) - } -} diff --git a/docs/4. examples/bookman/src/app/controller/controller-book.go b/docs/4. examples/bookman/src/app/controller/controller-book.go deleted file mode 100644 index 151fd231..00000000 --- a/docs/4. examples/bookman/src/app/controller/controller-book.go +++ /dev/null @@ -1,75 +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 controller - -import ( - "encoding/json" - "net/http" - - "bookman/src/biz/service/book_service" - "bookman/src/dao/book_dao" -) - -type BookController struct { - BookService *book_service.BookService `autowire:""` -} - -// ListBooks handles the HTTP request to list all books. -func (c *BookController) ListBooks(w http.ResponseWriter, r *http.Request) { - books, err := c.BookService.ListBooks() - if err != nil { - _, _ = w.Write([]byte(err.Error())) - return - } - _ = json.NewEncoder(w).Encode(books) -} - -// GetBook handles the HTTP request to get details of a specific book by ISBN. -func (c *BookController) GetBook(w http.ResponseWriter, r *http.Request) { - isbn := r.PathValue("isbn") - book, err := c.BookService.GetBook(isbn) - if err != nil { - _, _ = w.Write([]byte(err.Error())) - return - } - _ = json.NewEncoder(w).Encode(book) -} - -// SaveBook handles the HTTP request to save a new book. -func (c *BookController) SaveBook(w http.ResponseWriter, r *http.Request) { - var book book_dao.Book - if err := json.NewDecoder(r.Body).Decode(&book); err != nil { - _, _ = w.Write([]byte(err.Error())) - return - } - if err := c.BookService.SaveBook(book); err != nil { - _, _ = w.Write([]byte(err.Error())) - return - } - _ = json.NewEncoder(w).Encode("OK!") -} - -// DeleteBook handles the HTTP request to delete a book by ISBN. -func (c *BookController) DeleteBook(w http.ResponseWriter, r *http.Request) { - isbn := r.PathValue("isbn") - err := c.BookService.DeleteBook(isbn) - if err != nil { - _, _ = w.Write([]byte(err.Error())) - return - } - _ = json.NewEncoder(w).Encode("OK!") -} diff --git a/docs/4. examples/bookman/src/app/controller/controller.go b/docs/4. examples/bookman/src/app/controller/controller.go deleted file mode 100644 index f5ac3416..00000000 --- a/docs/4. examples/bookman/src/app/controller/controller.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 controller - -import ( - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&Controller{}) -} - -// Controller implements the controller interface defined in the idl package. -// In practice, controller methods can be grouped into different controllers. -// Each sub-controller can have its own dependencies and be tested independently, -// making the codebase more modular and maintainable. -type Controller struct { - BookController -} diff --git a/docs/4. examples/bookman/src/biz/biz.go b/docs/4. examples/bookman/src/biz/biz.go deleted file mode 100644 index e7edc7b5..00000000 --- a/docs/4. examples/bookman/src/biz/biz.go +++ /dev/null @@ -1,21 +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 biz - -import ( - _ "bookman/src/biz/job" -) diff --git a/docs/4. examples/bookman/src/biz/job/job.go b/docs/4. examples/bookman/src/biz/job/job.go deleted file mode 100644 index ef9bb335..00000000 --- a/docs/4. examples/bookman/src/biz/job/job.go +++ /dev/null @@ -1,52 +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 job - -import ( - "context" - "fmt" - "time" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&Job{}).AsJob() -} - -type Job struct{} - -// Run executes the background job until the context is canceled. -func (x *Job) Run(ctx context.Context) error { - for { - select { - case <-ctx.Done(): - // Gracefully exit when the context is canceled - fmt.Println("job exit") - return nil - default: - // Check if the app is shutting down. - // In long-running background tasks, checking for shutdown signals - // during idle periods or between stages helps ensure timely resource cleanup. - if gs.Exiting() { - return nil - } - time.Sleep(time.Millisecond * 300) - fmt.Println(time.Now().UnixMilli(), "job sleep end") - } - } -} diff --git a/docs/4. examples/bookman/src/biz/service/book_service/book_service.go b/docs/4. examples/bookman/src/biz/service/book_service/book_service.go deleted file mode 100644 index 22813956..00000000 --- a/docs/4. examples/bookman/src/biz/service/book_service/book_service.go +++ /dev/null @@ -1,90 +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 book_service - -import ( - "fmt" - "log/slog" - "strconv" - - "bookman/src/dao/book_dao" - "bookman/src/idl/http/proto" - "bookman/src/sdk/book_sdk" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&BookService{}) -} - -type BookService struct { - BookDao *book_dao.BookDao `autowire:""` - BookSDK *book_sdk.BookSDK `autowire:""` - Logger *slog.Logger `autowire:"biz"` - RefreshTime gs.Dync[int64] `value:"${refresh_time:=0}"` -} - -// ListBooks retrieves all books from the database and enriches them with -// pricing and refresh time. -func (s *BookService) ListBooks() ([]proto.Book, error) { - books, err := s.BookDao.ListBooks() - if err != nil { - s.Logger.Error(fmt.Sprintf("ListBooks return err: %s", err.Error())) - return nil, err - } - ret := make([]proto.Book, 0, len(books)) - for _, book := range books { - ret = append(ret, proto.Book{ - ISBN: book.ISBN, - Title: book.Title, - Author: book.Author, - Publisher: book.Publisher, - Price: s.BookSDK.GetPrice(book.ISBN), - RefreshTime: strconv.FormatInt(s.RefreshTime.Value(), 10), - }) - } - return ret, nil -} - -// GetBook retrieves a single book by its ISBN and enriches it with -// pricing and refresh time. -func (s *BookService) GetBook(isbn string) (proto.Book, error) { - book, err := s.BookDao.GetBook(isbn) - if err != nil { - s.Logger.Error(fmt.Sprintf("GetBook return err: %s", err.Error())) - return proto.Book{}, err - } - return proto.Book{ - ISBN: book.ISBN, - Title: book.Title, - Author: book.Author, - Publisher: book.Publisher, - Price: s.BookSDK.GetPrice(book.ISBN), - RefreshTime: strconv.FormatInt(s.RefreshTime.Value(), 10), - }, nil -} - -// SaveBook stores a new book in the database. -func (s *BookService) SaveBook(book book_dao.Book) error { - return s.BookDao.SaveBook(book) -} - -// DeleteBook removes a book from the database by its ISBN. -func (s *BookService) DeleteBook(isbn string) error { - return s.BookDao.DeleteBook(isbn) -} diff --git a/docs/4. examples/bookman/src/biz/service/book_service/book_service_test.go b/docs/4. examples/bookman/src/biz/service/book_service/book_service_test.go deleted file mode 100644 index 75e66577..00000000 --- a/docs/4. examples/bookman/src/biz/service/book_service/book_service_test.go +++ /dev/null @@ -1,99 +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 book_service - -import ( - "testing" - - "bookman/src/dao/book_dao" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/gs" - "github.com/go-spring/spring-core/gs/gstest" -) - -func init() { - // Mock the BookDao with initial test data - gstest.MockFor[*book_dao.BookDao]().With(&book_dao.BookDao{ - Store: map[string]book_dao.Book{ - "978-0132350884": { - Title: "Clean Code", - Author: "Robert C. Martin", - ISBN: "978-0132350884", - Publisher: "Prentice Hall", - }, - }, - }) - // Load local configuration files - gs.Config().LocalFile.AddDir("../../../../conf") -} - -func TestMain(m *testing.M) { - gstest.TestMain(m) -} - -func TestBookService(t *testing.T) { - - x := gstest.Wire(t, new(struct { - SvrAddr string `value:"${server.addr}"` - Service *BookService `autowire:""` - BookDao *book_dao.BookDao `autowire:""` - })) - - // Verify server address configuration - assert.That(t, x.SvrAddr).Equal("0.0.0.0:9090") - - s, o := x.Service, x.BookDao - assert.NotNil(t, o) - - // Test listing books - books, err := s.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(1) - assert.That(t, books[0].ISBN).Equal("978-0132350884") - - // Test saving a new book - err = s.SaveBook(book_dao.Book{ - Title: "Introduction to Algorithms", - Author: "Thomas H. Cormen, Charles E. Leiserson, ...", - ISBN: "978-0262033848", - Publisher: "MIT Press", - }) - assert.Nil(t, err) - - // Verify book was added successfully - books, err = s.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(2) - assert.That(t, books[1].ISBN).Equal("978-0262033848") - assert.That(t, books[1].Title).Equal("Introduction to Algorithms") - - // Test retrieving a book by ISBN - book, err := s.GetBook("978-0132350884") - assert.Nil(t, err) - assert.That(t, book.ISBN).Equal("978-0132350884") - assert.That(t, book.Title).Equal("Clean Code") - - // Test deleting a book - err = s.DeleteBook("978-0132350884") - assert.Nil(t, err) - - // Verify book deletion - books, err = s.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(1) -} diff --git a/docs/4. examples/bookman/src/dao/book_dao/book_dao.go b/docs/4. examples/bookman/src/dao/book_dao/book_dao.go deleted file mode 100644 index 69007707..00000000 --- a/docs/4. examples/bookman/src/dao/book_dao/book_dao.go +++ /dev/null @@ -1,77 +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 book_dao - -import ( - "log/slog" - "maps" - "slices" - "sort" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&BookDao{Store: map[string]Book{ - "978-0134190440": { - Title: "The Go Programming Language", - Author: "Alan A. A. Donovan, Brian W. Kernighan", - ISBN: "978-0134190440", - Publisher: "Addison-Wesley", - }, - }}) -} - -type Book struct { - Title string `json:"title"` - Author string `json:"author"` - ISBN string `json:"isbn"` - Publisher string `json:"publisher"` -} - -type BookDao struct { - Store map[string]Book - Logger *slog.Logger `autowire:"dao"` -} - -// ListBooks returns a sorted list of all books in the store. -func (dao *BookDao) ListBooks() ([]Book, error) { - r := slices.Collect(maps.Values(dao.Store)) - sort.Slice(r, func(i, j int) bool { - return r[i].ISBN < r[j].ISBN - }) - return r, nil -} - -// GetBook retrieves a book by its ISBN. -func (dao *BookDao) GetBook(isbn string) (Book, error) { - r, ok := dao.Store[isbn] - _ = ok - return r, nil -} - -// SaveBook adds or updates a book in the store. -func (dao *BookDao) SaveBook(book Book) error { - dao.Store[book.ISBN] = book - return nil -} - -// DeleteBook removes a book from the store by its ISBN. -func (dao *BookDao) DeleteBook(isbn string) error { - delete(dao.Store, isbn) - return nil -} diff --git a/docs/4. examples/bookman/src/dao/book_dao/book_dao_test.go b/docs/4. examples/bookman/src/dao/book_dao/book_dao_test.go deleted file mode 100644 index e425c670..00000000 --- a/docs/4. examples/bookman/src/dao/book_dao/book_dao_test.go +++ /dev/null @@ -1,85 +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 book_dao - -import ( - "os" - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/gs/gstest" -) - -func init() { - _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "../../../conf") -} - -func TestMain(m *testing.M) { - gstest.TestMain(m) -} - -func TestBookDao(t *testing.T) { - - // Wire dependencies and retrieve the server address - x := gstest.Wire(t, &struct { - SvrAddr string `value:"${server.addr}"` - }{}) - assert.That(t, x.SvrAddr).Equal("0.0.0.0:9090") - - // Retrieve BookDao instance - o := gstest.Get[*BookDao](t) - assert.NotNil(t, o) - - // Test listing books - books, err := o.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(1) - assert.That(t, books[0].ISBN).Equal("978-0134190440") - assert.That(t, books[0].Title).Equal("The Go Programming Language") - - // Test saving a new book - err = o.SaveBook(Book{ - Title: "Clean Code", - Author: "Robert C. Martin", - ISBN: "978-0132350884", - Publisher: "Prentice Hall", - }) - assert.That(t, err).Equal(nil) - - // Verify book was added - books, err = o.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(2) - assert.That(t, books[0].ISBN).Equal("978-0132350884") - assert.That(t, books[0].Title).Equal("Clean Code") - - // Test retrieving a book by ISBN - book, err := o.GetBook("978-0132350884") - assert.Nil(t, err) - assert.That(t, book.Title).Equal("Clean Code") - assert.That(t, book.Publisher).Equal("Prentice Hall") - - // Test deleting a book - err = o.DeleteBook("978-0132350884") - assert.Nil(t, err) - - // Verify book was deleted - books, err = o.ListBooks() - assert.Nil(t, err) - assert.That(t, len(books)).Equal(1) - assert.That(t, books[0].ISBN).Equal("978-0134190440") -} diff --git a/docs/4. examples/bookman/src/idl/http/proto/proto.go b/docs/4. examples/bookman/src/idl/http/proto/proto.go deleted file mode 100644 index c1b7de76..00000000 --- a/docs/4. examples/bookman/src/idl/http/proto/proto.go +++ /dev/null @@ -1,50 +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 proto defines the interfaces and route registrations generated from IDL files. -package proto - -import ( - "net/http" -) - -// Book represents the structure of a book entity. -type Book struct { - Title string `json:"title"` - Author string `json:"author"` - ISBN string `json:"isbn"` - Publisher string `json:"publisher"` - Price string `json:"price"` - RefreshTime string `json:"refreshTime"` -} - -// Controller defines the service interface for book-related operations. -type Controller interface { - ListBooks(w http.ResponseWriter, r *http.Request) - GetBook(w http.ResponseWriter, r *http.Request) - SaveBook(w http.ResponseWriter, r *http.Request) - DeleteBook(w http.ResponseWriter, r *http.Request) -} - -// RegisterRouter registers the HTTP routes for the Controller interface. -// It maps each method to its corresponding HTTP endpoint, -// and applies the given middleware (wrap) to each handler. -func RegisterRouter(mux *http.ServeMux, c Controller, wrap func(next http.Handler) http.Handler) { - mux.Handle("GET /books", wrap(http.HandlerFunc(c.ListBooks))) - mux.Handle("GET /books/{isbn}", wrap(http.HandlerFunc(c.GetBook))) - mux.Handle("POST /books", wrap(http.HandlerFunc(c.SaveBook))) - mux.Handle("DELETE /books/{isbn}", wrap(http.HandlerFunc(c.DeleteBook))) -} diff --git a/docs/4. examples/bookman/src/sdk/book_sdk/book_sdk.go b/docs/4. examples/bookman/src/sdk/book_sdk/book_sdk.go deleted file mode 100644 index c7bc250e..00000000 --- a/docs/4. examples/bookman/src/sdk/book_sdk/book_sdk.go +++ /dev/null @@ -1,32 +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 book_sdk - -import ( - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&BookSDK{}) -} - -type BookSDK struct{} - -// GetPrice returns a fixed price for any book. -func (s *BookSDK) GetPrice(isbn string) string { - return "¥10" -} diff --git a/docs/4. examples/chatAI/chatAI.html b/docs/4. examples/chatAI/chatAI.html deleted file mode 100644 index 6afd2246..00000000 --- a/docs/4. examples/chatAI/chatAI.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - Chat AI - - - -
-

🧠 Chat AI

-
- - -
-
-
- - - - diff --git a/docs/4. examples/chatAI/go.mod b/docs/4. examples/chatAI/go.mod deleted file mode 100644 index 364cbcd3..00000000 --- a/docs/4. examples/chatAI/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module chatai - -go 1.24 - -require github.com/go-spring/spring-core v0.0.0 - -require ( - github.com/expr-lang/expr v1.17.2 // indirect - github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // 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 - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../ diff --git a/docs/4. examples/chatAI/go.sum b/docs/4. examples/chatAI/go.sum deleted file mode 100644 index 8b941181..00000000 --- a/docs/4. examples/chatAI/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -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/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/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/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/chatAI/main.go b/docs/4. examples/chatAI/main.go deleted file mode 100644 index 0bbbd4b1..00000000 --- a/docs/4. examples/chatAI/main.go +++ /dev/null @@ -1,72 +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 main - -import ( - "embed" - "fmt" - "net/http" - "time" - - "github.com/go-spring/spring-core/gs" -) - -//go:embed chatAI.html -var files embed.FS - -func main() { - // Disable the write timeout for the HTTP server - gs.Property("http.server.writeTimeout", "0") - - // Serve static files from the embedded file system under the "/public/" path - http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(files)))) - - // Handle the Server-Sent Events (SSE) endpoint - http.HandleFunc("/chat/sse", func(w http.ResponseWriter, r *http.Request) { - - // Set the necessary HTTP headers for SSE - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.Header().Set("Access-Control-Allow-Origin", "*") - - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) - return - } - - // Send an SSE message every second for 10 seconds - for i := 0; i < 10; i++ { - select { - case <-r.Context().Done(): - // Exit the loop if the client disconnects - return - default: - // Each SSE message must end with two newlines to be recognized correctly by the client - // See more about SSE protocol: https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html - fmt.Fprintf(w, "data: Message %d at %s\n\n", i, time.Now().Format("15:04:05")) - flusher.Flush() - time.Sleep(1 * time.Second) - } - } - }) - - gs.Run() -} - -// open http://127.0.0.1:9090/public/chatAI.html in the browser diff --git a/docs/4. examples/examples.md b/docs/4. examples/examples.md deleted file mode 100644 index 76a69a37..00000000 --- a/docs/4. examples/examples.md +++ /dev/null @@ -1 +0,0 @@ -todo 完整的 demo 项目或代码片段 \ No newline at end of file diff --git a/docs/4. examples/miniapi/go.mod b/docs/4. examples/miniapi/go.mod deleted file mode 100644 index 1610b58c..00000000 --- a/docs/4. examples/miniapi/go.mod +++ /dev/null @@ -1,20 +0,0 @@ -module miniapi - -go 1.24 - -require ( - 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.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.9.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../ diff --git a/docs/4. examples/miniapi/go.sum b/docs/4. examples/miniapi/go.sum deleted file mode 100644 index 7e534aa4..00000000 --- a/docs/4. examples/miniapi/go.sum +++ /dev/null @@ -1,30 +0,0 @@ -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/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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.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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/miniapi/main.go b/docs/4. examples/miniapi/main.go deleted file mode 100644 index bce928a4..00000000 --- a/docs/4. examples/miniapi/main.go +++ /dev/null @@ -1,46 +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 main - -import ( - "context" - "net/http" - - "github.com/go-spring/log" - "github.com/go-spring/spring-core/gs" -) - -func main() { - // Register an HTTP handler for the "/echo" endpoint. - http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("hello world!")) - }) - - // Start the Go-Spring framework. - // Compared to http.ListenAndServe, gs.Run() starts a full-featured application context with: - // - Auto Configuration: Automatically loads properties and beans. - // - Property Binding: Binds external configs (YAML, ENV) into structs. - // - Dependency Injection: Wires beans automatically. - // - Dynamic Refresh: Updates configs at runtime without restart. - gs.RunWith(func(ctx context.Context) error { - log.Infof(ctx, log.TagAppDef, "app started") - return nil - }) -} - -//~ curl http://127.0.0.1:9090/echo -//hello world! diff --git a/docs/4. examples/noweb/go.mod b/docs/4. examples/noweb/go.mod deleted file mode 100644 index 9541fe1f..00000000 --- a/docs/4. examples/noweb/go.mod +++ /dev/null @@ -1,19 +0,0 @@ -module noweb - -go 1.24 - -require ( - github.com/go-spring/log v0.0.3 - github.com/go-spring/spring-core v0.0.0 -) - -require ( - github.com/expr-lang/expr v1.17.2 // 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 - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../ diff --git a/docs/4. examples/noweb/go.sum b/docs/4. examples/noweb/go.sum deleted file mode 100644 index 8b941181..00000000 --- a/docs/4. examples/noweb/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -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/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/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/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/noweb/main.go b/docs/4. examples/noweb/main.go deleted file mode 100644 index 903d9b67..00000000 --- a/docs/4. examples/noweb/main.go +++ /dev/null @@ -1,45 +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 main - -import ( - "context" - "os" - "time" - - "github.com/go-spring/log" - "github.com/go-spring/spring-core/gs" -) - -func main() { - // Disable the built-in HTTP service. - stopApp, err := gs.Web(false).RunAsync() - if err != nil { - log.Errorf(context.Background(), log.TagApp, "app run failed: %s", err.Error()) - os.Exit(1) - } - - log.Infof(context.Background(), log.TagApp, "app started") - time.Sleep(time.Minute) - - stopApp() -} - -// ~ telnet 127.0.0.1 9090 -// Trying 127.0.0.1... -// telnet: connect to address 127.0.0.1: Connection refused -// telnet: Unable to connect to remote host diff --git a/docs/4. examples/servers/gin/go.mod b/docs/4. examples/servers/gin/go.mod deleted file mode 100644 index 56c613cf..00000000 --- a/docs/4. examples/servers/gin/go.mod +++ /dev/null @@ -1,46 +0,0 @@ -module ginsvr - -go 1.24 - -require ( - github.com/gin-gonic/gin v1.10.1 - github.com/go-spring/spring-core v0.0.0 -) - -require ( - github.com/bytedance/sonic v1.13.2 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/expr-lang/expr v1.17.2 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.26.0 // indirect - github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.14 // indirect - golang.org/x/arch v0.17.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -replace github.com/go-spring/spring-core => ../../../../ diff --git a/docs/4. examples/servers/gin/go.sum b/docs/4. examples/servers/gin/go.sum deleted file mode 100644 index bfaa34a9..00000000 --- a/docs/4. examples/servers/gin/go.sum +++ /dev/null @@ -1,106 +0,0 @@ -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -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/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw= -github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/docs/4. examples/servers/gin/main.go b/docs/4. examples/servers/gin/main.go deleted file mode 100644 index 0b0fe204..00000000 --- a/docs/4. examples/servers/gin/main.go +++ /dev/null @@ -1,65 +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 main - -import ( - "fmt" - "io" - "net/http" - "os" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-spring/spring-core/gs" -) - -func init() { - gin.SetMode(gin.ReleaseMode) - gs.EnableSimpleHttpServer(false) - - gs.Object(&Controller{}) - gs.Provide(func(c *Controller) *gin.Engine { - e := gin.Default() - e.GET("/echo", c.Echo) - return e - }) -} - -type Controller struct{} - -func (c *Controller) Echo(ctx *gin.Context) { - ctx.String(http.StatusOK, "Hello, gin!") -} - -func main() { - _ = os.Unsetenv("_") - _ = os.Unsetenv("TERM") - _ = os.Unsetenv("TERM_SESSION_ID") - go func() { - time.Sleep(time.Millisecond * 500) - runTest() - }() - gs.Run() -} - -func runTest() { - resp, _ := http.Get("http://localhost:9090/echo") - b, _ := io.ReadAll(resp.Body) - defer resp.Body.Close() - fmt.Println("Response from server:", string(b)) - gs.ShutDown() -} diff --git a/docs/4. examples/servers/gin/server.go b/docs/4. examples/servers/gin/server.go deleted file mode 100644 index 70a49010..00000000 --- a/docs/4. examples/servers/gin/server.go +++ /dev/null @@ -1,76 +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 main - -import ( - "context" - "net" - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Provide( - NewSimpleGinServer, - gs.IndexArg(1, gs.BindArg(gs.SetHttpServerAddr, gs.TagArg("${http.server.addr:=0.0.0.0:9090}"))), - gs.IndexArg(1, gs.BindArg(gs.SetHttpServerReadTimeout, gs.TagArg("${http.server.readTimeout:=5s}"))), - gs.IndexArg(1, gs.BindArg(gs.SetHttpServerHeaderTimeout, gs.TagArg("${http.server.headerTimeout:=1s}"))), - gs.IndexArg(1, gs.BindArg(gs.SetHttpServerWriteTimeout, gs.TagArg("${http.server.writeTimeout:=5s}"))), - gs.IndexArg(1, gs.BindArg(gs.SetHttpServerIdleTimeout, gs.TagArg("${http.server.idleTimeout:=60s}"))), - ).AsServer() -} - -type SimpleGinServer struct { - svr *http.Server -} - -func NewSimpleGinServer(e *gin.Engine, opts ...gs.HttpServerOption) *SimpleGinServer { - arg := &gs.HttpServerConfig{ - Address: "0.0.0.0:9090", - ReadTimeout: time.Second * 5, - HeaderTimeout: time.Second, - WriteTimeout: time.Second * 5, - IdleTimeout: time.Second * 60, - } - for _, opt := range opts { - opt(arg) - } - return &SimpleGinServer{svr: &http.Server{ - Handler: e, - Addr: arg.Address, - ReadTimeout: arg.ReadTimeout, - ReadHeaderTimeout: arg.HeaderTimeout, - WriteTimeout: arg.WriteTimeout, - IdleTimeout: arg.IdleTimeout, - }} -} - -func (s *SimpleGinServer) ListenAndServe(sig gs.ReadySignal) error { - ln, err := net.Listen("tcp", s.svr.Addr) - if err != nil { - return err - } - <-sig.TriggerAndWait() - return s.svr.Serve(ln) -} - -func (s *SimpleGinServer) Shutdown(ctx context.Context) error { - return s.svr.Shutdown(ctx) -} diff --git a/docs/4. examples/servers/grpc/go.mod b/docs/4. examples/servers/grpc/go.mod deleted file mode 100644 index 9eb36629..00000000 --- a/docs/4. examples/servers/grpc/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module grpcsvr - -go 1.24 - -require ( - github.com/go-spring/spring-core v0.0.0 - google.golang.org/grpc v1.72.2 - google.golang.org/protobuf v1.36.6 -) - -require ( - github.com/expr-lang/expr v1.17.4 // indirect - github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/cast v1.8.0 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../../ diff --git a/docs/4. examples/servers/grpc/go.sum b/docs/4. examples/servers/grpc/go.sum deleted file mode 100644 index a17cad73..00000000 --- a/docs/4. examples/servers/grpc/go.sum +++ /dev/null @@ -1,60 +0,0 @@ -github.com/expr-lang/expr v1.17.4 h1:qhTVftZ2Z3WpOEXRHWErEl2xf1Kq011MnQmWgLq06CY= -github.com/expr-lang/expr v1.17.4/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-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= -github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/servers/grpc/idl/echo.proto b/docs/4. examples/servers/grpc/idl/echo.proto deleted file mode 100644 index 924680cf..00000000 --- a/docs/4. examples/servers/grpc/idl/echo.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto3"; - -option go_package = ".;proto"; - -service EchoService { - rpc Echo (EchoRequest) returns (EchoResponse); -} - -message EchoRequest { - string message = 1; -} - -message EchoResponse { - string message = 1; -} diff --git a/docs/4. examples/servers/grpc/idl/proto/echo.pb.go b/docs/4. examples/servers/grpc/idl/proto/echo.pb.go deleted file mode 100644 index 10f051a4..00000000 --- a/docs/4. examples/servers/grpc/idl/proto/echo.pb.go +++ /dev/null @@ -1,222 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.25.0-devel -// protoc v3.13.0 -// source: echo.proto - -package proto - -import ( - "reflect" - "sync" - - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type EchoRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message"` -} - -func (x *EchoRequest) Reset() { - *x = EchoRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_echo_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EchoRequest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EchoRequest) ProtoMessage() {} - -func (x *EchoRequest) ProtoReflect() protoreflect.Message { - mi := &file_echo_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. -func (*EchoRequest) Descriptor() ([]byte, []int) { - return file_echo_proto_rawDescGZIP(), []int{0} -} - -func (x *EchoRequest) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *EchoRequest) SetMessage(v string) { - if x != nil { - x.Message = v - } -} - -type EchoResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message"` -} - -func (x *EchoResponse) Reset() { - *x = EchoResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_echo_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *EchoResponse) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*EchoResponse) ProtoMessage() {} - -func (x *EchoResponse) ProtoReflect() protoreflect.Message { - mi := &file_echo_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead. -func (*EchoResponse) Descriptor() ([]byte, []int) { - return file_echo_proto_rawDescGZIP(), []int{1} -} - -func (x *EchoResponse) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *EchoResponse) SetMessage(v string) { - if x != nil { - x.Message = v - } -} - -var File_echo_proto protoreflect.FileDescriptor - -var file_echo_proto_rawDesc = []byte{ - 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x27, 0x0a, 0x0b, - 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x28, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, - 0x32, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x23, - 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x0c, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_echo_proto_rawDescOnce sync.Once - file_echo_proto_rawDescData = file_echo_proto_rawDesc -) - -func file_echo_proto_rawDescGZIP() []byte { - file_echo_proto_rawDescOnce.Do(func() { - file_echo_proto_rawDescData = protoimpl.X.CompressGZIP(file_echo_proto_rawDescData) - }) - return file_echo_proto_rawDescData -} - -var file_echo_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_echo_proto_goTypes = []interface{}{ - (*EchoRequest)(nil), // 0: EchoRequest - (*EchoResponse)(nil), // 1: EchoResponse -} -var file_echo_proto_depIdxs = []int32{ - 0, // 0: EchoService.Echo:input_type -> EchoRequest - 1, // 1: EchoService.Echo:output_type -> EchoResponse - 1, // [1:2] is the sub-list for method output_type - 0, // [0:1] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_echo_proto_init() } -func file_echo_proto_init() { - if File_echo_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_echo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EchoRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_echo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*EchoResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_echo_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 1, - }, - GoTypes: file_echo_proto_goTypes, - DependencyIndexes: file_echo_proto_depIdxs, - MessageInfos: file_echo_proto_msgTypes, - }.Build() - File_echo_proto = out.File - file_echo_proto_rawDesc = nil - file_echo_proto_goTypes = nil - file_echo_proto_depIdxs = nil -} diff --git a/docs/4. examples/servers/grpc/idl/proto/echo_grpc.pb.go b/docs/4. examples/servers/grpc/idl/proto/echo_grpc.pb.go deleted file mode 100644 index f37869d1..00000000 --- a/docs/4. examples/servers/grpc/idl/proto/echo_grpc.pb.go +++ /dev/null @@ -1,98 +0,0 @@ -// Code generated by protoc-gen-go-grpc. DO NOT EDIT. - -package proto - -import ( - "context" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion7 - -// EchoServiceClient is the client API for EchoService service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type EchoServiceClient interface { - Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) -} - -type echoServiceClient struct { - cc grpc.ClientConnInterface -} - -func NewEchoServiceClient(cc grpc.ClientConnInterface) EchoServiceClient { - return &echoServiceClient{cc} -} - -func (c *echoServiceClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { - out := new(EchoResponse) - err := c.cc.Invoke(ctx, "/EchoService/Echo", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// EchoServiceServer is the server API for EchoService service. -// All implementations must embed UnimplementedEchoServiceServer -// for forward compatibility -type EchoServiceServer interface { - Echo(context.Context, *EchoRequest) (*EchoResponse, error) - mustEmbedUnimplementedEchoServiceServer() -} - -// UnimplementedEchoServiceServer must be embedded to have forward compatible implementations. -type UnimplementedEchoServiceServer struct { -} - -func (UnimplementedEchoServiceServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") -} -func (UnimplementedEchoServiceServer) mustEmbedUnimplementedEchoServiceServer() {} - -// UnsafeEchoServiceServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to EchoServiceServer will -// result in compilation errors. -type UnsafeEchoServiceServer interface { - mustEmbedUnimplementedEchoServiceServer() -} - -func RegisterEchoServiceServer(s grpc.ServiceRegistrar, srv EchoServiceServer) { - s.RegisterService(&_EchoService_serviceDesc, srv) -} - -func _EchoService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(EchoRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(EchoServiceServer).Echo(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/EchoService/Echo", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(EchoServiceServer).Echo(ctx, req.(*EchoRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _EchoService_serviceDesc = grpc.ServiceDesc{ - ServiceName: "EchoService", - HandlerType: (*EchoServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Echo", - Handler: _EchoService_Echo_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "echo.proto", -} diff --git a/docs/4. examples/servers/grpc/main.go b/docs/4. examples/servers/grpc/main.go deleted file mode 100644 index 85d29e16..00000000 --- a/docs/4. examples/servers/grpc/main.go +++ /dev/null @@ -1,76 +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 main - -import ( - "context" - "fmt" - "log" - "os" - "time" - - "grpcsvr/idl/proto" - - "github.com/go-spring/spring-core/gs" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -func init() { - gs.Object(&Controller{}) - gs.Provide(func(c *Controller) GrpcServerConfiger { - return func(svr *grpc.Server) { - proto.RegisterEchoServiceServer(svr, c) - } - }) -} - -type Controller struct { - proto.UnimplementedEchoServiceServer -} - -func (c *Controller) Echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) { - return &proto.EchoResponse{Message: req.Message}, nil -} - -func main() { - _ = os.Unsetenv("_") - _ = os.Unsetenv("TERM") - _ = os.Unsetenv("TERM_SESSION_ID") - go func() { - time.Sleep(time.Millisecond * 500) - runTest() - }() - gs.Run() -} - -func runTest() { - conn, err := grpc.NewClient(":9494", grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - log.Fatalf("Failed to connect: %v", err) - } - defer conn.Close() - - client := proto.NewEchoServiceClient(conn) - response, err := client.Echo(context.Background(), &proto.EchoRequest{Message: "Hello, gRPC!"}) - if err != nil { - log.Fatalf("Error calling Echo: %v", err) - } - fmt.Println("Response from server:", response.Message) - - gs.ShutDown() -} diff --git a/docs/4. examples/servers/grpc/server.go b/docs/4. examples/servers/grpc/server.go deleted file mode 100644 index c4d07323..00000000 --- a/docs/4. examples/servers/grpc/server.go +++ /dev/null @@ -1,58 +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 main - -import ( - "context" - "log" - "net" - - "github.com/go-spring/spring-core/gs" - "google.golang.org/grpc" -) - -func init() { - gs.Object(&SimpleGrpcServer{}).AsServer().Condition( - gs.OnBean[GrpcServerConfiger](), - ) -} - -type GrpcServerConfiger func(svr *grpc.Server) - -type SimpleGrpcServer struct { - Addr string `value:"${grpc.server.addr:=0.0.0.0:9494}"` - Cfgs []GrpcServerConfiger `autowire:""` - svr *grpc.Server -} - -func (s *SimpleGrpcServer) ListenAndServe(sig gs.ReadySignal) error { - listener, err := net.Listen("tcp", s.Addr) - if err != nil { - log.Fatalf("Failed to listen: %v", err) - } - s.svr = grpc.NewServer() - for _, cfg := range s.Cfgs { - cfg(s.svr) - } - <-sig.TriggerAndWait() - return s.svr.Serve(listener) -} - -func (s *SimpleGrpcServer) Shutdown(ctx context.Context) error { - s.svr.GracefulStop() - return nil -} diff --git a/docs/4. examples/servers/thrift/go.mod b/docs/4. examples/servers/thrift/go.mod deleted file mode 100644 index ccddf83b..00000000 --- a/docs/4. examples/servers/thrift/go.mod +++ /dev/null @@ -1,21 +0,0 @@ -module thriftsvr - -go 1.24 - -require ( - github.com/apache/thrift v0.22.0 - github.com/go-spring/spring-core v0.0.0 -) - -require ( - github.com/expr-lang/expr v1.17.4 // indirect - github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/spf13/cast v1.8.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../../ diff --git a/docs/4. examples/servers/thrift/go.sum b/docs/4. examples/servers/thrift/go.sum deleted file mode 100644 index b67dff1d..00000000 --- a/docs/4. examples/servers/thrift/go.sum +++ /dev/null @@ -1,30 +0,0 @@ -github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc= -github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g= -github.com/expr-lang/expr v1.17.4 h1:qhTVftZ2Z3WpOEXRHWErEl2xf1Kq011MnQmWgLq06CY= -github.com/expr-lang/expr v1.17.4/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/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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= -github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/servers/thrift/idl/echo.thrift b/docs/4. examples/servers/thrift/idl/echo.thrift deleted file mode 100644 index 856b73b6..00000000 --- a/docs/4. examples/servers/thrift/idl/echo.thrift +++ /dev/null @@ -1,13 +0,0 @@ -namespace go proto - -struct EchoRequest { -1: required string message -} - -struct EchoResponse { -1: required string message -} - -service EchoService { - EchoResponse echo(1: EchoRequest req) -} diff --git a/docs/4. examples/servers/thrift/idl/proto/GoUnusedProtection__.go b/docs/4. examples/servers/thrift/idl/proto/GoUnusedProtection__.go deleted file mode 100644 index e6caa8b1..00000000 --- a/docs/4. examples/servers/thrift/idl/proto/GoUnusedProtection__.go +++ /dev/null @@ -1,5 +0,0 @@ -// Code generated by Thrift Compiler (0.16.0). DO NOT EDIT. - -package proto - -var GoUnusedProtection__ int diff --git a/docs/4. examples/servers/thrift/idl/proto/echo-consts.go b/docs/4. examples/servers/thrift/idl/proto/echo-consts.go deleted file mode 100644 index 16550e71..00000000 --- a/docs/4. examples/servers/thrift/idl/proto/echo-consts.go +++ /dev/null @@ -1,22 +0,0 @@ -// Code generated by Thrift Compiler (0.16.0). DO NOT EDIT. - -package proto - -import ( - "bytes" - "context" - "fmt" - "time" - - thrift "github.com/apache/thrift/lib/go/thrift" -) - -// (needed to ensure safety because of naive import list construction.) -var _ = thrift.ZERO -var _ = fmt.Printf -var _ = context.Background -var _ = time.Now -var _ = bytes.Equal - -func init() { -} diff --git a/docs/4. examples/servers/thrift/idl/proto/echo.go b/docs/4. examples/servers/thrift/idl/proto/echo.go deleted file mode 100644 index 3e6fab94..00000000 --- a/docs/4. examples/servers/thrift/idl/proto/echo.go +++ /dev/null @@ -1,653 +0,0 @@ -// Code generated by Thrift Compiler (0.16.0). DO NOT EDIT. - -package proto - -import ( - "bytes" - "context" - "fmt" - "time" - - "github.com/apache/thrift/lib/go/thrift" -) - -// (needed to ensure safety because of naive import list construction.) -var _ = thrift.ZERO -var _ = fmt.Printf -var _ = context.Background -var _ = time.Now -var _ = bytes.Equal - -// Attributes: -// - Message -type EchoRequest struct { - Message string `thrift:"message,1,required" db:"message" json:"message"` -} - -func NewEchoRequest() *EchoRequest { - return &EchoRequest{} -} - -func (p *EchoRequest) GetMessage() string { - return p.Message -} -func (p *EchoRequest) Read(ctx context.Context, iprot thrift.TProtocol) error { - if _, err := iprot.ReadStructBegin(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) - } - - var issetMessage bool = false - - for { - _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) - if err != nil { - return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) - } - if fieldTypeId == thrift.STOP { - break - } - switch fieldId { - case 1: - if fieldTypeId == thrift.STRING { - if err := p.ReadField1(ctx, iprot); err != nil { - return err - } - issetMessage = true - } else { - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - default: - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - if err := iprot.ReadFieldEnd(ctx); err != nil { - return err - } - } - if err := iprot.ReadStructEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) - } - if !issetMessage { - return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Message is not set")) - } - return nil -} - -func (p *EchoRequest) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { - if v, err := iprot.ReadString(ctx); err != nil { - return thrift.PrependError("error reading field 1: ", err) - } else { - p.Message = v - } - return nil -} - -func (p *EchoRequest) Write(ctx context.Context, oprot thrift.TProtocol) error { - if err := oprot.WriteStructBegin(ctx, "EchoRequest"); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) - } - if p != nil { - if err := p.writeField1(ctx, oprot); err != nil { - return err - } - } - if err := oprot.WriteFieldStop(ctx); err != nil { - return thrift.PrependError("write field stop error: ", err) - } - if err := oprot.WriteStructEnd(ctx); err != nil { - return thrift.PrependError("write struct stop error: ", err) - } - return nil -} - -func (p *EchoRequest) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { - if err := oprot.WriteFieldBegin(ctx, "message", thrift.STRING, 1); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:message: ", p), err) - } - if err := oprot.WriteString(ctx, string(p.Message)); err != nil { - return thrift.PrependError(fmt.Sprintf("%T.message (1) field write error: ", p), err) - } - if err := oprot.WriteFieldEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field end error 1:message: ", p), err) - } - return err -} - -func (p *EchoRequest) Equals(other *EchoRequest) bool { - if p == other { - return true - } else if p == nil || other == nil { - return false - } - if p.Message != other.Message { - return false - } - return true -} - -func (p *EchoRequest) String() string { - if p == nil { - return "" - } - return fmt.Sprintf("EchoRequest(%+v)", *p) -} - -// Attributes: -// - Message -type EchoResponse struct { - Message string `thrift:"message,1,required" db:"message" json:"message"` -} - -func NewEchoResponse() *EchoResponse { - return &EchoResponse{} -} - -func (p *EchoResponse) GetMessage() string { - return p.Message -} -func (p *EchoResponse) Read(ctx context.Context, iprot thrift.TProtocol) error { - if _, err := iprot.ReadStructBegin(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) - } - - var issetMessage bool = false - - for { - _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) - if err != nil { - return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) - } - if fieldTypeId == thrift.STOP { - break - } - switch fieldId { - case 1: - if fieldTypeId == thrift.STRING { - if err := p.ReadField1(ctx, iprot); err != nil { - return err - } - issetMessage = true - } else { - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - default: - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - if err := iprot.ReadFieldEnd(ctx); err != nil { - return err - } - } - if err := iprot.ReadStructEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) - } - if !issetMessage { - return thrift.NewTProtocolExceptionWithType(thrift.INVALID_DATA, fmt.Errorf("Required field Message is not set")) - } - return nil -} - -func (p *EchoResponse) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { - if v, err := iprot.ReadString(ctx); err != nil { - return thrift.PrependError("error reading field 1: ", err) - } else { - p.Message = v - } - return nil -} - -func (p *EchoResponse) Write(ctx context.Context, oprot thrift.TProtocol) error { - if err := oprot.WriteStructBegin(ctx, "EchoResponse"); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) - } - if p != nil { - if err := p.writeField1(ctx, oprot); err != nil { - return err - } - } - if err := oprot.WriteFieldStop(ctx); err != nil { - return thrift.PrependError("write field stop error: ", err) - } - if err := oprot.WriteStructEnd(ctx); err != nil { - return thrift.PrependError("write struct stop error: ", err) - } - return nil -} - -func (p *EchoResponse) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { - if err := oprot.WriteFieldBegin(ctx, "message", thrift.STRING, 1); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:message: ", p), err) - } - if err := oprot.WriteString(ctx, string(p.Message)); err != nil { - return thrift.PrependError(fmt.Sprintf("%T.message (1) field write error: ", p), err) - } - if err := oprot.WriteFieldEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field end error 1:message: ", p), err) - } - return err -} - -func (p *EchoResponse) Equals(other *EchoResponse) bool { - if p == other { - return true - } else if p == nil || other == nil { - return false - } - if p.Message != other.Message { - return false - } - return true -} - -func (p *EchoResponse) String() string { - if p == nil { - return "" - } - return fmt.Sprintf("EchoResponse(%+v)", *p) -} - -type EchoService interface { - // Parameters: - // - Req - Echo(ctx context.Context, req *EchoRequest) (_r *EchoResponse, _err error) -} - -type EchoServiceClient struct { - c thrift.TClient - meta thrift.ResponseMeta -} - -func NewEchoServiceClientFactory(t thrift.TTransport, f thrift.TProtocolFactory) *EchoServiceClient { - return &EchoServiceClient{ - c: thrift.NewTStandardClient(f.GetProtocol(t), f.GetProtocol(t)), - } -} - -func NewEchoServiceClientProtocol(t thrift.TTransport, iprot thrift.TProtocol, oprot thrift.TProtocol) *EchoServiceClient { - return &EchoServiceClient{ - c: thrift.NewTStandardClient(iprot, oprot), - } -} - -func NewEchoServiceClient(c thrift.TClient) *EchoServiceClient { - return &EchoServiceClient{ - c: c, - } -} - -func (p *EchoServiceClient) Client_() thrift.TClient { - return p.c -} - -func (p *EchoServiceClient) LastResponseMeta_() thrift.ResponseMeta { - return p.meta -} - -func (p *EchoServiceClient) SetLastResponseMeta_(meta thrift.ResponseMeta) { - p.meta = meta -} - -// Parameters: -// - Req -func (p *EchoServiceClient) Echo(ctx context.Context, req *EchoRequest) (_r *EchoResponse, _err error) { - var _args0 EchoServiceEchoArgs - _args0.Req = req - var _result2 EchoServiceEchoResult - var _meta1 thrift.ResponseMeta - _meta1, _err = p.Client_().Call(ctx, "echo", &_args0, &_result2) - p.SetLastResponseMeta_(_meta1) - if _err != nil { - return - } - if _ret3 := _result2.GetSuccess(); _ret3 != nil { - return _ret3, nil - } - return nil, thrift.NewTApplicationException(thrift.MISSING_RESULT, "echo failed: unknown result") -} - -type EchoServiceProcessor struct { - processorMap map[string]thrift.TProcessorFunction - handler EchoService -} - -func (p *EchoServiceProcessor) AddToProcessorMap(key string, processor thrift.TProcessorFunction) { - p.processorMap[key] = processor -} - -func (p *EchoServiceProcessor) GetProcessorFunction(key string) (processor thrift.TProcessorFunction, ok bool) { - processor, ok = p.processorMap[key] - return processor, ok -} - -func (p *EchoServiceProcessor) ProcessorMap() map[string]thrift.TProcessorFunction { - return p.processorMap -} - -func NewEchoServiceProcessor(handler EchoService) *EchoServiceProcessor { - - self4 := &EchoServiceProcessor{handler: handler, processorMap: make(map[string]thrift.TProcessorFunction)} - self4.processorMap["echo"] = &echoServiceProcessorEcho{handler: handler} - return self4 -} - -func (p *EchoServiceProcessor) Process(ctx context.Context, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { - name, _, seqId, err2 := iprot.ReadMessageBegin(ctx) - if err2 != nil { - return false, thrift.WrapTException(err2) - } - if processor, ok := p.GetProcessorFunction(name); ok { - return processor.Process(ctx, seqId, iprot, oprot) - } - iprot.Skip(ctx, thrift.STRUCT) - iprot.ReadMessageEnd(ctx) - x5 := thrift.NewTApplicationException(thrift.UNKNOWN_METHOD, "Unknown function "+name) - oprot.WriteMessageBegin(ctx, name, thrift.EXCEPTION, seqId) - x5.Write(ctx, oprot) - oprot.WriteMessageEnd(ctx) - oprot.Flush(ctx) - return false, x5 - -} - -type echoServiceProcessorEcho struct { - handler EchoService -} - -func (p *echoServiceProcessorEcho) Process(ctx context.Context, seqId int32, iprot, oprot thrift.TProtocol) (success bool, err thrift.TException) { - args := EchoServiceEchoArgs{} - var err2 error - if err2 = args.Read(ctx, iprot); err2 != nil { - iprot.ReadMessageEnd(ctx) - x := thrift.NewTApplicationException(thrift.PROTOCOL_ERROR, err2.Error()) - oprot.WriteMessageBegin(ctx, "echo", thrift.EXCEPTION, seqId) - x.Write(ctx, oprot) - oprot.WriteMessageEnd(ctx) - oprot.Flush(ctx) - return false, thrift.WrapTException(err2) - } - iprot.ReadMessageEnd(ctx) - - tickerCancel := func() {} - // Start a goroutine to do server side connectivity check. - if thrift.ServerConnectivityCheckInterval > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithCancel(ctx) - defer cancel() - var tickerCtx context.Context - tickerCtx, tickerCancel = context.WithCancel(context.Background()) - defer tickerCancel() - go func(ctx context.Context, cancel context.CancelFunc) { - ticker := time.NewTicker(thrift.ServerConnectivityCheckInterval) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if !iprot.Transport().IsOpen() { - cancel() - return - } - } - } - }(tickerCtx, cancel) - } - - result := EchoServiceEchoResult{} - var retval *EchoResponse - if retval, err2 = p.handler.Echo(ctx, args.Req); err2 != nil { - tickerCancel() - if err2 == thrift.ErrAbandonRequest { - return false, thrift.WrapTException(err2) - } - x := thrift.NewTApplicationException(thrift.INTERNAL_ERROR, "Internal error processing echo: "+err2.Error()) - oprot.WriteMessageBegin(ctx, "echo", thrift.EXCEPTION, seqId) - x.Write(ctx, oprot) - oprot.WriteMessageEnd(ctx) - oprot.Flush(ctx) - return true, thrift.WrapTException(err2) - } else { - result.Success = retval - } - tickerCancel() - if err2 = oprot.WriteMessageBegin(ctx, "echo", thrift.REPLY, seqId); err2 != nil { - err = thrift.WrapTException(err2) - } - if err2 = result.Write(ctx, oprot); err == nil && err2 != nil { - err = thrift.WrapTException(err2) - } - if err2 = oprot.WriteMessageEnd(ctx); err == nil && err2 != nil { - err = thrift.WrapTException(err2) - } - if err2 = oprot.Flush(ctx); err == nil && err2 != nil { - err = thrift.WrapTException(err2) - } - if err != nil { - return - } - return true, err -} - -// HELPER FUNCTIONS AND STRUCTURES - -// Attributes: -// - Req -type EchoServiceEchoArgs struct { - Req *EchoRequest `thrift:"req,1" db:"req" json:"req"` -} - -func NewEchoServiceEchoArgs() *EchoServiceEchoArgs { - return &EchoServiceEchoArgs{} -} - -var EchoServiceEchoArgs_Req_DEFAULT *EchoRequest - -func (p *EchoServiceEchoArgs) GetReq() *EchoRequest { - if !p.IsSetReq() { - return EchoServiceEchoArgs_Req_DEFAULT - } - return p.Req -} -func (p *EchoServiceEchoArgs) IsSetReq() bool { - return p.Req != nil -} - -func (p *EchoServiceEchoArgs) Read(ctx context.Context, iprot thrift.TProtocol) error { - if _, err := iprot.ReadStructBegin(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) - } - - for { - _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) - if err != nil { - return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) - } - if fieldTypeId == thrift.STOP { - break - } - switch fieldId { - case 1: - if fieldTypeId == thrift.STRUCT { - if err := p.ReadField1(ctx, iprot); err != nil { - return err - } - } else { - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - default: - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - if err := iprot.ReadFieldEnd(ctx); err != nil { - return err - } - } - if err := iprot.ReadStructEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) - } - return nil -} - -func (p *EchoServiceEchoArgs) ReadField1(ctx context.Context, iprot thrift.TProtocol) error { - p.Req = &EchoRequest{} - if err := p.Req.Read(ctx, iprot); err != nil { - return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Req), err) - } - return nil -} - -func (p *EchoServiceEchoArgs) Write(ctx context.Context, oprot thrift.TProtocol) error { - if err := oprot.WriteStructBegin(ctx, "echo_args"); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) - } - if p != nil { - if err := p.writeField1(ctx, oprot); err != nil { - return err - } - } - if err := oprot.WriteFieldStop(ctx); err != nil { - return thrift.PrependError("write field stop error: ", err) - } - if err := oprot.WriteStructEnd(ctx); err != nil { - return thrift.PrependError("write struct stop error: ", err) - } - return nil -} - -func (p *EchoServiceEchoArgs) writeField1(ctx context.Context, oprot thrift.TProtocol) (err error) { - if err := oprot.WriteFieldBegin(ctx, "req", thrift.STRUCT, 1); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field begin error 1:req: ", p), err) - } - if err := p.Req.Write(ctx, oprot); err != nil { - return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Req), err) - } - if err := oprot.WriteFieldEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field end error 1:req: ", p), err) - } - return err -} - -func (p *EchoServiceEchoArgs) String() string { - if p == nil { - return "" - } - return fmt.Sprintf("EchoServiceEchoArgs(%+v)", *p) -} - -// Attributes: -// - Success -type EchoServiceEchoResult struct { - Success *EchoResponse `thrift:"success,0" db:"success" json:"success,omitempty"` -} - -func NewEchoServiceEchoResult() *EchoServiceEchoResult { - return &EchoServiceEchoResult{} -} - -var EchoServiceEchoResult_Success_DEFAULT *EchoResponse - -func (p *EchoServiceEchoResult) GetSuccess() *EchoResponse { - if !p.IsSetSuccess() { - return EchoServiceEchoResult_Success_DEFAULT - } - return p.Success -} -func (p *EchoServiceEchoResult) IsSetSuccess() bool { - return p.Success != nil -} - -func (p *EchoServiceEchoResult) Read(ctx context.Context, iprot thrift.TProtocol) error { - if _, err := iprot.ReadStructBegin(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read error: ", p), err) - } - - for { - _, fieldTypeId, fieldId, err := iprot.ReadFieldBegin(ctx) - if err != nil { - return thrift.PrependError(fmt.Sprintf("%T field %d read error: ", p, fieldId), err) - } - if fieldTypeId == thrift.STOP { - break - } - switch fieldId { - case 0: - if fieldTypeId == thrift.STRUCT { - if err := p.ReadField0(ctx, iprot); err != nil { - return err - } - } else { - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - default: - if err := iprot.Skip(ctx, fieldTypeId); err != nil { - return err - } - } - if err := iprot.ReadFieldEnd(ctx); err != nil { - return err - } - } - if err := iprot.ReadStructEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) - } - return nil -} - -func (p *EchoServiceEchoResult) ReadField0(ctx context.Context, iprot thrift.TProtocol) error { - p.Success = &EchoResponse{} - if err := p.Success.Read(ctx, iprot); err != nil { - return thrift.PrependError(fmt.Sprintf("%T error reading struct: ", p.Success), err) - } - return nil -} - -func (p *EchoServiceEchoResult) Write(ctx context.Context, oprot thrift.TProtocol) error { - if err := oprot.WriteStructBegin(ctx, "echo_result"); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) - } - if p != nil { - if err := p.writeField0(ctx, oprot); err != nil { - return err - } - } - if err := oprot.WriteFieldStop(ctx); err != nil { - return thrift.PrependError("write field stop error: ", err) - } - if err := oprot.WriteStructEnd(ctx); err != nil { - return thrift.PrependError("write struct stop error: ", err) - } - return nil -} - -func (p *EchoServiceEchoResult) writeField0(ctx context.Context, oprot thrift.TProtocol) (err error) { - if p.IsSetSuccess() { - if err := oprot.WriteFieldBegin(ctx, "success", thrift.STRUCT, 0); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field begin error 0:success: ", p), err) - } - if err := p.Success.Write(ctx, oprot); err != nil { - return thrift.PrependError(fmt.Sprintf("%T error writing struct: ", p.Success), err) - } - if err := oprot.WriteFieldEnd(ctx); err != nil { - return thrift.PrependError(fmt.Sprintf("%T write field end error 0:success: ", p), err) - } - } - return err -} - -func (p *EchoServiceEchoResult) String() string { - if p == nil { - return "" - } - return fmt.Sprintf("EchoServiceEchoResult(%+v)", *p) -} diff --git a/docs/4. examples/servers/thrift/main.go b/docs/4. examples/servers/thrift/main.go deleted file mode 100644 index fbb19896..00000000 --- a/docs/4. examples/servers/thrift/main.go +++ /dev/null @@ -1,75 +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 main - -import ( - "context" - "fmt" - "log" - "os" - "time" - - "thriftsvr/idl/proto" - - "github.com/apache/thrift/lib/go/thrift" - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&Controller{}) - gs.Provide(func(c *Controller) thrift.TProcessor { - return proto.NewEchoServiceProcessor(c) - }) -} - -type Controller struct{} - -func (c *Controller) Echo(ctx context.Context, req *proto.EchoRequest) (r *proto.EchoResponse, err error) { - return &proto.EchoResponse{Message: req.Message}, nil -} - -func main() { - _ = os.Unsetenv("_") - _ = os.Unsetenv("TERM") - _ = os.Unsetenv("TERM_SESSION_ID") - go func() { - time.Sleep(time.Millisecond * 500) - runTest() - }() - gs.Run() -} - -func runTest() { - transport := thrift.NewTSocketConf(":9292", nil) - defer transport.Close() - - protocolFactory := thrift.NewTBinaryProtocolFactoryConf(nil) - client := proto.NewEchoServiceClientFactory(transport, protocolFactory) - - if err := transport.Open(); err != nil { - log.Fatalf("Error opening transport: %v", err) - } - - response, err := client.Echo(context.Background(), &proto.EchoRequest{Message: "Hello, Thrift!"}) - if err != nil { - log.Fatalf("Error calling Echo: %v", err) - } - - fmt.Println("Response from server:", response.Message) - - gs.ShutDown() -} diff --git a/docs/4. examples/servers/thrift/server.go b/docs/4. examples/servers/thrift/server.go deleted file mode 100644 index 207818b5..00000000 --- a/docs/4. examples/servers/thrift/server.go +++ /dev/null @@ -1,52 +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 main - -import ( - "context" - "time" - - "github.com/apache/thrift/lib/go/thrift" - "github.com/go-spring/spring-core/gs" -) - -func init() { - gs.Object(&SimpleThriftServer{}).AsServer().Condition( - gs.OnBean[thrift.TProcessor](), - ) -} - -type SimpleThriftServer struct { - Addr string `value:"${thrift.server.addr:=0.0.0.0:9292}"` - Proc thrift.TProcessor `autowire:""` - svr *thrift.TSimpleServer -} - -func (s *SimpleThriftServer) ListenAndServe(sig gs.ReadySignal) error { - transport, err := thrift.NewTServerSocket(s.Addr) - if err != nil { - return err - } - s.svr = thrift.NewTSimpleServer2(s.Proc, transport) - <-sig.TriggerAndWait() - return s.svr.Serve() -} - -func (s *SimpleThriftServer) Shutdown(ctx context.Context) error { - thrift.ServerStopTimeout = time.Second - return s.svr.Stop() -} diff --git a/docs/4. examples/startup/README.md b/docs/4. examples/startup/README.md deleted file mode 100644 index cd6a1d62..00000000 --- a/docs/4. examples/startup/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Startup - -[中文](README_CN.md) - -This project, though small in size, showcases the essential capabilities of **Go-Spring**. - -## Features - -### 1. Layered Configuration - -Configurations can be loaded from various sources including: - -- `sysconf` -- Local files -- Remote files -- Environment variables -- Command-line arguments - -### 2. Property Binding - -Configurations from files, command-line arguments, or environment variables can be bound directly to struct fields. - -```go -type Service struct { - StartTime time.Time `value:"${start-time}"` -} -``` - -### 3. Dependency Injection - -Go-Spring automatically organizes dependencies between beans and injects them at runtime. - -```go -gs.Provide(func (s *Service) *http.ServeMux { - http.HandleFunc("/echo", s.Echo) - http.HandleFunc("/refresh", s.Refresh) - return http.DefaultServeMux -}) -``` - -### 4. Dynamic Configuration Refresh - -Configurations can be refreshed at runtime without restarting the application, enabling real-time updates. - -```go -type Service struct { - RefreshTime gs.Dync[time.Time] `value:"${refresh-time}"` -} -``` - -### 5. Route Registration - -Supports HTTP server setup with customizable routing. - -```go -gs.Provide(func (s *Service) *http.ServeMux { - http.HandleFunc("/echo", s.Echo) - http.HandleFunc("/refresh", s.Refresh) - return http.DefaultServeMux -}) -``` diff --git a/docs/4. examples/startup/README_CN.md b/docs/4. examples/startup/README_CN.md deleted file mode 100644 index e4dfc98c..00000000 --- a/docs/4. examples/startup/README_CN.md +++ /dev/null @@ -1,61 +0,0 @@ -# Startup - -[English](README.md) - -本项目虽小,但展示了 **Go-Spring** 最核心的功能。 - -## 功能介绍 - -### 1. 分层配置 - -支持从多种来源加载配置,包括: - -- `sysconf` -- 本地文件 -- 远程文件 -- 环境变量 -- 命令行参数 - -### 2. 属性绑定 - -可以将文件、命令行参数或环境变量中的配置,直接绑定到结构体字段。 - -```go -type Service struct { - StartTime time.Time `value:"${start-time}"` -} -``` - -### 3. 依赖注入 - -Go-Spring 能自动组织 Bean 之间的依赖关系,在运行时完成注入。 - -```go -gs.Provide(func (s *Service) *http.ServeMux { - http.HandleFunc("/echo", s.Echo) - http.HandleFunc("/refresh", s.Refresh) - return http.DefaultServeMux -}) -``` - -### 4. 配置刷新 - -支持配置在运行时动态刷新,无需重启应用,即可应用最新配置。 - -```go -type Service struct { - RefreshTime gs.Dync[time.Time] `value:"${refresh-time}"` -} -``` - -### 5. 路由注册 - -支持 HTTP 服务器,且可以灵活定制所需路由。 - -```go -gs.Provide(func (s *Service) *http.ServeMux { - http.HandleFunc("/echo", s.Echo) - http.HandleFunc("/refresh", s.Refresh) - return http.DefaultServeMux -}) -``` diff --git a/docs/4. examples/startup/go.mod b/docs/4. examples/startup/go.mod deleted file mode 100644 index c3c5d360..00000000 --- a/docs/4. examples/startup/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module startup - -go 1.24 - -require github.com/go-spring/spring-core v0.0.0 - -require ( - github.com/expr-lang/expr v1.17.2 // indirect - github.com/go-spring/gs-mock v0.0.4 // indirect - github.com/go-spring/log v0.0.3 // 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 - gopkg.in/yaml.v2 v2.4.0 // indirect -) - -replace github.com/go-spring/spring-core => ../../../ diff --git a/docs/4. examples/startup/go.sum b/docs/4. examples/startup/go.sum deleted file mode 100644 index 8b941181..00000000 --- a/docs/4. examples/startup/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -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/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/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/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= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -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= -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= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/docs/4. examples/startup/main.go b/docs/4. examples/startup/main.go deleted file mode 100644 index d034a316..00000000 --- a/docs/4. examples/startup/main.go +++ /dev/null @@ -1,75 +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 main - -import ( - "fmt" - "net/http" - "time" - - "github.com/go-spring/spring-core/gs" -) - -func init() { - // Register the Service struct as a bean. - gs.Object(&Service{}) - - // Provide a [*http.ServeMux] as a bean. - gs.Provide(func(s *Service) *http.ServeMux { - http.HandleFunc("/echo", s.Echo) - http.HandleFunc("/refresh", s.Refresh) - return http.DefaultServeMux - }) - - gs.Property("start-time", time.Now().Format(timeLayout)) - gs.Property("refresh-time", time.Now().Format(timeLayout)) -} - -const timeLayout = "2006-01-02 15:04:05.999 -0700 MST" - -type Service struct { - StartTime time.Time `value:"${start-time}"` - RefreshTime gs.Dync[time.Time] `value:"${refresh-time}"` -} - -func (s *Service) Echo(w http.ResponseWriter, r *http.Request) { - str := fmt.Sprintf("start-time: %s refresh-time: %s", - s.StartTime.Format(timeLayout), - s.RefreshTime.Value().Format(timeLayout)) - _, _ = w.Write([]byte(str)) -} - -func (s *Service) Refresh(w http.ResponseWriter, r *http.Request) { - gs.Property("refresh-time", time.Now().Format(timeLayout)) - _ = gs.RefreshProperties() - _, _ = w.Write([]byte("OK!")) -} - -func main() { - gs.Run() -} - -// ➜ curl http://127.0.0.1:9090/echo -// start-time: 2025-03-14 13:32:51.608 +0800 CST refresh-time: 2025-03-14 13:32:51.608 +0800 CST% -// ➜ curl http://127.0.0.1:9090/refresh -// OK!% -// ➜ curl http://127.0.0.1:9090/echo -// start-time: 2025-03-14 13:32:51.608 +0800 CST refresh-time: 2025-03-14 13:33:02.936 +0800 CST% -// ➜ curl http://127.0.0.1:9090/refresh -// OK!% -// ➜ curl http://127.0.0.1:9090/echo -// start-time: 2025-03-14 13:32:51.608 +0800 CST refresh-time: 2025-03-14 13:33:08.88 +0800 CST% diff --git a/docs/5. advanced/advanced.md b/docs/5. advanced/advanced.md deleted file mode 100644 index af1e9a05..00000000 --- a/docs/5. advanced/advanced.md +++ /dev/null @@ -1 +0,0 @@ -todo 条件注入、自定义扩展、插件机制、热更新、模块组合 \ No newline at end of file diff --git a/docs/6. integrations/integrations.md b/docs/6. integrations/integrations.md deleted file mode 100644 index 7dd16c2b..00000000 --- a/docs/6. integrations/integrations.md +++ /dev/null @@ -1 +0,0 @@ -todo 与数据库、消息队列、缓存、中间件等的结合方式 \ No newline at end of file diff --git a/docs/7. faq.md b/docs/7. faq.md deleted file mode 100644 index d21aba8c..00000000 --- a/docs/7. faq.md +++ /dev/null @@ -1 +0,0 @@ -todo FAQ,错误处理、故障排查、性能调优建议 \ No newline at end of file diff --git a/docs/8. contributing.md b/docs/8. contributing.md deleted file mode 100644 index 1f02b41c..00000000 --- a/docs/8. contributing.md +++ /dev/null @@ -1 +0,0 @@ -todo 如何参与开发、测试、PR 流程、代码规范 \ No newline at end of file diff --git a/docs/9. changelog.md b/docs/9. changelog.md deleted file mode 100644 index 64b54164..00000000 --- a/docs/9. changelog.md +++ /dev/null @@ -1 +0,0 @@ -todo Changelog、升级指南、版本兼容性 \ No newline at end of file diff --git a/go.mod b/go.mod index 9435a68b..2601bd88 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,12 @@ module github.com/go-spring/spring-core 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.6 + github.com/expr-lang/expr v1.17.6 + github.com/go-spring/gs-mock v0.0.5 + github.com/go-spring/log v0.0.12 + github.com/go-spring/spring-base v1.2.4 github.com/magiconair/properties v1.8.10 github.com/pelletier/go-toml v1.9.5 - github.com/spf13/cast v1.9.2 + github.com/spf13/cast v1.10.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 0673ca22..ffee8d02 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,13 @@ -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/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= +github.com/expr-lang/expr v1.17.6/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.6 h1:R+0mKWCNzaEIZtqdfCc4e/Ha4FBhr01If5oJXjEwRO0= -github.com/go-spring/log v0.0.6/go.mod h1:WrLbwbjmU8Vk5ampzBXjG4rTv+BoxmmTXxX82L2hugg= +github.com/go-spring/gs-mock v0.0.5 h1:OGC+Lx1XgOoaKfmiN6mLasASQUtbhgVVoDZOhR4d+GA= +github.com/go-spring/gs-mock v0.0.5/go.mod h1:QK0PqZ+Vu9F+BU97zl8fip5XKibvDSoN+ofky413Z6Q= +github.com/go-spring/log v0.0.12 h1:q7we9bk+rZ/1r1HwiEMj5k6v9c4j790VdboYlB6+4/0= +github.com/go-spring/log v0.0.12/go.mod h1:l2L8e4cpQYZETRV2wHPII7CZTAnn2SUBrZnaiTR3QH4= +github.com/go-spring/spring-base v1.2.4 h1:z113Werjmcvoo/78Wp8/QEmxpfga+UpBrVcp9xffShU= +github.com/go-spring/spring-base v1.2.4/go.mod h1:IZDihx2XI4IpAdY3mkKOOHhU3nQbg5xLpi/06EqTvHU= 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 +20,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.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= -github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/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/gs/app.go b/gs/app.go index 9ccf7b06..11acc576 100644 --- a/gs/app.go +++ b/gs/app.go @@ -18,58 +18,100 @@ package gs import ( "context" + "os" + "os/signal" + "syscall" "github.com/go-spring/log" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs_app" ) +// AppStarter is a wrapper to manage the lifecycle of a Spring application. +// It handles initialization, running, graceful shutdown, and logging. type AppStarter struct{} -// initApp initializes the app. -func (s *AppStarter) initApp() error { +// startApp initializes logging, runs the Boot implementation, +// and then starts the main application. +func (s *AppStarter) startApp() error { + + // Print application banner at startup printBanner() + + // Initialize logger if err := initLog(); err != nil { return err } + + // Run Boot implementation (pre-app setup) if err := B.(*gs_app.BootImpl).Run(); err != nil { return err } - B = nil + B = nil // Release Boot instance after running + + // Start the application + if err := app.Start(); err != nil { + return err + } return nil } -// Run runs the app and waits for an interrupt signal to exit. -func (s *AppStarter) Run() { - s.RunWith(nil) +// stopApp waits for the application to shut down and cleans up resources. +// NOTE: ShutDown() must be called before invoking this function. +func (s *AppStarter) stopApp() { + app.WaitForShutdown() + log.Destroy() } -// RunWith runs the app with a given function and waits for an interrupt signal to exit. -func (s *AppStarter) RunWith(fn func(ctx context.Context) error) { - var err error - defer func() { - if err != nil { - log.Errorf(context.Background(), log.TagAppDef, "app run failed: %v", err) - } - }() - if err = s.initApp(); err != nil { +// Run starts the application, optionally runs a user-defined callback, +// and waits for termination signals (e.g., SIGTERM, Ctrl+C) to trigger graceful shutdown. +func (s *AppStarter) Run(fn ...func() error) { + + // Start application + if err := s.startApp(); err != nil { + err = util.WrapError(err, "start app failed") + log.Errorf(context.Background(), log.TagAppDef, "%s", err) return } - if err = app.RunWith(fn); err != nil { - return + + // Execute user-provided callback after app starts + if len(fn) > 0 && fn[0] != nil { + if err := fn[0](); err != nil { + err = util.WrapError(err, "start app failed") + log.Errorf(context.Background(), log.TagAppDef, "%s", err) + return + } } - log.Destroy() + + // Start a goroutine to listen for OS interrupt or termination signals + go func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt, syscall.SIGTERM) + sig := <-ch + signal.Stop(ch) + close(ch) + log.Infof(context.Background(), log.TagAppDef, "Received signal: %v", sig) + app.ShutDown() + }() + + // Wait until shutdown completes + s.stopApp() } -// RunAsync runs the app asynchronously and returns a function to stop the app. +// RunAsync starts the application asynchronously and returns a function +// that can be used to trigger shutdown from outside. func (s *AppStarter) RunAsync() (func(), error) { - if err := s.initApp(); err != nil { - return nil, err - } - if err := app.Start(); err != nil { + + // Start application + if err := s.startApp(); err != nil { + err = util.WrapError(err, "start app failed") + log.Errorf(context.Background(), log.TagAppDef, "%s", err) return nil, err } + + // Return a shutdown function return func() { - app.Stop() - log.Destroy() + app.ShutDown() + s.stopApp() }, nil } diff --git a/gs/banner.go b/gs/banner.go index 1ab10b9a..e541c60d 100644 --- a/gs/banner.go +++ b/gs/banner.go @@ -47,9 +47,9 @@ func printBanner() { maxLength := 0 for s := range strings.SplitSeq(appBanner, "\n") { - sb.WriteString("\x1b[36m") + sb.WriteString("\x1b[36m") // ANSI code for cyan color sb.WriteString(s) - sb.WriteString("\x1b[0m\n") + sb.WriteString("\x1b[0m\n") // ANSI code to reset color if len(s) > maxLength { maxLength = len(s) } @@ -59,14 +59,18 @@ func printBanner() { sb.WriteString("\n") } + // print version and website + const info = Version + " " + Website + var padding []byte - if n := (maxLength - len(Version)) / 2; n > 0 { + if n := (maxLength - len(info)) / 2; n > 0 { padding = make([]byte, n) for i := range padding { padding[i] = ' ' } } sb.WriteString(string(padding)) - sb.WriteString(Version) + sb.WriteString(info) + sb.WriteString("\n") fmt.Println(sb.String()) } diff --git a/gs/gs.go b/gs/gs.go index ca828edd..74b0ca01 100644 --- a/gs/gs.go +++ b/gs/gs.go @@ -20,13 +20,13 @@ import ( "context" "reflect" "runtime" + "strings" "github.com/go-spring/log" "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_app" "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" "github.com/go-spring/spring-core/gs/internal/gs_conf" "github.com/go-spring/spring-core/gs/internal/gs_dync" @@ -34,38 +34,51 @@ import ( const ( Version = "go-spring@v1.2.3" - Website = "https://go-spring.com/" + Website = "https://github.com/go-spring/" ) -// As returns the [reflect.Type] of the given interface type. +// Dync is a generic alias for a dynamic configuration value. +// It represents a property that can change at runtime. +type Dync[T any] = gs_dync.Value[T] + +// BeanSelector is an alias for gs.BeanSelector used to locate beans +// within the ioc context. +type BeanSelector = gs.BeanSelector + +// BeanSelectorFor creates a BeanSelector for the specified type T +// and optional bean name. +func BeanSelectorFor[T any](name ...string) BeanSelector { + return gs.BeanSelectorFor[T](name...) +} + +// As returns the [reflect.Type] for a given interface type T. func As[T any]() reflect.Type { return gs.As[T]() } /************************************ arg ***********************************/ +// Arg represents an argument used when binding constructor parameters. type Arg = gs.Arg -// TagArg returns a TagArg with the specified tag. -// Used for property binding or object injection when providing constructor parameters. +// TagArg creates an argument that injects a property or bean +// identified by the specified struct-tag expression. func TagArg(tag string) Arg { return gs_arg.Tag(tag) } -// ValueArg returns a ValueArg with the specified value. -// Used to provide specific values for constructor parameters. +// ValueArg creates an argument with a fixed value. func ValueArg(v any) Arg { return gs_arg.Value(v) } -// IndexArg returns an IndexArg with the specified index and argument. -// When most constructor parameters can use default values, IndexArg helps reduce configuration effort. +// IndexArg targets a specific constructor parameter by index +// and provides the given Arg as its value. func IndexArg(n int, arg Arg) Arg { return gs_arg.Index(n, arg) } -// BindArg returns a BindArg for the specified function and arguments. -// Used to provide argument binding for option-style constructor parameters. +// BindArg binds arguments dynamically to an option-style constructor. func BindArg(fn any, args ...Arg) *gs_arg.BindArg { return gs_arg.Bind(fn, args...) } @@ -73,14 +86,19 @@ func BindArg(fn any, args ...Arg) *gs_arg.BindArg { /************************************ cond ***********************************/ type ( - Condition = gs.Condition - ConditionContext = gs.ConditionContext + // Condition represents a logical predicate that decides whether + // a bean or module should be activated. + Condition = gs.Condition + + // ConditionContext provides the evaluation context for a Condition. + ConditionContext = gs.ConditionContext + + // ConditionOnProperty is a convenience wrapper for property-based conditions. 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. +// OnOnce wraps the given conditions so they are evaluated only once. +// Subsequent calls return the same result. (Not concurrency-safe.) func OnOnce(conditions ...Condition) Condition { var ( done bool @@ -96,219 +114,138 @@ func OnOnce(conditions ...Condition) Condition { }) } -// OnFunc creates a Condition based on the provided function. +// OnFunc creates a Condition backed by the given function. 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. +// OnProperty creates a property-based condition. func OnProperty(name string) ConditionOnProperty { return gs_cond.OnProperty(name) } -// OnBean creates a Condition for when a specific bean exists. +// OnBean requires that a bean of the given type (and optional name) exists. func OnBean[T any](name ...string) Condition { return gs_cond.OnBean[T](name...) } -// OnMissingBean creates a Condition for when a specific bean is missing. +// OnMissingBean requires that no bean of the given type (and optional name) exists. func OnMissingBean[T any](name ...string) Condition { return gs_cond.OnMissingBean[T](name...) } -// OnSingleBean creates a Condition for when only one instance of a bean exists. +// OnSingleBean requires that exactly one instance of the given bean type exists. func OnSingleBean[T any](name ...string) Condition { return gs_cond.OnSingleBean[T](name...) } -// RegisterExpressFunc registers a custom expression function. +// RegisterExpressFunc registers a custom expression function +// that can be used inside conditional expressions. func RegisterExpressFunc(name string, fn any) { gs_cond.RegisterExpressFunc(name, fn) } -// OnExpression creates a Condition based on a custom expression. +// OnExpression creates a condition from an expression. func OnExpression(expression string) Condition { return gs_cond.OnExpression(expression) } -// Not creates a Condition that negates the given Condition. +// Not returns the logical negation of the given condition. func Not(c Condition) Condition { return gs_cond.Not(c) } -// Or creates a Condition that is true if any of the given Conditions are true. +// Or combines multiple conditions using logical OR. func Or(conditions ...Condition) Condition { return gs_cond.Or(conditions...) } -// And creates a Condition that is true if all the given Conditions are true. +// And combines multiple conditions using logical AND. func And(conditions ...Condition) Condition { return gs_cond.And(conditions...) } -// None creates a Condition that is true if none of the given Conditions are true. +// None returns a condition that is true if all provided conditions are false. func None(conditions ...Condition) Condition { return gs_cond.None(conditions...) } -// OnEnableJobs creates a Condition that checks whether the EnableJobsProp property is true. +// OnEnableJobs is a shortcut for checking whether scheduled jobs are enabled. func OnEnableJobs() ConditionOnProperty { return OnProperty(EnableJobsProp).HavingValue("true").MatchIfMissing() } -// OnEnableServers creates a Condition that checks whether the EnableServersProp property is true. +// OnEnableServers is a shortcut for checking whether servers are enabled. func OnEnableServers() ConditionOnProperty { return OnProperty(EnableServersProp).HavingValue("true").MatchIfMissing() } -/************************************ ioc ************************************/ +/*********************************** app *************************************/ type ( - BeanID = gs.BeanID -) + // Server is an alias for gs.Server. + Server = gs.Server -type ( - Dync[T any] = gs_dync.Value[T] + // ReadySignal represents a signal sent when the application is ready. + ReadySignal = gs.ReadySignal ) -type ( - RegisteredBean = gs.RegisteredBean - BeanDefinition = gs.BeanDefinition -) +var ( + // B is the global bootstrapper for initializing the application. + B = gs_app.NewBoot() -type ( - BeanSelector = gs.BeanSelector - BeanInitFunc = gs.BeanInitFunc - BeanDestroyFunc = gs.BeanDestroyFunc + // app is the global application instance. + app = gs_app.NewApp() ) -// NewBean creates a new BeanDefinition. -func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { - return gs_bean.NewBean(objOrCtor, ctorArgs...).Caller(1) -} - -// BeanSelectorFor returns a BeanSelector for the given type. -func BeanSelectorFor[T any](name ...string) BeanSelector { - return gs.BeanSelectorFor[T](name...) +// Config returns the current application configuration. +func Config() *gs_conf.AppConfig { + return app.P } -/*********************************** app *************************************/ - // Property sets a system property. func Property(key string, val string) { _, 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) + log.Errorf(context.Background(), log.TagAppDef, "failed to set property key=%s err=%v", key, err) } } -type ( - Runner = gs.Runner - Job = gs.Job - Server = gs.Server - ReadySignal = gs.ReadySignal -) - -var B = gs_app.NewBoot() -var app = gs_app.NewApp() - -// funcRunner is a function type that implements the Runner interface. -type funcRunner func() error - -func (f funcRunner) Run() error { - return f() -} - -// FuncRunner creates a Runner from a function. -func FuncRunner(fn func() error) *RegisteredBean { - return Object(funcRunner(fn)).AsRunner().Caller(1) -} - -// funcJob is a function type that implements the Job interface. -type funcJob func(ctx context.Context) error - -func (f funcJob) Run(ctx context.Context) error { - return f(ctx) -} - -// FuncJob creates a Job from a function. -func FuncJob(fn func(ctx context.Context) error) *RegisteredBean { - return Object(funcJob(fn)).AsJob().Caller(1) -} - -// Web enables or disables the built-in web server. -func Web(enable bool) *AppStarter { - EnableSimpleHttpServer(enable) - return &AppStarter{} -} - -// Run runs the app and waits for an interrupt signal to exit. -func Run() { - new(AppStarter).Run() -} - -// RunWith runs the app with a given function and waits for an interrupt signal to exit. -func RunWith(fn func(ctx context.Context) error) { - new(AppStarter).RunWith(fn) -} - -// RunAsync runs the app asynchronously and returns a function to stop the app. -func RunAsync() (func(), error) { - return new(AppStarter).RunAsync() -} - -// Exiting returns a boolean indicating whether the application is exiting. -func Exiting() bool { - return app.Exiting() -} - -// ShutDown shuts down the app with an optional message. -func ShutDown() { - app.ShutDown() -} - -// Config returns the app configuration. -func Config() *gs_conf.AppConfig { - 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)) - app.C.Register(b).Caller(1) - return i -} - -// RootBean registers a root bean definition. -func RootBean(b *RegisteredBean) { - app.C.RootBean(b) +// RefreshProperties reloads application properties from all sources. +func RefreshProperties() error { + p, err := app.P.Refresh() + if err != nil { + return err + } + return app.C.RefreshProperties(p) } -// Object registers a bean definition for a given object. -func Object(i any) *RegisteredBean { - b := gs_bean.NewBean(reflect.ValueOf(i)) - return app.C.Register(b).Caller(1) +// Root registers a root bean in the application context. +func Root(b *gs.RegisteredBean) { + app.C.Root(b) } -// Provide registers a bean definition for a given constructor. -func Provide(ctor any, args ...Arg) *RegisteredBean { - b := gs_bean.NewBean(ctor, args...) - return app.C.Register(b).Caller(1) +// Object registers a bean definition for an existing object instance. +func Object(i any) *gs.RegisteredBean { + return app.C.Object(i).Caller(1) } -// Register registers a bean definition. -func Register(b *BeanDefinition) *RegisteredBean { - return app.C.Register(b) +// Provide registers a bean definition using the provided constructor function. +func Provide(ctor any, args ...Arg) *gs.RegisteredBean { + return app.C.Provide(ctor, args...).Caller(1) } -// Module registers a module. +// Module registers a configuration module that is conditionally activated +// based on property values. func Module(conditions []ConditionOnProperty, fn func(p conf.Properties) error) { app.C.Module(conditions, 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) { +// Group registers a set of beans based on a configuration property map. +// Each map entry spawns a bean constructed via fn and optionally destroyed via d. +func Group[T any, R any](tag string, fn func(c T) (R, error), d func(R) error) { + key := strings.TrimSuffix(strings.TrimPrefix(tag, "${"), "}") app.C.Module([]ConditionOnProperty{ OnProperty(key), }, func(p conf.Properties) error { @@ -326,11 +263,39 @@ func Group[T any, R any](key string, fn func(c T) (R, error), d func(R) error) { }) } -// RefreshProperties refreshes the app configuration. -func RefreshProperties() error { - p, err := app.P.Refresh() - if err != nil { - return err - } - return app.C.RefreshProperties(p) +// Runner registers a function as a runner bean. +func Runner(fn func() error) *gs.RegisteredBean { + return Object(gs.FuncRunner(fn)).AsRunner().Caller(1) +} + +// Job registers a function as a job bean. +func Job(fn func(ctx context.Context) error) *gs.RegisteredBean { + return Object(gs.FuncJob(fn)).AsJob().Caller(1) +} + +// Web enables or disables the built-in HTTP server. +func Web(enable bool) *AppStarter { + EnableSimpleHttpServer(enable) + return &AppStarter{} +} + +// Run starts the application with a custom run function. +func Run(fn ...func() error) { + new(AppStarter).Run(fn...) +} + +// RunAsync starts the application asynchronously and +// returns a stop function to gracefully shut it down. +func RunAsync() (func(), error) { + return new(AppStarter).RunAsync() +} + +// Exiting returns true if the application is shutting down. +func Exiting() bool { + return app.Exiting() +} + +// ShutDown gracefully stops the application. +func ShutDown() { + app.ShutDown() } diff --git a/gs/gs_test.go b/gs/gs_test.go deleted file mode 100644 index 86b28501..00000000 --- a/gs/gs_test.go +++ /dev/null @@ -1,46 +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 gs_test - -import ( - "fmt" - "testing" - - "github.com/go-spring/spring-core/gs" -) - -func TestMain(m *testing.M) { - gs.AddTester(&Tester{}) - gs.Object(&Dep{}) - gs.TestMain(m) -} - -func TestAA(t *testing.T) { - -} - -type Dep struct { - Name string `value:"${name:=TestAA}"` -} - -type Tester struct { - Dep *Dep `autowire:""` -} - -func (o *Tester) TestAA(t *testing.T) { - fmt.Println(o.Dep.Name) -} diff --git a/gs/http.go b/gs/http.go index 64ecddf6..d27312b6 100644 --- a/gs/http.go +++ b/gs/http.go @@ -18,124 +18,91 @@ package gs import ( "context" + "errors" "net" "net/http" "time" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/conf" - "github.com/go-spring/spring-core/gs/internal/gs" ) func init() { - 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 - }) + Module([]ConditionOnProperty{ + OnEnableServers(), + OnProperty(EnableSimpleHttpServerProp).HavingValue("true").MatchIfMissing(), + }, func(p conf.Properties) error { + + // Register the default HTTP multiplexer as a bean + // if no other http.Handler bean has been defined. + Provide(http.DefaultServeMux). + Export(As[http.Handler]()). + Condition(OnMissingBean[http.Handler]()) + + // Provide a new SimpleHttpServer instance with + // http.Handler injection and configuration binding. + Provide(NewSimpleHttpServer).AsServer() + + return nil + }) } -// HttpServerConfig holds configuration options for the HTTP server. -type HttpServerConfig struct { - Address string // The address to bind the server to. - ReadTimeout time.Duration // The read timeout duration. - HeaderTimeout time.Duration // The header timeout duration. - WriteTimeout time.Duration // The write timeout duration. - IdleTimeout time.Duration // The idle timeout duration. -} +// SimpleHttpServerConfig holds configuration for the SimpleHttpServer. +type SimpleHttpServerConfig struct { + // Address specifies the TCP address the server listens on. + // Example: ":9090" (listen on all interfaces, port 9090). + Address string `value:"${http.server.addr:=:9090}"` -// HttpServerOption is a function type for setting options on HttpServerConfig. -type HttpServerOption func(arg *HttpServerConfig) + // ReadTimeout is the maximum duration for reading the entire + // request, including the body. + ReadTimeout time.Duration `value:"${http.server.readTimeout:=5s}"` -// SetHttpServerAddr sets the address of the HTTP server. -func SetHttpServerAddr(addr string) HttpServerOption { - return func(arg *HttpServerConfig) { - arg.Address = addr - } -} + // HeaderTimeout is the maximum duration for reading request headers. + HeaderTimeout time.Duration `value:"${http.server.headerTimeout:=1s}"` -// SetHttpServerReadTimeout sets the read timeout for the HTTP server. -func SetHttpServerReadTimeout(timeout time.Duration) HttpServerOption { - return func(arg *HttpServerConfig) { - arg.ReadTimeout = timeout - } -} + // WriteTimeout is the maximum duration before timing out + // a response write. + WriteTimeout time.Duration `value:"${http.server.writeTimeout:=5s}"` -// SetHttpServerHeaderTimeout sets the header timeout for the HTTP server. -func SetHttpServerHeaderTimeout(timeout time.Duration) HttpServerOption { - return func(arg *HttpServerConfig) { - arg.HeaderTimeout = timeout - } + // IdleTimeout is the maximum amount of time to wait for + // the next request when keep-alive connections are enabled. + IdleTimeout time.Duration `value:"${http.server.idleTimeout:=60s}"` } -// SetHttpServerWriteTimeout sets the write timeout for the HTTP server. -func SetHttpServerWriteTimeout(timeout time.Duration) HttpServerOption { - return func(arg *HttpServerConfig) { - arg.WriteTimeout = timeout - } -} - -// SetHttpServerIdleTimeout sets the idle timeout for the HTTP server. -func SetHttpServerIdleTimeout(timeout time.Duration) HttpServerOption { - return func(arg *HttpServerConfig) { - arg.IdleTimeout = timeout - } -} - -// SimpleHttpServer wraps a [http.Server] instance. +// SimpleHttpServer wraps a standard [http.Server] to integrate +// it into the Go-Spring application lifecycle. type SimpleHttpServer struct { svr *http.Server // The HTTP server instance. } -// NewSimpleHttpServer creates a new instance of SimpleHttpServer. -func NewSimpleHttpServer(h http.Handler, opts ...HttpServerOption) *SimpleHttpServer { - arg := &HttpServerConfig{ - Address: "0.0.0.0:9090", - ReadTimeout: time.Second * 5, - HeaderTimeout: time.Second, - WriteTimeout: time.Second * 5, - IdleTimeout: time.Second * 60, - } - for _, opt := range opts { - opt(arg) - } +// NewSimpleHttpServer constructs a new SimpleHttpServer using +// the provided HTTP handler and configuration. +func NewSimpleHttpServer(h http.Handler, cfg SimpleHttpServerConfig) *SimpleHttpServer { return &SimpleHttpServer{svr: &http.Server{ - Addr: arg.Address, + Addr: cfg.Address, Handler: h, - ReadTimeout: arg.ReadTimeout, - WriteTimeout: arg.WriteTimeout, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, }} } -// ListenAndServe starts the HTTP server and listens for incoming connections. +// ListenAndServe starts the HTTP server and blocks until it is stopped. +// It waits for the given ReadySignal to be triggered before accepting traffic. func (s *SimpleHttpServer) ListenAndServe(sig ReadySignal) error { ln, err := net.Listen("tcp", s.svr.Addr) if err != nil { - return err + return util.FormatError(err, "failed to listen on %s", s.svr.Addr) } <-sig.TriggerAndWait() - return s.svr.Serve(ln) + err = s.svr.Serve(ln) + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return util.FormatError(err, "failed to serve on %s", s.svr.Addr) } -// Shutdown gracefully shuts down the HTTP server with the given context. +// Shutdown gracefully stops the HTTP server using the provided context, +// allowing in-flight requests to complete before closing. func (s *SimpleHttpServer) Shutdown(ctx context.Context) error { return s.svr.Shutdown(ctx) } diff --git a/gs/internal/gs/gs.go b/gs/internal/gs/gs.go index ff9b6300..dd3db544 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=ConditionContext,ArgContext,Runner,Job,Server +//go:generate gs mock -o=gs_mock.go -i=ConditionContext,ArgContext,Server package gs @@ -28,9 +28,8 @@ import ( // anyType is the [reflect.Type] of the [any] type. var anyType = reflect.TypeFor[any]() -// As returns the [reflect.Type] of the given interface type. -// It ensures that the provided generic type parameter T is an interface. -// If T is not an interface, the function panics. +// As returns the [reflect.Type] of the given generic interface type T. +// It ensures that T is an interface type; otherwise, it panics. func As[T any]() reflect.Type { t := reflect.TypeFor[T]() if t.Kind() != reflect.Interface { @@ -39,20 +38,22 @@ func As[T any]() reflect.Type { return t } -// BeanSelector is an interface for selecting beans. +// BeanSelector is an abstraction that represents a way to select beans +// within the IoC container. It identifies a bean by its type and optionally its name. type BeanSelector interface { - // TypeAndName returns the type and name of the bean. + // TypeAndName returns the [reflect.Type] and name that uniquely identify the bean. TypeAndName() (reflect.Type, string) } -// BeanSelectorImpl is an implementation of BeanSelector. +// BeanSelectorImpl is a concrete implementation of BeanSelector. type BeanSelectorImpl struct { - Type reflect.Type // The type of the bean - Name string // The name of the bean + Type reflect.Type // The [reflect.Type] of the bean + Name string // The optional name of the bean } -// BeanSelectorFor returns a BeanSelectorImpl for the given type. -// If a name is provided, it is set; otherwise, only the type is used. +// BeanSelectorFor creates a BeanSelector for a specific type T. +// If a name is provided, it will be associated with the selector; +// otherwise, only the type is used to identify the bean. func BeanSelectorFor[T any](name ...string) BeanSelector { if len(name) == 0 { return BeanSelectorImpl{Type: reflect.TypeFor[T]()} @@ -60,11 +61,13 @@ func BeanSelectorFor[T any](name ...string) BeanSelector { return BeanSelectorImpl{Type: reflect.TypeFor[T](), Name: name[0]} } -// TypeAndName returns the type and name of the bean. +// TypeAndName returns the type and name of the bean selector. func (s BeanSelectorImpl) TypeAndName() (reflect.Type, string) { return s.Type, s.Name } +// String returns a human-readable string representation of the selector. +// Example: "{Type:*mypkg.MyBean,Name:myBeanInstance}" func (s BeanSelectorImpl) String() string { var sb strings.Builder sb.WriteString("{") @@ -85,70 +88,89 @@ func (s BeanSelectorImpl) String() string { /************************************ cond ***********************************/ -// Condition is an interface used for defining conditional logic -// when registering beans in the IoC container. +// Condition defines a contract for conditional bean registration. +// A Condition can decide at runtime whether a particular bean should be registered. type Condition interface { - // Matches checks whether the condition is satisfied. + // Matches evaluates the condition against the given ConditionContext. + // It returns true if the condition is satisfied. Matches(ctx ConditionContext) (bool, error) } -// ConditionBean represents a bean with Name and Type. +// ConditionBean represents a bean in the IoC container that can be queried by conditions. type ConditionBean interface { - Name() string // Name of the bean - Type() reflect.Type // Type of the bean + Name() string // Returns the bean's name + Type() reflect.Type // Returns the bean's type } -// ConditionContext defines methods for the IoC container used by conditions. +// ConditionContext provides access to the IoC container for conditions. +// Conditions can query properties or find beans in the container. type ConditionContext interface { - // Has checks whether the IoC container has a property with the given key. + // Has checks if a property with the given key exists in the IoC container. Has(key string) bool - // Prop retrieves the value of a property from the IoC container. + // Prop retrieves a property value from the IoC container with an optional default. Prop(key string, def ...string) string - // Find searches for bean definitions matching the given BeanSelector. + // Find searches for beans that match the given BeanSelector. Find(s BeanSelector) ([]ConditionBean, error) } /************************************* arg ***********************************/ -// Arg is an interface for retrieving argument values in function parameter binding. +// Arg defines an interface for resolving arguments used in dependency injection. +// It determines how to obtain values for function or method parameters. type Arg interface { - // GetArgValue retrieves the argument value based on the type. + // GetArgValue retrieves the argument value for the given type + // using the provided ArgContext. GetArgValue(ctx ArgContext, t reflect.Type) (reflect.Value, error) } -// ArgContext defines methods for the IoC container used by Arg types. +// ArgContext provides the runtime context for resolving arguments. +// It allows checking conditions, binding properties, and wiring dependencies. type ArgContext interface { - // Check checks if the given condition is met. + // Check evaluates whether a given condition is satisfied. Check(c Condition) (bool, error) - // Bind binds property values to the provided [reflect.Value]. + // Bind binds configuration or property values into the provided [reflect.Value]. Bind(v reflect.Value, tag string) error - // Wire wires dependent beans to the provided [reflect.Value]. + // Wire injects dependencies (beans) into the provided [reflect.Value]. Wire(v reflect.Value, tag string) error } /*********************************** app ************************************/ -// Runner defines an interface for components that should run after -// all beans are injected but before the application servers start. +// Runner is an interface for components that need to run +// after all beans have been injected but before the application’s servers start. type Runner interface { Run() error } -// Job defines an interface for components that run tasks with a given context -// after all beans are injected but before the application servers start. +// FuncRunner is a function type adapter that allows ordinary functions +// to be used as Runner components. +type FuncRunner func() error + +func (f FuncRunner) Run() error { + return f() +} + +// Job is similar to Runner but allows passing a context to the task. +// It is typically used for background tasks or setup work that may be cancellable. type Job interface { Run(ctx context.Context) error } -// ReadySignal defines an interface for components that can trigger a signal -// when the application is ready to serve requests. +// FuncJob is a function type adapter for the Job interface. +type FuncJob func(ctx context.Context) error + +func (f FuncJob) Run(ctx context.Context) error { + return f(ctx) +} + +// ReadySignal represents a synchronization mechanism that signals +// when the application is ready to accept requests. type ReadySignal interface { TriggerAndWait() <-chan struct{} } -// Server defines an interface for managing the lifecycle of application servers, -// such as HTTP, gRPC, Thrift, or MQ servers. It includes methods for starting -// and shutting down the server gracefully. +// Server defines the lifecycle of application servers (e.g., HTTP, gRPC). +// It provides methods for starting and gracefully shutting down the server. type Server interface { ListenAndServe(sig ReadySignal) error Shutdown(ctx context.Context) error @@ -156,33 +178,35 @@ type Server interface { /*********************************** bean ************************************/ -// BeanMock defines a mock object and its target bean selector for overriding. +// BeanMock represents a mocked bean instance that can replace a real bean +// for testing purposes. type BeanMock struct { - Object any // Mock instance to replace the target bean - Target BeanSelector // Selector to identify the target bean + Object any // The mock instance + Target BeanSelector // The selector identifying the bean to replace } -// BeanID represents the unique identifier for a bean. +// BeanID uniquely identifies a bean in the IoC container by type and name. type BeanID struct { - Type reflect.Type // Type of the bean - Name string // Name of the bean + Type reflect.Type // The bean's type + Name string // The bean's name } -// BeanInitFunc defines the prototype for initialization functions. -// Examples: `func(bean)` or `func(bean) error`. +// BeanInitFunc defines the type for bean initialization functions. +// Example: `func(bean)` or `func(bean) error`. type BeanInitFunc = any -// BeanDestroyFunc defines the prototype for destruction functions. -// Examples: `func(bean)` or `func(bean) error`. +// BeanDestroyFunc defines the type for bean destruction (cleanup) functions. +// Example: `func(bean)` or `func(bean) error`. type BeanDestroyFunc = any -// Configuration holds parameters for bean setup configuration. +// Configuration specifies parameters for configuring beans during registration. type Configuration struct { Includes []string // Methods to include Excludes []string // Methods to exclude } -// BeanRegistration provides methods for configuring and registering bean metadata. +// BeanRegistration defines the API for configuring and registering a bean’s metadata +// in the IoC container. type BeanRegistration interface { Name() string Type() reflect.Type @@ -200,17 +224,17 @@ type BeanRegistration interface { OnProfiles(profiles string) } -// beanBuilder helps configure a bean during its creation. +// beanBuilder is a generic helper for configuring beans during their creation. type beanBuilder[T any] struct { b BeanRegistration } -// TypeAndName returns the type and name of the bean. +// TypeAndName returns the bean’s type and name. func (d *beanBuilder[T]) TypeAndName() (reflect.Type, string) { return d.b.Type(), d.b.Name() } -// GetArgValue returns the value of the bean. +// GetArgValue returns the bean’s value for argument injection. func (d *beanBuilder[T]) GetArgValue(ctx ArgContext, t reflect.Type) (reflect.Value, error) { return d.b.Value(), nil } @@ -304,27 +328,26 @@ func (d *beanBuilder[T]) OnProfiles(profiles string) *T { return *(**T)(unsafe.Pointer(&d)) } -// RegisteredBean represents a bean that has been registered in the IoC container. -type RegisteredBean struct { - beanBuilder[RegisteredBean] -} - -// NewRegisteredBean creates a new RegisteredBean instance. -func NewRegisteredBean(d BeanRegistration) *RegisteredBean { - return &RegisteredBean{ - beanBuilder: beanBuilder[RegisteredBean]{d}, - } -} - -// BeanDefinition represents a bean that has not yet been registered. -// todo 等 Group 函数确定下来之后,BD 定义应该就不需要了。 +// BeanDefinition represents a bean that is defined but not yet registered in the IoC container. type BeanDefinition struct { beanBuilder[BeanDefinition] } -// NewBeanDefinition creates a new BeanDefinition instance. +// NewBeanDefinition creates a new BeanDefinition with the provided BeanRegistration. func NewBeanDefinition(d BeanRegistration) *BeanDefinition { return &BeanDefinition{ beanBuilder: beanBuilder[BeanDefinition]{d}, } } + +// RegisteredBean represents a bean that has already been registered in the IoC container. +type RegisteredBean struct { + beanBuilder[RegisteredBean] +} + +// NewRegisteredBean creates a new RegisteredBean with the provided BeanRegistration. +func NewRegisteredBean(d BeanRegistration) *RegisteredBean { + return &RegisteredBean{ + beanBuilder: beanBuilder[RegisteredBean]{d}, + } +} diff --git a/gs/internal/gs/gs_mock.go b/gs/internal/gs/gs_mock.go index 3327a360..47ad2da3 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.4. DO NOT EDIT. +// Code generated by gs-mock v0.0.5. DO NOT EDIT. // Source: https://github.com/go-spring/gs-mock -// gs mock -o gs_mock.go -i 'ConditionContext,ArgContext,Runner,Job,Server' +// gs mock -o gs_mock.go -i 'ConditionContext,ArgContext,Server' package gs @@ -10,171 +10,165 @@ import ( "reflect" ) +// ConditionContextMockImpl is a generated mock implementation of the ConditionContext interface. +// It embeds the original interfaces and provides methods to register mock behaviors. type ConditionContextMockImpl struct { r *gsmock.Manager } +// NewConditionContextMockImpl creates a new mock instance for ConditionContext with the given +// gsmock.Manager and initializes the embedded interfaces. func NewConditionContextMockImpl(r *gsmock.Manager) *ConditionContextMockImpl { return &ConditionContextMockImpl{r: r} } +// Has executes the mocked Has method. +// with 1 parameters and 1 return values. 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") + panic("no mock code matched for ConditionContext.Has") } +// MockHas returns a MockerNN instance to register mock behavior for Has. func (impl *ConditionContextMockImpl) MockHas() *gsmock.Mocker11[string, bool] { t := reflect.TypeFor[ConditionContextMockImpl]() return gsmock.NewMocker11[string, bool](impl.r, t, "Has") } +// Prop executes the mocked Prop method. +// with 2 parameters and 1 return values. 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") + panic("no mock code matched for ConditionContext.Prop") } +// MockProp returns a MockerNN instance to register mock behavior for Prop. func (impl *ConditionContextMockImpl) MockProp() *gsmock.Mocker21[string, []string, string] { t := reflect.TypeFor[ConditionContextMockImpl]() return gsmock.NewMocker21[string, []string, string](impl.r, t, "Prop") } +// Find executes the mocked Find method. +// with 1 parameters and 2 return values. 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[[]ConditionBean, error](ret) } - panic("no mock code matched") + panic("no mock code matched for ConditionContext.Find") } +// MockFind returns a MockerNN instance to register mock behavior for Find. func (impl *ConditionContextMockImpl) MockFind() *gsmock.Mocker12[BeanSelector, []ConditionBean, error] { t := reflect.TypeFor[ConditionContextMockImpl]() return gsmock.NewMocker12[BeanSelector, []ConditionBean, error](impl.r, t, "Find") } +// ArgContextMockImpl is a generated mock implementation of the ArgContext interface. +// It embeds the original interfaces and provides methods to register mock behaviors. type ArgContextMockImpl struct { r *gsmock.Manager } +// NewArgContextMockImpl creates a new mock instance for ArgContext with the given +// gsmock.Manager and initializes the embedded interfaces. func NewArgContextMockImpl(r *gsmock.Manager) *ArgContextMockImpl { return &ArgContextMockImpl{r: r} } +// Check executes the mocked Check method. +// with 1 parameters and 2 return values. func (impl *ArgContextMockImpl) Check(c Condition) (bool, error) { t := reflect.TypeFor[ArgContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Check", c); ok { return gsmock.Unbox2[bool, error](ret) } - panic("no mock code matched") + panic("no mock code matched for ArgContext.Check") } +// MockCheck returns a MockerNN instance to register mock behavior for Check. func (impl *ArgContextMockImpl) MockCheck() *gsmock.Mocker12[Condition, bool, error] { t := reflect.TypeFor[ArgContextMockImpl]() return gsmock.NewMocker12[Condition, bool, error](impl.r, t, "Check") } +// Bind executes the mocked Bind method. +// with 2 parameters and 1 return values. func (impl *ArgContextMockImpl) Bind(v reflect.Value, tag string) error { t := reflect.TypeFor[ArgContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Bind", v, tag); ok { return gsmock.Unbox1[error](ret) } - panic("no mock code matched") + panic("no mock code matched for ArgContext.Bind") } +// MockBind returns a MockerNN instance to register mock behavior for Bind. func (impl *ArgContextMockImpl) MockBind() *gsmock.Mocker21[reflect.Value, string, error] { t := reflect.TypeFor[ArgContextMockImpl]() return gsmock.NewMocker21[reflect.Value, string, error](impl.r, t, "Bind") } +// Wire executes the mocked Wire method. +// with 2 parameters and 1 return values. func (impl *ArgContextMockImpl) Wire(v reflect.Value, tag string) error { t := reflect.TypeFor[ArgContextMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Wire", v, tag); ok { return gsmock.Unbox1[error](ret) } - panic("no mock code matched") + panic("no mock code matched for ArgContext.Wire") } +// MockWire returns a MockerNN instance to register mock behavior for Wire. func (impl *ArgContextMockImpl) MockWire() *gsmock.Mocker21[reflect.Value, string, error] { t := reflect.TypeFor[ArgContextMockImpl]() return gsmock.NewMocker21[reflect.Value, string, error](impl.r, t, "Wire") } -type RunnerMockImpl struct { - r *gsmock.Manager -} - -func NewRunnerMockImpl(r *gsmock.Manager) *RunnerMockImpl { - return &RunnerMockImpl{r: r} -} - -func (impl *RunnerMockImpl) Run() error { - t := reflect.TypeFor[RunnerMockImpl]() - if ret, ok := gsmock.Invoke(impl.r, t, "Run"); ok { - return gsmock.Unbox1[error](ret) - } - panic("no mock code matched") -} - -func (impl *RunnerMockImpl) MockRun() *gsmock.Mocker01[error] { - t := reflect.TypeFor[RunnerMockImpl]() - return gsmock.NewMocker01[error](impl.r, t, "Run") -} - -type JobMockImpl struct { - r *gsmock.Manager -} - -func NewJobMockImpl(r *gsmock.Manager) *JobMockImpl { - return &JobMockImpl{r: r} -} - -func (impl *JobMockImpl) Run(ctx context.Context) error { - t := reflect.TypeFor[JobMockImpl]() - if ret, ok := gsmock.Invoke(impl.r, t, "Run", ctx); ok { - return gsmock.Unbox1[error](ret) - } - panic("no mock code matched") -} - -func (impl *JobMockImpl) MockRun() *gsmock.Mocker11[context.Context, error] { - t := reflect.TypeFor[JobMockImpl]() - return gsmock.NewMocker11[context.Context, error](impl.r, t, "Run") -} - +// ServerMockImpl is a generated mock implementation of the Server interface. +// It embeds the original interfaces and provides methods to register mock behaviors. type ServerMockImpl struct { r *gsmock.Manager } +// NewServerMockImpl creates a new mock instance for Server with the given +// gsmock.Manager and initializes the embedded interfaces. func NewServerMockImpl(r *gsmock.Manager) *ServerMockImpl { return &ServerMockImpl{r: r} } +// ListenAndServe executes the mocked ListenAndServe method. +// with 1 parameters and 1 return values. func (impl *ServerMockImpl) ListenAndServe(sig ReadySignal) error { t := reflect.TypeFor[ServerMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "ListenAndServe", sig); ok { return gsmock.Unbox1[error](ret) } - panic("no mock code matched") + panic("no mock code matched for Server.ListenAndServe") } +// MockListenAndServe returns a MockerNN instance to register mock behavior for ListenAndServe. func (impl *ServerMockImpl) MockListenAndServe() *gsmock.Mocker11[ReadySignal, error] { t := reflect.TypeFor[ServerMockImpl]() return gsmock.NewMocker11[ReadySignal, error](impl.r, t, "ListenAndServe") } +// Shutdown executes the mocked Shutdown method. +// with 1 parameters and 1 return values. func (impl *ServerMockImpl) Shutdown(ctx context.Context) error { t := reflect.TypeFor[ServerMockImpl]() if ret, ok := gsmock.Invoke(impl.r, t, "Shutdown", ctx); ok { return gsmock.Unbox1[error](ret) } - panic("no mock code matched") + panic("no mock code matched for Server.Shutdown") } +// MockShutdown returns a MockerNN instance to register mock behavior for Shutdown. func (impl *ServerMockImpl) MockShutdown() *gsmock.Mocker11[context.Context, error] { t := reflect.TypeFor[ServerMockImpl]() return gsmock.NewMocker11[context.Context, error](impl.r, t, "Shutdown") diff --git a/gs/internal/gs/gs_test.go b/gs/internal/gs/gs_test.go index 34ac9b51..1e1fcc32 100644 --- a/gs/internal/gs/gs_test.go +++ b/gs/internal/gs/gs_test.go @@ -22,14 +22,21 @@ import ( "reflect" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestAs(t *testing.T) { - As[io.Reader]() - assert.Panic(t, func() { - As[int]() - }, "T must be interface") + + t.Run("interface type", func(t *testing.T) { + s := As[io.Reader]() + assert.That(t, s.String()).Equal("io.Reader") + }) + + t.Run("non-interface type", func(t *testing.T) { + assert.Panic(t, func() { + As[int]() + }, "T must be interface") + }) } func TestBeanSelector(t *testing.T) { diff --git a/gs/internal/gs_app/app.go b/gs/internal/gs_app/app.go index dac1a2c0..6fb43ee5 100644 --- a/gs/internal/gs_app/app.go +++ b/gs/internal/gs_app/app.go @@ -20,13 +20,11 @@ import ( "context" "errors" "net/http" - "os" - "os/signal" "sync" "sync/atomic" - "syscall" "github.com/go-spring/log" + "github.com/go-spring/spring-base/util" "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_conf" @@ -37,13 +35,13 @@ import ( // App represents the core application, managing its lifecycle, // configuration, and dependency injection. type App struct { - C *gs_core.Container - P *gs_conf.AppConfig + C *gs_core.Container // IoC container + P *gs_conf.AppConfig // Application configuration - exiting atomic.Bool - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup + exiting atomic.Bool // Indicates whether the application is shutting down + ctx context.Context // Root context for managing cancellation + cancel context.CancelFunc // Function to cancel the root context + wg sync.WaitGroup // WaitGroup to track running jobs and servers Runners []gs.Runner `autowire:"${spring.app.runners:=?}"` Jobs []gs.Job `autowire:"${spring.app.jobs:=?}"` @@ -64,50 +62,18 @@ func NewApp() *App { } } -// Run starts the application and listens for termination signals -// (e.g., SIGINT, SIGTERM). Upon receiving a signal, it initiates -// a graceful shutdown. -func (app *App) Run() error { - return app.RunWith(nil) -} - -// RunWith starts the application and listens for termination signals -// (e.g., SIGINT, SIGTERM). Upon receiving a signal, it initiates -// a graceful shutdown. -func (app *App) RunWith(fn func(ctx context.Context) error) error { - if err := app.Start(); err != nil { - return err - } - - // runs the user-defined function - if fn != nil { - if err := fn(app.ctx); err != nil { - return err - } - } - - // listens for OS termination signals - go func() { - ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt, syscall.SIGTERM) - sig := <-ch - log.Infof(context.Background(), log.TagAppDef, "Received signal: %v", sig) - app.ShutDown() - }() - - // waits for the shutdown signal - <-app.ctx.Done() - app.Stop() - return nil -} - -// Start initializes and starts the application. It performs configuration -// loading, IoC container refreshing, dependency injection, and runs -// runners, jobs and servers. +// Start initializes and launches the application. It performs the following steps: +// 1. Registers the App itself as a root bean. +// 2. Loads application configuration. +// 3. Refreshes the IoC container to initialize and wire beans. +// 4. Runs all registered Runners. +// 5. Launches Jobs (if enabled) as background goroutines. +// 6. Starts all Servers (if enabled) and waits for readiness. func (app *App) Start() error { - app.C.RootBean(app.C.Object(app)) + // Register App as a root bean in the container + app.C.Root(app.C.Object(app)) - // loads the layered app properties + // Load layered application properties var p conf.Properties { var err error @@ -116,47 +82,49 @@ func (app *App) Start() error { } } - // refreshes the container + // Refresh the container to wire all beans if err := app.C.Refresh(p); err != nil { return err } - // runs all runners + // Run all registered Runners for _, r := range app.Runners { if err := r.Run(); err != nil { return err } } - // runs all jobs + // Launch all Jobs (if enabled) as background tasks if app.EnableJobs { for _, job := range app.Jobs { app.wg.Add(1) - goutil.GoFunc(func() { + goutil.Go(app.ctx, func(ctx context.Context) { defer app.wg.Done() defer func() { + // Handle unexpected panics by shutting down the app if r := recover(); r != nil { app.ShutDown() panic(r) } }() - if err := job.Run(app.ctx); err != nil { - log.Errorf(context.Background(), log.TagAppDef, "job run error: %v", err) + if err := job.Run(ctx); err != nil { + log.Errorf(ctx, log.TagAppDef, "job run error: %v", err) app.ShutDown() } }) } } - // starts all servers + // Start all Servers (if enabled) if app.EnableServers { - sig := NewReadySignal() + sig := NewReadySignal() // Used to coordinate readiness among servers for _, svr := range app.Servers { sig.Add() app.wg.Add(1) - goutil.GoFunc(func() { + goutil.Go(app.ctx, func(ctx context.Context) { defer app.wg.Done() defer func() { + // Handle server panics by intercepting readiness and shutting down if r := recover(); r != nil { sig.Intercept() app.ShutDown() @@ -165,45 +133,56 @@ func (app *App) Start() error { }() err := svr.ListenAndServe(sig) if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Errorf(context.Background(), log.TagAppDef, "server serve error: %v", err) + log.Errorf(ctx, log.TagAppDef, "server serve error: %v", err) sig.Intercept() app.ShutDown() + } else { + log.Infof(ctx, log.TagAppDef, "server closed") } }) } + + // Wait for all servers to be ready sig.Wait() if sig.Intercepted() { - return nil + log.Infof(app.ctx, log.TagAppDef, "server intercepted") + return util.FormatError(nil, "server intercepted") } - log.Infof(context.Background(), log.TagAppDef, "ready to serve requests") + log.Infof(app.ctx, log.TagAppDef, "ready to serve requests") sig.Close() } return nil } -// Stop gracefully shuts down the application, ensuring all servers and -// resources are properly closed. -func (app *App) Stop() { +// WaitForShutdown waits for the application to be signaled to shut down +// and then gracefully stops all servers and jobs. +func (app *App) WaitForShutdown() { + // Wait until the application context is cancelled (triggered by ShutDown) + <-app.ctx.Done() + + // Gracefully shut down all running servers for _, svr := range app.Servers { - goutil.GoFunc(func() { - if err := svr.Shutdown(app.ctx); err != nil { - log.Errorf(context.Background(), log.TagAppDef, "shutdown server failed: %v", err) + goutil.Go(app.ctx, func(ctx context.Context) { + if err := svr.Shutdown(context.Background()); err != nil { + log.Errorf(ctx, log.TagAppDef, "shutdown server failed: %v", err) } }) } app.wg.Wait() app.C.Close() - log.Infof(context.Background(), log.TagAppDef, "shutdown complete") + log.Infof(app.ctx, log.TagAppDef, "shutdown complete") } -// Exiting returns a boolean indicating whether the application is exiting. +// Exiting returns whether the application is currently in the process of shutting down. func (app *App) Exiting() bool { return app.exiting.Load() } -// ShutDown gracefully terminates the application. This method should -// be called to trigger a proper shutdown process. +// ShutDown initiates a graceful shutdown of the application by +// setting the exiting flag and cancelling the root context. func (app *App) ShutDown() { - app.exiting.Store(true) - app.cancel() + if app.exiting.CompareAndSwap(false, true) { + log.Infof(app.ctx, log.TagAppDef, "shutting down") + app.cancel() + } } diff --git a/gs/internal/gs_app/app_test.go b/gs/internal/gs_app/app_test.go index dc9055ec..7cc97a86 100644 --- a/gs/internal/gs_app/app_test.go +++ b/gs/internal/gs_app/app_test.go @@ -22,13 +22,12 @@ import ( "errors" "net/http" "os" - "runtime/debug" "testing" "time" - "github.com/go-spring/gs-assert/assert" "github.com/go-spring/gs-mock/gsmock" "github.com/go-spring/log" + "github.com/go-spring/spring-base/testing/assert" "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_conf" @@ -38,8 +37,8 @@ import ( var logBuf = &bytes.Buffer{} func init() { - goutil.OnPanic = func(ctx context.Context, r any) { - log.Panicf(ctx, log.TagAppDef, "panic: %v\n%s\n", r, debug.Stack()) + goutil.OnPanic = func(ctx context.Context, r any, stack []byte) { + log.Panicf(ctx, log.TagAppDef, "panic: %v\n%s\n", r, stack) } } @@ -53,63 +52,80 @@ func Reset() { func TestApp(t *testing.T) { - t.Run("os signals", func(t *testing.T) { - t.Skip() - + t.Run("property conflict", func(t *testing.T) { Reset() t.Cleanup(Reset) + fileID := gs_conf.SysConf.AddFile("app_test.go") + _ = gs_conf.SysConf.Set("a", "123", fileID) + _ = os.Setenv("GS_A_B", "456") app := NewApp() - go func() { - time.Sleep(50 * time.Millisecond) - p, err := os.FindProcess(os.Getpid()) - assert.That(t, err).Nil() - err = p.Signal(os.Interrupt) - assert.That(t, err).Nil() - time.Sleep(50 * time.Millisecond) - }() - err := app.Run() - assert.That(t, err).Nil() - time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("Received signal: interrupt") + err := app.Start() + assert.Error(t, err).Matches("property conflict at path a.b") }) - t.Run("config refresh error", func(t *testing.T) { + t.Run("bean creation failure", func(t *testing.T) { Reset() t.Cleanup(Reset) - 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() - assert.ThatError(t, err).Matches("property conflict at path a.b") + app.C.Root(app.C.Provide(func() (*http.Server, error) { + return nil, errors.New("fail to create bean") + })) + err := app.Start() + assert.Error(t, err).Matches("fail to create bean") }) - t.Run("container refresh error", func(t *testing.T) { + t.Run("runner panic", func(t *testing.T) { Reset() t.Cleanup(Reset) + r := gs.FuncRunner(func() error { + panic("runner panic") + }) + app := NewApp() - 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") + app.C.Object(r).AsRunner() + + assert.Panic(t, func() { + _ = app.Start() + }, "runner panic") }) t.Run("runner return error", func(t *testing.T) { Reset() t.Cleanup(Reset) - m := gsmock.NewManager() - r := gs.NewRunnerMockImpl(m) - r.MockRun().ReturnValue(errors.New("runner return error")) + r := gs.FuncRunner(func() error { + return errors.New("runner return error") + }) app := NewApp() app.C.Object(r).AsRunner() - err := app.Run() - assert.ThatError(t, err).Matches("runner return error") + err := app.Start() + assert.Error(t, err).Matches("runner return error") + }) + + t.Run("multiple runners with error", func(t *testing.T) { + Reset() + t.Cleanup(Reset) + + app := NewApp() + + // success + r1 := gs.FuncRunner(func() error { + return nil + }) + app.C.Object(r1).AsRunner().Name("r1") + + // error + r2 := gs.FuncRunner(func() error { + return errors.New("runner error") + }) + app.C.Object(r2).AsRunner().Name("r2") + + err := app.Start() + assert.Error(t, err).Matches("runner error") }) t.Run("disable jobs & servers", func(t *testing.T) { @@ -129,44 +145,69 @@ func TestApp(t *testing.T) { assert.That(t, len(app.Runners)).Equal(0) app.ShutDown() }() - err := app.Run() + err := app.Start() assert.That(t, err).Nil() + app.WaitForShutdown() time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("shutdown complete") + assert.String(t, logBuf.String()).Contains("shutdown complete") }) - t.Run("job return error", func(t *testing.T) { + t.Run("job panic", func(t *testing.T) { Reset() t.Cleanup(Reset) - m := gsmock.NewManager() - r := gs.NewJobMockImpl(m) - r.MockRun().ReturnValue(errors.New("job return error")) + r := gs.FuncJob(func(ctx context.Context) error { + panic("job panic") + }) app := NewApp() app.C.Object(r).AsJob() - err := app.Run() + err := app.Start() assert.That(t, err).Nil() time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("job run error: job return error") + assert.String(t, logBuf.String()).Contains("panic: job panic") }) - t.Run("job panic", func(t *testing.T) { + t.Run("job return error", func(t *testing.T) { Reset() t.Cleanup(Reset) - m := gsmock.NewManager() - r := gs.NewJobMockImpl(m) - r.MockRun().Handle(func(ctx context.Context) error { - panic("job panic") + r := gs.FuncJob(func(ctx context.Context) error { + return errors.New("job return error") }) app := NewApp() app.C.Object(r).AsJob() - err := app.Run() + err := app.Start() assert.That(t, err).Nil() time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("panic: job panic") + assert.String(t, logBuf.String()).Contains("job run error: job return error") + }) + + t.Run("job context cancel", func(t *testing.T) { + Reset() + t.Cleanup(Reset) + + jobFinished := make(chan bool, 1) + r := gs.FuncJob(func(ctx context.Context) error { + <-ctx.Done() + jobFinished <- true + return ctx.Err() + }) + + app := NewApp() + app.C.Object(r).AsJob() + + go func() { + time.Sleep(50 * time.Millisecond) + app.ShutDown() + }() + + err := app.Start() + assert.That(t, err).Nil() + app.WaitForShutdown() + <-jobFinished + assert.String(t, logBuf.String()).Contains("job run error: context canceled") }) t.Run("server return error", func(t *testing.T) { @@ -180,10 +221,10 @@ func TestApp(t *testing.T) { app := NewApp() app.C.Object(r).AsServer() - err := app.Run() - assert.That(t, err).Nil() + err := app.Start() + assert.Error(t, err).String("server intercepted") time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("server serve error: server return error") + assert.String(t, logBuf.String()).Contains("server serve error: server return error") }) t.Run("server panic", func(t *testing.T) { @@ -199,10 +240,10 @@ func TestApp(t *testing.T) { app := NewApp() app.C.Object(r).AsServer() - err := app.Run() - assert.That(t, err).Nil() + err := app.Start() + assert.Error(t, err).String("server intercepted") time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("panic: server panic") + assert.String(t, logBuf.String()).Contains("panic: server panic") }) t.Run("success", func(t *testing.T) { @@ -211,34 +252,27 @@ func TestApp(t *testing.T) { app := NewApp() { - m := gsmock.NewManager() - r := gs.NewRunnerMockImpl(m) - r.MockRun().ReturnDefault() - + r := gs.FuncRunner(func() error { + return nil + }) app.C.Object(r).AsRunner().Name("r1") } { - m := gsmock.NewManager() - r := gs.NewRunnerMockImpl(m) - r.MockRun().ReturnDefault() - + r := gs.FuncRunner(func() error { + return nil + }) app.C.Object(r).AsRunner().Name("r2") } { - m := gsmock.NewManager() - r := gs.NewJobMockImpl(m) - r.MockRun().Handle(func(ctx context.Context) error { + r := gs.FuncJob(func(ctx context.Context) error { <-ctx.Done() return nil }) - app.C.Object(r).AsJob().Name("j1") } j2Wait := make(chan struct{}, 1) { - m := gsmock.NewManager() - r := gs.NewJobMockImpl(m) - r.MockRun().Handle(func(ctx context.Context) error { + r := gs.FuncJob(func(ctx context.Context) error { for { time.Sleep(time.Millisecond) if app.Exiting() { @@ -247,7 +281,6 @@ func TestApp(t *testing.T) { } } }) - app.C.Object(r).AsJob().Name("j2") } { @@ -281,11 +314,12 @@ func TestApp(t *testing.T) { assert.That(t, len(app.Runners)).Equal(2) app.ShutDown() }() - err := app.Run() + err := app.Start() assert.That(t, err).Nil() + app.WaitForShutdown() time.Sleep(50 * time.Millisecond) <-j2Wait - assert.ThatString(t, logBuf.String()).Contains("shutdown complete") + assert.String(t, logBuf.String()).Contains("shutdown complete") }) t.Run("shutdown error", func(t *testing.T) { @@ -309,9 +343,10 @@ func TestApp(t *testing.T) { time.Sleep(50 * time.Millisecond) app.ShutDown() }() - err := app.Run() + err := app.Start() assert.That(t, err).Nil() + app.WaitForShutdown() time.Sleep(50 * time.Millisecond) - assert.ThatString(t, logBuf.String()).Contains("shutdown server failed: server shutdown error") + assert.String(t, logBuf.String()).Contains("shutdown server failed: server shutdown error") }) } diff --git a/gs/internal/gs_app/boot.go b/gs/internal/gs_app/boot.go index 862484c8..cec94f7d 100644 --- a/gs/internal/gs_app/boot.go +++ b/gs/internal/gs_app/boot.go @@ -17,40 +17,29 @@ package gs_app import ( - "reflect" - "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_bean" "github.com/go-spring/spring-core/gs/internal/gs_conf" "github.com/go-spring/spring-core/gs/internal/gs_core" ) -// funcRunner is a function type that implements the Runner interface. -type funcRunner func() error - -func (f funcRunner) Run() error { - return f() -} - // Boot defines the interface for application bootstrapping. type Boot interface { // Config returns the boot configuration. Config() *gs_conf.BootConfig - // Object registers an object bean. + // Object registers an existing object as a bean in the container. Object(i any) *gs.RegisteredBean - // Provide registers a bean using a constructor function. + // Provide registers a bean using a constructor function and optional arguments. Provide(ctor any, args ...gs.Arg) *gs.RegisteredBean - // Register registers a BeanDefinition instance. - Register(bd *gs.BeanDefinition) *gs.RegisteredBean - // FuncRunner creates a Runner from a function. - FuncRunner(fn func() error) *gs.RegisteredBean + // Runner creates and registers a Runner from a given function. + Runner(fn func() error) *gs.RegisteredBean } -// BootImpl is the bootstrapper of the application. +// BootImpl is the concrete implementation of the Boot interface. +// It manages the application's bootstrapping process. type BootImpl struct { - c *gs_core.Container - p *gs_conf.BootConfig + c *gs_core.Container // The IoC container + p *gs_conf.BootConfig // The boot configuration // flag indicates whether the bootstrapper has been used. flag bool @@ -58,7 +47,7 @@ type BootImpl struct { Runners []gs.Runner `autowire:"${spring.boot.runners:=?}"` } -// NewBoot creates a new Boot instance. +// NewBoot creates and returns a new BootImpl instance. func NewBoot() Boot { return &BootImpl{ c: gs_core.New(), @@ -71,44 +60,37 @@ 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) +// Root registers a root bean definition in the container. +func (b *BootImpl) Root(x *gs.RegisteredBean) { + b.c.Root(x) } -// Object registers an object bean. +// Object registers an existing object as a bean in the container. func (b *BootImpl) Object(i any) *gs.RegisteredBean { b.flag = true - bd := gs_bean.NewBean(reflect.ValueOf(i)) - return b.c.Register(bd).Caller(1) + return b.c.Object(i).Caller(1) } -// Provide registers a bean using a constructor function. +// Provide registers a bean using a constructor function and optional arguments. func (b *BootImpl) Provide(ctor any, args ...gs.Arg) *gs.RegisteredBean { b.flag = true - bd := gs_bean.NewBean(ctor, args...) - return b.c.Register(bd).Caller(1) -} - -// Register registers a BeanDefinition instance. -func (b *BootImpl) Register(bd *gs.BeanDefinition) *gs.RegisteredBean { - b.flag = true - return b.c.Register(bd) + return b.c.Provide(ctor, args...).Caller(1) } -// FuncRunner creates a Runner from a function. -func (b *BootImpl) FuncRunner(fn func() error) *gs.RegisteredBean { +// Runner creates a Runner from a function and registers it as a bean. +func (b *BootImpl) Runner(fn func() error) *gs.RegisteredBean { b.flag = true - bd := gs_bean.NewBean(reflect.ValueOf(funcRunner(fn))) - return b.c.Register(bd).AsRunner().Caller(1) + i := gs.FuncRunner(fn) + return b.c.Object(i).AsRunner().Caller(1) } // Run executes the application's boot process. func (b *BootImpl) Run() error { + // If no beans were registered, there’s nothing to run. if !b.flag { return nil } - b.c.RootBean(b.c.Object(b)) + b.c.Root(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 3edaa30a..fd1a7ed4 100644 --- a/gs/internal/gs_app/boot_test.go +++ b/gs/internal/gs_app/boot_test.go @@ -20,11 +20,9 @@ import ( "bytes" "errors" "os" - "reflect" "testing" - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/gs/internal/gs_bean" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/gs/internal/gs_conf" ) @@ -39,10 +37,10 @@ func TestBoot(t *testing.T) { _ = os.Setenv("GS_A_B", "456") boot := NewBoot().(*BootImpl) err := boot.Run() - assert.ThatError(t, err).Nil() + assert.Error(t, err).Nil() }) - t.Run("config refresh error", func(t *testing.T) { + t.Run("config refresh fails", func(t *testing.T) { Reset() t.Cleanup(Reset) @@ -52,19 +50,19 @@ func TestBoot(t *testing.T) { boot := NewBoot().(*BootImpl) boot.Object(bytes.NewBuffer(nil)) err := boot.Run() - assert.ThatError(t, err).Matches("property conflict at path a.b") + assert.Error(t, err).Matches("property conflict at path a.b") }) - t.Run("container refresh error", func(t *testing.T) { + t.Run("container refresh fails", func(t *testing.T) { Reset() t.Cleanup(Reset) boot := NewBoot().(*BootImpl) - boot.RootBean(boot.Provide(func() (*bytes.Buffer, error) { + boot.Root(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") + assert.Error(t, err).Matches("fail to create bean") }) t.Run("runner return error", func(t *testing.T) { @@ -72,11 +70,11 @@ func TestBoot(t *testing.T) { t.Cleanup(Reset) boot := NewBoot().(*BootImpl) - boot.FuncRunner(func() error { + boot.Runner(func() error { return errors.New("runner return error") }) err := boot.Run() - assert.ThatError(t, err).Matches("runner return error") + assert.Error(t, err).Matches("runner return error") }) t.Run("success", func(t *testing.T) { @@ -84,12 +82,50 @@ func TestBoot(t *testing.T) { t.Cleanup(Reset) boot := NewBoot().(*BootImpl) - bd := gs_bean.NewBean(reflect.ValueOf(funcRunner(func() error { - return nil - }))).AsRunner().Caller(1) - boot.Register(bd) + boot.Runner(func() error { return nil }) boot.Config().LocalFile.Reset() err := boot.Run() - assert.ThatError(t, err).Nil() + assert.Error(t, err).Nil() + }) + + t.Run("multiple runners", func(t *testing.T) { + Reset() + t.Cleanup(Reset) + + boot := NewBoot().(*BootImpl) + var results []string + + // success + boot.Runner(func() error { + results = append(results, "runner1") + return nil + }).Name("runner1") + + // success + boot.Runner(func() error { + results = append(results, "runner2") + return nil + }).Name("runner2") + + err := boot.Run() + assert.Error(t, err).Nil() + assert.That(t, len(results)).Equal(2) + assert.That(t, results[0]).Equal("runner1") + assert.That(t, results[1]).Equal("runner2") + }) + + t.Run("object conflict", func(t *testing.T) { + Reset() + t.Cleanup(Reset) + + boot := NewBoot().(*BootImpl) + buf := bytes.NewBuffer(nil) + + // duplicate registration + boot.Object(buf) + boot.Object(buf) + + err := boot.Run() + assert.Error(t, err).NotNil() }) } diff --git a/gs/internal/gs_app/signal.go b/gs/internal/gs_app/signal.go index bb395f87..9c8d2b6d 100644 --- a/gs/internal/gs_app/signal.go +++ b/gs/internal/gs_app/signal.go @@ -21,14 +21,15 @@ import ( "sync/atomic" ) -// ReadySignal is used to notify that the application is ready to serve requests. +// ReadySignal is a synchronization helper used to indicate +// when an application is ready to serve requests. type ReadySignal struct { wg sync.WaitGroup ch chan struct{} b atomic.Bool } -// NewReadySignal creates a new ReadySignal instance. +// NewReadySignal creates and returns a new ReadySignal instance. func NewReadySignal() *ReadySignal { return &ReadySignal{ ch: make(chan struct{}), @@ -40,8 +41,8 @@ func (s *ReadySignal) Add() { s.wg.Add(1) } -// TriggerAndWait marks an operation as done by decrementing the WaitGroup counter, -// and then returns the readiness signal channel for waiting. +// TriggerAndWait marks an operation as done by decrementing the WaitGroup +// counter, and then returns the readiness signal channel for waiting. func (s *ReadySignal) TriggerAndWait() <-chan struct{} { s.wg.Done() return s.ch diff --git a/gs/internal/gs_app/signal_test.go b/gs/internal/gs_app/signal_test.go index d4adecdd..d0da18f2 100644 --- a/gs/internal/gs_app/signal_test.go +++ b/gs/internal/gs_app/signal_test.go @@ -20,11 +20,17 @@ import ( "sync" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestReadySignal(t *testing.T) { + t.Run("zero workers", func(t *testing.T) { + signal := NewReadySignal() + signal.Wait() + assert.That(t, signal.Intercepted()).False() + }) + t.Run("intercept", func(t *testing.T) { const workers = 3 @@ -45,6 +51,25 @@ func TestReadySignal(t *testing.T) { assert.That(t, signal.Intercepted()).True() }) + t.Run("multiple intercept", func(t *testing.T) { + const workers = 3 + + signal := NewReadySignal() + for i := range workers { + signal.Add() + go func(num int) { + if num < 2 { + signal.Intercept() + } else { + <-signal.TriggerAndWait() + } + }(i) + } + + signal.Wait() + assert.That(t, signal.Intercepted()).True() + }) + t.Run("success", func(t *testing.T) { const workers = 3 diff --git a/gs/internal/gs_arg/arg.go b/gs/internal/gs_arg/arg.go index ed6b0c27..0a8940fb 100644 --- a/gs/internal/gs_arg/arg.go +++ b/gs/internal/gs_arg/arg.go @@ -14,68 +14,67 @@ * limitations under the License. */ -/* -Package gs_arg provides implementation for function argument resolution and binding - -Key Features: - - Configuration property binding and dependency injection via struct tags - - Precise positional binding through index-based arguments - - Direct injection of fixed value arguments - - Full support for variadic function parameters - - Conditional execution with runtime evaluation -*/ +// Package gs_arg provides implementations for argument resolution and binding +// used by the Go-Spring framework. +// +// Key Features: +// - Configuration property binding and dependency injection via struct tags. +// - Precise positional binding through index-based arguments. +// - Direct injection of fixed value arguments. +// - Full support for variadic function parameters. +// - Conditional execution with runtime evaluation. package gs_arg import ( - "errors" "fmt" "reflect" "runtime" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs" - "github.com/go-spring/spring-core/util" - "github.com/go-spring/spring-core/util/errutil" ) -// TagArg represents an argument resolved using a tag for property binding or dependency injection. +// TagArg represents an argument resolved using a tag for property binding +// or dependency injection. type TagArg struct { Tag string } -// Tag creates a TagArg with the given tag. +// Tag creates a TagArg with the given tag string. func Tag(tag string) gs.Arg { return TagArg{Tag: tag} } // GetArgValue resolves the tag to a value based on the target type. -// For primitive types (int, string), it binds from configuration. -// For structs/interfaces, it wires dependencies from the container. +// - For primitive types (int, string), it binds from configuration. +// - For structs/interfaces, it wires dependencies from the container. // It returns an error if the type is neither bindable nor injectable. func (arg TagArg) GetArgValue(ctx gs.ArgContext, t reflect.Type) (reflect.Value, error) { - // Binds property values based on the argument type. + // Bind property values based on the argument type. if util.IsPropBindingTarget(t) { v := reflect.New(t).Elem() if err := ctx.Bind(v, arg.Tag); err != nil { - return reflect.Value{}, errutil.WrapError(err, "TagArg::GetArgValue error") + return reflect.Value{}, err } return v, nil } - // Wires dependent beans based on the argument type. + // Wire dependencies based on the argument type. if util.IsBeanInjectionTarget(t) { v := reflect.New(t).Elem() if err := ctx.Wire(v, arg.Tag); err != nil { - return reflect.Value{}, errutil.WrapError(err, "TagArg::GetArgValue error") + return reflect.Value{}, err } return v, nil } - err := fmt.Errorf("unsupported argument type: %s", t.String()) - return reflect.Value{}, errutil.WrapError(err, "TagArg::GetArgValue error") + err := util.FormatError(nil, "unsupported argument type: %s", t.String()) + return reflect.Value{}, err } -// IndexArg represents an argument with an explicit positional index in the function signature. +// IndexArg represents an argument that is bound by its explicit position +// (index) in the target function’s parameter list. type IndexArg struct { Idx int //The positional index (0-based). Arg gs.Arg //The wrapped argument value. @@ -86,55 +85,72 @@ func Index(n int, arg gs.Arg) gs.Arg { return IndexArg{Idx: n, Arg: arg} } -// GetArgValue panics if called directly. IndexArg must be processed by ArgList. +// GetArgValue for IndexArg should never be called directly. +// IndexArg is resolved by ArgList when assembling the function’s +// argument list. If called, it panics to indicate incorrect usage. func (arg IndexArg) GetArgValue(ctx gs.ArgContext, t reflect.Type) (reflect.Value, error) { panic(util.ErrUnimplementedMethod) } -// ValueArg represents a fixed-value argument. +// ValueArg represents a constant (fixed) value argument that does not need +// any resolution or injection. type ValueArg struct { v any } -// Value creates a fixed-value argument. +// Value creates a ValueArg from a fixed constant value. func Value(v any) gs.Arg { return ValueArg{v: v} } -// GetArgValue returns the fixed value and validates type compatibility. -// It returns an error if the value type is incompatible with the target type. +// GetArgValue returns the fixed value wrapped by ValueArg. +// If the value is nil, it returns the zero value of the target type. +// If the value’s type is not assignable to the target type, it returns an error. func (arg ValueArg) GetArgValue(ctx gs.ArgContext, t reflect.Type) (reflect.Value, error) { if arg.v == nil { return reflect.Zero(t), nil } v := reflect.ValueOf(arg.v) if !v.Type().AssignableTo(t) { - err := fmt.Errorf("cannot assign type:%T to type:%s", arg.v, t.String()) - return reflect.Value{}, errutil.WrapError(err, "ValueArg::GetArgValue error") + err := util.FormatError(nil, "cannot assign type:%T to type:%s", arg.v, t.String()) + return reflect.Value{}, err } return v, nil } -// ArgList manages arguments for a function, supporting both fixed and variadic parameters. +// ArgList manages a collection of arguments for a target function, +// including both fixed and variadic parameters. +// +// It supports two modes: +// - Indexed mode: all arguments are provided with explicit positions (IndexArg). +// - Sequential mode: arguments are provided in order (no explicit indices). type ArgList struct { fnType reflect.Type // The reflected type of the target function. args []gs.Arg // The argument list (indexed or non-indexed). } -// NewArgList validates and creates an ArgList for a function. It returns errors -// for invalid function types, mixed indexed/non-indexed args, or out-of-bounds indexes. +// NewArgList validates and constructs an ArgList for the given function type. +// +// Validation checks: +// - fnType must be a function type. +// - Cannot mix indexed and non-indexed arguments. +// - Index values must be within valid parameter bounds. func NewArgList(fnType reflect.Type, args []gs.Arg) (*ArgList, error) { if fnType.Kind() != reflect.Func { - err := fmt.Errorf("invalid function type:%s", fnType.String()) - return nil, errutil.WrapError(err, "NewArgList error") + err := util.FormatError(nil, "invalid function type:%s", fnType.String()) + return nil, util.FormatError(err, "NewArgList error") } - // Calculates the number of fixed arguments in the function. + // Determine number of fixed arguments. fixedArgCount := fnType.NumIn() if fnType.IsVariadic() { fixedArgCount-- + } else if len(args) > fixedArgCount { + err := util.FormatError(nil, "too many arguments for function:%s", fnType.String()) + return nil, util.FormatError(err, "NewArgList error") } + // Initialize argument list with empty Tag() placeholders. fnArgs := make([]gs.Arg, fixedArgCount) for i := range fnArgs { fnArgs[i] = Tag("") @@ -145,17 +161,18 @@ func NewArgList(fnType reflect.Type, args []gs.Arg) (*ArgList, error) { notIdx bool ) + // Process each provided argument. for i := range args { switch arg := args[i].(type) { case IndexArg: useIdx = true if notIdx { - err := errors.New("arguments must be all indexed or non-indexed") - return nil, errutil.WrapError(err, "NewArgList error") + err := util.FormatError(nil, "arguments must be all indexed or non-indexed") + return nil, util.FormatError(err, "NewArgList error") } if arg.Idx < 0 || arg.Idx >= fnType.NumIn() { - err := fmt.Errorf("invalid argument index %d", arg.Idx) - return nil, errutil.WrapError(err, "NewArgList error") + err := util.FormatError(nil, "invalid argument index %d", arg.Idx) + return nil, util.FormatError(err, "NewArgList error") } if arg.Idx < fixedArgCount { fnArgs[arg.Idx] = arg.Arg @@ -165,8 +182,8 @@ func NewArgList(fnType reflect.Type, args []gs.Arg) (*ArgList, error) { default: notIdx = true if useIdx { - err := errors.New("arguments must be all indexed or non-indexed") - return nil, errutil.WrapError(err, "NewArgList error") + err := util.FormatError(nil, "arguments must be all indexed or non-indexed") + return nil, util.FormatError(err, "NewArgList error") } if i < fixedArgCount { fnArgs[i] = arg @@ -179,7 +196,8 @@ func NewArgList(fnType reflect.Type, args []gs.Arg) (*ArgList, error) { return &ArgList{fnType: fnType, args: fnArgs}, nil } -// get resolves all arguments and returns their reflected values. +// get resolves all arguments in the ArgList using the provided ArgContext. +// It returns a slice of reflect.Value ready for invocation of the target function. func (r *ArgList) get(ctx gs.ArgContext) ([]reflect.Value, error) { fnType := r.fnType @@ -192,6 +210,7 @@ func (r *ArgList) get(ctx gs.ArgContext) ([]reflect.Value, error) { var t reflect.Type if variadic && idx >= numIn-1 { + // For variadic parameters, use element type of the variadic slice. t = fnType.In(numIn - 1).Elem() } else { t = fnType.In(idx) @@ -208,17 +227,17 @@ func (r *ArgList) get(ctx gs.ArgContext) ([]reflect.Value, error) { return result, nil } -// CallableFunc is a function that can be called. +// CallableFunc is an alias for any callable function. type CallableFunc = any -// Callable wraps a function and its bound arguments for invocation. +// Callable wraps a target function together with its resolved ArgList. +// It can be invoked at runtime with the correct arguments. type Callable struct { fn CallableFunc argList *ArgList } -// NewCallable binds arguments to a function and creates a Callable. It -// returns errors for invalid function types or argument validation failures. +// NewCallable creates a Callable by binding the given arguments to the function. func NewCallable(fn CallableFunc, args []gs.Arg) (*Callable, error) { fnType := reflect.TypeOf(fn) argList, err := NewArgList(fnType, args) @@ -228,7 +247,7 @@ func NewCallable(fn CallableFunc, args []gs.Arg) (*Callable, error) { return &Callable{fn: fn, argList: argList}, nil } -// Call invokes the function with resolved arguments. +// Call resolves all arguments and invokes the underlying function. func (r *Callable) Call(ctx gs.ArgContext) ([]reflect.Value, error) { ret, err := r.argList.get(ctx) if err != nil { @@ -237,37 +256,33 @@ func (r *Callable) Call(ctx gs.ArgContext) ([]reflect.Value, error) { return reflect.ValueOf(r.fn).Call(ret), nil } -// BindArg represents a bound function with conditions for conditional execution. +// BindArg represents a bound function ready to be executed conditionally. type BindArg struct { - r *Callable // The wrapped Callable. - fileline string // Source location of the Bind call (for debugging). - conditions []gs.Condition // Conditions that must be met to execute the function. + r *Callable // Wrapped Callable + fileline string // File:line info for debugging + conditions []gs.Condition // Conditions for conditional execution } -// validBindFunc validates if a function is a valid binding target. -// Valid signatures: -// - func(...) error -// - func(...) (T, error) +// validBindFunc validates that a function is a proper bind target. func validBindFunc(fn CallableFunc) error { t := reflect.TypeOf(fn) if t.Kind() != reflect.Func { - return errors.New("invalid function type") + return util.FormatError(nil, "invalid function type") } - if numOut := t.NumOut(); numOut == 1 { + if numOut := t.NumOut(); numOut == 1 { // func(...) error if o := t.Out(0); !util.IsErrorType(o) { return nil } - } else if numOut == 2 { + } else if numOut == 2 { // func(...) (T, error) if o := t.Out(t.NumOut() - 1); util.IsErrorType(o) { return nil } } - return errors.New("invalid function type") + return util.FormatError(nil, "invalid function type") } -// Bind creates a binding for an option function. It panics on validation errors. -// `fn` is The target function (must return error or (T, error)). `args` is the -// bound arguments (indexed or non-indexed). +// Bind creates a BindArg for a given function and its arguments. +// The function must have a valid bindable signature. func Bind(fn CallableFunc, args ...gs.Arg) *BindArg { if err := validBindFunc(fn); err != nil { panic(err) @@ -282,12 +297,12 @@ func Bind(fn CallableFunc, args ...gs.Arg) *BindArg { return arg } -// SetFileLine sets the source location of the Bind call. +// SetFileLine records the source location of the Bind() call. func (arg *BindArg) SetFileLine(file string, line int) { arg.fileline = fmt.Sprintf("%s:%d", file, line) } -// Condition adds pre-execution conditions to the binding. +// Condition appends runtime conditions that must be satisfied before execution. func (arg *BindArg) Condition(conditions ...gs.Condition) *BindArg { arg.conditions = append(arg.conditions, conditions...) return arg @@ -298,7 +313,7 @@ func (arg *BindArg) Condition(conditions ...gs.Condition) *BindArg { // errors from the function or condition checks. func (arg *BindArg) GetArgValue(ctx gs.ArgContext, t reflect.Type) (reflect.Value, error) { - // Checks if the condition is met. + // Evaluate all conditions. for _, c := range arg.conditions { ok, err := ctx.Check(c) if err != nil { @@ -308,14 +323,14 @@ func (arg *BindArg) GetArgValue(ctx gs.ArgContext, t reflect.Type) (reflect.Valu } } - // Calls the function and returns its result. + // Execute the function. out, err := arg.r.Call(ctx) if err != nil { return reflect.Value{}, err } - if len(out) == 1 { + if len(out) == 1 { // func(...) error return out[0], nil } err, _ = out[1].Interface().(error) - return out[0], err + return out[0], err // func(...) (T, error) } diff --git a/gs/internal/gs_arg/arg_test.go b/gs/internal/gs_arg/arg_test.go index fea22374..b6d68420 100644 --- a/gs/internal/gs_arg/arg_test.go +++ b/gs/internal/gs_arg/arg_test.go @@ -25,14 +25,28 @@ import ( "strings" "testing" - "github.com/go-spring/gs-assert/assert" "github.com/go-spring/gs-mock/gsmock" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/gs/internal/gs" "github.com/go-spring/spring-core/gs/internal/gs_cond" ) func TestTagArg(t *testing.T) { + t.Run("empty tag", func(t *testing.T) { + m := gsmock.NewManager() + c := gs.NewArgContextMockImpl(m) + c.MockBind().Handle(func(v reflect.Value, s string) error { + v.SetString("default") + return nil + }) + + tag := Tag("") + v, err := tag.GetArgValue(c, reflect.TypeFor[string]()) + assert.That(t, err).Nil() + assert.That(t, v.String()).Equal("default") + }) + t.Run("bind success", func(t *testing.T) { m := gsmock.NewManager() c := gs.NewArgContextMockImpl(m) @@ -56,7 +70,7 @@ func TestTagArg(t *testing.T) { tag := Tag("${int:=3}") _, err := tag.GetArgValue(c, reflect.TypeFor[string]()) - assert.ThatError(t, err).Matches("GetArgValue error << bind error") + assert.Error(t, err).Matches("bind error") }) t.Run("wire success", func(t *testing.T) { @@ -82,19 +96,45 @@ func TestTagArg(t *testing.T) { tag := Tag("server") _, err := tag.GetArgValue(c, reflect.TypeFor[*bytes.Buffer]()) - assert.ThatError(t, err).Matches("GetArgValue error << wire error") + assert.Error(t, err).Matches("wire error") }) - t.Run("type error", func(t *testing.T) { + t.Run("unsupported type", func(t *testing.T) { tag := Tag("server") _, err := tag.GetArgValue(nil, reflect.TypeFor[*string]()) - assert.ThatError(t, err).Matches("GetArgValue error << unsupported argument type: \\*string") + assert.Error(t, err).Matches("unsupported argument type: \\*string") }) } func TestValueArg(t *testing.T) { - t.Run("index", func(t *testing.T) { + t.Run("different types", func(t *testing.T) { + tag := Value(42) + v, err := tag.GetArgValue(nil, reflect.TypeFor[int]()) + assert.That(t, err).Nil() + assert.That(t, v.Int()).Equal(int64(42)) + + tag = Value(true) + v, err = tag.GetArgValue(nil, reflect.TypeFor[bool]()) + assert.That(t, err).Nil() + assert.That(t, v.Bool()).True() + + tag = Value(3.14) + v, err = tag.GetArgValue(nil, reflect.TypeFor[float64]()) + assert.That(t, err).Nil() + assert.That(t, v.Float()).Equal(3.14) + }) + + t.Run("slice value", func(t *testing.T) { + slice := []string{"a", "b", "c"} + tag := Value(slice) + v, err := tag.GetArgValue(nil, reflect.TypeFor[[]string]()) + assert.That(t, err).Nil() + result := v.Interface().([]string) + assert.That(t, result).Equal(slice) + }) + + t.Run("index arg", func(t *testing.T) { arg := Index(0, Value(1)) assert.That(t, arg.(IndexArg).Idx).Equal(0) assert.Panic(t, func() { @@ -102,24 +142,24 @@ func TestValueArg(t *testing.T) { }, "unimplemented method") }) - t.Run("zero", func(t *testing.T) { + t.Run("zero value", func(t *testing.T) { tag := Value(nil) v, err := tag.GetArgValue(nil, reflect.TypeFor[*http.Server]()) assert.That(t, err).Nil() assert.That(t, v.Interface()) }) - t.Run("value", func(t *testing.T) { + t.Run("assignable value", func(t *testing.T) { tag := Value(&http.Server{Addr: ":9090"}) v, err := tag.GetArgValue(nil, reflect.TypeFor[*http.Server]()) assert.That(t, err).Nil() assert.That(t, v.Interface().(*http.Server).Addr).Equal(":9090") }) - t.Run("type error", func(t *testing.T) { + t.Run("incompatible types", func(t *testing.T) { tag := Value(new(int)) _, err := tag.GetArgValue(nil, reflect.TypeFor[*http.Server]()) - assert.ThatError(t, err).Matches("GetArgValue error << cannot assign type:\\*int to type:\\*http.Server") + assert.Error(t, err).Matches("cannot assign type:\\*int to type:\\*http.Server") }) } @@ -128,7 +168,7 @@ func TestArgList_New(t *testing.T) { t.Run("invalid function type", func(t *testing.T) { fnType := reflect.TypeFor[int]() _, err := NewArgList(fnType, nil) - assert.ThatError(t, err).Matches("NewArgList error << invalid function type:int") + assert.Error(t, err).Matches("NewArgList error: invalid function type:int") }) t.Run("mixed index and non-index args", func(t *testing.T) { @@ -138,7 +178,7 @@ func TestArgList_New(t *testing.T) { Value("test"), } _, err := NewArgList(fnType, args) - assert.ThatError(t, err).Matches("NewArgList error << arguments must be all indexed or non-indexed") + assert.Error(t, err).Matches("NewArgList error: arguments must be all indexed or non-indexed") }) t.Run("mixed non-index and index args", func(t *testing.T) { @@ -148,25 +188,25 @@ func TestArgList_New(t *testing.T) { Index(1, Value("test")), } _, err := NewArgList(fnType, args) - assert.ThatError(t, err).Matches("NewArgList error << arguments must be all indexed or non-indexed") + assert.Error(t, err).Matches("NewArgList error: arguments must be all indexed or non-indexed") }) - t.Run("invalid argument index - 1", func(t *testing.T) { + t.Run("negative argument index", func(t *testing.T) { fnType := reflect.TypeOf(func(a int, b string) {}) args := []gs.Arg{ Index(-1, Value(1)), } _, err := NewArgList(fnType, args) - assert.ThatError(t, err).Matches("NewArgList error << invalid argument index -1") + assert.Error(t, err).Matches("NewArgList error: invalid argument index -1") }) - t.Run("invalid argument index - 2", func(t *testing.T) { + t.Run("out of range argument index", func(t *testing.T) { fnType := reflect.TypeOf(func(a int, b string) {}) args := []gs.Arg{ Index(2, Value(1)), } _, err := NewArgList(fnType, args) - assert.ThatError(t, err).Matches("NewArgList error << invalid argument index 2") + assert.Error(t, err).Matches("NewArgList error: invalid argument index 2") }) t.Run("non-index args success", func(t *testing.T) { @@ -199,7 +239,7 @@ func TestArgList_New(t *testing.T) { }) }) - t.Run("variadic function with non-index args success", func(t *testing.T) { + t.Run("variadic success with non-index args", func(t *testing.T) { fnType := reflect.TypeOf(func(a int, b ...string) {}) args := []gs.Arg{ Value(1), @@ -216,7 +256,7 @@ func TestArgList_New(t *testing.T) { }) }) - t.Run("variadic function with index args success - 1", func(t *testing.T) { + t.Run("variadic success with indexed args", func(t *testing.T) { fnType := reflect.TypeOf(func(a int, b ...string) {}) args := []gs.Arg{ Index(0, Value(1)), @@ -233,7 +273,7 @@ func TestArgList_New(t *testing.T) { }) }) - t.Run("variadic function with index args success - 2", func(t *testing.T) { + t.Run("variadic success with partial indexed args", func(t *testing.T) { fnType := reflect.TypeOf(func(a error, b ...string) {}) args := []gs.Arg{ Index(1, Value("test1")), @@ -248,6 +288,26 @@ func TestArgList_New(t *testing.T) { Value("test2"), }) }) + + t.Run("function with no parameters", func(t *testing.T) { + fnType := reflect.TypeOf(func() {}) + var args []gs.Arg + argList, err := NewArgList(fnType, args) + assert.That(t, err).Nil() + assert.That(t, argList).NotNil() + assert.That(t, len(argList.args)).Equal(0) + }) + + t.Run("too many arguments for non-variadic function", func(t *testing.T) { + fnType := reflect.TypeOf(func(a int, b string) {}) + args := []gs.Arg{ + Value(1), + Value("test"), + Value("extra"), // Extra argument + } + _, err := NewArgList(fnType, args) + assert.Error(t, err).Matches("NewArgList error: too many arguments") + }) } func TestArgList_Get(t *testing.T) { @@ -299,7 +359,38 @@ func TestArgList_Get(t *testing.T) { ctx := gs.NewArgContextMockImpl(nil) _, err = argList.get(ctx) - assert.ThatError(t, err).Matches("GetArgValue error << cannot assign type:int to type:string") + assert.Error(t, err).Matches("cannot assign type:int to type:string") + }) + + t.Run("variadic function with no extra args", func(t *testing.T) { + fnType := reflect.TypeOf(func(a int, b ...string) {}) + args := []gs.Arg{ + Value(1), + // No extra args + } + argList, err := NewArgList(fnType, args) + assert.That(t, err).Nil() + + ctx := gs.NewArgContextMockImpl(nil) + values, err := argList.get(ctx) + assert.That(t, err).Nil() + assert.That(t, 1).Equal(len(values)) + assert.That(t, 1).Equal(values[0].Interface().(int)) + }) + + t.Run("function with any parameter", func(t *testing.T) { + fnType := reflect.TypeOf(func(a any) {}) + args := []gs.Arg{ + Value("test"), + } + argList, err := NewArgList(fnType, args) + assert.That(t, err).Nil() + + ctx := gs.NewArgContextMockImpl(nil) + values, err := argList.get(ctx) + assert.That(t, err).Nil() + assert.That(t, 1).Equal(len(values)) + assert.That(t, "test").Equal(values[0].Interface()) }) } @@ -312,7 +403,7 @@ func TestCallable_New(t *testing.T) { Value("test"), } _, err := NewCallable(fn, args) - assert.ThatError(t, err).Matches("NewArgList error << invalid function type:string") + assert.Error(t, err).Matches("NewArgList error: invalid function type:string") }) t.Run("error in argument processing", func(t *testing.T) { @@ -328,7 +419,7 @@ func TestCallable_New(t *testing.T) { ctx := gs.NewArgContextMockImpl(nil) _, err = callable.Call(ctx) - assert.ThatError(t, err).Matches("GetArgValue error << cannot assign type:int to type:string") + assert.Error(t, err).Matches("cannot assign type:int to type:string") }) } @@ -347,7 +438,7 @@ func TestCallable_Call(t *testing.T) { ctx := gs.NewArgContextMockImpl(nil) _, err = callable.Call(ctx) - assert.ThatError(t, err).Matches("GetArgValue error << cannot assign type:int to type:string") + assert.Error(t, err).Matches("cannot assign type:int to type:string") }) t.Run("function return none", func(t *testing.T) { @@ -381,10 +472,10 @@ func TestCallable_Call(t *testing.T) { assert.That(t, err).Nil() assert.That(t, 2).Equal(len(v)) assert.That(t, "").Equal(v[0].Interface().(string)) - assert.That(t, "execution error").Equal(v[1].Interface().(error).Error()) + assert.Error(t, v[1].Interface().(error)).String("execution error") }) - t.Run("success - 1", func(t *testing.T) { + t.Run("success with function with error", func(t *testing.T) { fn := func(a int, b string) (string, error) { return fmt.Sprintf("%d-%s", a, b), nil } @@ -402,7 +493,7 @@ func TestCallable_Call(t *testing.T) { assert.That(t, "1-test").Equal(v[0].Interface().(string)) }) - t.Run("success - 2", func(t *testing.T) { + t.Run("success with function without error", func(t *testing.T) { fn := func(a int, b string) string { return fmt.Sprintf("%d-%s", a, b) } @@ -438,18 +529,76 @@ func TestCallable_Call(t *testing.T) { assert.That(t, len(v)).Equal(1) assert.That(t, "1-test1,test2").Equal(v[0].Interface().(string)) }) + + t.Run("function with pointer receiver", func(t *testing.T) { + type MyStruct struct { + Value string + } + + fn := func(s *MyStruct, suffix string) string { + return s.Value + "-" + suffix + } + + args := []gs.Arg{ + Value(&MyStruct{Value: "test"}), + Value("suffix"), + } + + callable, err := NewCallable(fn, args) + assert.That(t, err).Nil() + + ctx := gs.NewArgContextMockImpl(nil) + v, err := callable.Call(ctx) + assert.That(t, err).Nil() + assert.That(t, len(v)).Equal(1) + assert.That(t, "test-suffix").Equal(v[0].Interface().(string)) + }) + + t.Run("variadic function with no variadic args", func(t *testing.T) { + fn := func(a int, b ...string) []string { + return append([]string{fmt.Sprint(a)}, b...) + } + + args := []gs.Arg{ + Value(1), + // No variadic arguments + } + + callable, err := NewCallable(fn, args) + assert.That(t, err).Nil() + + ctx := gs.NewArgContextMockImpl(nil) + v, err := callable.Call(ctx) + assert.That(t, err).Nil() + assert.That(t, len(v)).Equal(1) + result := v[0].Interface().([]string) + assert.That(t, result).Equal([]string{"1"}) + }) } func TestBindArg_Bind(t *testing.T) { - t.Run("invalid function type - 1", func(t *testing.T) { + t.Run("function returning only error", func(t *testing.T) { + fn := func(a int, b string) error { + return nil + } + args := []gs.Arg{ + Value(1), + Value("test"), + } + assert.Panic(t, func() { + Bind(fn, args...) + }, "invalid function type") + }) + + t.Run("non-function type", func(t *testing.T) { fn := "not a function" assert.Panic(t, func() { Bind(fn) }, "invalid function type") }) - t.Run("invalid function type - 2", func(t *testing.T) { + t.Run("function returning only error", func(t *testing.T) { fn := func(a int, b string) error { return nil } @@ -458,7 +607,7 @@ func TestBindArg_Bind(t *testing.T) { }, "invalid function type") }) - t.Run("invalid function type - 3", func(t *testing.T) { + t.Run("function with invalid return types", func(t *testing.T) { fn := func(a int, b string) (string, bool) { return fmt.Sprintf("%d-%s", a, b), true } @@ -467,6 +616,19 @@ func TestBindArg_Bind(t *testing.T) { }, "invalid function type") }) + t.Run("function with too many return values", func(t *testing.T) { + fn := func(a int, b string) ([]string, map[string]int, error) { + return []string{b}, map[string]int{"value": a}, nil + } + args := []gs.Arg{ + Value(1), + Value("test"), + } + assert.Panic(t, func() { + Bind(fn, args...) + }, "invalid function type") + }) + t.Run("error in argument processing", func(t *testing.T) { fn := func(a int, b string) string { return fmt.Sprintf("%d-%s", a, b) @@ -477,10 +639,10 @@ func TestBindArg_Bind(t *testing.T) { } assert.Panic(t, func() { Bind(fn, args...) - }, "NewArgList error << arguments must be all indexed or non-indexed") + }, "NewArgList error: arguments must be all indexed or non-indexed") }) - t.Run("success - 1", func(t *testing.T) { + t.Run("success with returning single value", func(t *testing.T) { fn := func(a int, b string) string { return fmt.Sprintf("%d-%s", a, b) } @@ -489,10 +651,10 @@ func TestBindArg_Bind(t *testing.T) { Value("test"), } arg := Bind(fn, args...) - assert.ThatString(t, arg.fileline).Matches("gs/internal/gs_arg/arg_test.go:491") + assert.String(t, arg.fileline).Matches("gs/internal/gs_arg/arg_test.go:.*") }) - t.Run("success - 2", func(t *testing.T) { + t.Run("success with returning value and error", func(t *testing.T) { fn := func(a int, b string) (string, error) { return fmt.Sprintf("%d-%s", a, b), nil } @@ -501,7 +663,7 @@ func TestBindArg_Bind(t *testing.T) { Value("test"), } arg := Bind(fn, args...) - assert.ThatString(t, arg.fileline).Matches("gs/internal/gs_arg/arg_test.go:503") + assert.String(t, arg.fileline).Matches("gs/internal/gs_arg/arg_test.go:.*") }) } @@ -518,7 +680,31 @@ func TestBindArg_GetArgValue(t *testing.T) { arg := Bind(fn, args...) ctx := gs.NewArgContextMockImpl(nil) _, err := arg.GetArgValue(ctx, reflect.TypeFor[string]()) - assert.ThatError(t, err).Matches("GetArgValue error << cannot assign type:int to type:string") + assert.Error(t, err).Matches("cannot assign type:int to type:string") + }) + + t.Run("function returning slice", func(t *testing.T) { + fn := func(prefix string, items ...int) []string { + result := make([]string, len(items)) + for i, item := range items { + result[i] = fmt.Sprintf("%s%d", prefix, item) + } + return result + } + args := []gs.Arg{ + Value("item"), + Value(1), + Value(2), + Value(3), + } + arg := Bind(fn, args...) + ctx := gs.NewArgContextMockImpl(nil) + + v, err := arg.GetArgValue(ctx, reflect.TypeFor[[]string]()) + assert.That(t, err).Nil() + result := v.Interface().([]string) + expected := []string{"item1", "item2", "item3"} + assert.That(t, result).Equal(expected) }) t.Run("success", func(t *testing.T) { @@ -547,7 +733,7 @@ func TestBindArg_GetArgValue(t *testing.T) { arg := Bind(fn, args...) ctx := gs.NewArgContextMockImpl(nil) _, err := arg.GetArgValue(ctx, reflect.TypeFor[string]()) - assert.ThatError(t, err).Matches("execution error") + assert.Error(t, err).Matches("execution error") }) t.Run("no error in function execution", func(t *testing.T) { @@ -602,7 +788,7 @@ func TestBindArg_GetArgValue(t *testing.T) { }) _, err := arg.GetArgValue(c, reflect.TypeFor[string]()) - assert.ThatError(t, err).Matches("condition error") + assert.Error(t, err).Matches("condition error") }) t.Run("condition return false", func(t *testing.T) { @@ -655,3 +841,64 @@ func TestBindArg_GetArgValue(t *testing.T) { assert.That(t, "1-test").Equal(v.Interface().(string)) }) } + +func TestBindArg_Condition(t *testing.T) { + + t.Run("multiple conditions - all true", func(t *testing.T) { + fn := func(a int, b string) string { + return fmt.Sprintf("%d-%s", a, b) + } + args := []gs.Arg{ + Value(1), + Value("test"), + } + arg := Bind(fn, args...) + + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { + return true, nil + })) + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { + return true, nil + })) + + m := gsmock.NewManager() + c := gs.NewArgContextMockImpl(m) + c.MockCheck().Handle(func(c gs.Condition) (bool, error) { + ok, err := c.Matches(nil) + return ok, err + }) + + v, err := arg.GetArgValue(c, reflect.TypeFor[string]()) + assert.That(t, err).Nil() + assert.That(t, "1-test").Equal(v.Interface().(string)) + }) + + t.Run("multiple conditions - one false", func(t *testing.T) { + fn := func(a int, b string) string { + return fmt.Sprintf("%d-%s", a, b) + } + args := []gs.Arg{ + Value(1), + Value("test"), + } + arg := Bind(fn, args...) + + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { + return false, nil + })) + arg.Condition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { + return true, nil + })) + + m := gsmock.NewManager() + c := gs.NewArgContextMockImpl(m) + c.MockCheck().Handle(func(c gs.Condition) (bool, error) { + ok, err := c.Matches(nil) + return ok, err + }) + + v, err := arg.GetArgValue(c, reflect.TypeFor[string]()) + assert.That(t, err).Nil() + assert.That(t, v.IsValid()).False() + }) +} diff --git a/gs/internal/gs_bean/bean.go b/gs/internal/gs_bean/bean.go index 93c298ff..bc2fc5e8 100644 --- a/gs/internal/gs_bean/bean.go +++ b/gs/internal/gs_bean/bean.go @@ -14,15 +14,7 @@ * limitations under the License. */ -/* -Package gs_bean provides core bean management for Go-Spring framework, featuring: - - - Full lifecycle management (instantiation, DI, destruction) - - Method-as-factory mechanism (generate child beans via configured rules) - - Conditional registration (profile-based activation) - - Type-safe interface export validation - - Mock replacement mechanism -*/ +// Package gs_bean provides core bean management for Go-Spring framework. package gs_bean import ( @@ -32,10 +24,10 @@ import ( "slices" "strings" + "github.com/go-spring/spring-base/util" "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_cond" - "github.com/go-spring/spring-core/util" ) // BeanStatus represents the different lifecycle statuses of a bean. @@ -51,7 +43,7 @@ const ( StatusWired // Bean has been wired. ) -// String returns a human-readable string of the bean status. +// String returns a human-readable string for the bean status. func (status BeanStatus) String() string { switch status { case StatusDeleted: @@ -73,18 +65,19 @@ func (status BeanStatus) String() string { } } -// BeanMetadata holds the metadata information of a bean. +// BeanMetadata holds static (design-time) metadata about a bean, +// such as lifecycle functions, dependencies, conditions, and configuration. type BeanMetadata struct { - f *gs_arg.Callable - init gs.BeanInitFunc - destroy gs.BeanDestroyFunc - dependsOn []gs.BeanSelector - exports []reflect.Type - conditions []gs.Condition - status BeanStatus - mocked bool - fileLine string - configuration *gs.Configuration + f *gs_arg.Callable // Callable for constructor functions + init gs.BeanInitFunc // Bean initialization function + destroy gs.BeanDestroyFunc // Bean destruction function + dependsOn []gs.BeanSelector // Explicit dependencies of the bean + exports []reflect.Type // Interfaces exported by this bean + conditions []gs.Condition // Conditions controlling bean creation + status BeanStatus // Current lifecycle status + mocked bool // Indicates if the bean is mocked + fileLine string // File and line where bean is defined + configuration *gs.Configuration // Configuration for sub/child beans } // Mocked returns true if the bean is mocked. @@ -92,7 +85,10 @@ func (d *BeanMetadata) Mocked() bool { return d.mocked } -// validLifeCycleFunc checks whether the provided function is a valid lifecycle function. +// validLifeCycleFunc checks if the given function is a valid lifecycle function. +// Valid lifecycle functions must have the signature: +// +// func(bean) or func(bean) error func validLifeCycleFunc(fnType reflect.Type, beanType reflect.Type) bool { if !util.IsFuncType(fnType) || fnType.NumIn() != 1 { return false @@ -107,12 +103,12 @@ func validLifeCycleFunc(fnType reflect.Type, beanType reflect.Type) bool { return util.ReturnNothing(fnType) || util.ReturnOnlyError(fnType) } -// Init returns the initialization function of the bean. +// Init returns the bean's initialization function. func (d *BeanMetadata) Init() gs.BeanInitFunc { return d.init } -// Destroy returns the destruction function of the bean. +// Destroy returns the bean's destruction function. func (d *BeanMetadata) Destroy() gs.BeanDestroyFunc { return d.destroy } @@ -122,12 +118,12 @@ func (d *BeanMetadata) DependsOn() []gs.BeanSelector { return d.dependsOn } -// SetDependsOn sets the list of dependencies for the bean. +// SetDependsOn adds dependencies to the bean. func (d *BeanMetadata) SetDependsOn(selectors ...gs.BeanSelector) { d.dependsOn = append(d.dependsOn, selectors...) } -// Exports returns the list of exported types for the bean. +// Exports returns the interfaces exported by the bean. func (d *BeanMetadata) Exports() []reflect.Type { return d.exports } @@ -137,17 +133,17 @@ func (d *BeanMetadata) Conditions() []gs.Condition { return d.conditions } -// SetCondition adds a condition to the list of conditions for the bean. +// SetCondition appends conditions for the bean. func (d *BeanMetadata) SetCondition(conditions ...gs.Condition) { d.conditions = append(d.conditions, conditions...) } -// Configuration returns the configuration parameters for the bean. +// Configuration returns the configuration for the bean. func (d *BeanMetadata) Configuration() *gs.Configuration { return d.configuration } -// SetConfiguration sets the configuration flag and parameters for the bean. +// SetConfiguration sets configuration (include/exclude) for the bean. func (d *BeanDefinition) SetConfiguration(c ...gs.Configuration) { var cfg gs.Configuration if len(c) > 0 { @@ -159,18 +155,18 @@ func (d *BeanDefinition) SetConfiguration(c ...gs.Configuration) { } } -// SetCaller sets the caller for the bean. +// SetCaller records the source file and line number of the bean. func (d *BeanMetadata) SetCaller(skip int) { _, file, line, _ := runtime.Caller(skip) d.SetFileLine(file, line) } -// FileLine returns the file and line number for the bean. +// FileLine returns the source file and line number of the bean. func (d *BeanMetadata) FileLine() string { return d.fileLine } -// SetFileLine sets the file and line number for the bean. +// SetFileLine sets the source file and line number of the bean. func (d *BeanMetadata) SetFileLine(file string, line int) { d.fileLine = fmt.Sprintf("%s:%d", file, line) } @@ -182,27 +178,27 @@ type BeanRuntime struct { name string // The name of the bean. } -// Name returns the name of the bean. +// Name returns the bean's name. func (d *BeanRuntime) Name() string { return d.name } -// Type returns the type of the bean. +// Type returns the bean's type. func (d *BeanRuntime) Type() reflect.Type { return d.t } -// Value returns the value of the bean as [reflect.Value]. +// Value returns the bean as reflect.Value. func (d *BeanRuntime) Value() reflect.Value { return d.v } -// Interface returns the underlying value of the bean. +// Interface returns the underlying bean. func (d *BeanRuntime) Interface() any { return d.v.Interface() } -// Callable returns the callable for the bean. +// Callable returns the bean's callable constructor. func (d *BeanRuntime) Callable() *gs_arg.Callable { return nil } @@ -223,7 +219,7 @@ type BeanDefinition struct { *BeanRuntime } -// makeBean creates a new bean definition. +// makeBean creates a new BeanDefinition. func makeBean(t reflect.Type, v reflect.Value, f *gs_arg.Callable, name string) *BeanDefinition { return &BeanDefinition{ BeanMetadata: &BeanMetadata{ @@ -238,7 +234,7 @@ func makeBean(t reflect.Type, v reflect.Value, f *gs_arg.Callable, name string) } } -// SetMock sets the mock object for the bean, replacing its runtime information. +// SetMock replaces the bean's runtime instance with a mock object. func (d *BeanDefinition) SetMock(obj any) { *d = BeanDefinition{ BeanMetadata: &BeanMetadata{ @@ -253,27 +249,27 @@ func (d *BeanDefinition) SetMock(obj any) { } } -// Callable returns the callable for the bean. +// Callable returns the bean's callable constructor. func (d *BeanDefinition) Callable() *gs_arg.Callable { return d.f } -// SetName sets the name of the bean. +// SetName sets the bean's name. func (d *BeanDefinition) SetName(name string) { d.name = name } -// Status returns the current status of the bean. +// Status returns the bean's current lifecycle status. func (d *BeanDefinition) Status() BeanStatus { return d.status } -// SetStatus sets the current status of the bean. +// SetStatus sets the bean's current lifecycle status. func (d *BeanDefinition) SetStatus(status BeanStatus) { d.status = status } -// SetInit sets the initialization function for the bean. +// SetInit sets the bean's initialization function. func (d *BeanDefinition) SetInit(fn gs.BeanInitFunc) { if validLifeCycleFunc(reflect.TypeOf(fn), d.Type()) { d.init = fn @@ -282,7 +278,7 @@ func (d *BeanDefinition) SetInit(fn gs.BeanInitFunc) { panic("init should be func(bean) or func(bean)error") } -// SetDestroy sets the destruction function for the bean. +// SetDestroy sets the bean's destruction function. func (d *BeanDefinition) SetDestroy(fn gs.BeanDestroyFunc) { if validLifeCycleFunc(reflect.TypeOf(fn), d.Type()) { d.destroy = fn @@ -291,7 +287,7 @@ func (d *BeanDefinition) SetDestroy(fn gs.BeanDestroyFunc) { panic("destroy should be func(bean) or func(bean)error") } -// SetInitMethod sets the initialization function for the bean by method name. +// SetInitMethod sets the bean's initialization method by name. func (d *BeanDefinition) SetInitMethod(method string) { m, ok := d.t.MethodByName(method) if !ok { @@ -300,7 +296,7 @@ func (d *BeanDefinition) SetInitMethod(method string) { d.SetInit(m.Func.Interface()) } -// SetDestroyMethod sets the destruction function for the bean by method name. +// SetDestroyMethod sets the bean's destruction method by name. func (d *BeanDefinition) SetDestroyMethod(method string) { m, ok := d.t.MethodByName(method) if !ok { @@ -309,7 +305,7 @@ func (d *BeanDefinition) SetDestroyMethod(method string) { d.SetDestroy(m.Func.Interface()) } -// SetExport sets the exported interfaces for the bean. +// SetExport registers interfaces exported by the bean. func (d *BeanDefinition) SetExport(exports ...reflect.Type) { for _, t := range exports { if t.Kind() != reflect.Interface { @@ -325,7 +321,7 @@ func (d *BeanDefinition) SetExport(exports ...reflect.Type) { } } -// OnProfiles sets the conditions for the bean based on the active profiles. +// OnProfiles adds conditions based on active profiles. func (d *BeanDefinition) OnProfiles(profiles string) { d.SetCondition(gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { val := strings.TrimSpace(ctx.Prop("spring.profiles.active")) @@ -342,18 +338,19 @@ func (d *BeanDefinition) OnProfiles(profiles string) { })) } -// TypeAndName returns the type and name of the bean. +// TypeAndName returns the bean's type and name. func (d *BeanDefinition) TypeAndName() (reflect.Type, string) { return d.Type(), d.Name() } -// String returns a string representation of the bean. +// String returns a human-readable description of the bean. func (d *BeanDefinition) String() string { return fmt.Sprintf("name=%s %s", d.name, d.fileLine) } -// NewBean creates a new bean definition. When registering a normal function, -// use reflect.ValueOf(fn) to avoid conflicts with constructors. +// NewBean creates a new BeanDefinition. +// If objOrCtor is a constructor function, it binds its arguments and infers bean name. +// Otherwise, it wraps an existing instance as a bean. func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { var f *gs_arg.Callable @@ -375,13 +372,12 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { panic("bean must be ref type") } - // Ensure the value is valid and not nil + // Ensure the bean instance is valid and not nil if !v.IsValid() || v.IsNil() { panic("bean can't be nil") } - // If objOrCtor is a function (not from reflect.Value), - // process it as a constructor + // Handle constructor functions if !fromValue && t.Kind() == reflect.Func { if !util.IsConstructor(t) { @@ -390,7 +386,7 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { panic(fmt.Sprintf("constructor should be %s or %s", t1, t2)) } - // Bind the constructor arguments + // Bind constructor arguments var err error f, err = gs_arg.NewCallable(objOrCtor, ctorArgs) if err != nil { @@ -402,7 +398,7 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { in0 = t.In(0) } - // Obtain the return type of the constructor + // Prepare the return type out0 := t.Out(0) v = reflect.New(out0) if util.IsBeanType(out0) { @@ -414,7 +410,7 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { panic("bean must be ref type") } - // Extract function name for naming the bean + // Derive bean name from constructor function name fnPtr := reflect.ValueOf(objOrCtor).Pointer() fnInfo := runtime.FuncForPC(fnPtr) funcName := fnInfo.Name() @@ -424,7 +420,7 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { name = name[strings.Index(name, ".")+1:] } - // Check if the function is a method and set a condition if needed + // If the constructor is a method, set a condition for its owner bean method := strings.LastIndexByte(fnInfo.Name(), ')') > 0 if method { var s gs.BeanSelector = gs.BeanSelectorImpl{Type: in0} @@ -453,7 +449,7 @@ func NewBean(objOrCtor any, ctorArgs ...gs.Arg) *gs.BeanDefinition { } } - // Extract the final type name for bean naming + // Fallback: derive name from the type if name == "" { s := strings.Split(t.String(), ".") name = strings.TrimPrefix(s[len(s)-1], "*") diff --git a/gs/internal/gs_bean/bean_test.go b/gs/internal/gs_bean/bean_test.go index 15e603c6..7f4c4c48 100644 --- a/gs/internal/gs_bean/bean_test.go +++ b/gs/internal/gs_bean/bean_test.go @@ -23,11 +23,11 @@ import ( "reflect" "testing" - "github.com/go-spring/gs-assert/assert" "github.com/go-spring/gs-mock/gsmock" + "github.com/go-spring/spring-base/testing/assert" + "github.com/go-spring/spring-base/util" "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/util" ) func TestBeanStatus(t *testing.T) { @@ -77,7 +77,7 @@ func TestBeanDefinition(t *testing.T) { assert.That(t, StatusCreated).Equal(bean.Status()) bean.SetCaller(1) - assert.ThatString(t, bean.FileLine()).HasSuffix("gs/internal/gs_bean/bean_test.go:79") + assert.String(t, bean.FileLine()).HasSuffix("gs/internal/gs_bean/bean_test.go:79") bean.SetName("test-1") assert.That(t, bean.Name()).Equal("test-1") @@ -85,7 +85,7 @@ func TestBeanDefinition(t *testing.T) { beanType, beanName := bean.TypeAndName() assert.That(t, beanType).Equal(reflect.TypeFor[*TestBean]()) assert.That(t, beanName).Equal("test-1") - assert.ThatString(t, bean.String()).Matches(`name=test-1 .*/gs/internal/gs_bean/bean_test.go:79`) + assert.String(t, bean.String()).Matches(`name=test-1 .*/gs/internal/gs_bean/bean_test.go:79`) assert.That(t, bean.BeanRuntime.Callable()).Nil() assert.That(t, bean.BeanRuntime.Status()).Equal(StatusWired) @@ -288,7 +288,7 @@ func TestBeanDefinition(t *testing.T) { func TestNewBean(t *testing.T) { - t.Run("type error", func(t *testing.T) { + t.Run("invalid bean type", func(t *testing.T) { assert.Panic(t, func() { NewBean(new(int)) @@ -300,7 +300,7 @@ func TestNewBean(t *testing.T) { }, "bean must be ref type") }) - t.Run("value error", func(t *testing.T) { + t.Run("nil bean value", func(t *testing.T) { assert.Panic(t, func() { NewBean((*TestBean)(nil)) }, "bean can't be nil") @@ -313,14 +313,14 @@ func TestNewBean(t *testing.T) { assert.That(t, beanX.Type()).Equal(reflect.TypeFor[*TestBean]()) }) - t.Run("object - reflect.Value", func(t *testing.T) { + t.Run("object by reflect.Value", func(t *testing.T) { bean := NewBean(reflect.ValueOf(&TestBean{})) beanX := bean.BeanRegistration().(*BeanDefinition) assert.That(t, beanX.Name()).Equal("TestBean") assert.That(t, beanX.Type()).Equal(reflect.TypeFor[*TestBean]()) }) - t.Run("function - reflect.Value", func(t *testing.T) { + t.Run("function by reflect.Value", func(t *testing.T) { fn := func(int, int) string { return "" } bean := NewBean(reflect.ValueOf(fn)).Name("TestFunc") beanX := bean.BeanRegistration().(*BeanDefinition) @@ -344,14 +344,14 @@ func TestNewBean(t *testing.T) { gs_arg.Tag("${v:=3}"), gs_arg.Index(1, gs_arg.Tag("${v:=3}")), ) - }, "NewArgList error << arguments must be all indexed or non-indexed") + }, "NewArgList error: arguments must be all indexed or non-indexed") assert.Panic(t, func() { NewBean(func() int { return 0 }) }, "bean must be ref type") }) - t.Run("constructor", func(t *testing.T) { + t.Run("constructor success", func(t *testing.T) { fn := func(int, int) *TestBean { return nil } bean := NewBean(fn).Name("NewTestBean") beanX := bean.BeanRegistration().(*BeanDefinition) diff --git a/gs/internal/gs_cond/cond.go b/gs/internal/gs_cond/cond.go index 8ae8dea5..b26bb04e 100755 --- a/gs/internal/gs_cond/cond.go +++ b/gs/internal/gs_cond/cond.go @@ -14,88 +14,47 @@ * limitations under the License. */ -/* -Package gs_cond provides conditional component registration for Go-Spring framework, -offering runtime decision-making through various condition types. - -1. Property Conditions - - - Existence check: - cond := OnProperty("db.host") - - - Value matching: - cond := OnProperty("env").HavingValue("prod") - - - Expression evaluation: - cond := OnProperty("port").HavingValue("expr:int($) > 1024") - - - Missing handling: - cond := OnProperty("debug").MatchIfMissing() - -2. Bean Conditions - - - Existence verification: - cond := OnBean[Database]() - - - Absence check: - cond := OnMissingBean[Logger]() - - - Singleton validation: - cond := OnSingleBean[Config]() - -3. Logical Combinators - - - Conjunction: - cond := And(condition1, condition2) - - - Disjunction: - cond := Or(condition1, condition2) - - - Negation: - cond := Not(condition) - - - Universal negation: - cond := None(condition1, condition2) - -4. Custom Conditions - - - Functional condition: - cond := OnFunc(func(ctx gs.ConditionContext) (bool, error) { - return time.Now().Hour() > 9, nil - }) - - - Expression condition (Pending implementation): - cond := OnExpression("beans('redis').size() > 0") -*/ +// Package gs_cond provides a set of composable conditions used to control +// bean registration for Go-Spring framework. +// +// It defines various condition types such as: +// +// - OnFunc: Uses a custom function to evaluate a condition. +// - OnProperty: Matches based on the presence or value of a property. +// - OnBean: Matches if at least one bean exists for a given selector. +// - OnMissingBean: Matches if no beans exist for a given selector. +// - OnSingleBean: Matches if exactly one bean exists for a given selector. +// - OnExpression: Evaluates a custom expression (currently unimplemented). +// - Not / Or / And / None: Logical combinators for composing multiple conditions. package gs_cond import ( "fmt" "strings" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs" - "github.com/go-spring/spring-core/util" - "github.com/go-spring/spring-core/util/errutil" ) /********************************* OnFunc ************************************/ -// 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. +// onFunc is an implementation of [gs.Condition] that wraps a user-defined function. +// This allows defining a custom condition that is evaluated at runtime +// based on the provided function. type onFunc struct { fn func(ctx gs.ConditionContext) (bool, error) } -// OnFunc creates a Conditional that evaluates using a custom function. +// OnFunc creates a condition that evaluates using the provided custom function. 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. +// Matches executes the wrapped function to determine if the condition is satisfied. 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) + return false, util.FormatError(err, "condition %s matches error", c) } return ok, nil } @@ -107,24 +66,25 @@ func (c *onFunc) String() string { /******************************* OnProperty **********************************/ -// ConditionOnProperty defines the methods for evaluating a condition based on a property. -// This interface provides flexibility for matching missing properties and checking their values. +// ConditionOnProperty defines a condition that is evaluated based on the value +// of a property in the application context. It provides methods to customize +// behavior for missing properties and specific property values. type ConditionOnProperty interface { gs.Condition - MatchIfMissing() ConditionOnProperty - HavingValue(s string) ConditionOnProperty + MatchIfMissing() ConditionOnProperty // Match if the property is missing + HavingValue(s string) ConditionOnProperty // Match if the property has a specific value } -// onProperty evaluates a condition based on the existence and value of a property -// in the context. It allows for complex matching behaviors such as matching missing -// properties or evaluating expressions. +// onProperty implements [ConditionOnProperty], allowing conditions based on +// the existence and value of properties in the context. type onProperty struct { - name string // The name of the property to check. - matchIfMissing bool // Whether to match if the property is missing. - havingValue any // The expected value or expression to match. + name string // Property name to check + matchIfMissing bool // Whether to match when the property is missing + havingValue any // Expected value or expression for comparison } -// OnProperty creates a condition based on the presence and value of a specified property. +// OnProperty creates a new condition that checks for the presence +// and/or value of a specified property. func OnProperty(name string) ConditionOnProperty { return &onProperty{name: name} } @@ -141,31 +101,31 @@ func (c *onProperty) HavingValue(s string) ConditionOnProperty { return c } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates the condition based on the property's existence and value. func (c *onProperty) Matches(ctx gs.ConditionContext) (bool, error) { - // If the context doesn't have the property, handle accordingly. + // If property does not exist if !ctx.Has(c.name) { return c.matchIfMissing, nil } - // If the expected value is nil, the condition is always true. + // If no specific value is required, condition passes if c.havingValue == nil { return true, nil } - havingValue := c.havingValue.(string) + havingValue, _ := c.havingValue.(string) - // Retrieve the property's value and compare it with the expected value. + // Compare the property's value with the expected value. val := ctx.Prop(c.name) if !strings.HasPrefix(havingValue, "expr:") { return val == havingValue, nil } - // Evaluate the expression and return the result. + // Evaluate as an expression if prefixed with "expr:" ok, err := EvalExpr(havingValue[5:], val) if err != nil { - return false, errutil.WrapError(err, "condition matches error: %s", c) + return false, util.FormatError(err, "condition %s matches error", c) } return ok, nil } @@ -187,14 +147,14 @@ func (c *onProperty) String() string { /********************************* OnBean ************************************/ -// onBean checks for the existence of beans that match a selector. -// It returns true if at least one bean matches the selector, and false otherwise. +// onBean represents a condition that checks for the existence of beans +// matching a specific selector in the application context. type onBean struct { - s gs.BeanSelector // The selector used to match beans in the context. + s gs.BeanSelector // Bean selector used to find matching beans } // OnBean creates a condition that evaluates to true if at least one bean -// matches the specified type and name. +// matches the specified type and optional name. func OnBean[T any](name ...string) gs.Condition { return &onBean{s: gs.BeanSelectorFor[T](name...)} } @@ -205,11 +165,11 @@ func OnBeanSelector(s gs.BeanSelector) gs.Condition { return &onBean{s: s} } -// Matches checks if the condition is met according to the provided context. +// Matches checks if there is at least one matching bean in the context. 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) + return false, util.FormatError(err, "condition %s matches error", c) } return len(beans) > 0, nil } @@ -220,29 +180,29 @@ func (c *onBean) String() string { /***************************** OnMissingBean *********************************/ -// onMissingBean checks for the absence of beans matching a selector. -// It returns true if no beans match the selector, and false otherwise. +// onMissingBean represents a condition that evaluates to true if no bean +// matches the specified selector in the context. type onMissingBean struct { - s gs.BeanSelector // The selector used to find beans. + s gs.BeanSelector // Bean selector for finding beans } -// OnMissingBean creates a condition that evaluates to true if no beans match -// the specified type and name. +// OnMissingBean creates a condition that evaluates to true if no bean +// matches the given type and optional name. func OnMissingBean[T any](name ...string) gs.Condition { return &onMissingBean{s: gs.BeanSelectorFor[T](name...)} } -// OnMissingBeanSelector creates a condition that evaluates to true if no beans -// match the provided selector. +// OnMissingBeanSelector creates a condition that evaluates to true if no bean +// matches the provided selector. func OnMissingBeanSelector(s gs.BeanSelector) gs.Condition { return &onMissingBean{s: s} } -// Matches checks if the condition is met according to the provided context. +// Matches returns true if no beans matching the selector are found. 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) + return false, util.FormatError(err, "condition %s matches error", c) } return len(beans) == 0, nil } @@ -253,14 +213,14 @@ func (c *onMissingBean) String() string { /***************************** OnSingleBean **********************************/ -// onSingleBean checks if exactly one matching bean exists in the context. -// It returns true if exactly one bean matches the selector, and false otherwise. +// onSingleBean represents a condition that checks if exactly one bean +// matches the specified selector in the context. type onSingleBean struct { - s gs.BeanSelector // The selector used to find beans. + s gs.BeanSelector // Bean selector for finding beans } // OnSingleBean creates a condition that evaluates to true if exactly one bean -// matches the specified type and name. +// matches the given type and optional name. func OnSingleBean[T any](name ...string) gs.Condition { return &onSingleBean{s: gs.BeanSelectorFor[T](name...)} } @@ -271,11 +231,11 @@ func OnSingleBeanSelector(s gs.BeanSelector) gs.Condition { return &onSingleBean{s: s} } -// Matches checks if the condition is met according to the provided context. +// Matches returns true if exactly one bean matching the selector is found. 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) + return false, util.FormatError(err, "condition %s matches error", c) } return len(beans) == 1, nil } @@ -286,22 +246,21 @@ func (c *onSingleBean) String() string { /***************************** OnExpression **********************************/ -// onExpression evaluates a custom expression within the context. The expression should -// return true or false, and the evaluation is expected to happen within the context. +// onExpression represents a condition that evaluates a custom expression +// in the context. The expression should return a boolean value. type onExpression struct { - expression string // The string expression to evaluate. + expression string // Expression string to evaluate } -// OnExpression creates a condition that evaluates based on a custom string expression. -// The expression is expected to return true or false. +// OnExpression creates a condition that evaluates a custom boolean expression. func OnExpression(expression string) gs.Condition { return &onExpression{expression: expression} } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates the expression (currently unimplemented). func (c *onExpression) Matches(ctx gs.ConditionContext) (bool, error) { err := util.ErrUnimplementedMethod - return false, errutil.WrapError(err, "condition matches error: %s", c) + return false, util.FormatError(err, "condition %s matches error", c) } func (c *onExpression) String() string { @@ -310,22 +269,21 @@ func (c *onExpression) String() string { /********************************** Not ***************************************/ -// onNot is a condition that negates another condition. It returns true if the wrapped -// condition evaluates to false, and false if the wrapped condition evaluates to true. +// onNot represents a condition that inverts the result of another condition. type onNot struct { c gs.Condition // The condition to negate. } -// Not creates a condition that inverts the result of the provided condition. +// Not creates a condition that returns the negation of another condition. func Not(c gs.Condition) gs.Condition { return &onNot{c: c} } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates the wrapped condition and returns its negation. 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) + return false, util.FormatError(err, "condition %s matches error", c) } return !ok, nil } @@ -336,14 +294,13 @@ func (c *onNot) String() string { /********************************** Or ***************************************/ -// onOr is a condition that combines multiple conditions with an OR operator. -// It evaluates to true if at least one condition is satisfied. +// onOr represents a condition that combines multiple conditions using +// a logical OR operator. It succeeds if at least one condition is satisfied. type onOr struct { - conditions []gs.Condition // The list of conditions to evaluate with OR. + conditions []gs.Condition // List of conditions combined with OR } -// Or combines multiple conditions with an OR operator, returning true if at -// least one condition is satisfied. +// Or combines multiple conditions using OR logic. func Or(conditions ...gs.Condition) gs.Condition { if n := len(conditions); n == 0 { return nil @@ -353,11 +310,11 @@ func Or(conditions ...gs.Condition) gs.Condition { return &onOr{conditions: conditions} } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates all conditions and returns true if at least one is satisfied. 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) + return false, util.FormatError(err, "condition %s matches error", g) } else if ok { return true, nil } @@ -371,14 +328,13 @@ func (g *onOr) String() string { /********************************* And ***************************************/ -// onAnd is a condition that combines multiple conditions with an AND operator. -// It evaluates to true only if all conditions are satisfied. +// onAnd represents a condition that combines multiple conditions using +// a logical AND operator. It succeeds only if all conditions are satisfied. type onAnd struct { - conditions []gs.Condition // The list of conditions to evaluate with AND. + conditions []gs.Condition // List of conditions combined with AND } -// And combines multiple conditions with an AND operator, returning true if -// all conditions are satisfied. +// And combines multiple conditions using AND logic. func And(conditions ...gs.Condition) gs.Condition { if n := len(conditions); n == 0 { return nil @@ -388,12 +344,12 @@ func And(conditions ...gs.Condition) gs.Condition { return &onAnd{conditions: conditions} } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates all conditions and returns true only if all are satisfied. func (g *onAnd) Matches(ctx gs.ConditionContext) (bool, error) { for _, c := range g.conditions { ok, err := c.Matches(ctx) if err != nil { - return false, errutil.WrapError(err, "condition matches error: %s", g) + return false, util.FormatError(err, "condition %s matches error", g) } else if !ok { return false, nil } @@ -407,14 +363,14 @@ func (g *onAnd) String() string { /********************************** None *************************************/ -// onNone is a condition that combines multiple conditions with a NONE operator. -// It evaluates to true only if none of the conditions are satisfied. +// onNone represents a condition that succeeds only if none of the +// provided conditions are satisfied. type onNone struct { - conditions []gs.Condition // The list of conditions to evaluate with NONE. + conditions []gs.Condition // List of conditions combined with NONE } -// None combines multiple conditions with a NONE operator, returning true if -// none of the conditions are satisfied. +// None combines multiple conditions using NONE logic. +// Returns true only if all conditions are false. func None(conditions ...gs.Condition) gs.Condition { if n := len(conditions); n == 0 { return nil @@ -424,11 +380,11 @@ func None(conditions ...gs.Condition) gs.Condition { return &onNone{conditions: conditions} } -// Matches checks if the condition is met according to the provided context. +// Matches evaluates all conditions and returns true only if all are false. 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) + return false, util.FormatError(err, "condition %s matches error", g) } else if ok { return false, nil } @@ -442,7 +398,8 @@ func (g *onNone) String() string { /******************************* utilities ***********************************/ -// FormatGroup generates a formatted string for a group of conditions (AND, OR, NONE). +// FormatGroup formats a group of conditions (e.g., AND, OR, NONE) as a string +// for debugging and logging purposes. func FormatGroup(op string, conditions []gs.Condition) string { var sb strings.Builder sb.WriteString(op) diff --git a/gs/internal/gs_cond/cond_test.go b/gs/internal/gs_cond/cond_test.go index 996eaf7d..837be6ad 100644 --- a/gs/internal/gs_cond/cond_test.go +++ b/gs/internal/gs_cond/cond_test.go @@ -21,10 +21,10 @@ import ( "fmt" "testing" - "github.com/go-spring/gs-assert/assert" "github.com/go-spring/gs-mock/gsmock" + "github.com/go-spring/spring-base/testing/assert" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs" - "github.com/go-spring/spring-core/util" ) var ( @@ -108,11 +108,19 @@ func TestOnFunc(t *testing.T) { assert.That(t, err).Nil() }) - t.Run("error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { 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") + assert.Error(t, err).Matches("test error") + }) + + t.Run("returns false", func(t *testing.T) { + fn := func(ctx gs.ConditionContext) (bool, error) { return false, nil } + cond := OnFunc(fn) + ok, err := cond.Matches(nil) + assert.That(t, ok).False() + assert.That(t, err).Nil() }) } @@ -162,6 +170,16 @@ func TestOnProperty(t *testing.T) { assert.That(t, ok).True() }) + t.Run("property not exist without MatchIfMissing", func(t *testing.T) { + m := gsmock.NewManager() + ctx := gs.NewConditionContextMockImpl(m) + ctx.MockHas().ReturnValue(false) + + cond := OnProperty("missing.prop") + ok, _ := cond.Matches(ctx) + assert.That(t, ok).False() + }) + t.Run("expression", func(t *testing.T) { t.Run("number expression", func(t *testing.T) { @@ -194,7 +212,7 @@ func TestOnProperty(t *testing.T) { cond := OnProperty("test.prop").HavingValue("expr:invalid syntax") _, err := cond.Matches(ctx) - assert.ThatError(t, err).Matches("eval \\\"invalid syntax\\\" returns error") + assert.Error(t, err).Matches("eval \\\"invalid syntax\\\" returns error") }) }) } @@ -223,14 +241,14 @@ func TestOnBean(t *testing.T) { assert.That(t, ok).False() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnBean[any]("b") ok, err := cond.Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -259,14 +277,14 @@ func TestOnMissingBean(t *testing.T) { assert.That(t, ok).False() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnMissingBean[any]("b") ok, err := cond.Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -293,14 +311,14 @@ func TestOnSingleBean(t *testing.T) { assert.That(t, ok).False() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") ok, err := cond.Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -316,28 +334,28 @@ func TestOnExpression(t *testing.T) { func TestNot(t *testing.T) { - t.Run("true", func(t *testing.T) { + t.Run("returns true", func(t *testing.T) { cond := Not(trueCond) ok, err := cond.Matches(nil) assert.That(t, err).Nil() assert.That(t, ok).False() }) - t.Run("false", func(t *testing.T) { + t.Run("returns false", func(t *testing.T) { cond := Not(falseCond) ok, err := cond.Matches(nil) assert.That(t, err).Nil() assert.That(t, ok).True() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") ok, err := Not(cond).Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -368,14 +386,14 @@ func TestAnd(t *testing.T) { assert.That(t, ok).False() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") ok, err := And(cond, trueCond).Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -406,14 +424,14 @@ func TestOr(t *testing.T) { assert.That(t, ok).False() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") ok, err := Or(cond, trueCond).Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } @@ -446,14 +464,14 @@ func TestNone(t *testing.T) { assert.That(t, ok).True() }) - t.Run("return error", func(t *testing.T) { + t.Run("returns error", func(t *testing.T) { m := gsmock.NewManager() ctx := gs.NewConditionContextMockImpl(m) ctx.MockFind().ReturnValue(nil, errors.New("test error")) cond := OnSingleBean[any]("b") ok, err := None(cond, trueCond).Matches(ctx) - assert.ThatError(t, err).Matches("test error") + assert.Error(t, err).Matches("test error") assert.That(t, ok).False() }) } diff --git a/gs/internal/gs_cond/expr.go b/gs/internal/gs_cond/expr.go index 977b55ad..abe529c8 100644 --- a/gs/internal/gs_cond/expr.go +++ b/gs/internal/gs_cond/expr.go @@ -17,10 +17,10 @@ package gs_cond import ( - "fmt" "maps" "github.com/expr-lang/expr" + "github.com/go-spring/spring-base/util" ) // funcMap stores registered functions that can be referenced in expressions. @@ -35,18 +35,18 @@ func RegisterExpressFunc(name string, fn any) { } // EvalExpr evaluates a boolean expression using the provided value as the "$" variable. -// `input` is a boolean expression string to evaluate, it must return a boolean result. -// `val` is a string value accessible as "$" within the expression context. +// - `input` is a boolean expression string to evaluate, it must return a boolean result. +// - `val` is a string value accessible as "$" within the expression context. func EvalExpr(input string, val string) (bool, error) { env := map[string]any{"$": val} maps.Copy(env, funcMap) r, err := expr.Eval(input, env) if err != nil { - return false, fmt.Errorf("eval %q returns error, %w", input, err) + return false, util.FormatError(err, "eval %q returns error", input) } ret, ok := r.(bool) if !ok { - return false, fmt.Errorf("eval %q doesn't return bool value", input) + return false, util.FormatError(nil, "eval %q doesn't return bool value", input) } return ret, nil } diff --git a/gs/internal/gs_cond/expr_test.go b/gs/internal/gs_cond/expr_test.go index 0c9b0fe8..e66a522e 100644 --- a/gs/internal/gs_cond/expr_test.go +++ b/gs/internal/gs_cond/expr_test.go @@ -18,23 +18,62 @@ package gs_cond import ( "strconv" + "strings" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" ) func TestEvalExpr(t *testing.T) { - _, err := EvalExpr("$", "3") - assert.ThatError(t, err).Matches("doesn't return bool value") + t.Run("doesn't return bool", func(t *testing.T) { + _, err := EvalExpr("$", "3") + assert.Error(t, err).Matches("doesn't return bool value") + }) + + t.Run("simple integer comparison", func(t *testing.T) { + ok, err := EvalExpr("int($)==3", "3") + assert.That(t, err).Nil() + assert.That(t, ok).True() + }) + + t.Run("custom function", func(t *testing.T) { + RegisterExpressFunc("equal", func(s string, i int) bool { + return s == strconv.Itoa(i) + }) + ok, err := EvalExpr("equal($,9)", "9") + assert.That(t, err).Nil() + assert.That(t, ok).True() + }) + + t.Run("complex boolean expression", func(t *testing.T) { + ok, err := EvalExpr("int($)>0 && int($)<10", "5") + assert.That(t, err).Nil() + assert.That(t, ok).True() + }) + + t.Run("boundary value testing", func(t *testing.T) { + ok, err := EvalExpr("int($)==0", "0") + assert.That(t, err).Nil() + assert.That(t, ok).True() + }) - ok, err := EvalExpr("int($)==3", "3") - assert.That(t, err).Nil() - assert.That(t, ok).True() + t.Run("string operations", func(t *testing.T) { + RegisterExpressFunc("string_contains", func(s, substr string) bool { + return len(s) >= len(substr) && strings.Contains(s, substr) + }) + ok, err := EvalExpr("string_contains($, \"test\")", "this is a test") + assert.That(t, err).Nil() + assert.That(t, ok).True() + }) + + t.Run("unregistered function call", func(t *testing.T) { + _, err := EvalExpr("unknownFunc($)", "3") + assert.That(t, err).NotNil() + }) - RegisterExpressFunc("equal", func(s string, i int) bool { - return s == strconv.Itoa(i) + t.Run("empty string input", func(t *testing.T) { + ok, err := EvalExpr("len($)==0", "") + assert.That(t, err).Nil() + assert.That(t, ok).True() }) - ok, err = EvalExpr("equal($,9)", "9") - assert.That(t, err).Nil() - assert.That(t, ok).True() } diff --git a/gs/internal/gs_conf/cmd.go b/gs/internal/gs_conf/cmd.go index b2332774..257aa78f 100644 --- a/gs/internal/gs_conf/cmd.go +++ b/gs/internal/gs_conf/cmd.go @@ -17,10 +17,10 @@ package gs_conf import ( - "fmt" "os" "strings" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/conf" ) @@ -37,38 +37,54 @@ func NewCommandArgs() *CommandArgs { return &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]`. +// CopyTo extracts command-line parameters and stores them as key-value pairs. +// Supported formats include: +// +// - key=value +// - key (defaults to "true") +// - key=value (inline form) +// +// The default prefix is "-D", which can be overridden by the environment +// variable `GS_ARGS_PREFIX`. func (c *CommandArgs) CopyTo(p *conf.MutableProperties) error { - if len(os.Args) == 0 { + if len(os.Args) <= 1 { return nil } fileID := p.AddFile("Args") - // Default option prefix is "-D", but it can be overridden by the - // environment variable `GS_ARGS_PREFIX`. + // Determine the option prefix. option := "-D" if s := strings.TrimSpace(os.Getenv(CommandArgsPrefix)); s != "" { option = s } cmdArgs := os.Args[1:] - n := len(cmdArgs) - for i := range n { + for i := 0; i < len(cmdArgs); i++ { + var str string if cmdArgs[i] == option { - if i+1 >= n { - return fmt.Errorf("cmd option %s needs arg", option) - } - next := cmdArgs[i+1] - ss := strings.SplitN(next, "=", 2) - if len(ss) == 1 { - ss = append(ss, "true") - } - if err := p.Set(ss[0], ss[1], fileID); err != nil { - return err + // separated form: key=value + if i+1 >= len(cmdArgs) { + return util.FormatError(nil, "cmd option %s: needs arg", option) } + i++ + str = cmdArgs[i] + } else if s, ok := strings.CutPrefix(cmdArgs[i], option); ok { + // inline form: key=value + str = s + } else { + // not a Go-Spring command-line option + continue + } + if str = strings.TrimSpace(str); str == "" { + return util.FormatError(nil, "cmd option %s: needs arg", option) + } + ss := strings.SplitN(str, "=", 2) + if len(ss) == 1 { + ss = append(ss, "true") + } + if err := p.Set(ss[0], ss[1], fileID); err != nil { + return util.FormatError(err, "set cmd option %s error", str) } } return nil diff --git a/gs/internal/gs_conf/cmd_test.go b/gs/internal/gs_conf/cmd_test.go index 4b56c393..a3603758 100644 --- a/gs/internal/gs_conf/cmd_test.go +++ b/gs/internal/gs_conf/cmd_test.go @@ -20,13 +20,13 @@ import ( "os" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) func TestCommandArgs(t *testing.T) { - t.Run("no args - 1", func(t *testing.T) { + t.Run("no args - empty", func(t *testing.T) { os.Args = nil props := conf.New() @@ -35,7 +35,7 @@ func TestCommandArgs(t *testing.T) { assert.That(t, len(props.Keys()) == 0).True() }) - t.Run("no args - 2", func(t *testing.T) { + t.Run("no args - only executable", func(t *testing.T) { os.Args = []string{"test"} props := conf.New() @@ -59,17 +59,17 @@ func TestCommandArgs(t *testing.T) { props := conf.New() err := NewCommandArgs().CopyTo(props) - assert.ThatError(t, err).Matches("cmd option -D needs arg") + assert.Error(t, err).Matches("cmd option -D: needs arg") }) - t.Run("set return error", func(t *testing.T) { + t.Run("property conflict", func(t *testing.T) { os.Args = []string{"test", "-D", "name=go-spring", "-D", "debug"} p := conf.Map(map[string]any{ "debug": []string{"true"}, }) err := NewCommandArgs().CopyTo(p) - assert.ThatError(t, err).Matches("property conflict at path debug") + assert.Error(t, err).Matches("property conflict at path debug") }) t.Run("custom prefix", func(t *testing.T) { @@ -85,7 +85,7 @@ func TestCommandArgs(t *testing.T) { assert.That(t, "8080").Equal(props.Get("port")) }) - t.Run("ignore other args", func(t *testing.T) { + t.Run("ignore args", func(t *testing.T) { os.Args = []string{"test", "-v", "-D", "env=prod", "--log-level=info"} props := conf.New() @@ -95,4 +95,33 @@ func TestCommandArgs(t *testing.T) { assert.That(t, props.Has("--log-level")).False() assert.That(t, props.Has("-v")).False() }) + + t.Run("empty value assignment", func(t *testing.T) { + os.Args = []string{"test", "-D", "name="} + + props := conf.New() + err := NewCommandArgs().CopyTo(props) + assert.That(t, err).Nil() + assert.That(t, "").Equal(props.Get("name")) + }) + + t.Run("multiple equal signs", func(t *testing.T) { + os.Args = []string{"test", "-D", "database.url=localhost:3306"} + + props := conf.New() + err := NewCommandArgs().CopyTo(props) + assert.That(t, err).Nil() + assert.That(t, "localhost:3306").Equal(props.Get("database.url")) + }) + + t.Run("mixed args", func(t *testing.T) { + os.Args = []string{"test", "-D", "valid=key", "-x", "-D", "another=value"} + + props := conf.New() + err := NewCommandArgs().CopyTo(props) + assert.That(t, err).Nil() + assert.That(t, "key").Equal(props.Get("valid")) + assert.That(t, "value").Equal(props.Get("another")) + assert.That(t, props.Has("-x")).False() + }) } diff --git a/gs/internal/gs_conf/conf.go b/gs/internal/gs_conf/conf.go index 91865d99..dc228276 100644 --- a/gs/internal/gs_conf/conf.go +++ b/gs/internal/gs_conf/conf.go @@ -14,55 +14,56 @@ * limitations under the License. */ -/* -Package gs_conf provides hierarchical configuration management -with multi-source support for Go-Spring framework. - -Key Features: - -1. Command-line argument parsing - - Supports `-D key[=value]` format arguments - - Customizable prefix via `GS_ARGS_PREFIX` environment variable - - Example: `./app -D server.port=8080 -D debug` - -2. Environment variable handling - - Automatic loading of `GS_` prefixed variables - - Conversion rules: `GS_DB_HOST=127.0.0.1` → `db.host=127.0.0.1` - - Direct mapping of non-prefixed environment variables - -3. Configuration file management - - Supports properties/yaml/toml/json formats - - Local configurations: ./conf/app.{properties|yaml|toml|json} - - Remote configurations: ./conf/remote/app.{properties|yaml|toml|json} - - Profile-based configurations (e.g., app-dev.properties) - -4. Layered configuration hierarchy - - Priority order: System config → File config → Env variables → CLI arguments - - Provides AppConfig (application context) and BootConfig (boot context) - - High-priority configurations override lower ones -*/ +// Package gs_conf provides a layered configuration system for Go-Spring +// applications. It unifies multiple configuration sources and resolves them +// into a single immutable property set. +// +// The supported sources include: +// +// - Built-in system defaults (SysConf) +// - Local configuration files (e.g., ./conf/app.yaml) +// - Remote configuration files (from config servers) +// - Dynamically supplied remote properties +// - Operating system environment variables +// - Command-line arguments +// +// Sources are merged in a defined order so that later sources override +// properties from earlier ones. This enables flexible deployment patterns: +// defaults and packaged files supply baseline values, while environment +// variables and CLI options can easily override them in containerized or +// cloud-native environments. +// +// The package also supports profile-specific configuration files (e.g., +// app-dev.yaml) and allows adding extra directories or files at runtime. package gs_conf import ( - "fmt" + "errors" "os" + "path/filepath" "strings" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/conf" ) // osStat only for test. var osStat = os.Stat -// SysConf is the builtin configuration. +// SysConf is the global built-in configuration instance +// which usually holds the framework’s own default properties. +// It is loaded before any environment, file or command-line overrides. var SysConf = conf.New() -// PropertyCopier defines the interface for copying properties. +// PropertyCopier defines the interface for any configuration source +// that can copy its key-value pairs into a target conf.MutableProperties. type PropertyCopier interface { CopyTo(out *conf.MutableProperties) error } -// NamedPropertyCopier defines the interface for copying properties with a name. +// NamedPropertyCopier is a wrapper around PropertyCopier that also +// carries a human-readable Name. The Name is used for logging, +// debugging or error reporting when merging multiple sources. type NamedPropertyCopier struct { PropertyCopier Name string @@ -82,11 +83,16 @@ func (c *NamedPropertyCopier) CopyTo(out *conf.MutableProperties) error { /******************************** SysConfig **********************************/ +// SysConfig represents the init-level configuration layer +// composed of environment variables and command-line arguments. type SysConfig struct { Environment *Environment // Environment variables as configuration source. - CommandArgs *CommandArgs // Command line arguments as configuration source. + CommandArgs *CommandArgs // Command-line arguments as configuration source. } +// Refresh collects properties from the system configuration sources +// (built-in SysConf, environment variables, and command-line arguments) +// and merges them into a single immutable conf.Properties. func (c *SysConfig) Refresh() (conf.Properties, error) { return merge( NewNamedPropertyCopier("sys", SysConf), @@ -97,13 +103,23 @@ func (c *SysConfig) Refresh() (conf.Properties, error) { /******************************** AppConfig **********************************/ -// AppConfig represents a layered application configuration. +// AppConfig represents a layered configuration for the application runtime. +// The layers, in their merge order, typically include: +// +// 1. System defaults (SysConf) +// 2. Local configuration files +// 3. Remote configuration files +// 4. Dynamically supplied remote properties +// 5. Environment variables +// 6. Command-line arguments +// +// Layers appearing later in the list override earlier ones when keys conflict. type AppConfig struct { LocalFile *PropertySources // Configuration sources from local files. RemoteFile *PropertySources // Configuration sources from remote files. - RemoteProp conf.Properties // Remote properties. + RemoteProp conf.Properties // Properties fetched from a remote server. Environment *Environment // Environment variables as configuration source. - CommandArgs *CommandArgs // Command line arguments as configuration source. + CommandArgs *CommandArgs // Command-line arguments as configuration source. } // NewAppConfig creates a new instance of AppConfig. @@ -116,12 +132,16 @@ func NewAppConfig() *AppConfig { } } -func merge(sources ...PropertyCopier) (conf.Properties, error) { +// merge combines multiple NamedPropertyCopier instances into a single +// conf.Properties. The sources are applied in order; properties from +// later sources override earlier ones. If any source fails to copy, +// the merge aborts and returns an error indicating the failing source. +func merge(sources ...*NamedPropertyCopier) (conf.Properties, error) { out := conf.New() for _, s := range sources { if s != nil { if err := s.CopyTo(out); err != nil { - return nil, err + return nil, util.WrapError(err, "merge error in source %s", s.Name) } } } @@ -132,37 +152,38 @@ func merge(sources ...PropertyCopier) (conf.Properties, error) { func (c *AppConfig) Refresh() (conf.Properties, error) { p, err := new(SysConfig).Refresh() if err != nil { - return nil, err + return nil, util.WrapError(err, "refresh error in source sys") } localFiles, err := c.LocalFile.loadFiles(p) if err != nil { - return nil, err + return nil, util.WrapError(err, "refresh error in source local") } remoteFiles, err := c.RemoteFile.loadFiles(p) if err != nil { - return nil, err + return nil, util.WrapError(err, "refresh error in source remote") } - var sources []PropertyCopier + var sources []*NamedPropertyCopier sources = append(sources, NewNamedPropertyCopier("sys", SysConf)) sources = append(sources, localFiles...) sources = append(sources, remoteFiles...) sources = append(sources, NewNamedPropertyCopier("remote", c.RemoteProp)) sources = append(sources, NewNamedPropertyCopier("env", c.Environment)) sources = append(sources, NewNamedPropertyCopier("cmd", c.CommandArgs)) - return merge(sources...) } /******************************** BootConfig *********************************/ -// BootConfig represents a layered boot configuration. +// BootConfig represents a layered configuration used during application boot. +// It typically includes only system, local file, environment and command-line +// sources — no remote sources. type BootConfig struct { LocalFile *PropertySources // Configuration sources from local files. Environment *Environment // Environment variables as configuration source. - CommandArgs *CommandArgs // Command line arguments as configuration source. + CommandArgs *CommandArgs // Command-line arguments as configuration source. } // NewBootConfig creates a new instance of BootConfig. @@ -178,20 +199,19 @@ func NewBootConfig() *BootConfig { func (c *BootConfig) Refresh() (conf.Properties, error) { p, err := new(SysConfig).Refresh() if err != nil { - return nil, err + return nil, util.WrapError(err, "refresh error in source sys") } localFiles, err := c.LocalFile.loadFiles(p) if err != nil { - return nil, err + return nil, util.WrapError(err, "refresh error in source local") } - var sources []PropertyCopier + var sources []*NamedPropertyCopier sources = append(sources, NewNamedPropertyCopier("sys", SysConf)) sources = append(sources, localFiles...) sources = append(sources, NewNamedPropertyCopier("env", c.Environment)) sources = append(sources, NewNamedPropertyCopier("cmd", c.CommandArgs)) - return merge(sources...) } @@ -205,12 +225,15 @@ const ( ConfigTypeRemote ConfigType = "remote" ) -// PropertySources is a collection of configuration files. +// PropertySources represents a collection of configuration files +// associated with a particular configuration type and logical name. +// It supports both default directories and additional user-supplied +// directories or files. type PropertySources struct { configType ConfigType // Type of the configuration (local or remote). - configName string // Name of the configuration. - extraDirs []string // Extra directories to be included in the configuration. - extraFiles []string // Extra files to be included in the configuration. + configName string // Base name of the configuration files. + extraDirs []string // Extra directories to search for configuration files. + extraFiles []string // Extra individual files to include. } // NewPropertySources creates a new instance of PropertySources. @@ -221,13 +244,15 @@ func NewPropertySources(configType ConfigType, configName string) *PropertySourc } } -// Reset resets all the extra files. +// Reset clears all previously added extra directories and files. func (p *PropertySources) Reset() { p.extraFiles = nil p.extraDirs = nil } -// AddDir adds a or more than one extra directories. +// AddDir registers one or more additional directories to search for +// configuration files. Non-existent directories are silently ignored, +// but if the path exists and is not a directory, it panics. func (p *PropertySources) AddDir(dirs ...string) { for _, d := range dirs { info, err := osStat(d) @@ -238,13 +263,15 @@ func (p *PropertySources) AddDir(dirs ...string) { continue } if !info.IsDir() { - panic("should be a directory") + panic(util.FormatError(nil, "should be a directory %s", d)) } } p.extraDirs = append(p.extraDirs, dirs...) } -// AddFile adds a or more than one extra files. +// AddFile registers one or more additional configuration files. +// Non-existent files are silently ignored, but if the path exists +// and is a directory, it panics. func (p *PropertySources) AddFile(files ...string) { for _, f := range files { info, err := osStat(f) @@ -255,30 +282,34 @@ func (p *PropertySources) AddFile(files ...string) { continue } if info.IsDir() { - panic("should be a file") + panic(util.FormatError(nil, "should be a file %s", f)) } } p.extraFiles = append(p.extraFiles, files...) } -// getDefaultDir returns the default configuration directory based on the configuration type. -func (p *PropertySources) getDefaultDir(resolver conf.Properties) (configDir string, err error) { +// getDefaultDir determines the default configuration directory +// according to the configuration type and current resolved properties. +func (p *PropertySources) getDefaultDir(resolver conf.Properties) (string, error) { switch p.configType { case ConfigTypeLocal: return resolver.Resolve("${spring.app.config-local.dir:=./conf}") case ConfigTypeRemote: return resolver.Resolve("${spring.app.config-remote.dir:=./conf/remote}") default: - return "", fmt.Errorf("unknown config type: %s", p.configType) + return "", util.FormatError(nil, "unknown config type: %s", p.configType) } } -// getFiles returns the list of configuration files based on the configuration directory and active profiles. -func (p *PropertySources) getFiles(dir string, resolver conf.Properties) (files []string, err error) { +// getFiles generates the list of configuration file paths to try, +// including both the base config name and profile-specific variants. +// For example, with profile "dev", it will try "app-dev.yaml" etc. +func (p *PropertySources) getFiles(dir string, resolver conf.Properties) ([]string, error) { extensions := []string{".properties", ".yaml", ".yml", ".toml", ".tml", ".json"} + var files []string for _, ext := range extensions { - files = append(files, fmt.Sprintf("%s/%s%s", dir, p.configName, ext)) + files = append(files, filepath.Join(dir, p.configName+ext)) } activeProfiles, err := resolver.Resolve("${spring.profiles.active:=}") @@ -290,7 +321,7 @@ func (p *PropertySources) getFiles(dir string, resolver conf.Properties) (files for s := range strings.SplitSeq(activeProfiles, ",") { if s = strings.TrimSpace(s); s != "" { for _, ext := range extensions { - files = append(files, fmt.Sprintf("%s/%s-%s%s", dir, p.configName, s, ext)) + files = append(files, filepath.Join(dir, p.configName+"-"+s+ext)) } } } @@ -298,9 +329,10 @@ func (p *PropertySources) getFiles(dir string, resolver conf.Properties) (files return files, nil } -// loadFiles loads all configuration files and returns them as a list of Properties. -func (p *PropertySources) loadFiles(resolver conf.Properties) ([]PropertyCopier, error) { - +// loadFiles loads all candidate configuration files in order and wraps +// successfully loaded ones as NamedPropertyCopier. Non-existent files +// are skipped silently, while other loading errors abort the process. +func (p *PropertySources) loadFiles(resolver conf.Properties) ([]*NamedPropertyCopier, error) { defaultDir, err := p.getDefaultDir(resolver) if err != nil { return nil, err @@ -309,8 +341,7 @@ func (p *PropertySources) loadFiles(resolver conf.Properties) ([]PropertyCopier, var files []string for _, dir := range dirs { - var temp []string - temp, err = p.getFiles(dir, resolver) + temp, err := p.getFiles(dir, resolver) if err != nil { return nil, err } @@ -318,7 +349,7 @@ func (p *PropertySources) loadFiles(resolver conf.Properties) ([]PropertyCopier, } files = append(files, p.extraFiles...) - var ret []PropertyCopier + var ret []*NamedPropertyCopier for _, s := range files { filename, err := resolver.Resolve(s) if err != nil { @@ -326,7 +357,7 @@ func (p *PropertySources) loadFiles(resolver conf.Properties) ([]PropertyCopier, } c, err := conf.Load(filename) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { continue } return nil, err diff --git a/gs/internal/gs_conf/conf_test.go b/gs/internal/gs_conf/conf_test.go index 6837b60b..14ef0097 100644 --- a/gs/internal/gs_conf/conf_test.go +++ b/gs/internal/gs_conf/conf_test.go @@ -21,7 +21,7 @@ import ( "os" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) @@ -34,18 +34,18 @@ func clean() { func TestAppConfig(t *testing.T) { clean() - t.Run("resolve error - 1", func(t *testing.T) { + t.Run("local dir resolve error", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "${a}") _, err := NewAppConfig().Refresh() - assert.ThatError(t, err).Matches(`resolve string "\${a}" error << property a not exist`) + assert.Error(t, err).Matches(`resolve string "\${a}" error: property \"a\" not exist`) }) - t.Run("resolve error - 2", func(t *testing.T) { + t.Run("remote dir resolve error", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-REMOTE_DIR", "${a}") _, err := NewAppConfig().Refresh() - assert.ThatError(t, err).Matches(`resolve string "\${a}" error << property a not exist`) + assert.Error(t, err).Matches(`resolve string "\${a}" error: property \"a\" not exist`) }) t.Run("success", func(t *testing.T) { @@ -62,32 +62,41 @@ func TestAppConfig(t *testing.T) { }) }) - t.Run("merge error - 1", func(t *testing.T) { + t.Run("merge error - env", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_A", "a") _ = os.Setenv("GS_A_B", "a.b") _, err := NewAppConfig().Refresh() - assert.ThatError(t, err).Matches("property conflict at path a.b") + assert.Error(t, err).Matches("property conflict at path a.b") }) - t.Run("merge error - 2", func(t *testing.T) { + t.Run("merge error - sys", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf") 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") + assert.Error(t, err).Matches("property conflict at path http.server.addr") + }) + + t.Run("load from sys conf", func(t *testing.T) { + t.Cleanup(clean) + fileID := SysConf.AddFile("test") + _ = SysConf.Set("spring.app.name", "sysconf-test", fileID) + p, err := NewAppConfig().Refresh() + assert.That(t, err).Nil() + assert.That(t, p.Get("spring.app.name")).Equal("sysconf-test") }) } func TestBootConfig(t *testing.T) { clean() - t.Run("resolve error", func(t *testing.T) { + t.Run("local dir resolve error", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "${a}") _, err := NewBootConfig().Refresh() - assert.ThatError(t, err).Matches(`resolve string "\${a}" error << property a not exist`) + assert.Error(t, err).Matches(`resolve string "\${a}" error: property \"a\" not exist`) }) t.Run("success", func(t *testing.T) { @@ -102,35 +111,44 @@ func TestBootConfig(t *testing.T) { }) }) - t.Run("merge error - 1", func(t *testing.T) { + t.Run("merge error - env", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_A", "a") _ = os.Setenv("GS_A_B", "a.b") _, err := NewBootConfig().Refresh() - assert.ThatError(t, err).Matches("property conflict at path a.b") + assert.Error(t, err).Matches("property conflict at path a.b") }) - t.Run("merge error - 2", func(t *testing.T) { + t.Run("merge error - sys", func(t *testing.T) { t.Cleanup(clean) _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf") 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") + assert.Error(t, err).Matches("property conflict at path http.server.addr") + }) + + t.Run("boot config with profiles", func(t *testing.T) { + t.Cleanup(clean) + _ = os.Setenv("GS_SPRING_PROFILES_ACTIVE", "dev") + _ = os.Setenv("GS_SPRING_APP_CONFIG-LOCAL_DIR", "./testdata/conf") + p, err := NewBootConfig().Refresh() + assert.That(t, err).Nil() + assert.That(t, p.Get("spring.app.name")).Equal("test") }) } func TestPropertySources(t *testing.T) { clean() - t.Run("add dir error - 1", func(t *testing.T) { + t.Run("non existent directory", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddDir("non_existent_dir") assert.That(t, 1).Equal(len(ps.extraDirs)) }) - t.Run("add dir error - 2", func(t *testing.T) { + t.Run("dir is file", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") assert.Panic(t, func() { @@ -138,7 +156,7 @@ func TestPropertySources(t *testing.T) { }, "should be a directory") }) - t.Run("add dir error - 3", func(t *testing.T) { + t.Run("dir access denied", func(t *testing.T) { t.Cleanup(clean) defer func() { osStat = os.Stat }() osStat = func(name string) (os.FileInfo, error) { @@ -150,14 +168,14 @@ func TestPropertySources(t *testing.T) { }, "permission denied") }) - t.Run("add file error - 1", func(t *testing.T) { + t.Run("non existent file", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddFile("non_existent_file") assert.That(t, 1).Equal(len(ps.extraFiles)) }) - t.Run("add file error - 2", func(t *testing.T) { + t.Run("file is directory", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") assert.Panic(t, func() { @@ -165,7 +183,7 @@ func TestPropertySources(t *testing.T) { }, "should be a file") }) - t.Run("add file error - 3", func(t *testing.T) { + t.Run("file access denied", func(t *testing.T) { t.Cleanup(clean) defer func() { osStat = os.Stat }() osStat = func(name string) (os.FileInfo, error) { @@ -177,7 +195,7 @@ func TestPropertySources(t *testing.T) { }, "permission denied") }) - t.Run("reset", func(t *testing.T) { + t.Run("reset extra dirs and files", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddFile("./testdata/conf/app.properties") @@ -189,7 +207,7 @@ func TestPropertySources(t *testing.T) { assert.That(t, 0).Equal(len(ps.extraDirs)) }) - t.Run("getDefaultDir - 1", func(t *testing.T) { + t.Run("get default local config directory", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") dir, err := ps.getDefaultDir(conf.Map(nil)) @@ -197,7 +215,7 @@ func TestPropertySources(t *testing.T) { assert.That(t, "./conf").Equal(dir) }) - t.Run("getDefaultDir - 2", func(t *testing.T) { + t.Run("get default remote config directory", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeRemote, "app") dir, err := ps.getDefaultDir(conf.Map(nil)) @@ -205,22 +223,22 @@ func TestPropertySources(t *testing.T) { assert.That(t, "./conf/remote").Equal(dir) }) - t.Run("getFiles - 1", func(t *testing.T) { + t.Run("get config files without profiles", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") files, err := ps.getFiles("./conf", conf.Map(nil)) assert.That(t, err).Nil() assert.That(t, files).Equal([]string{ - "./conf/app.properties", - "./conf/app.yaml", - "./conf/app.yml", - "./conf/app.toml", - "./conf/app.tml", - "./conf/app.json", + "conf/app.properties", + "conf/app.yaml", + "conf/app.yml", + "conf/app.toml", + "conf/app.tml", + "conf/app.json", }) }) - t.Run("getFiles - 2", func(t *testing.T) { + t.Run("get config files with profiles", func(t *testing.T) { t.Cleanup(clean) p := conf.Map(map[string]any{ "spring.profiles.active": "dev,test", @@ -229,28 +247,28 @@ func TestPropertySources(t *testing.T) { files, err := ps.getFiles("./conf", p) assert.That(t, err).Nil() assert.That(t, files).Equal([]string{ - "./conf/app.properties", - "./conf/app.yaml", - "./conf/app.yml", - "./conf/app.toml", - "./conf/app.tml", - "./conf/app.json", - "./conf/app-dev.properties", - "./conf/app-dev.yaml", - "./conf/app-dev.yml", - "./conf/app-dev.toml", - "./conf/app-dev.tml", - "./conf/app-dev.json", - "./conf/app-test.properties", - "./conf/app-test.yaml", - "./conf/app-test.yml", - "./conf/app-test.toml", - "./conf/app-test.tml", - "./conf/app-test.json", + "conf/app.properties", + "conf/app.yaml", + "conf/app.yml", + "conf/app.toml", + "conf/app.tml", + "conf/app.json", + "conf/app-dev.properties", + "conf/app-dev.yaml", + "conf/app-dev.yml", + "conf/app-dev.toml", + "conf/app-dev.tml", + "conf/app-dev.json", + "conf/app-test.properties", + "conf/app-test.yaml", + "conf/app-test.yml", + "conf/app-test.toml", + "conf/app-test.tml", + "conf/app-test.json", }) }) - t.Run("loadFiles", func(t *testing.T) { + t.Run("load files from property sources", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddFile("./testdata/conf/app.properties") @@ -259,36 +277,67 @@ func TestPropertySources(t *testing.T) { assert.That(t, 1).Equal(len(files)) }) - t.Run("loadFiles - getDefaultDir error", func(t *testing.T) { + t.Run("unknown config type", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources("invalid", "app") _, err := ps.loadFiles(conf.Map(nil)) - assert.ThatError(t, err).Matches("unknown config type: invalid") + assert.Error(t, err).Matches("unknown config type: invalid") }) - t.Run("loadFiles - getFiles error", func(t *testing.T) { + t.Run("profile resolve error", func(t *testing.T) { t.Cleanup(clean) p := conf.Map(map[string]any{ "spring.profiles.active": "${a}", }) ps := NewPropertySources(ConfigTypeLocal, "app") _, err := ps.loadFiles(p) - assert.ThatError(t, err).Matches(`resolve string "\${a}" error << property a not exist`) + assert.Error(t, err).Matches(`resolve string "\${a}" error: property \"a\" not exist`) }) - t.Run("loadFiles - resolve error", func(t *testing.T) { + t.Run("file path resolve error", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddFile("./testdata/conf/app-${a}.properties") _, err := ps.loadFiles(conf.Map(nil)) - assert.ThatError(t, err).Matches("property a not exist") + assert.Error(t, err).Matches("property \"a\" not exist") }) - t.Run("loadFiles - confLoad error", func(t *testing.T) { + t.Run("config file load error", func(t *testing.T) { t.Cleanup(clean) ps := NewPropertySources(ConfigTypeLocal, "app") ps.AddFile("./testdata/conf/error.json") _, err := ps.loadFiles(conf.Map(nil)) - assert.ThatError(t, err).Matches("cannot unmarshal .*") + assert.Error(t, err).Matches("cannot unmarshal .*") + }) + + t.Run("load files with non-existent dir", func(t *testing.T) { + t.Cleanup(clean) + ps := NewPropertySources(ConfigTypeLocal, "app") + ps.AddDir("non_existent_dir") + files, err := ps.loadFiles(conf.Map(nil)) + assert.That(t, err).Nil() + assert.That(t, 0).Equal(len(files)) + }) + + t.Run("get default dir with active profile", func(t *testing.T) { + t.Cleanup(clean) + ps := NewPropertySources(ConfigTypeLocal, "app") + p := conf.Map(map[string]any{ + "spring.profiles.active": "test", + }) + dir, err := ps.getDefaultDir(p) + assert.That(t, err).Nil() + assert.That(t, "./conf").Equal(dir) + }) + + t.Run("add multiple directories and files", func(t *testing.T) { + t.Cleanup(clean) + ps := NewPropertySources(ConfigTypeLocal, "app") + ps.AddDir("./testdata/conf") + ps.AddDir("./testdata/conf/remote") + ps.AddFile("./testdata/conf/app.properties") + ps.AddFile("./testdata/conf/remote/app.properties") + assert.That(t, 2).Equal(len(ps.extraDirs)) + assert.That(t, 2).Equal(len(ps.extraFiles)) }) } diff --git a/gs/internal/gs_conf/env.go b/gs/internal/gs_conf/env.go index b1200d51..2c75efbc 100644 --- a/gs/internal/gs_conf/env.go +++ b/gs/internal/gs_conf/env.go @@ -20,6 +20,7 @@ import ( "os" "strings" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/conf" ) @@ -31,34 +32,41 @@ func NewEnvironment() *Environment { return &Environment{} } -// CopyTo add environment variables that matches IncludeEnvPatterns and -// exclude environment variables that matches ExcludeEnvPatterns. +// CopyTo adds environment variables. +// Variables with the prefix "GS_" are transformed: +// - Prefix "GS_" is removed. +// - Remaining underscores '_' are replaced by dots '.'. +// - Keys are converted to lowercase. +// +// All other variables are stored as-is. func (c *Environment) CopyTo(p *conf.MutableProperties) error { environ := os.Environ() if len(environ) == 0 { return nil } + const prefix = "GS_" fileID := p.AddFile("Environment") + for _, env := range environ { ss := strings.SplitN(env, "=", 2) + if len(ss[0]) == 0 { + continue // Skip malformed env vars like "=::=:" + } + k, v := ss[0], "" if len(ss) > 1 { v = ss[1] } - if k == "" { // e.g., =::=:: - continue - } - var propKey string - if strings.HasPrefix(k, prefix) { - propKey = strings.TrimPrefix(k, prefix) - propKey = strings.ReplaceAll(propKey, "_", ".") + + propKey := k + if s, ok := strings.CutPrefix(k, prefix); ok { + propKey = strings.ReplaceAll(s, "_", ".") propKey = strings.ToLower(propKey) - } else { - propKey = k } + if err := p.Set(propKey, v, fileID); err != nil { - return err + return util.FormatError(err, "set env %s error", env) } } return nil diff --git a/gs/internal/gs_conf/env_test.go b/gs/internal/gs_conf/env_test.go index 3ac65f25..c1f756a4 100644 --- a/gs/internal/gs_conf/env_test.go +++ b/gs/internal/gs_conf/env_test.go @@ -20,7 +20,7 @@ import ( "os" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) @@ -48,7 +48,7 @@ func TestEnvironment(t *testing.T) { assert.That(t, props.Get("API_KEY")).Equal("key123") }) - t.Run("set return error", func(t *testing.T) { + t.Run("property conflict", func(t *testing.T) { _ = os.Setenv("GS_DB_HOST", "db1") defer func() { _ = os.Unsetenv("GS_DB_HOST") @@ -57,6 +57,18 @@ func TestEnvironment(t *testing.T) { "db": []string{"db2"}, }) err := NewEnvironment().CopyTo(props) - assert.ThatError(t, err).Matches("property conflict at path db.host") + assert.Error(t, err).Matches("property conflict at path db.host") + }) + + t.Run("nested property conflict", func(t *testing.T) { + _ = os.Setenv("GS_DB_HOST", "db1") + defer func() { + _ = os.Unsetenv("GS_DB_HOST") + }() + props := conf.Map(map[string]any{ + "db": "123", + }) + err := NewEnvironment().CopyTo(props) + assert.Error(t, err).Matches("property conflict at path db.host") }) } diff --git a/gs/internal/gs_core/core.go b/gs/internal/gs_core/core.go index 3dee7bcd..d8e47998 100755 --- a/gs/internal/gs_core/core.go +++ b/gs/internal/gs_core/core.go @@ -25,27 +25,37 @@ import ( "github.com/go-spring/spring-core/gs/internal/gs_core/resolving" ) +// Container represents the core IoC container of the Go-Spring framework. +// It orchestrates two major phases: +// 1. Resolving: Registers, filters, and activates bean definitions. +// 2. Injecting: Performs dependency injection and final wiring of active beans. type Container struct { *resolving.Resolving *injecting.Injecting } -// New creates a IoC container. +// New creates and returns a new IoC container instance. func New() *Container { return &Container{ Resolving: resolving.New(), } } -// Refresh initializes and wires all beans in the container. +// Refresh performs the full lifecycle initialization of the container. func (c *Container) Refresh(p conf.Properties) error { + + // Step 1: Resolve and prepare all bean definitions. if err := c.Resolving.Refresh(p); err != nil { return err } + + // Step 2: Run the injecting phase and perform dependency wiring. c.Injecting = injecting.New(p) if err := c.Injecting.Refresh(c.Roots(), c.Beans()); err != nil { return err } + + // Clear the resolving phase reference to free resources. c.Resolving = nil return nil } diff --git a/gs/internal/gs_core/core_test.go b/gs/internal/gs_core/core_test.go index 9f0ac17f..d2adabc7 100644 --- a/gs/internal/gs_core/core_test.go +++ b/gs/internal/gs_core/core_test.go @@ -21,9 +21,10 @@ import ( "net/http" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "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_cond" ) @@ -31,7 +32,7 @@ func TestContainer(t *testing.T) { t.Run("success", func(t *testing.T) { c := New() - c.Object(&http.Server{}) + c.Root(c.Object(&http.Server{})) err := c.Refresh(conf.New()) assert.That(t, err).Nil() c.Close() @@ -39,19 +40,51 @@ func TestContainer(t *testing.T) { t.Run("resolve error", func(t *testing.T) { c := New() - c.Object(&http.Server{}).Condition( + c.Root(c.Object(&http.Server{}).Condition( gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { return false, errors.New("condition error") }), - ) + )) err := c.Refresh(conf.New()) - assert.ThatError(t, err).Matches("condition error") + assert.Error(t, err).Matches("condition error") }) t.Run("inject error", func(t *testing.T) { c := New() - c.RootBean(c.Provide(func(addr string) *http.Server { return nil })) + c.Root(c.Provide(func(addr string) *http.Server { return nil })) err := c.Refresh(conf.New()) - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("property \"\" not exist") + }) + + t.Run("duplicate object registration", func(t *testing.T) { + c := New() + c.Root(c.Object(&http.Server{})) + c.Root(c.Object(&http.Server{})) + err := c.Refresh(conf.New()) + assert.Error(t, err).Matches("found duplicate beans") + }) + + t.Run("provide with dependency", func(t *testing.T) { + c := New() + + c.Root(c.Provide(func(addr string) *http.Server { + return &http.Server{Addr: addr} + }, gs_arg.Tag("${server.address:=:9090}"))) + + err := c.Refresh(conf.Map(map[string]any{ + "server.address": ":8080", + })) + assert.That(t, err).Nil() + }) + + t.Run("provide with missing dependency", func(t *testing.T) { + c := New() + + c.Root(c.Provide(func(addr string) *http.Server { + return &http.Server{Addr: addr} + }, gs_arg.Tag("${server.address}"))) + + err := c.Refresh(conf.New()) + assert.Error(t, err).Matches("property \"server.address\" not exist") }) } diff --git a/gs/internal/gs_core/injecting/injecting.go b/gs/internal/gs_core/injecting/injecting.go index 669c0355..b61b341d 100644 --- a/gs/internal/gs_core/injecting/injecting.go +++ b/gs/internal/gs_core/injecting/injecting.go @@ -20,7 +20,6 @@ import ( "bytes" "container/list" "context" - "errors" "fmt" "reflect" "slices" @@ -29,25 +28,25 @@ import ( "testing" "github.com/go-spring/log" + "github.com/go-spring/spring-base/util" "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_dync" "github.com/go-spring/spring-core/gs/internal/gs_util" - "github.com/go-spring/spring-core/util" "github.com/spf13/cast" ) -// BeanRuntime defines an interface for runtime bean information. +// BeanRuntime defines an interface that provides runtime metadata. type BeanRuntime interface { Name() string // The name of the bean - Type() reflect.Type // The type of the bean - Value() reflect.Value // The value of the bean - Interface() any // The Underlying value - Callable() *gs_arg.Callable // Constructor, if any - Status() gs_bean.BeanStatus // Lifecycle status - String() string // String representation + Type() reflect.Type // The reflect.Type of the bean + Value() reflect.Value // The reflect.Value of the bean + Interface() any // The underlying Go interface of the bean + Callable() *gs_arg.Callable // Optional constructor or factory metadata + Status() gs_bean.BeanStatus // Lifecycle status of the bean + String() string // A readable string representation } // refreshState represents the state of a refresh operation. @@ -59,7 +58,8 @@ const ( Refreshed // Successfully refreshed ) -// Injecting defines a bean injection container. +// Injecting is the IoC component that handles dependency injection and +// lifecycle management for beans once they have been resolved. type Injecting struct { p *gs_dync.Properties // Dynamic properties provider beansByName map[string][]BeanRuntime // Beans indexed by name @@ -74,12 +74,19 @@ func New(p conf.Properties) *Injecting { } } -// RefreshProperties refreshes the properties of the container. +// RefreshProperties updates the dynamic property source for the container. func (c *Injecting) RefreshProperties(p conf.Properties) error { return c.p.Refresh(p) } -// Refresh loads and wires all provided bean definitions. +// Refresh wires all provided beans and prepares them for use. +// +// It performs the following steps: +// 1. Builds indexes for bean lookup by name and type. +// 2. Wires root beans (entry points of the dependency graph). +// 3. Handles lazy wiring for circular dependencies if allowed. +// 4. Captures all registered destroyer callbacks for proper shutdown order. +// 5. Optionally cleans up metadata if running outside testing. 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")) @@ -90,16 +97,18 @@ func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) for _, b := range beans { c.beansByName[b.Name()] = append(c.beansByName[b.Name()], b) c.beansByType[b.Type()] = append(c.beansByType[b.Type()], b) - for _, t := range b.Exports() { + for _, t := range b.Exports() { // Register additional exported types c.beansByType[t] = append(c.beansByType[t], b) } } stack := NewStack() defer func() { + // If an error occurred, or there are unresolved beans in the stack, + // enrich the error message with the dependency path for easier debugging. if err != nil || len(stack.beans) > 0 { - err = fmt.Errorf("%s ↩\n%s", err, stack.Path()) - log.Errorf(context.Background(), log.TagAppDef, "%v", err) + err = util.FormatError(nil, "%s ↩\n%s", err, stack.Path()) + log.Errorf(context.Background(), log.TagAppDef, "%s", err) } }() @@ -111,7 +120,7 @@ func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) forceAutowireIsNullable: forceAutowireIsNullable, } - // Injects all beans + // Step 1: Wire all root beans. r.state = Refreshing for _, b := range roots { if err = r.wireBean(b, stack); err != nil { @@ -120,21 +129,22 @@ func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) } r.state = Refreshed - // Handle lazy fields if circular references are allowed + // Step 2: Handle lazy fields caused by circular dependencies. if allowCircularReferences { for _, f := range stack.lazyFields { tag := strings.TrimSuffix(f.tag, ",lazy") if err = r.autowire(f.v, tag, stack); err != nil { - return fmt.Errorf("%q wired error: %s", f.path, err.Error()) + return err } } } else if len(stack.lazyFields) > 0 { - return errors.New("found circular autowire") + return util.FormatError(nil, "found circular autowire") } - // Collect destroyer functions sorted by dependencies + // Step 3: Collect destroyer callbacks in dependency-safe order. c.destroyers = stack.getSortedDestroyers() + // Optional cleanup in non-testing environments. forceClean := cast.ToBool(c.p.Data().Get("spring.force-clean")) if !testing.Testing() || forceClean { if c.p.ObjectsCount() == 0 { @@ -145,7 +155,7 @@ func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) return nil } - // Retain beansByName and beansByType for further Wire calls + // In testing mode, retain bean indexes to allow further Wire() calls. c.beansByName = make(map[string][]BeanRuntime) c.beansByType = make(map[reflect.Type][]BeanRuntime) for _, b := range beans { @@ -158,7 +168,7 @@ func (c *Injecting) Refresh(roots, beans []*gs_bean.BeanDefinition) (err error) return nil } -// Wire injects dependencies into the given object. +// Wire injects dependencies into an externally provided object. func (c *Injecting) Wire(obj any) error { r := &Injector{ state: Refreshed, @@ -167,19 +177,32 @@ func (c *Injecting) Wire(obj any) error { beansByType: c.beansByType, forceAutowireIsNullable: true, } + stack := NewStack() t := reflect.TypeOf(obj) v := reflect.ValueOf(obj) - return r.wireBeanValue(v, t, NewStack()) + if err := r.wireBeanValue(v, t, stack); err != nil { + return err + } + for _, f := range stack.lazyFields { + tag := strings.TrimSuffix(f.tag, ",lazy") + if err := r.autowire(f.v, tag, stack); err != nil { + return err + } + } + return nil } -// Close closes the container and cleans up resources. +// Close shuts down the container by invoking all registered destroyer +// callbacks in reverse registration order, ensuring dependent resources +// are released safely. func (c *Injecting) Close() { for _, f := range slices.Backward(c.destroyers) { f() } } -// Injector handles the core autowiring and bean creation logic. +// Injector is the component that executes core autowiring and +// bean lifecycle management (constructor, field, and method injection). type Injector struct { state refreshState // Current wiring state p *gs_dync.Properties // Property resolver @@ -188,7 +211,7 @@ type Injector struct { forceAutowireIsNullable bool // Treat missing references as nullable } -// findBeans finds beans based on a given selector. +// findBeans retrieves all beans that match a given selector. func (c *Injector) findBeans(s gs.BeanSelector) []BeanRuntime { t, name := s.TypeAndName() var beans []BeanRuntime @@ -207,13 +230,14 @@ func (c *Injector) findBeans(s gs.BeanSelector) []BeanRuntime { return beans } -// WireTag represents a parsed injection tag in the format TypeName:BeanName?. +// WireTag represents the parsed structure of an injection tag +// in the form "BeanName?" where "?" indicates that the dependency is optional. type WireTag struct { - beanName string // Bean name for injection. - nullable bool // Whether the injection can be nil. + beanName string // The target bean's name + nullable bool // Whether the injection can be nil } -// String converts a wireTag back to its string representation. +// String converts a WireTag back to its string representation. func (tag WireTag) String() string { var sb strings.Builder sb.WriteString(tag.beanName) @@ -223,7 +247,7 @@ func (tag WireTag) String() string { return sb.String() } -// parseWireTag parses a wire tag string and returns a wireTag struct. +// parseWireTag parses a raw wire tag string into a structured WireTag. func parseWireTag(str string) (tag WireTag) { if str != "" { if n := len(str) - 1; str[n] == '?' { @@ -236,7 +260,7 @@ func parseWireTag(str string) (tag WireTag) { return } -// toWireString converts a slice of wireTags to a comma-separated string. +// toWireString converts a slice of WireTags into a comma-separated string. func toWireString(tags []WireTag) string { var buf bytes.Buffer for i, tag := range tags { @@ -248,13 +272,13 @@ func toWireString(tags []WireTag) string { return buf.String() } -// getBean retrieves a single bean matching type t and WireTag criteria. -// Returns error if not found or multiple matches. +// getBean locates a single bean that matches the given type and WireTag. +// If the container is still in the Refreshing state, the matched bean will +// be wired before it is returned. func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRuntime, error) { - - // Check if the type of `v` is a valid bean receiver type. + // Ensure the target type is valid for injection. if !util.IsBeanInjectionTarget(t) { - return nil, fmt.Errorf("%s is not a valid receiver type", t.String()) + return nil, util.FormatError(nil, "%s is not a valid receiver type", t.String()) } var foundBeans []BeanRuntime @@ -264,7 +288,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti } } - // When a specific bean name is provided, find it by name. + // Special handling for interface types with explicit bean names. if t.Kind() == reflect.Interface && tag.beanName != "" { for _, b := range c.beansByName[tag.beanName] { if !b.Type().AssignableTo(t) { @@ -272,7 +296,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti } if !slices.Contains(foundBeans, b) { foundBeans = append(foundBeans, b) - log.Warnf(context.Background(), log.TagAppDef, "you should call Export() on %s", b) + log.Warnf(context.Background(), log.TagAppDef, "call Export() on %s", b) } } } @@ -281,7 +305,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti if tag.nullable { return nil, nil } - return nil, fmt.Errorf("can't find bean, bean:%q type:%q", tag, t) + return nil, util.FormatError(nil, "can't find bean, bean:%q type:%q", tag, t) } if len(foundBeans) > 1 { @@ -290,7 +314,7 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti msg += "( " + b.String() + " ), " } msg = msg[:len(msg)-2] + "]" - return nil, errors.New(msg) + return nil, util.FormatError(nil, "%s", msg) } b := foundBeans[0] @@ -302,13 +326,13 @@ func (c *Injector) getBean(t reflect.Type, tag WireTag, stack *Stack) (BeanRunti return b, nil } -// getBeans retrieves a collection (slice or map) of beans matching element type and WireTags. -// Supports ordering with '*' wildcard and optional elements. +// getBeans retrieves a slice or map of beans that match the required element type and optional WireTags. +// It supports filtering and ordering via tags, including the "*" wildcard to include unordered beans. func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack *Stack) ([]BeanRuntime, error) { et := t.Elem() if !util.IsBeanInjectionTarget(et) { - return nil, fmt.Errorf("%s is not a valid receiver type", t.String()) + return nil, util.FormatError(nil, "%s is not a valid receiver type", t.String()) } beans := c.beansByType[et] @@ -316,43 +340,49 @@ func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack // Process bean tags to filter and order beans if len(tags) > 0 { var ( - anyBeans []int - afterAny []int - beforeAny []int + anyBeans []int // indices of beans to be placed in the '*' section + afterAny []int // beans to appear after the '*' + beforeAny []int // beans to appear before the '*' ) foundAny := false for _, item := range tags { - // 是否遇到了"无序"标记 + // If we see the "*" wildcard, record its presence if item.beanName == "*" { if foundAny { - return nil, fmt.Errorf("more than one * in collection %q", tags) + return nil, util.FormatError(nil, "more than one * in collection %q", tags) } foundAny = true continue } + // Find beans with the specified name var founds []int for i, b := range beans { if item.beanName == b.Name() { founds = append(founds, i) } } + + // Error if there are multiple beans with the same name if len(founds) > 1 { msg := fmt.Sprintf("found %d beans, bean:%q type:%q [", len(founds), item, t) for _, i := range founds { msg += "( " + beans[i].String() + " ), " } msg = msg[:len(msg)-2] + "]" - return nil, errors.New(msg) + return nil, util.FormatError(nil, "%s", msg) } + + // Error if no matching bean is found (unless the tag is nullable) if len(founds) == 0 { if item.nullable { continue } - return nil, fmt.Errorf("can't find bean, bean:%q type:%q", item, t) + return nil, util.FormatError(nil, "can't find bean, bean:%q type:%q", item, t) } + // Classify beans as before or after the '*' if foundAny { afterAny = append(afterAny, founds[0]) } else { @@ -360,6 +390,7 @@ func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack } } + // For the '*' wildcard, include all other beans that were not explicitly listed if foundAny { temp := append(beforeAny, afterAny...) for i := range len(beans) { @@ -370,6 +401,7 @@ func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack } } + // Assemble beans in the correct order: beforeAny -> anyBeans -> afterAny n := len(beforeAny) + len(anyBeans) + len(afterAny) arr := make([]BeanRuntime, 0, n) for _, i := range beforeAny { @@ -385,15 +417,15 @@ func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack beans = arr } - // Handle empty beans + // Handle the case where no beans were found if len(beans) == 0 { if nullable { return nil, nil } - return nil, fmt.Errorf("no beans collected for %q", toWireString(tags)) + return nil, util.FormatError(nil, "no beans collected for %q", toWireString(tags)) } - // Wire the beans based on the current state of the container + // If the container is in the refreshing state, wire the beans before returning them if c.state == Refreshing { for _, b := range beans { if err := c.wireBean(b.(*gs_bean.BeanDefinition), stack); err != nil { @@ -404,17 +436,22 @@ func (c *Injector) getBeans(t reflect.Type, tags []WireTag, nullable bool, stack return beans, nil } -// autowire injects dependencies into a field or collection based on its kind and tag. +// autowire injects dependencies into a single field or a collection (slice/map) based on its kind and tag. func (c *Injector) autowire(v reflect.Value, str string, stack *Stack) error { + // Resolve placeholder expressions (e.g., ${...}) from configuration str, err := c.p.Data().Resolve(str) if err != nil { return err } + switch v.Kind() { case reflect.Map, reflect.Slice, reflect.Array: { + // Handle collection types var nullable bool var tags []WireTag + + // Parse the tag string to determine nullability and tag list if str != "" { nullable = true if str != "?" { @@ -427,19 +464,25 @@ func (c *Injector) autowire(v reflect.Value, str string, stack *Stack) error { } } } + + // If forced nullable mode is enabled, override all tags if c.forceAutowireIsNullable { for i := range len(tags) { tags[i].nullable = true } nullable = true } + + // Retrieve the beans matching the tag and type beans, err := c.getBeans(v.Type(), tags, nullable, stack) if err != nil { return err } - // Populate the slice or map with the resolved beans + + // Populate the collection field with the resolved beans switch v.Kind() { case reflect.Slice: + // Sort beans by name for deterministic order sort.Slice(beans, func(i, j int) bool { return beans[i].Name() < beans[j].Name() }) @@ -454,11 +497,12 @@ func (c *Injector) autowire(v reflect.Value, str string, stack *Stack) error { ret.SetMapIndex(reflect.ValueOf(b.Name()), b.Value()) } v.Set(ret) - default: + default: // for linter } return nil } default: + // Handle single bean injection g := parseWireTag(str) if c.forceAutowireIsNullable { g.nullable = true @@ -474,19 +518,20 @@ func (c *Injector) autowire(v reflect.Value, str string, stack *Stack) error { } } -// wireBean ensures a BeanDefinition is created, its dependencies wired, and init/destroy managed. +// wireBean ensures that the specified BeanDefinition is fully constructed, +// injected, initialized, and registered for destruction. func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { haveDestroy := false - // Ensure destroy functions are cleaned up in case of failure. + // Ensure that the destroyer is popped from the stack defer func() { if haveDestroy { stack.popDestroyer() } }() - // Record the destroy function for the bean, if it exists. + // If the bean has a destroy callback, record it for later execution if b.Destroy() != nil { haveDestroy = true stack.pushDestroyer(b) @@ -494,10 +539,10 @@ func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { stack.pushBean(b) - // Detect circular dependency. + // Detect circular dependencies if b.Status() == gs_bean.StatusCreating && b.Callable() != nil { if slices.Contains(stack.beans, b) { - return errors.New("found circular autowire") + return util.FormatError(nil, "found circular autowire") } } @@ -507,10 +552,10 @@ func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { return nil } - // Mark the bean as being created. + // Mark the bean as currently being created b.SetStatus(gs_bean.StatusCreating) - // Inject dependencies for the current bean. + // Wire all dependent beans before creating the current bean for _, s := range b.DependsOn() { beans := c.findBeans(s) for _, d := range beans { @@ -521,7 +566,7 @@ func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { } } - // Get the value of the current bean. + // Retrieve the actual value for the bean (e.g., via its factory method) v, err := c.getBeanValue(b, stack) if err != nil { return err @@ -529,16 +574,15 @@ func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { b.SetStatus(gs_bean.StatusCreated) - // Check if the bean has a value and wire it if it does. + // If the bean is valid and not mocked, inject its internal dependencies if v.IsValid() && !b.Mocked() { - // Wire the value of the bean. - err = c.wireBeanValue(v, v.Type(), stack) - if err != nil { + // Perform field-level wiring on the bean value + if err = c.wireBeanValue(v, v.Type(), stack); err != nil { return err } - // Execute the bean's initialization function, if it exists. + // Invoke the bean's initialization method if defined if b.Init() != nil { fnValue := reflect.ValueOf(b.Init()) out := fnValue.Call([]reflect.Value{b.Value()}) @@ -548,21 +592,21 @@ func (c *Injector) wireBean(b *gs_bean.BeanDefinition, stack *Stack) error { } } - // Mark the bean as wired and pop it from the stack. + // Mark the bean as fully wired and remove it from the stack b.SetStatus(gs_bean.StatusWired) stack.popBean() return nil } -// getBeanValue invokes the factory method for a bean definition, handling return values and errors. +// getBeanValue invokes the constructor (if present) of a bean and handles return values and errors. func (c *Injector) getBeanValue(b BeanRuntime, stack *Stack) (reflect.Value, error) { - // If the bean has no callable function, return its value directly. + // If there is no constructor, return the pre-existing value if b.Callable() == nil { return b.Value(), nil } - // Call the bean's constructor and handle errors. + // Invoke the constructor out, err := b.Callable().Call(NewArgContext(c, stack)) if err != nil { if c.forceAutowireIsNullable { @@ -572,20 +616,20 @@ func (c *Injector) getBeanValue(b BeanRuntime, stack *Stack) (reflect.Value, err return reflect.Value{}, err } - // Check last return value for error + // Check if the last return value is an error if o := out[len(out)-1]; util.IsErrorType(o.Type()) { - if i := o.Interface(); i != nil { + if err, ok := o.Interface().(error); ok && err != nil { if c.forceAutowireIsNullable { log.Warnf(context.Background(), log.TagAppDef, "autowire error: %v", err) return reflect.Value{}, nil } - return reflect.Value{}, i.(error) + return reflect.Value{}, err } } - // If the return value is of bean type, handle it accordingly. + // Assign the returned value to the bean if val := out[0]; util.IsBeanType(val.Type()) { - // If it's a non-pointer value type, convert it into a pointer and set it. + // Convert interface values to pointers if necessary if !val.IsNil() && val.Kind() == reflect.Interface && util.IsPropBindingTarget(val.Elem().Type()) { v := reflect.New(val.Elem().Type()) v.Elem().Set(val.Elem()) @@ -597,36 +641,36 @@ func (c *Injector) getBeanValue(b BeanRuntime, stack *Stack) (reflect.Value, err b.Value().Elem().Set(val) } - // Return an error if the value is nil. + // Ensure the value is not nil if b.Value().IsNil() { - return reflect.Value{}, fmt.Errorf("%s return nil", b.String()) + return reflect.Value{}, util.FormatError(nil, "%s return nil", b.String()) } + // If the value is an interface, unwrap it v := b.Value() - // If the result is an interface, extract the original value. if v.Kind() == reflect.Interface { v = v.Elem() } return v, nil } -// wireBeanValue injects struct fields for a given reflect.Value and type. +// wireBeanValue injects dependencies into a bean's struct fields. func (c *Injector) wireBeanValue(v reflect.Value, t reflect.Type, stack *Stack) error { - // Dereference pointer types and adjust the target type. + // Dereference pointers to obtain the underlying struct if v.Kind() == reflect.Ptr { v = v.Elem() t = t.Elem() } - // If v is not a struct type, no injection is needed. + // If it's not a struct, nothing to wire if v.Kind() != reflect.Struct { return nil } + // Use the type name for binding paths typeName := t.Name() if typeName == "" { - // Simple types don't have names, use their string representation. typeName = t.String() } @@ -634,33 +678,32 @@ func (c *Injector) wireBeanValue(v reflect.Value, t reflect.Type, stack *Stack) return c.wireStruct(v, t, param, stack) } -// wireStruct inspects struct fields and applies autowire or configuration binding based on tags. +// wireStruct inspects struct fields and performs autowiring or configuration binding as needed. func (c *Injector) wireStruct(v reflect.Value, t reflect.Type, opt conf.BindParam, stack *Stack) error { - // Loop through each field of the struct. for i := range t.NumField() { ft := t.Field(i) fv := v.Field(i) - // If the field is unexported, try to patch it. + // Patch unexported fields so they can be set via reflection if !fv.CanInterface() { fv = util.PatchValue(fv) } fieldPath := opt.Path + "." + ft.Name - // Check for autowire or inject tags. + // Look for "autowire" or "inject" tags tag, ok := ft.Tag.Lookup("autowire") if !ok { tag, ok = ft.Tag.Lookup("inject") } if ok { - // Handle lazy injection. + // Handle lazy-injected fields if strings.HasSuffix(tag, ",lazy") { f := LazyField{v: fv, path: fieldPath, tag: tag} stack.lazyFields = append(stack.lazyFields, f) } else { if err := c.autowire(fv, tag, stack); err != nil { - return fmt.Errorf("%q wired error: %w", fieldPath, err) + return util.FormatError(err, "%q wired error", fieldPath) } } continue @@ -671,28 +714,26 @@ func (c *Injector) wireStruct(v reflect.Value, t reflect.Type, opt conf.BindPara Path: fieldPath, } - // Bind values if the field has a "value" tag. + // If the field has a "value" tag, bind configuration to it if tag, ok = ft.Tag.Lookup("value"); ok { if err := subParam.BindTag(tag, ft.Tag); err != nil { return err } if ft.Anonymous { - // Recursively wire anonymous structs. - err := c.wireStruct(fv, ft.Type, subParam, stack) - if err != nil { + // Recursively process embedded structs + if err := c.wireStruct(fv, ft.Type, subParam, stack); err != nil { return err } } else { - // Refresh field value if needed. - err := c.p.RefreshField(fv.Addr(), subParam) - if err != nil { + // Refresh the field value from configuration + if err := c.p.RefreshField(fv.Addr(), subParam); err != nil { return err } } continue } - // Recursively wire anonymous struct fields. + // Recursively process anonymous struct fields if ft.Anonymous && ft.Type.Kind() == reflect.Struct { if err := c.wireStruct(fv, ft.Type, subParam, stack); err != nil { return err @@ -702,18 +743,20 @@ func (c *Injector) wireStruct(v reflect.Value, t reflect.Type, opt conf.BindPara return nil } -// destroyer represents a bean's cleanup function and its dependencies on other beans. +// destroyer represents a bean's cleanup (destroy) function +// and the dependencies that must be destroyed before it. type destroyer struct { current *gs_bean.BeanDefinition // Bean that provides this destroyer depends []*gs_bean.BeanDefinition // Beans that must be destroyed before current } -// isDependOn reports whether this destroyer depends on bean b. +// isDependOn reports whether this destroyer depends on the given bean. func (d *destroyer) isDependOn(b *gs_bean.BeanDefinition) bool { return slices.Contains(d.depends, b) } -// dependOn adds b to the list of dependencies if not already present. +// dependOn adds the given bean to this destroyer's dependency list +// if it is not already present. func (d *destroyer) dependOn(b *gs_bean.BeanDefinition) { if d.isDependOn(b) { return @@ -721,22 +764,24 @@ func (d *destroyer) dependOn(b *gs_bean.BeanDefinition) { d.depends = append(d.depends, b) } -// LazyField represents a lazy-injected field with metadata. +// LazyField represents a field in a struct that should be injected lazily. type LazyField struct { - v reflect.Value // The value to be injected. - path string // Path for the field in the injection hierarchy. - tag string // Associated tag for the field. + v reflect.Value // The field value that will be injected later + path string // Hierarchical path of the field + tag string // Original tag (e.g. "autowire") for this field } -// Stack tracks bean wiring context: current beans, lazy fields, and destroyers. +// Stack represents the runtime context during bean wiring. +// It keeps track of the current wiring call stack, lazily injected fields, +// and the ordering of destroyers for proper shutdown. type Stack struct { - beans []*gs_bean.BeanDefinition // Current wiring call stack + beans []*gs_bean.BeanDefinition // The stack of beans currently being wired lazyFields []LazyField // Fields deferred due to lazy injection - destroyers *list.List // Linked list of destroyer structs - destroyerMap map[gs.BeanID]*destroyer // Lookup map for destroyer entries + destroyers *list.List // Ordered list of destroyers + destroyerMap map[gs.BeanID]*destroyer // Fast lookup map for destroyers by bean ID } -// NewStack initializes a fresh wiring stack for a new Refresh or Wire call. +// NewStack creates and initializes a new Stack for a fresh Refresh or Wire operation. func NewStack() *Stack { return &Stack{ destroyers: list.New(), @@ -744,22 +789,24 @@ func NewStack() *Stack { } } -// pushBean records that bean b is being wired, used for cycle detection. +// pushBean pushes a bean onto the wiring stack. +// Used to keep track of current wiring path for cycle detection. func (s *Stack) pushBean(b *gs_bean.BeanDefinition) { log.Debugf(context.Background(), log.TagAppDef, "push %s %s", b, b.Status()) s.beans = append(s.beans, b) } -// popBean removes the most recently pushed bean from the wiring stack. +// popBean pops the most recently added bean from the wiring stack. func (s *Stack) popBean() { n := len(s.beans) b := s.beans[n-1] - s.beans[n-1] = nil + s.beans[n-1] = nil // avoid memory leak s.beans = s.beans[:n-1] log.Debugf(context.Background(), log.TagAppDef, "pop %s %s", b, b.Status()) } -// Path builds a readable representation of the wiring stack path for errors. +// Path returns a formatted string representation of the current wiring stack, +// which is useful for debugging and error messages. func (s *Stack) Path() (path string) { if len(s.beans) == 0 { return "" @@ -767,29 +814,37 @@ func (s *Stack) Path() (path string) { for _, b := range s.beans { path += fmt.Sprintf("=> %s ↩\n", b) } - return path[:len(path)-1] // Remove the trailing newline. + return path[:len(path)-1] // Trim the trailing newline } -// pushDestroyer registers a destroyer callback for bean b, tracking dependencies. +// pushDestroyer registers a destroyer for the given bean. +// It also records dependencies so that beans are destroyed in the correct order. func (s *Stack) pushDestroyer(b *gs_bean.BeanDefinition) { beanID := gs.BeanID{Name: b.Name(), Type: b.Type()} + + // Get or create the destroyer entry for this bean d, ok := s.destroyerMap[beanID] if !ok { d = &destroyer{current: b} s.destroyerMap[beanID] = d } + + // If there is a previously registered destroyer, current depends on it if i := s.destroyers.Back(); i != nil { d.dependOn(i.Value.(*gs_bean.BeanDefinition)) } + + // Add the current bean to the end of the destroyer list s.destroyers.PushBack(b) } -// popDestroyer removes the last destroyer from the ordering list. +// popDestroyer removes the last registered destroyer from the ordering list. func (s *Stack) popDestroyer() { s.destroyers.Remove(s.destroyers.Back()) } -// getBeforeDestroyers filters the destroyer list to those that current depends on. +// getBeforeDestroyers returns a list of destroyers that the given destroyer depends on. +// This helper is used during topological sorting of destroyers. func getBeforeDestroyers(destroyers *list.List, i any) *list.List { d := i.(*destroyer) result := list.New() @@ -802,9 +857,11 @@ func getBeforeDestroyers(destroyers *list.List, i any) *list.List { return result } -// getSortedDestroyers returns callbacks sorted respecting bean dependencies. +// getSortedDestroyers computes and returns a slice of destroyer functions +// in the correct order, respecting declared dependencies between beans. func (s *Stack) getSortedDestroyers() []func() { + // Helper to wrap a bean's destroy method as a no-argument function destroy := func(v reflect.Value, fn any) func() { return func() { fnValue := reflect.ValueOf(fn) @@ -815,13 +872,17 @@ func (s *Stack) getSortedDestroyers() []func() { } } + // Copy all destroyers into a new list for sorting destroyers := list.New() for _, d := range s.destroyerMap { destroyers.PushBack(d) } - // the injection process should first discover cyclic dependencies + + // Perform a topological sort to respect dependencies + // (e.g. a bean must be destroyed after the beans it depends on) destroyers, _ = gs_util.TripleSort(destroyers, getBeforeDestroyers) + // Convert the sorted destroyers into a slice of executable cleanup functions var ret []func() for e := destroyers.Front(); e != nil; e = e.Next() { d := e.Value.(*destroyer).current @@ -830,8 +891,9 @@ func (s *Stack) getSortedDestroyers() []func() { return ret } -// ArgContext provides injection context for factory callables, -// exposing property access and bean lookup. +// ArgContext provides runtime context when calling bean factory functions. +// It exposes access to configuration properties, bean lookups, condition checks, +// and allows wiring of parameters during construction. type ArgContext struct { c *Injector stack *Stack @@ -852,7 +914,7 @@ func (a *ArgContext) Prop(key string, def ...string) string { return a.c.p.Data().Get(key, def...) } -// Find returns beans satisfying a selector, as conditional beans. +// Find retrieves beans matching the given selector. func (a *ArgContext) Find(s gs.BeanSelector) ([]gs.ConditionBean, error) { beans := a.c.findBeans(s) var ret []gs.ConditionBean @@ -862,17 +924,19 @@ func (a *ArgContext) Find(s gs.BeanSelector) ([]gs.ConditionBean, error) { return ret, nil } -// Check evaluates whether a condition matches in the current context. +// Check evaluates a condition against the current ArgContext. func (a *ArgContext) Check(c gs.Condition) (bool, error) { return c.Matches(a) } -// Bind binds a configuration property to a [reflect.Value] based on struct tag. +// Bind binds configuration data into the provided reflect.Value +// based on the given struct tag. func (a *ArgContext) Bind(v reflect.Value, tag string) error { return a.c.p.Data().Bind(v, tag) } -// Wire performs autowiring on a [reflect.Value], using the given tag. +// Wire performs dependency injection on the given reflect.Value +// using the specified tag, leveraging the current wiring stack. func (a *ArgContext) Wire(v reflect.Value, tag string) error { return a.c.autowire(v, tag, a.stack) } diff --git a/gs/internal/gs_core/injecting/injecting_test.go b/gs/internal/gs_core/injecting/injecting_test.go index 8bdcfc89..20c33f34 100644 --- a/gs/internal/gs_core/injecting/injecting_test.go +++ b/gs/internal/gs_core/injecting/injecting_test.go @@ -26,7 +26,7 @@ import ( "testing" "time" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "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" @@ -217,7 +217,7 @@ type LazyB struct { func TestInjecting(t *testing.T) { - t.Run("lazy error - 1", func(t *testing.T) { + t.Run("lazy error - missing bean", func(t *testing.T) { r := New(conf.Map(map[string]any{ "spring": map[string]any{ "allow-circular-references": true, @@ -228,17 +228,36 @@ func TestInjecting(t *testing.T) { objectBean(&LazyB{}), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("can't find bean") + assert.Error(t, err).Matches("can't find bean") }) - t.Run("lazy error - 2", func(t *testing.T) { + t.Run("lazy error - circular reference", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(&LazyA{}), objectBean(&LazyB{}), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("found circular autowire") + assert.Error(t, err).Matches("found circular autowire") + }) + + t.Run("lazy success", func(t *testing.T) { + r := New(conf.Map(map[string]any{ + "spring": map[string]any{ + "allow-circular-references": true, + }, + })) + beans := []*gs.BeanDefinition{ + objectBean(&LazyA{}), + objectBean(&LazyB{}).Name("b"), + } + err := r.Refresh(extractBeans(beans)) + assert.That(t, err).Nil() + + var a LazyA + err = r.Wire(&a) + assert.That(t, err).Nil() + assert.That(t, a.LazyB).NotNil() }) t.Run("success", func(t *testing.T) { @@ -369,7 +388,7 @@ func TestInjecting(t *testing.T) { assert.That(t, s.Service.Status).Equal(0) }) - t.Run("wire error - 2", func(t *testing.T) { + t.Run("wire error - primitive type", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -377,10 +396,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("int is not a valid receiver type") + assert.Error(t, err).Matches("int is not a valid receiver type") }) - t.Run("wire error - 3", func(t *testing.T) { + t.Run("wire error - ambiguous bean for single value", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -390,10 +409,10 @@ func TestInjecting(t *testing.T) { objectBean(&SimpleLogger{}).Name("b").Export(gs.As[Logger]()), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("found 2 beans") + assert.Error(t, err).Matches("found 2 beans") }) - t.Run("wire error - 4", func(t *testing.T) { + t.Run("wire error - slice", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -401,10 +420,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("\\[]int is not a valid receiver type") + assert.Error(t, err).Matches("\\[]int is not a valid receiver type") }) - t.Run("wire error - 5", func(t *testing.T) { + t.Run("wire error - invalid collection pattern", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -412,10 +431,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("more than one \\* in collection") + assert.Error(t, err).Matches("more than one \\* in collection") }) - t.Run("wire error - 6", func(t *testing.T) { + t.Run("wire error - ambiguous bean for collection", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -425,10 +444,10 @@ func TestInjecting(t *testing.T) { objectBean(&SimpleLogger{}).Name("biz").Export(gs.As[Logger]()), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("found 2 beans") + assert.Error(t, err).Matches("found 2 beans") }) - t.Run("wire error - 7", func(t *testing.T) { + t.Run("wire error - no matching beans", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -436,10 +455,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("no beans collected") + assert.Error(t, err).Matches("no beans collected") }) - t.Run("wire error - 8", func(t *testing.T) { + t.Run("wire error - bean not found", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -447,10 +466,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("can't find bean") + assert.Error(t, err).Matches("can't find bean") }) - t.Run("wire error - 9", func(t *testing.T) { + t.Run("wire error - init failure", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -461,10 +480,10 @@ func TestInjecting(t *testing.T) { }).Export(gs.As[Logger]()).Name("sys"), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("init error") + assert.Error(t, err).Matches("init error") }) - t.Run("wire error - 10", func(t *testing.T) { + t.Run("wire error - invalid tag", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -472,10 +491,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("resolve string .* error: invalid syntax") + assert.Error(t, err).Matches("resolve string .* error: invalid syntax") }) - t.Run("wire error - 11", func(t *testing.T) { + t.Run("wire error - invalid collection tag", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -483,10 +502,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("resolve string .* error: invalid syntax") + assert.Error(t, err).Matches("resolve string .* error: invalid syntax") }) - t.Run("wire error - 12", func(t *testing.T) { + t.Run("wire error - collection with no matches", func(t *testing.T) { r := New(conf.New()) s := new(struct { Loggers [3]Logger `autowire:"*?"` @@ -500,7 +519,7 @@ func TestInjecting(t *testing.T) { assert.That(t, s.Loggers).Equal([3]Logger{nil, nil, nil}) }) - t.Run("wire error - 13", func(t *testing.T) { + t.Run("wire error - missing property", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(&SimpleLogger{}).DependsOn( @@ -509,10 +528,10 @@ func TestInjecting(t *testing.T) { provideBean(NewZeroLogger), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("parse tag '' error: invalid syntax") + assert.Error(t, err).Matches("property \"\" not exist") }) - t.Run("wire error - 14", func(t *testing.T) { + t.Run("wire error - missing required dependencies", func(t *testing.T) { r := New(conf.Map(map[string]any{ "spring": map[string]any{ "force-autowire-is-nullable": true, @@ -530,7 +549,7 @@ func TestInjecting(t *testing.T) { assert.That(t, s.Logger).Equal((*ZeroLogger)(nil)) }) - t.Run("wire error - 15", func(t *testing.T) { + t.Run("wire error - provider returning error - 1", func(t *testing.T) { r := New(conf.New()) s := struct { Logger *ZeroLogger `inject:""` @@ -542,10 +561,10 @@ func TestInjecting(t *testing.T) { }), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("init error") + assert.Error(t, err).Matches("init error") }) - t.Run("wire error - 16", func(t *testing.T) { + t.Run("wire error - provider returning error - 2", func(t *testing.T) { r := New(conf.Map(map[string]any{ "spring": map[string]any{ "force-autowire-is-nullable": true, @@ -565,7 +584,7 @@ func TestInjecting(t *testing.T) { assert.That(t, s.Logger).Equal((*ZeroLogger)(nil)) }) - t.Run("wire error - 17", func(t *testing.T) { + t.Run("wire error - provider returning error - 3", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ provideBean(func() (*ZeroLogger, error) { @@ -573,10 +592,10 @@ func TestInjecting(t *testing.T) { }), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("name=.* return nil") + assert.Error(t, err).Matches("name=.* return nil") }) - t.Run("wire error - 22", func(t *testing.T) { + t.Run("wire error - primitive type", func(t *testing.T) { r := New(conf.New()) err := r.Refresh(extractBeans(nil)) assert.That(t, err).Nil() @@ -584,7 +603,7 @@ func TestInjecting(t *testing.T) { assert.That(t, err).Nil() }) - t.Run("wire error - 23", func(t *testing.T) { + t.Run("wire error - malformed tag", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -592,10 +611,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("parse tag .* error: invalid syntax") + assert.Error(t, err).Matches("parse tag .* error: invalid syntax") }) - t.Run("wire error - 24", func(t *testing.T) { + t.Run("wire error - struct - missing properties", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -603,10 +622,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("property config.int not exist") + assert.Error(t, err).Matches("property \"config.int\" not exist") }) - t.Run("wire error - 25", func(t *testing.T) { + t.Run("wire error - struct - missing prefixed properties", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(new(struct { @@ -614,10 +633,10 @@ func TestInjecting(t *testing.T) { })), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("property svr.config.int not exist") + assert.Error(t, err).Matches("property \"svr.config.int\" not exist") }) - t.Run("wire error - 26", func(t *testing.T) { + t.Run("wire error - destruction failure", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(&SimpleLogger{}).Destroy(func(l *SimpleLogger) error { @@ -628,11 +647,24 @@ func TestInjecting(t *testing.T) { assert.That(t, err).Nil() r.Close() }) + + t.Run("map injection", func(t *testing.T) { + r := New(conf.New()) + beans := []*gs.BeanDefinition{ + objectBean(&ZeroLogger{}).Name("logger1").Export(gs.As[CtxLogger]()), + objectBean(&ZeroLogger{}).Name("logger2").Export(gs.As[CtxLogger]()), + objectBean(new(struct { + Loggers map[string]CtxLogger `inject:"*"` + })), + } + err := r.Refresh(extractBeans(beans)) + assert.That(t, err).Nil() + }) } func TestWireTag(t *testing.T) { - t.Run("empty str", func(t *testing.T) { + t.Run("empty", func(t *testing.T) { tag := parseWireTag("") assert.That(t, tag).Equal(WireTag{}) assert.That(t, tag.String()).Equal("") @@ -656,14 +688,14 @@ func TestWireTag(t *testing.T) { assert.That(t, tag.String()).Equal("a?") }) - t.Run("tags - 1", func(t *testing.T) { + t.Run("tags - single", func(t *testing.T) { tags := []WireTag{ {"a", true}, } assert.That(t, toWireString(tags)).Equal("a?") }) - t.Run("tags - 2", func(t *testing.T) { + t.Run("tags - multiple", func(t *testing.T) { tags := []WireTag{ {"a", true}, {"b", false}, @@ -671,7 +703,7 @@ func TestWireTag(t *testing.T) { assert.That(t, toWireString(tags)).Equal("a?,b") }) - t.Run("tags - 3", func(t *testing.T) { + t.Run("tags - mixed nullable", func(t *testing.T) { tags := []WireTag{ {"a", true}, {"b", false}, @@ -741,7 +773,7 @@ func NewJ() *J { func TestCircularBean(t *testing.T) { - t.Run("not truly circular - 1", func(t *testing.T) { + t.Run("not truly circular - object", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(&A{}), @@ -762,7 +794,7 @@ func TestCircularBean(t *testing.T) { assert.That(t, s.C.A).Equal(s.A) }) - t.Run("not truly circular - 2", func(t *testing.T) { + t.Run("not truly circular - constructor", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ objectBean(&C{}), @@ -783,7 +815,7 @@ func TestCircularBean(t *testing.T) { assert.That(t, s.E.c).Equal(s.C) }) - t.Run("found circular - 1", func(t *testing.T) { + t.Run("found circular - direct", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ provideBean(NewE, gs_arg.Tag("?")), @@ -791,10 +823,10 @@ func TestCircularBean(t *testing.T) { provideBean(NewG), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("found circular autowire") + assert.Error(t, err).Matches("found circular autowire") }) - t.Run("found circular - 2", func(t *testing.T) { + t.Run("found circular - indirect", func(t *testing.T) { r := New(conf.New()) beans := []*gs.BeanDefinition{ provideBean(NewH), @@ -802,10 +834,10 @@ func TestCircularBean(t *testing.T) { provideBean(NewJ), } err := r.Refresh(extractBeans(beans)) - assert.ThatError(t, err).Matches("found circular autowire") + assert.Error(t, err).Matches("found circular autowire") }) - t.Run("found circular - 3", func(t *testing.T) { + t.Run("found circular - lazy", func(t *testing.T) { r := New(conf.Map(map[string]any{ "spring": map[string]any{ "allow-circular-references": true, @@ -928,7 +960,7 @@ type DyncValue struct { func TestForceClean(t *testing.T) { - t.Run("no dync value", func(t *testing.T) { + t.Run("without dync value", func(t *testing.T) { release := make(map[string]struct{}) r := New(conf.Map(map[string]any{ @@ -962,7 +994,7 @@ func TestForceClean(t *testing.T) { }) }) - t.Run("has dync value", func(t *testing.T) { + t.Run("with dync value", func(t *testing.T) { r := New(conf.Map(map[string]any{ "spring": map[string]any{ "force-clean": true, diff --git a/gs/internal/gs_core/resolving/resolving.go b/gs/internal/gs_core/resolving/resolving.go index 55b85d9a..6ddad2eb 100644 --- a/gs/internal/gs_core/resolving/resolving.go +++ b/gs/internal/gs_core/resolving/resolving.go @@ -17,17 +17,15 @@ package resolving import ( - "errors" - "fmt" "reflect" "regexp" "slices" + "github.com/go-spring/spring-base/util" "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_bean" "github.com/go-spring/spring-core/gs/internal/gs_cond" - "github.com/go-spring/spring-core/util" ) // RefreshState represents the current state of the container. @@ -40,19 +38,22 @@ const ( Refreshed ) -// Module represents a module registered in the container. +// Module represents a module that can register additional beans +// when certain conditions are met. type Module struct { f func(p conf.Properties) error c gs.Condition } -// Resolving manages bean definitions, mocks, and dynamic bean registration functions. +// Resolving is the core container responsible for holding bean definitions, +// processing modules, applying mocks, scanning configuration beans, and +// resolving beans against conditions. type Resolving struct { - 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 + state RefreshState // current refresh state + mocks []gs.BeanMock // registered mocks to override beans + beans []*gs_bean.BeanDefinition // all beans managed by the container + roots []*gs_bean.BeanDefinition // root beans to wire at the end + modules []Module // registered modules } // New creates an empty Resolving instance. @@ -77,24 +78,26 @@ func (c *Resolving) Beans() []*gs_bean.BeanDefinition { return beans } -// AddMock adds a mock bean to the container. +// AddMock registers a mock bean which can override an existing bean +// during the refresh phase. func (c *Resolving) AddMock(mock gs.BeanMock) { c.mocks = append(c.mocks, mock) } -// Object registers a pre-constructed instance as a bean. +// Object registers a pre-constructed instance as a bean definition. func (c *Resolving) Object(i any) *gs.RegisteredBean { b := gs_bean.NewBean(reflect.ValueOf(i)) return c.Register(b).Caller(1) } -// Provide registers a constructor function to create a bean. +// Provide registers a constructor function and optional arguments as a bean. func (c *Resolving) Provide(ctor any, args ...gs.Arg) *gs.RegisteredBean { b := gs_bean.NewBean(ctor, args...) return c.Register(b).Caller(1) } // Register adds a bean definition to the container. +// It must be called before the container starts refreshing. func (c *Resolving) Register(b *gs.BeanDefinition) *gs.RegisteredBean { if c.state >= Refreshing { panic("container is refreshing or already refreshed") @@ -104,7 +107,8 @@ func (c *Resolving) Register(b *gs.BeanDefinition) *gs.RegisteredBean { return gs.NewRegisteredBean(bd) } -// Module registers a module into the container. +// Module registers a conditional module that will be executed +// to add beans before the container starts refreshing. func (c *Resolving) Module(conditions []gs_cond.ConditionOnProperty, fn func(p conf.Properties) error) { var arr []gs.Condition for _, cond := range conditions { @@ -116,22 +120,23 @@ func (c *Resolving) Module(conditions []gs_cond.ConditionOnProperty, fn func(p c }) } -// RootBean adds a root bean to the container. -func (c *Resolving) RootBean(b *gs.RegisteredBean) { +// Root marks a registered bean as a root bean. +func (c *Resolving) Root(b *gs.RegisteredBean) { bd := b.BeanRegistration().(*gs_bean.BeanDefinition) c.roots = append(c.roots, bd) } -// Refresh performs the full initialization process of the container. -// It transitions through several phases: -// - Executes group functions to register additional beans. -// - Scans configuration beans and registers their methods as beans. -// - Applies mock beans to override specific targets. -// - Resolves all beans based on their conditions. -// - Validates that no duplicate beans exist. +// Refresh performs the full lifecycle of container initialization. +// The phases are as follows: +// 1. Apply registered modules to register additional beans. +// 2. Scan configuration beans and register methods as beans. +// 3. Apply mock beans to override specific target beans. +// 4. Resolve conditions for all beans and mark inactive ones as deleted. +// 5. Check for duplicate beans (by type and name). +// 6. Validate that all root beans are resolved and ready to wire. func (c *Resolving) Refresh(p conf.Properties) error { if c.state != RefreshDefault { - return errors.New("container is already refreshing or refreshed") + return util.FormatError(nil, "container is already refreshing or refreshed") } c.state = RefreshPrepare @@ -162,7 +167,7 @@ func (c *Resolving) Refresh(p conf.Properties) error { continue } if b.Status() != gs_bean.StatusResolved { - return fmt.Errorf("bean %q status is invalid for wiring", b) + return util.FormatError(nil, "bean %q status is invalid for wiring", b) } } @@ -170,7 +175,7 @@ func (c *Resolving) Refresh(p conf.Properties) error { return nil } -// applyModules executes registered modules to add beans. +// applyModules executes all registered modules that match their conditions. func (c *Resolving) applyModules(p conf.Properties) error { ctx := &ConditionContext{p: p, c: c} for _, m := range c.modules { @@ -182,19 +187,21 @@ func (c *Resolving) applyModules(p conf.Properties) error { } } if err := m.f(p); err != nil { - return err + return util.FormatError(err, "apply module error") } } return nil } -// scanConfigurations processes configuration beans to register their methods as beans. +// scanConfigurations iterates over all beans that represent configuration +// objects and scans their methods to register additional beans. func (c *Resolving) scanConfigurations() error { for _, b := range c.beans { if b.Configuration() == nil { continue } - // Check if the configuration bean has a mock override + + // First, check if a mock is defined for this configuration bean. var foundMocks []gs.BeanMock for _, mock := range c.mocks { t, s := mock.Target.TypeAndName() @@ -207,25 +214,25 @@ func (c *Resolving) scanConfigurations() error { foundMocks = append(foundMocks, mock) } if n := len(foundMocks); n > 1 { - return fmt.Errorf("found duplicate mock bean for '%s'", b.Name()) + return util.FormatError(nil, "found duplicate mock bean for '%s'", b.Name()) } else if n == 1 { b.SetMock(foundMocks[0].Object) continue } - // Scan methods if no mock is applied + + // If not mocked, scan configuration methods. beans, err := c.scanConfiguration(b) if err != nil { - return err + return util.FormatError(err, "scan configuration error") } c.beans = append(c.beans, beans...) } return nil } -// scanConfiguration inspects the methods of a configuration bean, and for each -// method that matches the include patterns and not the exclude patterns, -// registers it as a bean. This enables dynamic bean registration based on method -// naming conventions or regex. +// scanConfiguration inspects methods of a configuration bean and registers +// methods as beans if they match the inclusion/exclusion patterns. +// By default, include methods named like "NewXxx" func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.BeanDefinition, error) { var ( includes []*regexp.Regexp @@ -240,7 +247,7 @@ func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.Be for _, s := range ss { p, err := regexp.Compile(s) if err != nil { - return nil, err + return nil, util.FormatError(err, "invalid regexp '%s'", s) } includes = append(includes, p) } @@ -249,7 +256,7 @@ func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.Be for _, s := range ss { p, err := regexp.Compile(s) if err != nil { - return nil, err + return nil, util.FormatError(err, "invalid regexp '%s'", s) } excludes = append(excludes, p) } @@ -258,6 +265,8 @@ func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.Be n := bd.Type().NumMethod() for i := range n { m := bd.Type().Method(i) + + // Skip methods matching any exclusion pattern. skip := false for _, p := range excludes { if p.MatchString(m.Name) { @@ -268,6 +277,8 @@ func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.Be if skip { continue } + + // Register method as a bean if it matches inclusion pattern. for _, p := range includes { if !p.MatchString(m.Name) { continue @@ -285,7 +296,7 @@ func (c *Resolving) scanConfiguration(bd *gs_bean.BeanDefinition) ([]*gs_bean.Be return ret, nil } -// isBeanMatched checks if a bean matches the target type and name selector. +// isBeanMatched checks whether a bean matches the given type and name selector. func isBeanMatched(t reflect.Type, s string, b *gs_bean.BeanDefinition) bool { if s != "" && s != b.Name() { return false @@ -298,7 +309,7 @@ func isBeanMatched(t reflect.Type, s string, b *gs_bean.BeanDefinition) bool { return true } -// applyMocks overrides target beans with registered mock objects. +// applyMocks iterates over all registered mocks and applies them to matching beans. func (c *Resolving) applyMocks() error { for _, mock := range c.mocks { if err := c.applyMock(mock); err != nil { @@ -308,10 +319,9 @@ func (c *Resolving) applyMocks() error { return nil } -// applyMock applies a mock object to its target bean. It ensures that the mock -// implements all the interfaces that the original bean exported. If multiple -// matching beans are found, or if the mock doesn't implement required interfaces, -// an error is returned. +// applyMock applies a mock object to a target bean. +// It ensures the mock implements all exported interfaces of the target bean. +// If more than one target bean is found or the mock is invalid, an error is returned. func (c *Resolving) applyMock(mock gs.BeanMock) error { var foundBeans []*gs_bean.BeanDefinition vt := reflect.TypeOf(mock.Object) @@ -324,7 +334,7 @@ func (c *Resolving) applyMock(mock gs.BeanMock) error { // Verify mock implements all exported interfaces for _, et := range b.Exports() { if !vt.Implements(et) { - return fmt.Errorf("found unimplemented interface") + return util.FormatError(nil, "mock %T does not implement required interface %v", mock.Object, et) } } foundBeans = append(foundBeans, b) @@ -333,24 +343,25 @@ func (c *Resolving) applyMock(mock gs.BeanMock) error { return nil } if len(foundBeans) > 1 { - return fmt.Errorf("found duplicate mocked beans") + return util.FormatError(nil, "found duplicate mocked beans") } foundBeans[0].SetMock(mock.Object) return nil } -// resolveBeans evaluates conditions for all beans and marks inactive ones. +// resolveBeans iterates over all beans and resolves their conditions, +// marking them as resolved or deleted. func (c *Resolving) resolveBeans(p conf.Properties) error { ctx := &ConditionContext{p: p, c: c} for _, b := range c.beans { if err := ctx.resolveBean(b); err != nil { - return err + return util.FormatError(err, "resolve bean error") } } return nil } -// checkDuplicateBeans ensures no duplicate type/name combinations exist. +// checkDuplicateBeans ensures that no two beans share the same type and name. func (c *Resolving) checkDuplicateBeans() error { beansByID := make(map[gs.BeanID]*gs_bean.BeanDefinition) for _, b := range c.beans { @@ -360,7 +371,7 @@ func (c *Resolving) checkDuplicateBeans() error { for _, t := range append(b.Exports(), b.Type()) { beanID := gs.BeanID{Name: b.Name(), Type: t} if d, ok := beansByID[beanID]; ok { - return fmt.Errorf("found duplicate beans [%s] [%s]", b, d) + return util.FormatError(nil, "found duplicate beans [%s] [%s]", b, d) } beansByID[beanID] = b } @@ -368,13 +379,14 @@ func (c *Resolving) checkDuplicateBeans() error { return nil } -// ConditionContext provides condition evaluation context during resolution. +// ConditionContext provides an evaluation context for conditions +// during bean resolution. type ConditionContext struct { c *Resolving p conf.Properties } -// resolveBean evaluates a bean's conditions, updating its status accordingly. +// resolveBean evaluates a bean's conditions and updates its status accordingly. // If any condition fails, the bean is marked as deleted. func (c *ConditionContext) resolveBean(b *gs_bean.BeanDefinition) error { if b.Status() >= gs_bean.StatusResolving { @@ -398,12 +410,14 @@ func (c *ConditionContext) Has(key string) bool { return c.p.Has(key) } -// Prop retrieves a configuration property with optional default value. +// Prop retrieves a configuration property by key, +// optionally returning a default value if the key is not found. 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. +// Find returns all beans that match the provided selector +// and are successfully resolved (active). func (c *ConditionContext) Find(s gs.BeanSelector) ([]gs.ConditionBean, error) { var found []gs.ConditionBean t, name := s.TypeAndName() diff --git a/gs/internal/gs_core/resolving/resolving_test.go b/gs/internal/gs_core/resolving/resolving_test.go index ca2ac709..63c2cdc6 100644 --- a/gs/internal/gs_core/resolving/resolving_test.go +++ b/gs/internal/gs_core/resolving/resolving_test.go @@ -24,7 +24,7 @@ import ( "net/http" "testing" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "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" @@ -81,7 +81,7 @@ func (b *TestBean) Echo() {} func TestResolving(t *testing.T) { - t.Run("register error", func(t *testing.T) { + t.Run("register error when container is refreshed", func(t *testing.T) { r := New() err := r.Refresh(conf.New()) assert.That(t, err).Nil() @@ -90,7 +90,7 @@ func TestResolving(t *testing.T) { }, "container is refreshing or already refreshed") }) - t.Run("configuration error - 1", func(t *testing.T) { + t.Run("duplicate mock bean", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Configuration() r.AddMock(gs.BeanMock{ @@ -102,10 +102,10 @@ func TestResolving(t *testing.T) { Target: gs.BeanSelectorFor[*TestBean](), }) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("found duplicate mock bean for 'TestBean'") + assert.Error(t, err).Matches("found duplicate mock bean for 'TestBean'") }) - t.Run("configuration error - 2", func(t *testing.T) { + t.Run("invalid include pattern", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Configuration( gs.Configuration{ @@ -113,10 +113,10 @@ func TestResolving(t *testing.T) { }, ) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("error parsing regexp: missing argument to repetition operator: `*`") + assert.Error(t, err).Matches("error parsing regexp: missing argument to repetition operator: `*`") }) - t.Run("configuration error - 3", func(t *testing.T) { + t.Run("invalid exclude pattern", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Configuration( gs.Configuration{ @@ -124,10 +124,10 @@ func TestResolving(t *testing.T) { }, ) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("error parsing regexp: missing argument to repetition operator: `*`") + assert.Error(t, err).Matches("error parsing regexp: missing argument to repetition operator: `*`") }) - t.Run("mock error - 1", func(t *testing.T) { + t.Run("mock error with incompatible interface", func(t *testing.T) { r := New() r.Provide(NewZeroLogger, gs_arg.Value("a")). Export(gs.As[Logger](), gs.As[CtxLogger]()) @@ -136,10 +136,10 @@ func TestResolving(t *testing.T) { Target: gs.BeanSelectorFor[Logger](), }) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("found unimplemented interface") + assert.Error(t, err).String("mock *resolving.SimpleLogger does not implement required interface resolving.CtxLogger") }) - t.Run("mock error - 2", func(t *testing.T) { + t.Run("mock error with multiple target beans", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Name("TestBean-1") r.Object(&TestBean{Value: 2}).Name("TestBean-2") @@ -148,10 +148,21 @@ func TestResolving(t *testing.T) { Target: gs.BeanSelectorFor[*TestBean](), }) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("found duplicate mocked beans") + assert.Error(t, err).Matches("found duplicate mocked beans") }) - t.Run("resolve error - 1", func(t *testing.T) { + t.Run("module error", func(t *testing.T) { + r := New() + r.Module(nil, func(p conf.Properties) error { + return errors.New("module error") + }) + + err := r.Refresh(conf.New()) + assert.That(t, err).NotNil() + assert.Error(t, err).Matches("module error") + }) + + t.Run("resolve error in bean condition", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Condition( gs_cond.OnFunc(func(ctx gs.ConditionContext) (bool, error) { @@ -159,10 +170,10 @@ func TestResolving(t *testing.T) { }), ) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("condition matches error: .* << condition error") + assert.Error(t, err).Matches("resolve bean error: condition OnFunc(.*) matches error: condition error") }) - t.Run("resolve error - 2", func(t *testing.T) { + t.Run("resolve error with multiple conditions", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}).Condition( gs_cond.OnBean[*TestBean](), @@ -173,31 +184,61 @@ func TestResolving(t *testing.T) { }), ) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("condition matches error: .* << condition error") + assert.Error(t, err).Matches("resolve bean error: condition OnBean(.*) matches error") + assert.Error(t, err).Matches("condition OnFunc(.*) matches error: condition error") + }) + + t.Run("condition not match", func(t *testing.T) { + r := New() + r.Object(&TestBean{Value: 1}).Condition( + gs_cond.OnProperty("test.property").HavingValue("true"), + ) + err := r.Refresh(conf.New()) + assert.That(t, err).Nil() + assert.That(t, len(r.Beans())).Equal(0) }) - t.Run("duplicate bean - 1", func(t *testing.T) { + t.Run("duplicate bean", func(t *testing.T) { r := New() r.Object(&TestBean{Value: 1}) r.Object(&TestBean{Value: 2}) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("found duplicate beans") + assert.Error(t, err).Matches("found duplicate beans") }) - t.Run("duplicate bean - 1", func(t *testing.T) { + t.Run("duplicate bean with same name", func(t *testing.T) { r := New() r.Object(&ZeroLogger{}).Name("a").Export(gs.As[Logger]()) r.Object(&SimpleLogger{}).Name("a").Export(gs.As[Logger]()) err := r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("found duplicate beans") + assert.Error(t, err).Matches("found duplicate beans") }) - t.Run("repeat refresh", func(t *testing.T) { + t.Run("refresh container multiple times", func(t *testing.T) { r := New() err := r.Refresh(conf.New()) assert.That(t, err).Nil() err = r.Refresh(conf.New()) - assert.ThatError(t, err).Matches("container is already refreshing or refreshed") + assert.Error(t, err).Matches("container is already refreshing or refreshed") + }) + + t.Run("configuration success", func(t *testing.T) { + r := New() + r.Object(&TestBean{Value: 1}).Configuration( + gs.Configuration{ + Includes: []string{"^NewChild$"}, + }, + ).Name("TestBean") + + p := conf.Map(map[string]any{}) + err := r.Refresh(p) + assert.That(t, err).Nil() + + var names []string + for _, b := range r.Beans() { + names = append(names, b.Name()) + } + assert.That(t, len(names)).Equal(2) }) t.Run("success", func(t *testing.T) { diff --git a/gs/internal/gs_dync/dync.go b/gs/internal/gs_dync/dync.go index 40870638..26ee37ac 100644 --- a/gs/internal/gs_dync/dync.go +++ b/gs/internal/gs_dync/dync.go @@ -14,50 +14,23 @@ * limitations under the License. */ -/* -Package gs_dync provides dynamic configuration binding and refresh capabilities for Go applications. - -This package is built on thread-safe atomic.Value storage with automatic type conversion and supports change listeners, -making it suitable for components or services that need to react to configuration updates at runtime. - -Key Features: - - Type-safe and thread-safe encapsulation of configuration values - - Change notification mechanism using channels - - Hierarchical key resolution for nested structs and map keys - - Fine-grained refresh logic that only updates affected objects - - JSON serialization support for value persistence and transmission - -Examples: - -Basic value binding: - - var v Value[int] - _ = v.onRefresh(conf.Map(map[string]any{"key": 42}), conf.BindParam{Key: "key"}) - fmt.Print(v.Value()) // Output: 42 - -Binding nested structs: - - type Config struct { - Server struct { - Port Value[int] `value:"${port}"` - } `value:"${server}"` - } - - var cfg Config - _ = p.RefreshField(reflect.ValueOf(&cfg), conf.BindParam{Key: "config"}) - -Change notification: - - listener := v.NewListener() - go func() { - <-listener.C - fmt.Print("value changed!") - }() - _ = v.onRefresh(conf.Map(map[string]any{"key": 100}), conf.BindParam{Key: "key"}) - -This package is ideal for use cases that require hot-reloading of configuration, have complex config structures, -or demand reactive behavior to configuration changes. -*/ +// Package gs_dync provides dynamic configuration binding and refresh +// capabilities for Go-Spring applications. +// +// It allows application components to register themselves as refreshable +// objects that automatically update their internal state whenever the +// underlying configuration changes. +// +// Key components: +// - Properties: holds the current configuration and manages all +// registered `refreshable` objects. +// - Value[T]: a type-safe container for dynamic configuration values. +// - Listener: allows components to receive change notifications. +// - `refreshable`: interface that application components can implement +// to react to configuration updates. +// +// This package is designed to be thread-safe and suitable for hot-reload +// scenarios in long-running applications. package gs_dync import ( @@ -67,7 +40,7 @@ import ( "sync" "sync/atomic" - "github.com/go-spring/barky" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/conf" ) @@ -91,7 +64,7 @@ type listeners struct { func (r *listeners) NewListener() *Listener { r.m.Lock() defer r.m.Unlock() - l := &Listener{C: make(chan struct{})} + l := &Listener{C: make(chan struct{}, 1)} r.a = append(r.a, l) return l } @@ -209,7 +182,7 @@ func (p *Properties) Refresh(prop conf.Properties) (err error) { changes[k] = struct{}{} } - keys := barky.OrderedMapKeys(changes) + keys := util.OrderedMapKeys(changes) return p.refreshKeys(keys) } @@ -233,7 +206,7 @@ func (p *Properties) refreshKeys(keys []string) (err error) { // Sort and collect objects that need updating. updateObjects := make([]*refreshObject, 0, len(updateIndexes)) { - ints := barky.OrderedMapKeys(updateIndexes) + ints := util.OrderedMapKeys(updateIndexes) for _, k := range ints { updateObjects = append(updateObjects, updateIndexes[k]) } @@ -295,7 +268,7 @@ type filter struct { // Do attempts to refresh a single object if it implements the [refreshable] interface. func (f *filter) Do(i any, param conf.BindParam) (bool, error) { v, ok := i.(refreshable) - if !ok { + if !ok || v == nil { return false, nil } f.objects = append(f.objects, &refreshObject{ diff --git a/gs/internal/gs_dync/dync_test.go b/gs/internal/gs_dync/dync_test.go index 4e70cdc1..45a55092 100644 --- a/gs/internal/gs_dync/dync_test.go +++ b/gs/internal/gs_dync/dync_test.go @@ -19,12 +19,13 @@ package gs_dync import ( "encoding/json" "errors" + "fmt" "reflect" "sync" "testing" "time" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/conf" ) @@ -59,7 +60,7 @@ func TestValue(t *testing.T) { "value": "42", }, })) - assert.ThatError(t, err).Matches("bind path= type=int error << property key isn't simple value") + assert.Error(t, err).Matches("bind path= type=int error: property \"key\" isn't simple value") var wg sync.WaitGroup for i := range 5 { @@ -86,11 +87,161 @@ func TestValue(t *testing.T) { b, err := json.Marshal(map[string]any{"key": &v}) assert.That(t, err).Nil() - assert.ThatString(t, string(b)).JSONEqual(`{"key":59}`) + assert.String(t, string(b)).JSONEqual(`{"key":59}`) +} + +func TestValue_DifferentTypes(t *testing.T) { + + t.Run("string", func(t *testing.T) { + var v Value[string] + err := v.onRefresh( + conf.Map(map[string]any{"key": "hello"}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + assert.That(t, v.Value()).Equal("hello") + }) + + t.Run("bool", func(t *testing.T) { + var v Value[bool] + err := v.onRefresh( + conf.Map(map[string]any{"key": "true"}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + assert.That(t, v.Value()).Equal(true) + }) + + t.Run("float64", func(t *testing.T) { + var v Value[float64] + err := v.onRefresh( + conf.Map(map[string]any{"key": "3.14"}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + assert.That(t, v.Value()).Equal(3.14) + }) + + t.Run("slice", func(t *testing.T) { + var v Value[[]int] + err := v.onRefresh( + conf.Map(map[string]any{"key": []any{1, 2, 3}}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + assert.That(t, v.Value()).Equal([]int{1, 2, 3}) + }) +} + +func TestValue_ConcurrentAccess(t *testing.T) { + var v Value[int] + + err := v.onRefresh( + conf.Map(map[string]any{"key": "100"}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + assert.That(t, v.Value()).Equal(100) + + var wg sync.WaitGroup + const goroutines = 100 + + for range goroutines { + wg.Add(1) + go func() { + defer wg.Done() + val := v.Value() + assert.Number(t, val).Between(0, 100) + }() + } + + for i := range goroutines { + wg.Add(1) + go func(idx int) { + defer wg.Done() + err := v.onRefresh( + conf.Map(map[string]any{"key": fmt.Sprintf("%d", idx)}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + }(i) + } + + wg.Wait() +} + +func TestValue_Listener(t *testing.T) { + var v Value[int] + + listeners := make([]*Listener, 5) + for i := range listeners { + listeners[i] = v.NewListener() + } + + go func() { + err := v.onRefresh( + conf.Map(map[string]any{"key": "42"}), + conf.BindParam{Key: "key"}, + ) + assert.That(t, err).Nil() + }() + + var wg sync.WaitGroup + for _, l := range listeners { + wg.Add(1) + go func() { + defer wg.Done() + select { + case <-l.C: + assert.That(t, v.Value()).Equal(42) + case <-time.After(time.Second): + t.Errorf("timeout") + } + }() + } + wg.Wait() } func TestDync(t *testing.T) { + t.Run("invalid property format", func(t *testing.T) { + p := New(conf.New()) + + var v Value[int] + err := p.RefreshField( + reflect.ValueOf(&v), + conf.BindParam{Key: "${invalid..key}"}, + ) + assert.That(t, err).NotNil() + }) + + t.Run("missing required property", func(t *testing.T) { + p := New(conf.New()) + + var cfg struct { + Value Value[int] `value:"${required.property}"` + } + + err := p.RefreshField( + reflect.ValueOf(&cfg), + conf.BindParam{Key: "config"}, + ) + assert.That(t, err).NotNil() + }) + + t.Run("type mismatch error", func(t *testing.T) { + p := New(conf.Map(map[string]any{ + "config.value": "not_a_number", + })) + + var v Value[int] + err := p.RefreshField( + reflect.ValueOf(&v), + conf.BindParam{Key: "config.value"}, + ) + assert.Error(t, err).Matches("strconv.ParseInt: parsing.*invalid syntax") + }) + t.Run("refresh panic", func(t *testing.T) { p := New(conf.New()) @@ -169,8 +320,8 @@ func TestDync(t *testing.T) { "config.s3.value": "xyz", }) err = p.Refresh(prop) - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing \"xyz\": invalid syntax") - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing \"abc\": invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing \"xyz\": invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing \"abc\": invalid syntax") s1 := &Value[string]{} err = p.RefreshField(reflect.ValueOf(s1), conf.BindParam{Key: "config.s3.value"}) @@ -180,7 +331,7 @@ func TestDync(t *testing.T) { s2 := &Value[int]{} err = p.RefreshField(reflect.ValueOf(s2), conf.BindParam{Key: "config.s3.value"}) - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing \\\"xyz\\\": invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing \\\"xyz\\\": invalid syntax") assert.That(t, p.ObjectsCount()).Equal(4) }) @@ -206,7 +357,7 @@ func TestDync(t *testing.T) { err = p.Refresh(conf.Map(map[string]any{ "config.s1.value": "xyz", })) - assert.ThatError(t, err).Matches("strconv.ParseInt: parsing \"xyz\": invalid syntax") + assert.Error(t, err).Matches("strconv.ParseInt: parsing \"xyz\": invalid syntax") err = p.Refresh(conf.Map(map[string]any{ "config.s1.value": "10", @@ -214,4 +365,30 @@ func TestDync(t *testing.T) { assert.That(t, err).Nil() assert.That(t, v.Value().S1.Value).Equal(10) }) + + t.Run("with default value", func(t *testing.T) { + p := New(conf.New()) + + var cfg struct { + Value Value[int] `value:"${property:=42}"` + } + + err := p.RefreshField(reflect.ValueOf(&cfg), conf.BindParam{Key: "config"}) + assert.That(t, err).Nil() + assert.That(t, cfg.Value.Value()).Equal(42) + }) + + t.Run("override default value", func(t *testing.T) { + p := New(conf.Map(map[string]any{ + "config.property": "100", + })) + + var cfg struct { + Value Value[int] `value:"${property:=42}"` + } + + err := p.RefreshField(reflect.ValueOf(&cfg), conf.BindParam{Key: "config"}) + assert.That(t, err).Nil() + assert.That(t, cfg.Value.Value()).Equal(100) + }) } diff --git a/gs/internal/gs_util/util.go b/gs/internal/gs_util/util.go index 648e9674..c366bfac 100644 --- a/gs/internal/gs_util/util.go +++ b/gs/internal/gs_util/util.go @@ -18,7 +18,8 @@ package gs_util import ( "container/list" - "errors" + + "github.com/go-spring/spring-base/util" ) // GetBeforeItems is a function type that returns a list of items @@ -86,7 +87,7 @@ func tripleSortByAfter(sorting *list.List, toSort *list.List, sorted *list.List, // Detect circular dependencies by checking if `c` is already being processed. if searchInList(processing, c) != nil { - return errors.New("found sorting cycle") // todo: more details + return util.FormatError(nil, "found sorting cycle") // todo: more details } // Check if the dependency `c` is already sorted or still in the toSort list. diff --git a/gs/internal/gs_util/util_test.go b/gs/internal/gs_util/util_test.go index 77569f0f..ffab6e81 100644 --- a/gs/internal/gs_util/util_test.go +++ b/gs/internal/gs_util/util_test.go @@ -20,8 +20,8 @@ import ( "container/list" "testing" - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" + "github.com/go-spring/spring-base/testing/assert" + "github.com/go-spring/spring-base/util" ) func TestTripleSort(t *testing.T) { @@ -108,6 +108,6 @@ func TestTripleSort(t *testing.T) { } sorting := util.ListOf("A", "B", "C") _, err := TripleSort(sorting, getBefore) - assert.ThatError(t, err).Matches("found sorting cycle") + assert.Error(t, err).Matches("found sorting cycle") }) } diff --git a/gs/log.go b/gs/log.go index bf67c302..c8fcea0b 100644 --- a/gs/log.go +++ b/gs/log.go @@ -17,52 +17,74 @@ package gs import ( - "os" + "context" "path/filepath" "strings" "github.com/go-spring/log" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs_conf" ) -// initLog initializes the log system. +// initLog initializes the application's logging system. func initLog() error { + + // Step 1: Refresh the global system configuration. p, err := new(gs_conf.SysConfig).Refresh() if err != nil { - return err + return util.FormatError(err, "refresh error in source sys") } + + // Step 2: Load logging-related configuration parameters. var c struct { + // LocalDir is the directory that contains configuration files. + // Defaults to "./conf" if not provided. LocalDir string `value:"${spring.app.config-local.dir:=./conf}"` + + // Profiles specifies the active application profile(s), + // such as "dev", "prod", etc. + // Multiple profiles can be provided as a comma-separated list. Profiles string `value:"${spring.profiles.active:=}"` } if err = p.Bind(&c); err != nil { - return err + return util.FormatError(err, "bind error in source sys") } - var ( - logFileDefault string - logFileProfile string - ) - logFileDefault = filepath.Join(c.LocalDir, "log.xml") - if c.Profiles != "" { - profile := strings.Split(c.Profiles, ",")[0] - logFileProfile = filepath.Join(c.LocalDir, "log-"+profile+".xml") - } - var logFile string - for _, s := range []string{logFileProfile, logFileDefault} { - if _, err = os.Stat(s); err != nil { - if os.IsNotExist(err) { - continue + + extensions := []string{".properties", ".yaml", ".yml", ".xml", ".json"} + + // Step 3: Build a list of candidate configuration files. + var files []string + if profiles := strings.TrimSpace(c.Profiles); profiles != "" { + for s := range strings.SplitSeq(profiles, ",") { // NOTE: range returns index + if s = strings.TrimSpace(s); s != "" { + for _, ext := range extensions { + files = append(files, filepath.Join(c.LocalDir, "log-"+s+ext)) + } } + } + } + for _, ext := range extensions { + files = append(files, filepath.Join(c.LocalDir, "log"+ext)) + } + + // Step 4: Detect existing configuration files. + var logFiles []string + for _, s := range files { + if ok, err := util.PathExists(s); err != nil { return err + } else if ok { + logFiles = append(logFiles, s) } - logFile = s - break } - if logFile == "" { // no log file exists + + // Step 5: Apply logging configuration or fall back to defaults. + switch n := len(logFiles); { + case n == 0: + log.Infof(context.Background(), log.TagAppDef, "no log configuration file found, using default logger") return nil + case n > 1: + return util.FormatError(nil, "multiple log files found: %s", logFiles) + default: + return log.RefreshFile(logFiles[0]) } - if err = log.RefreshFile(logFile); err != nil { - return err - } - return nil } diff --git a/gs/pprof.go b/gs/pprof.go index 74726034..f029fc30 100644 --- a/gs/pprof.go +++ b/gs/pprof.go @@ -22,30 +22,35 @@ import ( ) func init() { - // Registers a SimplePProfServer object to the container. + // Registers a SimplePProfServer bean in the IoC container. Provide( NewSimplePProfServer, - TagArg("${pprof.server.addr:=0.0.0.0:9981}"), + TagArg("${pprof.server.addr:=:9981}"), ).Condition( OnEnableServers(), OnProperty(EnableSimplePProfServerProp).HavingValue("true").MatchIfMissing(), ).AsServer() } -// SimplePProfServer is a simple pprof server. +// SimplePProfServer is a simple HTTP server that exposes pprof endpoints. type SimplePProfServer struct { *SimpleHttpServer } -// NewSimplePProfServer creates a new SimplePProfServer. +// NewSimplePProfServer creates a new SimplePProfServer at the given address. +// It registers the standard pprof handlers for runtime profiling and debugging. func NewSimplePProfServer(addr string) *SimplePProfServer { mux := http.NewServeMux() + + // Register pprof handlers mux.HandleFunc("GET /debug/pprof/", pprof.Index) mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline) mux.HandleFunc("GET /debug/pprof/profile", pprof.Profile) mux.HandleFunc("GET /debug/pprof/symbol", pprof.Symbol) mux.HandleFunc("GET /debug/pprof/trace", pprof.Trace) + + cfg := SimpleHttpServerConfig{Address: addr} return &SimplePProfServer{ - SimpleHttpServer: NewSimpleHttpServer(mux, SetHttpServerAddr(addr)), + SimpleHttpServer: NewSimpleHttpServer(mux, cfg), } } diff --git a/gs/prop.go b/gs/prop.go index 6a521ede..77d46b48 100644 --- a/gs/prop.go +++ b/gs/prop.go @@ -21,46 +21,71 @@ import ( ) const ( + // AllowCircularReferencesProp controls whether the container + // allows circular dependencies between beans. AllowCircularReferencesProp = "spring.allow-circular-references" + + // ForceAutowireIsNullableProp forces autowired dependencies + // to be treated as nullable (i.e. allowed to be nil). ForceAutowireIsNullableProp = "spring.force-autowire-is-nullable" - ActiveProfilesProp = "spring.profiles.active" - EnableJobsProp = "spring.app.enable-jobs" - EnableServersProp = "spring.app.enable-servers" - EnableSimpleHttpServerProp = "spring.enable.simple-http-server" + + // ActiveProfilesProp defines the active application profiles + // (e.g. "dev", "test", "prod"). + ActiveProfilesProp = "spring.profiles.active" + + // EnableJobsProp enables or disables scheduled job execution. + EnableJobsProp = "spring.app.enable-jobs" + + // EnableServersProp enables or disables all server components. + EnableServersProp = "spring.app.enable-servers" + + // EnableSimpleHttpServerProp enables or disables the built-in + // lightweight HTTP server. + EnableSimpleHttpServerProp = "spring.enable.simple-http-server" + + // EnableSimplePProfServerProp enables or disables the built-in + // lightweight pprof server. EnableSimplePProfServerProp = "spring.enable.simple-pprof-server" ) -// AllowCircularReferences enables or disables circular references between beans. +// AllowCircularReferences sets whether circular references between beans +// are permitted during dependency injection. Default is usually false. func AllowCircularReferences(enable bool) { Property(AllowCircularReferencesProp, strconv.FormatBool(enable)) } -// ForceAutowireIsNullable forces autowire to be nullable. +// ForceAutowireIsNullable forces autowired dependencies to be treated as +// optional (nullable). This allows injection of nil when no candidate bean +// is available. Default is usually false. func ForceAutowireIsNullable(enable bool) { Property(ForceAutowireIsNullableProp, strconv.FormatBool(enable)) } -// SetActiveProfiles sets the active profiles for the app. +// SetActiveProfiles sets the active application profiles (e.g. "dev", "prod"). +// This influences which configuration files and conditional beans are loaded. func SetActiveProfiles(profiles string) { Property(ActiveProfilesProp, profiles) } -// EnableJobs enables or disables the app jobs. +// EnableJobs enables or disables the execution of scheduled jobs. func EnableJobs(enable bool) { Property(EnableJobsProp, strconv.FormatBool(enable)) } -// EnableServers enables or disables the app servers. +// EnableServers enables or disables all server components in the application +// (e.g. HTTP servers, gRPC servers). func EnableServers(enable bool) { Property(EnableServersProp, strconv.FormatBool(enable)) } -// EnableSimpleHttpServer enables or disables the simple HTTP server. +// EnableSimpleHttpServer enables or disables the built-in lightweight +// HTTP server provided by the framework. func EnableSimpleHttpServer(enable bool) { Property(EnableSimpleHttpServerProp, strconv.FormatBool(enable)) } -// EnableSimplePProfServer enables or disables the simple pprof server. +// EnableSimplePProfServer enables or disables the built-in lightweight +// pprof server for performance profiling. func EnableSimplePProfServer(enable bool) { Property(EnableSimplePProfServerProp, strconv.FormatBool(enable)) } diff --git a/gs/test.go b/gs/test.go index 1c6491b9..c5fb4591 100644 --- a/gs/test.go +++ b/gs/test.go @@ -21,23 +21,26 @@ import ( "strings" "testing" + "github.com/go-spring/spring-base/util" "github.com/go-spring/spring-core/gs/internal/gs" - "github.com/go-spring/spring-core/util" ) -// BeanMock is a mock for bean. +// BeanMock represents a mock bean for testing. type BeanMock[T any] struct { selector gs.BeanSelector } -// MockFor creates a mock for bean. +// MockFor creates a BeanMock for the given type and optional bean name. +// It allows you to specify which bean in the IoC container should be mocked. func MockFor[T any](name ...string) BeanMock[T] { return BeanMock[T]{ selector: gs.BeanSelectorFor[T](name...), } } -// With registers a mock bean. +// With registers a mock instance into the IoC container, +// replacing the original bean defined by the selector. +// This allows tests to use mocked dependencies. func (m BeanMock[T]) With(obj T) { app.C.AddMock(gs.BeanMock{ Object: obj, @@ -45,30 +48,40 @@ func (m BeanMock[T]) With(obj T) { }) } +// testers stores all registered tester instances. +// Each tester can contain multiple test methods. var testers []any -// AddTester adds a tester to the test suite. +// AddTester registers a tester instance into the test suite. +// The tester will be scanned for methods prefixed with "Test", +// which will be automatically added to the Go test framework. func AddTester(t any) { testers = append(testers, t) - app.C.RootBean(app.C.Object(t)) + app.C.Root(app.C.Object(t)) } -// TestMain is the entry point for testing. +// TestMain is the custom entry point for the Go test framework. +// It injects test methods defined in registered testers into the +// internal 'tests' slice of testing.M, then starts the app and tests. func TestMain(m *testing.M) { - // patch m.tests + // Patch m.tests using reflection (a non-standard hack). + // This allows dynamically adding test cases at runtime. mValue := util.PatchValue(reflect.ValueOf(m)) fValue := util.PatchValue(mValue.Elem().FieldByName("tests")) tests := fValue.Interface().([]testing.InternalTest) + + // Scan all registered testers for methods starting with "Test". for _, tester := range testers { tt := reflect.TypeOf(tester) typeName := tt.Elem().String() - for i := range tt.NumMethod() { + for i := 0; i < tt.NumMethod(); i++ { methodType := tt.Method(i) + // Only consider methods whose names start with "Test" if strings.HasPrefix(methodType.Name, "Test") { tests = append(tests, testing.InternalTest{ - Name: typeName + "." + methodType.Name, - F: func(t *testing.T) { + Name: typeName + "." + methodType.Name, // Full test name + F: func(t *testing.T) { // Test function to execute testMethod := reflect.ValueOf(tester).Method(i) testMethod.Call([]reflect.Value{reflect.ValueOf(t)}) }, @@ -78,15 +91,15 @@ func TestMain(m *testing.M) { } fValue.Set(reflect.ValueOf(tests)) - // run app + // Run the application asynchronously. stop, err := RunAsync() if err != nil { panic(err) } - // run test + // Run all collected tests. m.Run() - // stop app + // Stop the application gracefully after all tests complete. stop() } diff --git a/gs/test_test.go b/gs/test_test.go new file mode 100644 index 00000000..85d52375 --- /dev/null +++ b/gs/test_test.go @@ -0,0 +1,85 @@ +/* + * 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_test + +import ( + "fmt" + "testing" + + "github.com/go-spring/spring-base/testing/assert" + "github.com/go-spring/spring-core/gs" +) + +func init() { + // Register GreetingService as a regular bean in the IoC container. + gs.Object(&GreetingService{}) + + // Register GreetingTester as a tester so its test methods will be discovered + // and executed automatically by gs.TestMain. + gs.AddTester(&GreetingTester{}) +} + +func init() { + // Replace the real GreetingService bean with a mock version for testing. + gs.MockFor[*GreetingService]().With( + &GreetingService{ + Message: "Hello, World!", + }, + ) +} + +func TestMain(m *testing.M) { + gs.TestMain(m) +} + +// TestGreeting is a regular Go test function. +// It is discovered by the standard testing framework. +func TestGreeting(t *testing.T) { + fmt.Println(t.Name()) // Expected output: TestGreeting +} + +// TestFarewell is another regular Go test function. +func TestFarewell(t *testing.T) { + fmt.Println(t.Name()) // Expected output: TestFarewell +} + +type GreetingService struct { + Message string `value:"${app.greeting:=Hello, Go-Spring!}"` +} + +// GreetingTester is a tester struct used to define test methods +// that will be automatically registered and run by gs.TestMain. +// The Svc field is injected (autowired) from the IoC container. +type GreetingTester struct { + Svc *GreetingService `autowire:""` +} + +// TestGreeting is a test method defined on GreetingTester. +// It will be automatically discovered by gs.TestMain. +func (u *GreetingTester) TestGreeting(t *testing.T) { + fmt.Println(t.Name()) // Expected output: gs_test.GreetingTester.TestGreeting + assert.That(t, u.Svc).NotNil() + assert.String(t, u.Svc.Message).Equal("Hello, World!") +} + +// TestFarewell is another test method defined on GreetingTester. +// Like TestGreeting, it is automatically discovered by gs.TestMain. +func (u *GreetingTester) TestFarewell(t *testing.T) { + fmt.Println(t.Name()) // Expected output: gs_test.GreetingTester.TestFarewell + assert.That(t, u.Svc).NotNil() + assert.String(t, u.Svc.Message).Equal("Hello, World!") +} diff --git a/docs/4. examples/bookman/log/.keep b/mcp/.keep similarity index 100% rename from docs/4. examples/bookman/log/.keep rename to mcp/.keep diff --git a/sdk/mcp/.keep b/redis/.keep similarity index 100% rename from sdk/mcp/.keep rename to redis/.keep diff --git a/sdk/web/.keep b/sdk/web/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/util/error.go b/util/error.go deleted file mode 100644 index 7e56a850..00000000 --- a/util/error.go +++ /dev/null @@ -1,27 +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 ( - "errors" -) - -// ErrForbiddenMethod throws this error when calling a method is prohibited. -var ErrForbiddenMethod = errors.New("forbidden method") - -// ErrUnimplementedMethod throws this error when calling an unimplemented method. -var ErrUnimplementedMethod = errors.New("unimplemented method") diff --git a/util/errutil/errutil.go b/util/errutil/errutil.go deleted file mode 100644 index 0217e7d4..00000000 --- a/util/errutil/errutil.go +++ /dev/null @@ -1,30 +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 errutil - -import ( - "fmt" -) - -// LineBreak defines the separator used between errors with hierarchical relationships. -var LineBreak = " << " - -// WrapError wraps an existing error, creating a new error with hierarchical relationships. -func WrapError(err error, format string, args ...any) error { - msg := fmt.Sprintf(format, args...) - return fmt.Errorf("%s"+LineBreak+"%w", msg, err) -} diff --git a/util/errutil/errutil_test.go b/util/errutil/errutil_test.go deleted file mode 100644 index 51526fa8..00000000 --- a/util/errutil/errutil_test.go +++ /dev/null @@ -1,42 +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 errutil_test - -import ( - "os" - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util/errutil" -) - -func TestWrapError(t *testing.T) { - var err error - - err = os.ErrNotExist - err = errutil.WrapError(err, "open file error: file=%s", "test.php") - err = errutil.WrapError(err, "read file error") - assert.That(t, err).NotNil() - assert.That(t, err.Error()).Equal(`read file error << open file error: file=test.php << file does not exist`) - - errutil.LineBreak = " / " - err = os.ErrNotExist - err = errutil.WrapError(err, "open file error: file=%s", "test.php") - err = errutil.WrapError(err, "read file error") - assert.That(t, err).NotNil() - assert.That(t, err.Error()).Equal(`read file error / open file error: file=test.php / file does not exist`) -} diff --git a/util/file.go b/util/file.go deleted file mode 100644 index 87107c04..00000000 --- a/util/file.go +++ /dev/null @@ -1,35 +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 ( - "os" -) - -// PathExists checks whether the specified file or directory exists. -// Returns true if the path exists, false if it does not exist, -// and an error if the check fails for another reason (e.g., permission denied). -func PathExists(file string) (bool, error) { - _, err := os.Stat(file) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} diff --git a/util/file_test.go b/util/file_test.go deleted file mode 100644 index 792a2bde..00000000 --- a/util/file_test.go +++ /dev/null @@ -1,34 +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 ( - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func TestPathExists(t *testing.T) { - exists, err := util.PathExists("file.go") - assert.That(t, err).Nil() - assert.That(t, exists).True() - - exists, err = util.PathExists("file_not_exist.go") - assert.That(t, err).Nil() - assert.That(t, exists).False() -} diff --git a/util/goutil/goutil.go b/util/goutil/goutil.go index 388b8db6..69bea60d 100644 --- a/util/goutil/goutil.go +++ b/util/goutil/goutil.go @@ -14,39 +14,48 @@ * limitations under the License. */ -/* -Package goutil provides a safe way to execute goroutines with built-in panic recovery. - -In practice, goroutines may panic due to issues like nil pointer dereference or out-of-bounds access. -However, these panics can be recovered. This package offers a wrapper to safely run goroutines, -ensuring that any panic is caught and passed to a user-defined `OnPanic` handler. - -The `OnPanic` function allows developers to log the panic, report metrics, or perform other -custom recovery logic, making it easier to manage and observe unexpected failures in concurrent code. -*/ +// Package goutil provides safe goroutine utilities with built-in panic recovery. +// +// Goroutines may panic due to programming errors such as nil pointer dereference +// or out-of-bounds access. Panics can be recovered without crashing the whole +// process. This package offers wrappers to run goroutines safely and recover +// from such panics. +// +// A global `OnPanic` handler is triggered whenever a panic is recovered. It allows +// developers to log the panic, report metrics, or perform other custom recovery +// logic, making it easier to monitor and debug failures in concurrent code. package goutil import ( "context" - "errors" "fmt" "runtime/debug" "sync" + + "github.com/go-spring/spring-base/util" ) -// OnPanic is a global callback function triggered when a panic occurs. -var OnPanic = func(ctx context.Context, r any) { - fmt.Printf("panic: %v\n%s\n", r, debug.Stack()) +// OnPanic is a global callback function triggered whenever a panic is recovered +// inside a goroutine launched by this package. +// +// By default it prints the panic value and stack trace to stdout. +// Applications may override it during initialization to provide custom logging, +// metrics, or alerting. +// +// Note: being global means it is shared across all usages. In testing +// scenarios, remember to restore it after modification if necessary. +var OnPanic = func(ctx context.Context, r any, stack []byte) { + fmt.Printf("[PANIC] %v\n%s\n", r, stack) } /********************************** go ***************************************/ -// Status provides a mechanism to wait for a goroutine to finish. +// Status provides a handle to wait for a goroutine to finish. type Status struct { wg sync.WaitGroup } -// newStatus creates and initializes a new Status object. +// newStatus creates and initializes a new Status. func newStatus() *Status { s := &Status{} s.wg.Add(1) @@ -58,13 +67,17 @@ func (s *Status) done() { s.wg.Done() } -// Wait waits for the goroutine to finish. +// Wait blocks until the goroutine completes. func (s *Status) Wait() { s.wg.Wait() } -// Go runs a goroutine safely with context support and panic recovery. -// It ensures the process does not crash due to an uncaught panic in the goroutine. +// Go launches a goroutine that recovers from panics and invokes the global +// OnPanic handler when a panic occurs. +// +// The provided context is passed to the goroutine function `f` and to OnPanic. +// The goroutine does not stop automatically when the context is cancelled; +// `f` should check `ctx.Done()` and return when appropriate. func Go(ctx context.Context, f func(ctx context.Context)) *Status { s := newStatus() go func() { @@ -72,7 +85,7 @@ func Go(ctx context.Context, f func(ctx context.Context)) *Status { defer func() { if r := recover(); r != nil { if OnPanic != nil { - OnPanic(ctx, r) + OnPanic(ctx, r, debug.Stack()) } } }() @@ -81,34 +94,17 @@ func Go(ctx context.Context, f func(ctx context.Context)) *Status { return s } -// GoFunc runs a goroutine safely with panic recovery. -// It ensures the process does not crash due to an uncaught panic in the goroutine. -func GoFunc(f func()) *Status { - s := newStatus() - go func() { - defer s.done() - defer func() { - if r := recover(); r != nil { - if OnPanic != nil { - OnPanic(context.Background(), r) - } - } - }() - f() - }() - return s -} - /******************************* go with value *******************************/ -// ValueStatus provides a mechanism to wait for a goroutine that returns a value and an error. +// ValueStatus represents a goroutine that returns a value and an error. +// It allows the caller to wait for the result. type ValueStatus[T any] struct { wg sync.WaitGroup val T err error } -// newValueStatus creates and initializes a new ValueStatus object. +// newValueStatus creates and initializes a new ValueStatus. func newValueStatus[T any]() *ValueStatus[T] { s := &ValueStatus[T]{} s.wg.Add(1) @@ -120,25 +116,31 @@ func (s *ValueStatus[T]) done() { s.wg.Done() } -// Wait blocks until the goroutine finishes and returns its result and error. +// Wait blocks until the goroutine completes and returns its value and error. func (s *ValueStatus[T]) Wait() (T, error) { s.wg.Wait() return s.val, s.err } -// GoValue runs a goroutine safely with context support and panic recovery and -// returns its result and error. -// It ensures the process does not crash due to an uncaught panic in the goroutine. +// GoValue launches a goroutine that executes the provided function `f`, +// recovers from any panic, and invokes the global OnPanic handler. +// +// The context is passed to both `f` and OnPanic. The caller must ensure +// that `f` observes `ctx.Done()` if early cancellation is desired. +// +// If a panic occurs, the recovered panic and stack trace are also reported +// via OnPanic and wrapped into the returned error. func GoValue[T any](ctx context.Context, f func(ctx context.Context) (T, error)) *ValueStatus[T] { s := newValueStatus[T]() go func() { defer s.done() defer func() { if r := recover(); r != nil { + stack := debug.Stack() if OnPanic != nil { - OnPanic(ctx, r) + OnPanic(ctx, r, stack) } - s.err = errors.New("panic occurred") + s.err = util.FormatError(nil, "panic recovered: %v\n%s", r, stack) } }() s.val, s.err = f(ctx) diff --git a/util/goutil/goutil_test.go b/util/goutil/goutil_test.go index bc7501a1..11198965 100644 --- a/util/goutil/goutil_test.go +++ b/util/goutil/goutil_test.go @@ -18,10 +18,11 @@ package goutil_test import ( "context" - "fmt" + "errors" "testing" + "time" - "github.com/go-spring/gs-assert/assert" + "github.com/go-spring/spring-base/testing/assert" "github.com/go-spring/spring-core/util/goutil" ) @@ -39,33 +40,25 @@ func TestGo(t *testing.T) { assert.That(t, s).Equal("hello world!") } -func TestGoFunc(t *testing.T) { - - var s string - goutil.GoFunc(func() { - panic("something is wrong") - }).Wait() - assert.That(t, s).Equal("") - - goutil.GoFunc(func() { - s = "hello world!" - }).Wait() - assert.That(t, s).Equal("hello world!") -} - func TestGoValue(t *testing.T) { s, err := goutil.GoValue(t.Context(), func(ctx context.Context) (string, error) { panic("something is wrong") }).Wait() assert.That(t, s).Equal("") - assert.That(t, err).Equal(fmt.Errorf("panic occurred")) + assert.Error(t, err).Matches("panic recovered: .*") + + i, err := goutil.GoValue(t.Context(), func(ctx context.Context) (int, error) { + return 42, nil + }).Wait() + assert.That(t, err).Nil() + assert.That(t, i).Equal(42) s, err = goutil.GoValue(t.Context(), func(ctx context.Context) (string, error) { return "hello world!", nil }).Wait() - assert.That(t, s).Equal("hello world!") assert.That(t, err).Nil() + assert.That(t, s).Equal("hello world!") var arr []*goutil.ValueStatus[int] for i := range 3 { @@ -73,11 +66,28 @@ func TestGoValue(t *testing.T) { return i, nil })) } - - var v int for i, g := range arr { - v, err = g.Wait() + v, err := g.Wait() assert.That(t, v).Equal(i) assert.That(t, err).Nil() } + + expectedErr := errors.New("expected error") + _, err = goutil.GoValue(t.Context(), func(ctx context.Context) (string, error) { + return "", expectedErr + }).Wait() + assert.That(t, err).Equal(expectedErr) + + t.Run("context cancel", func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + _, err := goutil.GoValue(ctx, func(ctx context.Context) (string, error) { + time.Sleep(100 * time.Millisecond) + return "done", nil + }).Wait() + assert.That(t, err).Nil() + }) } diff --git a/util/list.go b/util/list.go deleted file mode 100644 index 361bba1f..00000000 --- a/util/list.go +++ /dev/null @@ -1,45 +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 ( - "container/list" -) - -// ListOf creates a list of the given items. -func ListOf[T any](a ...T) *list.List { - l := list.New() - for _, i := range a { - l.PushBack(i) - } - return l -} - -// AllOfList returns a slice of all items in the given list. -func AllOfList[T any](l *list.List) []T { - if l == nil { - return nil - } - if l.Len() == 0 { - return nil - } - ret := make([]T, 0, l.Len()) - for e := l.Front(); e != nil; e = e.Next() { - ret = append(ret, e.Value.(T)) - } - return ret -} diff --git a/util/list_test.go b/util/list_test.go deleted file mode 100644 index 360c941f..00000000 --- a/util/list_test.go +++ /dev/null @@ -1,32 +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 ( - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func TestListOf(t *testing.T) { - assert.That(t, util.AllOfList[string](nil)).Nil() - l := util.ListOf[string]() - assert.That(t, util.AllOfList[string](l)).Nil() - l = util.ListOf("a", "b", "c") - assert.That(t, []string{"a", "b", "c"}).Equal(util.AllOfList[string](l)) -} diff --git a/util/type.go b/util/type.go deleted file mode 100644 index 5154a3e6..00000000 --- a/util/type.go +++ /dev/null @@ -1,135 +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 ( - "reflect" -) - -// errorType is the [reflect.Type] of the error interface. -var errorType = reflect.TypeFor[error]() - -// IntType is the type of int, int8, int16, int32, int64. -type IntType interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 -} - -// UintType is the type of uint, uint8, uint16, uint32, uint64. -type UintType interface { - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 -} - -// FloatType is the type of float32, float64. -type FloatType interface { - ~float32 | ~float64 -} - -// IsFuncType returns true if the provided type t is a function type. -func IsFuncType(t reflect.Type) bool { - return t.Kind() == reflect.Func -} - -// IsErrorType returns true if the provided type t is an error type, -// either directly (error) or via an implementation (i.e., implements the error interface). -func IsErrorType(t reflect.Type) bool { - return t == errorType || t.Implements(errorType) -} - -// ReturnNothing returns true if the provided function type t has no return values. -func ReturnNothing(t reflect.Type) bool { - return t.NumOut() == 0 -} - -// ReturnOnlyError returns true if the provided function type t returns only one value, -// and that value is an error. -func ReturnOnlyError(t reflect.Type) bool { - return t.NumOut() == 1 && IsErrorType(t.Out(0)) -} - -// IsConstructor returns true if the provided function type t is a constructor. -// A constructor is defined as a function that returns one or two values. -// If it returns two values, the second value must be an error. -func IsConstructor(t reflect.Type) bool { - if !IsFuncType(t) { - return false - } - switch t.NumOut() { - case 1: - return !IsErrorType(t.Out(0)) - case 2: - return IsErrorType(t.Out(1)) - default: - return false - } -} - -// IsPrimitiveValueType returns true if the provided type t is a primitive value type, -// such as int, uint, float, bool, or string. -func IsPrimitiveValueType(t reflect.Type) bool { - switch t.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return true - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return true - case reflect.Float32, reflect.Float64: - return true - case reflect.String: - return true - case reflect.Bool: - return true - default: - return false - } -} - -// IsPropBindingTarget returns true if the provided type t is a valid target for property binding. -// This includes primitive value types or composite types (such as array, slice, map, or struct) -// where the elements are primitive value types. -func IsPropBindingTarget(t reflect.Type) bool { - switch t.Kind() { - case reflect.Map, reflect.Slice, reflect.Array: - t = t.Elem() // for collection types, check the element type - default: - // do nothing - } - return IsPrimitiveValueType(t) || t.Kind() == reflect.Struct -} - -// IsBeanType returns true if the provided type t is considered a "bean" type. -// A "bean" type includes a channel, function, interface, or a pointer to a struct. -func IsBeanType(t reflect.Type) bool { - switch t.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface: - return true - case reflect.Ptr: - return t.Elem().Kind() == reflect.Struct - default: - return false - } -} - -// IsBeanInjectionTarget returns true if the provided type t is a valid target for bean injection. -// This includes maps, slices, arrays, or any other bean type (including pointers to structs). -func IsBeanInjectionTarget(t reflect.Type) bool { - switch t.Kind() { - case reflect.Map, reflect.Slice, reflect.Array: - t = t.Elem() // for collection types, check the element type - default: - // do nothing - } - return IsBeanType(t) -} diff --git a/util/type_test.go b/util/type_test.go deleted file mode 100644 index f4f7c904..00000000 --- a/util/type_test.go +++ /dev/null @@ -1,169 +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 ( - "errors" - "fmt" - "os" - "reflect" - "testing" - "unsafe" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func TestIsErrorType(t *testing.T) { - err := fmt.Errorf("error") - assert.That(t, util.IsErrorType(reflect.TypeOf(err))).True() - err = os.ErrClosed - assert.That(t, util.IsErrorType(reflect.TypeOf(err))).True() - assert.That(t, util.IsErrorType(reflect.TypeFor[int]())).False() -} - -func TestReturnNothing(t *testing.T) { - assert.That(t, util.ReturnNothing(reflect.TypeOf(func() {}))).True() - assert.That(t, util.ReturnNothing(reflect.TypeOf(func(key string) {}))).True() - assert.That(t, util.ReturnNothing(reflect.TypeOf(func() string { return "" }))).False() -} - -func TestReturnOnlyError(t *testing.T) { - assert.That(t, util.ReturnOnlyError(reflect.TypeOf(func() error { return nil }))).True() - assert.That(t, util.ReturnOnlyError(reflect.TypeOf(func(string) error { return nil }))).True() - assert.That(t, util.ReturnOnlyError(reflect.TypeOf(func() (string, error) { return "", nil }))).False() -} - -func TestIsConstructor(t *testing.T) { - assert.That(t, util.IsConstructor(reflect.TypeFor[int]())).False() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() {}))).False() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() string { return "" }))).True() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() *string { return nil }))).True() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() *receiver { return nil }))).True() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() (*receiver, error) { return nil, nil }))).True() - assert.That(t, util.IsConstructor(reflect.TypeOf(func() (bool, *receiver, error) { return false, nil, nil }))).False() -} - -func TestIsPropBindingTarget(t *testing.T) { - data := []struct { - i any - v bool - }{ - {true, true}, // Bool - {int(1), true}, // Int - {int8(1), true}, // Int8 - {int16(1), true}, // Int16 - {int32(1), true}, // Int32 - {int64(1), true}, // Int64 - {uint(1), true}, // Uint - {uint8(1), true}, // Uint8 - {uint16(1), true}, // Uint16 - {uint32(1), true}, // Uint32 - {uint64(1), true}, // Uint64 - {uintptr(0), false}, // Uintptr - {float32(1), true}, // Float32 - {float64(1), true}, // Float64 - {complex64(1), false}, // Complex64 - {complex128(1), false}, // Complex128 - {[1]int{0}, true}, // Array - {make(chan struct{}), false}, // Chan - {func() {}, false}, // Func - {reflect.TypeFor[error](), false}, // Interface - {make(map[int]int), true}, // Map - {make(map[string]*int), false}, // - {new(int), false}, // Ptr - {new(struct{}), false}, // - {[]int{0}, true}, // Slice - {[]*int{}, false}, // - {"this is a string", true}, // String - {struct{}{}, true}, // Struct - {unsafe.Pointer(new(int)), false}, // UnsafePointer - } - for _, d := range data { - var typ reflect.Type - switch i := d.i.(type) { - case reflect.Type: - typ = i - default: - typ = reflect.TypeOf(i) - } - if r := util.IsPropBindingTarget(typ); d.v != r { - t.Errorf("%v expect %v but %v", typ, d.v, r) - } - } -} - -func TestIsBeanType(t *testing.T) { - data := []struct { - i any - v bool - }{ - {true, false}, // Bool - {int(1), false}, // Int - {int8(1), false}, // Int8 - {int16(1), false}, // Int16 - {int32(1), false}, // Int32 - {int64(1), false}, // Int64 - {uint(1), false}, // Uint - {uint8(1), false}, // Uint8 - {uint16(1), false}, // Uint16 - {uint32(1), false}, // Uint32 - {uint64(1), false}, // Uint64 - {uintptr(0), false}, // Uintptr - {float32(1), false}, // Float32 - {float64(1), false}, // Float64 - {complex64(1), false}, // Complex64 - {complex128(1), false}, // Complex128 - {[1]int{0}, false}, // Array - {make(chan struct{}), true}, // Chan - {func() {}, true}, // Func - {reflect.TypeFor[error](), true}, // Interface - {make(map[int]int), false}, // Map - {make(map[string]*int), false}, // - {new(int), false}, // - {new(struct{}), true}, // - {[]int{0}, false}, // Slice - {[]*int{}, false}, // - {"this is a string", false}, // String - {struct{}{}, false}, // Struct - {unsafe.Pointer(new(int)), false}, // UnsafePointer - } - for _, d := range data { - var typ reflect.Type - switch i := d.i.(type) { - case reflect.Type: - typ = i - default: - typ = reflect.TypeOf(i) - } - if r := util.IsBeanType(typ); d.v != r { - t.Errorf("%v expect %v but %v", typ, d.v, r) - } - } -} - -func TestIsBeanInjectionTarget(t *testing.T) { - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf("abc"))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf(new(string)))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf(errors.New("abc")))).True() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf([]string{}))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf([]*string{}))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf([]fmt.Stringer{}))).True() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf(map[string]string{}))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf(map[string]*string{}))).False() - assert.That(t, util.IsBeanInjectionTarget(reflect.TypeOf(map[string]fmt.Stringer{}))).True() -} diff --git a/util/value.go b/util/value.go deleted file mode 100644 index 9d7e8c68..00000000 --- a/util/value.go +++ /dev/null @@ -1,72 +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 ( - "reflect" - "runtime" - "strings" - "unsafe" -) - -// Ptr returns a pointer to the given value. -func Ptr[T any](i T) *T { - return &i -} - -const ( - flagStickyRO = 1 << 5 - flagEmbedRO = 1 << 6 - flagRO = flagStickyRO | flagEmbedRO -) - -// PatchValue modifies an unexported field to make it assignable by modifying the internal flag. -// It takes a [reflect.Value] and returns the patched value that can be written to. -// This is typically used to manipulate unexported fields in struct types. -func PatchValue(v reflect.Value) reflect.Value { - rv := reflect.ValueOf(&v) - flag := rv.Elem().FieldByName("flag") - ptrFlag := (*uintptr)(unsafe.Pointer(flag.UnsafeAddr())) - *ptrFlag = *ptrFlag &^ flagRO - return v -} - -// FuncName returns the function name for a given function. -func FuncName(fn any) string { - _, _, fnName := FileLine(fn) - return fnName -} - -// FileLine returns the file, line number, and function name for a given function. -// It uses reflection and runtime information to extract these details. -// 'fn' is expected to be a function or method value. -func FileLine(fn any) (file string, line int, fnName string) { - - fnPtr := reflect.ValueOf(fn).Pointer() - fnInfo := runtime.FuncForPC(fnPtr) - file, line = fnInfo.FileLine(fnPtr) - - s := fnInfo.Name() - i := strings.LastIndex(s, "/") - if i > 0 { - s = s[i+1:] - } - - // method values are printed as "T.m-fm" - s = strings.TrimRight(s, "-fm") - return file, line, s -} diff --git a/util/value_test.go b/util/value_test.go deleted file mode 100644 index 051b2cca..00000000 --- a/util/value_test.go +++ /dev/null @@ -1,96 +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 ( - "fmt" - "reflect" - "testing" - - "github.com/go-spring/gs-assert/assert" - "github.com/go-spring/spring-core/util" -) - -func TestPatchValue(t *testing.T) { - var r struct{ v int } - v := reflect.ValueOf(&r) - v = v.Elem().Field(0) - assert.Panic(t, func() { - v.SetInt(4) - }, "using value obtained using unexported field") - v = util.PatchValue(v) - v.SetInt(4) -} - -func TestFuncName(t *testing.T) { - assert.That(t, util.FuncName(func() {})).Equal("util_test.TestFuncName.func1") - assert.That(t, util.FuncName(func(i int) {})).Equal("util_test.TestFuncName.func2") - assert.That(t, util.FuncName(fnNoArgs)).Equal("util_test.fnNoArgs") - assert.That(t, util.FuncName(fnWithArgs)).Equal("util_test.fnWithArgs") - assert.That(t, util.FuncName((*receiver).ptrFnNoArgs)).Equal("util_test.(*receiver).ptrFnNoArgs") - assert.That(t, util.FuncName((*receiver).ptrFnWithArgs)).Equal("util_test.(*receiver).ptrFnWithArgs") -} - -func fnNoArgs() {} - -func fnWithArgs(i int) {} - -type receiver struct{} - -func (r *receiver) ptrFnNoArgs() {} - -func (r *receiver) ptrFnWithArgs(i int) {} - -func TestFileLine(t *testing.T) { - testcases := []struct { - fn any - file string - line int - fnName string - }{ - { - fnNoArgs, - "spring-core/util/value_test.go", - 48, - "util_test.fnNoArgs", - }, - { - fnWithArgs, - "spring-core/util/value_test.go", - 50, - "util_test.fnWithArgs", - }, - { - (*receiver).ptrFnNoArgs, - "spring-core/util/value_test.go", - 54, - "util_test.(*receiver).ptrFnNoArgs", - }, - { - (*receiver).ptrFnWithArgs, - "spring-core/util/value_test.go", - 56, - "util_test.(*receiver).ptrFnWithArgs", - }, - } - for i, c := range testcases { - file, line, fnName := util.FileLine(c.fn) - assert.That(t, line).Equal(c.line, fmt.Sprint(i)) - assert.That(t, fnName).Equal(c.fnName, fmt.Sprint(i)) - assert.ThatString(t, file).HasSuffix(c.file, fmt.Sprint(i)) - } -} diff --git a/sdk/redis/.keep b/web/.keep similarity index 100% rename from sdk/redis/.keep rename to web/.keep