├── .github ├── dependabot.yml ├── semantic.yml └── workflows │ ├── go.yml │ ├── golangci-lint.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── README_CN.md ├── Taskfile.yml ├── assets └── kod.excalidraw.png ├── cmd └── kod │ ├── internal │ ├── callgraph.go │ ├── callgraph_test.go │ ├── format.go │ ├── format_test.go │ ├── generate.go │ ├── generate_file.go │ ├── generate_file_test.go │ ├── generate_generator.go │ ├── generate_test.go │ ├── generate_types.go │ ├── mock_watcher_test.go │ ├── root.go │ ├── root_test.go │ ├── struct2interface.go │ ├── struct2interface_test.go │ ├── watcher.go │ └── watcher_test.go │ └── main.go ├── codecov.yaml ├── context.go ├── example_test.go ├── examples └── helloworld │ ├── config.toml │ ├── helloworld.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── go.mod ├── go.sum ├── interceptor ├── interceptor.go ├── interceptor_test.go ├── internal │ ├── circuitbreaker │ │ ├── circuitbreaker.go │ │ └── circuitbreaker_test.go │ ├── kerror │ │ ├── error.go │ │ └── error_test.go │ └── ratelimit │ │ ├── ratelimit.go │ │ └── ratelimit_test.go ├── kaccesslog │ └── accesslog.go ├── kcircuitbreaker │ └── circuitbreaker.go ├── kmetric │ └── metric.go ├── kratelimit │ └── ratelimit.go ├── krecovery │ └── recovery.go ├── kretry │ └── retry.go ├── ktimeout │ └── timeout.go ├── ktrace │ └── trace.go └── kvalidate │ └── validate.go ├── internal ├── callgraph │ ├── bin.go │ └── graph.go ├── hooks │ └── defer.go ├── kslog │ └── slog.go ├── mock │ └── testing.go ├── registry │ └── registry.go ├── rolling │ ├── README.md │ ├── point.go │ ├── point_test.go │ ├── reduce.go │ ├── reduce_test.go │ ├── time.go │ ├── time_test.go │ └── window.go ├── signals │ ├── signals.go │ └── signals_test.go ├── singleton │ └── singleton.go └── version │ ├── version.go │ └── version_test.go ├── kod.go ├── kod_test.go ├── registry.go ├── registry_test.go ├── testing.go ├── testing_test.go ├── tests ├── case1 │ ├── case.go │ ├── case_context.go │ ├── case_context_test.go │ ├── case_default_config.go │ ├── case_default_config_test.go │ ├── case_echo.go │ ├── case_echo_test.go │ ├── case_gin.go │ ├── case_gin_test.go │ ├── case_http.go │ ├── case_http_test.go │ ├── case_interceptor_retry.go │ ├── case_interceptor_retry_test.go │ ├── case_lazy_init.go │ ├── case_lazy_init_test.go │ ├── case_log_file_test.go │ ├── case_log_level_test.go │ ├── case_log_mock_test.go │ ├── case_runtest_test.go │ ├── case_test.go │ ├── kod-logfile.toml │ ├── kod.json │ ├── kod.toml │ ├── kod.yaml │ ├── kod2.toml │ ├── kod_gen.go │ ├── kod_gen_interface.go │ ├── kod_gen_mock.go │ ├── panic.go │ └── panic_test.go ├── case2 │ ├── case.go │ ├── case_test.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── case3 │ ├── case.go │ ├── case_test.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── case4 │ ├── case.go │ ├── case_test.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── case5 │ ├── case_ref_struct.go │ ├── case_ref_struct_test.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go ├── go.mod ├── go.sum └── graphcase │ ├── case.go │ ├── kod_gen.go │ ├── kod_gen_interface.go │ └── kod_gen_mock.go └── version.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: gomod 5 | directories: 6 | - / 7 | - /tests 8 | labels: 9 | - dependencies 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | gomod-normal-deps: 14 | update-types: 15 | - patch 16 | - minor 17 | gomod-breaking-deps: 18 | update-types: 19 | - major 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | labels: 24 | - dependencies 25 | schedule: 26 | interval: "weekly" 27 | groups: 28 | actions-deps: 29 | patterns: 30 | - "*" 31 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | allowRevertCommits: true 3 | types: 4 | - feat 5 | - fix 6 | - docs 7 | - style 8 | - refactor 9 | - perf 10 | - test 11 | - build 12 | - ci 13 | - chore 14 | - revert 15 | - change 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: [stable] 16 | 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.version }} 25 | cache: true 26 | 27 | - name: Install Task 28 | uses: arduino/setup-task@v2 29 | with: 30 | version: 3.x 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: generate and test 34 | run: | 35 | task github-action 36 | 37 | - uses: codecov/codecov-action@v5 38 | env: 39 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 40 | with: 41 | files: ./coverage.out # optional 42 | flags: unittests # optional 43 | name: codecov-umbrella # optional 44 | fail_ci_if_error: false # optional (default = false) 45 | verbose: false # optional (default = false) 46 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - "main" 6 | pull_request: 7 | branches: 8 | - "main" 9 | 10 | permissions: 11 | contents: read 12 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 13 | # pull-requests: read 14 | jobs: 15 | golangci: 16 | strategy: 17 | matrix: 18 | go: [stable, oldstable] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | name: lint 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go }} 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v8 29 | with: 30 | args: --timeout=5m 31 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | schedule: 8 | - cron: '0 0 * * *' # 每天0点触发 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: 'stable' 29 | 30 | - name: Goreleaser Release (Snapshot) 31 | if: github.event_name != 'push' || startsWith(github.ref, 'refs/heads/') # 触发条件为手动或非 tag 的 push 32 | uses: goreleaser/goreleaser-action@v6 33 | with: 34 | version: latest 35 | args: release --snapshot --clean # 快照发布 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Delete existing assets from release 40 | if: github.event_name != 'push' || startsWith(github.ref, 'refs/heads/') # 触发条件为手动或非 tag 的 push 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 或者使用 PAT_TOKEN 43 | run: | 44 | RELEASE_TAG="prerelease-nightly" 45 | # 获取 Release ID 46 | RELEASE_ID=$(gh api -X GET repos/${{ github.repository }}/releases --jq '.[] | select(.tag_name=="'"$RELEASE_TAG"'") | .id') 47 | if [ -z "$RELEASE_ID" ]; then 48 | echo "Release with tag $RELEASE_TAG does not exist. Exiting." 49 | exit 1 50 | fi 51 | # 获取所有资产(artifacts) 52 | ASSET_IDS=$(gh api -X GET repos/${{ github.repository }}/releases/$RELEASE_ID/assets --jq '.[].id') 53 | # 删除所有资产 54 | for ASSET_ID in $ASSET_IDS; do 55 | echo "Deleting asset with ID $ASSET_ID" 56 | gh api -X DELETE repos/${{ github.repository }}/releases/assets/$ASSET_ID 57 | done 58 | 59 | - name: Upload new assets to release 60 | if: github.event_name != 'push' || startsWith(github.ref, 'refs/heads/') # 触发条件为手动或非 tag 的 push 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 或者使用 PAT_TOKEN 63 | run: | 64 | RELEASE_TAG="prerelease-nightly" 65 | # 上传新的构建产物 66 | gh release upload "$RELEASE_TAG" ./dist/*.tar.gz ./dist/*_checksums.txt --clobber 67 | 68 | - name: Goreleaser Release (Official) 69 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') # 触发条件为 tag 的 push 70 | uses: goreleaser/goreleaser-action@v6 71 | with: 72 | version: latest 73 | args: release --clean # 正式发布 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,go 4 | 5 | ### Go ### 6 | # If you prefer the allow list template instead of the deny list, see community template: 7 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 8 | # 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | 16 | # Test binary, built with `go test -c` 17 | *.test 18 | 19 | # Output of the go coverage tool, specifically when used with LiteIDE 20 | *.out 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | # vendor/ 24 | 25 | # Go workspace file 26 | go.work 27 | go.work.sum 28 | 29 | ### Linux ### 30 | *~ 31 | 32 | # temporary files which can be created if a process still has a handle open of a deleted file 33 | .fuse_hidden* 34 | 35 | # KDE directory preferences 36 | .directory 37 | 38 | # Linux trash folder which might appear on any partition or disk 39 | .Trash-* 40 | 41 | # .nfs files are created when an open file is removed but is still being accessed 42 | .nfs* 43 | 44 | ### VisualStudioCode ### 45 | .vscode/* 46 | !.vscode/settings.json 47 | !.vscode/tasks.json 48 | !.vscode/launch.json 49 | !.vscode/extensions.json 50 | !.vscode/*.code-snippets 51 | 52 | # Local History for Visual Studio Code 53 | .history/ 54 | 55 | # Built Visual Studio Code Extensions 56 | *.vsix 57 | 58 | ### VisualStudioCode Patch ### 59 | # Ignore all local history of files 60 | .history 61 | .ionide 62 | 63 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,go 64 | 65 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 66 | 67 | .task 68 | mockgen 69 | golangci-lint 70 | dist/ -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - dupword 5 | - mirror 6 | - misspell 7 | - revive 8 | - thelper 9 | - usestdlibvars 10 | settings: 11 | misspell: 12 | locale: US 13 | revive: 14 | rules: 15 | - name: blank-imports 16 | - name: context-as-argument 17 | - name: context-keys-type 18 | - name: dot-imports 19 | - name: empty-block 20 | - name: error-naming 21 | - name: error-return 22 | - name: error-strings 23 | - name: errorf 24 | - name: increment-decrement 25 | - name: indent-error-flow 26 | - name: range 27 | - name: receiver-naming 28 | - name: redefines-builtin-id 29 | - name: superfluous-else 30 | - name: time-naming 31 | - name: unreachable-code 32 | - name: unused-parameter 33 | - name: var-declaration 34 | - name: var-naming 35 | exclusions: 36 | generated: lax 37 | presets: 38 | - comments 39 | - common-false-positives 40 | - legacy 41 | - std-error-handling 42 | paths: 43 | - .*_test.go 44 | - third_party$ 45 | - builtin$ 46 | - examples$ 47 | formatters: 48 | settings: 49 | gci: 50 | sections: 51 | - standard 52 | - default 53 | - localmodule 54 | exclusions: 55 | generated: lax 56 | paths: 57 | - third_party$ 58 | - builtin$ 59 | - examples$ 60 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=jcroql 3 | version: 2 4 | 5 | project_name: kod 6 | 7 | env: 8 | - GO111MODULE=on 9 | 10 | before: 11 | hooks: 12 | - go mod tidy 13 | 14 | snapshot: 15 | version_template: "{{ incpatch .Version }}-devel" 16 | 17 | report_sizes: true 18 | 19 | git: 20 | ignore_tags: 21 | - ext/** 22 | - prerelease-nightly 23 | 24 | metadata: 25 | mod_timestamp: "{{ .CommitTimestamp }}" 26 | 27 | builds: 28 | - main: "./cmd/kod" 29 | env: 30 | - CGO_ENABLED=0 31 | goos: 32 | - linux 33 | - windows 34 | - darwin 35 | goarch: 36 | - amd64 37 | - arm64 38 | mod_timestamp: "{{ .CommitTimestamp }}" 39 | ldflags: 40 | - -s -w 41 | 42 | archives: 43 | - format: tar.gz 44 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 45 | files: 46 | - ./README.md 47 | - ./LICENSE 48 | 49 | release: 50 | github: 51 | owner: go-kod 52 | name: kod 53 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch generate", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "cwd": "${workspaceFolder}", 13 | "program": "./cmd/kod", 14 | "args": "generate ./... ./tests/..." 15 | }, 16 | { 17 | "name": "Launch struct2interface", 18 | "type": "go", 19 | "request": "launch", 20 | "mode": "auto", 21 | "cwd": "${workspaceFolder}", 22 | "program": "./cmd/kod", 23 | "args": "struct2interface tests" 24 | }, 25 | { 26 | "name": "Launch log", 27 | "type": "go", 28 | "request": "launch", 29 | "mode": "auto", 30 | "cwd": "${workspaceFolder}", 31 | "program": "./examples/log", 32 | "args": "" 33 | }, 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testFlags": [ 3 | "-v", 4 | "-coverpkg=github.com/go-kod/kod/..." 5 | ] 6 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hnlq.sysu@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | env: 3 | GOBIN: { sh: pwd } 4 | 5 | tasks: 6 | default: 7 | cmds: 8 | - task: generate 9 | 10 | github-action: 11 | cmds: 12 | - task: test 13 | - task: lint:files-changed 14 | 15 | build:gowork: 16 | run: once 17 | status: 18 | - test -f ../go.work || test -f go.work 19 | cmds: 20 | - go work init 21 | - go work use -r . 22 | 23 | generate: 24 | cmds: 25 | - PATH=$PATH:$GOBIN go run ./cmd/kod generate -s ./... ./tests/... 26 | sources: 27 | - "**/**.go" 28 | deps: 29 | - task: build:gowork 30 | - task: mod 31 | - install:mockgen 32 | 33 | lint:files-changed: 34 | cmd: | 35 | git diff --exit-code 36 | 37 | test: 38 | cmd: | 39 | PATH=$PATH:$GOBIN GOEXPERIMENT=nocoverageredesign go test -race -cover -coverprofile=coverage.out \ 40 | -covermode=atomic ./... ./tests/... \ 41 | -coverpkg .,./cmd/...,./internal/...,./interceptor/... 42 | git checkout tests/case1/kod_gen_mock.go 43 | sources: 44 | - "**/**.go" 45 | generates: 46 | - coverage.out 47 | deps: 48 | - task: generate 49 | 50 | test:coverage: 51 | cmd: | 52 | go tool cover -func=coverage.out 53 | deps: 54 | - test 55 | 56 | install:mockgen: 57 | vars: 58 | VERSION: 59 | sh: | 60 | cat go.mod|grep go.uber.org/mock |awk -F ' ' '{print $2}' 61 | status: 62 | - test -f mockgen 63 | - go version -m $GOBIN/mockgen | grep go.uber.org/mock | grep {{.VERSION}} 64 | cmd: | 65 | go install go.uber.org/mock/mockgen@{{.VERSION}} 66 | 67 | mod: 68 | cmds: 69 | - go mod tidy 70 | - cd tests && go mod tidy 71 | -------------------------------------------------------------------------------- /assets/kod.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-kod/kod/73a49752bbe0b02173ddc69c67cb00d527b0b3e2/assets/kod.excalidraw.png -------------------------------------------------------------------------------- /cmd/kod/internal/callgraph.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/dominikbraun/graph/draw" 9 | "github.com/samber/lo" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/go-kod/kod/internal/callgraph" 13 | ) 14 | 15 | var callgraphCmd = &cobra.Command{ 16 | Use: "callgraph", 17 | Short: "generate kod callgraph for your kod application.", 18 | // Uncomment the following line if your bare application 19 | // has an action associated with it: 20 | // Run: func(cmd *cobra.Command, args []string) { }, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | if len(args) == 0 { 23 | cmd.PrintErr("please input the binary filepath") 24 | return 25 | } 26 | 27 | g := lo.Must(callgraph.ReadComponentGraph(args[0])) 28 | o := lo.Must(cmd.Flags().GetString("o")) 29 | t := lo.Must(cmd.Flags().GetString("t")) 30 | 31 | switch t { 32 | case "json": 33 | data := lo.Must(g.AdjacencyMap()) 34 | enc := json.NewEncoder(cmd.OutOrStdout()) 35 | enc.SetIndent("", " ") 36 | lo.Must0(enc.Encode(data)) 37 | case "dot": 38 | file := lo.Must(os.Create(o)) 39 | lo.Must0(draw.DOT(g, file)) 40 | default: 41 | fmt.Println("output type not supported") 42 | } 43 | }, 44 | } 45 | 46 | func init() { 47 | callgraphCmd.PersistentFlags().String("o", "my-graph.dot", "output file name") 48 | callgraphCmd.PersistentFlags().String("t", "dot", "output type, support json/dot") 49 | 50 | rootCmd.AddCommand(callgraphCmd) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/kod/internal/callgraph_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGraph(t *testing.T) { 12 | t.Run("no filepath", func(t *testing.T) { 13 | assert.Equal(t, "please input the binary filepath", execute(t, "callgraph")) 14 | }) 15 | 16 | t.Run("unknown format", func(t *testing.T) { 17 | assert.Panics(t, func() { 18 | execute(t, "callgraph callgraph.go") 19 | }) 20 | }) 21 | 22 | for _, test := range []struct{ os, arch string }{ 23 | {"linux", "amd64"}, 24 | {"windows", "amd64"}, 25 | {"darwin", "arm64"}, 26 | {"darwin", "amd64"}, 27 | } { 28 | t.Run(test.os+"_"+test.arch, func(t *testing.T) { 29 | cmd := exec.Command("go", "build", "-o", "graphcase", "../../../tests/graphcase") 30 | cmd.Env = append(os.Environ(), "GOOS="+test.os, "GOARCH="+test.arch) 31 | assert.Nil(t, cmd.Run()) 32 | 33 | execute(t, "callgraph graphcase") 34 | assert.FileExists(t, "my-graph.dot") 35 | 36 | data, err := os.ReadFile("my-graph.dot") 37 | assert.Nil(t, err) 38 | 39 | assert.Contains(t, string(data), "github.com/go-kod/kod/Main") 40 | os.Remove("my-graph.dot") 41 | os.Remove("graphcase") 42 | }) 43 | } 44 | 45 | t.Run("json format", func(t *testing.T) { 46 | cmd := exec.Command("go", "build", "-o", "graphcase", "../../../tests/graphcase") 47 | assert.Nil(t, cmd.Run()) 48 | 49 | data := execute(t, "callgraph graphcase --t json") 50 | 51 | assert.Contains(t, string(data), "github.com/go-kod/kod/Main") 52 | os.Remove("graphcase") 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /cmd/kod/internal/format.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "golang.org/x/tools/imports" 5 | ) 6 | 7 | func ImportsCode(code string) ([]byte, error) { 8 | opts := &imports.Options{ 9 | TabIndent: true, 10 | TabWidth: 2, 11 | Fragment: true, 12 | Comments: true, 13 | } 14 | 15 | formatcode, err := imports.Process("", []byte(code), opts) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | if string(formatcode) == code { 21 | return formatcode, err 22 | } 23 | 24 | return ImportsCode(string(formatcode)) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/kod/internal/format_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestImportsCode(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | code string 13 | want string 14 | wantErr bool 15 | }{ 16 | { 17 | name: "simple format", 18 | code: `package main 19 | import "fmt" 20 | func main(){ 21 | fmt.Println("hello")}`, 22 | want: `package main 23 | 24 | import "fmt" 25 | 26 | func main() { 27 | fmt.Println("hello") 28 | } 29 | `, 30 | wantErr: false, 31 | }, 32 | { 33 | name: "invalid code", 34 | code: "invalid{", 35 | wantErr: true, 36 | }, 37 | { 38 | name: "already formatted", 39 | code: `package main 40 | 41 | import "fmt" 42 | 43 | func main() { 44 | fmt.Println("hello") 45 | }`, 46 | want: `package main 47 | 48 | import "fmt" 49 | 50 | func main() { 51 | fmt.Println("hello") 52 | } 53 | `, 54 | wantErr: false, 55 | }, 56 | } 57 | 58 | for _, tt := range tests { 59 | t.Run(tt.name, func(t *testing.T) { 60 | got, err := ImportsCode(tt.code) 61 | if tt.wantErr { 62 | require.Error(t, err) 63 | return 64 | } 65 | require.NoError(t, err) 66 | require.Equal(t, tt.want, string(got)) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/kod/internal/generate.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/fsnotify/fsnotify" 10 | "github.com/samber/lo" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var generate = &cobra.Command{ 15 | Use: "generate", 16 | Short: "generate kod related codes for your kod application.", 17 | // Uncomment the following line if your bare application 18 | // has an action associated with it: 19 | // Run: func(cmd *cobra.Command, args []string) { }, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | { 22 | if watch, _ := cmd.Flags().GetBool("watch"); watch { 23 | startWatcher(cmd.Context(), cmd, args) 24 | } 25 | 26 | doGenerate(cmd, "./", args) 27 | } 28 | }, 29 | } 30 | 31 | func startWatcher(ctx context.Context, cmd *cobra.Command, args []string) { 32 | // Create new watcher. 33 | w := lo.Must(fsnotify.NewWatcher()) 34 | defer w.Close() 35 | 36 | Watch(&watcher{ctx: ctx, w: w}, ".", 37 | func(event fsnotify.Event) { doGenerate(cmd, filepath.Dir(event.Name), args) }, lo.Must(cmd.Flags().GetBool("verbose")), 38 | ) 39 | } 40 | 41 | func doGenerate(cmd *cobra.Command, dir string, args []string) { 42 | startTime := time.Now() 43 | 44 | if s2i, _ := cmd.Flags().GetBool("struct2interface"); s2i { 45 | if err := Struct2Interface(cmd, dir); err != nil { 46 | fmt.Fprint(cmd.ErrOrStderr(), err) 47 | return 48 | } 49 | fmt.Printf("[struct2interface] %s \n", time.Since(startTime).String()) 50 | } 51 | 52 | startTime = time.Now() 53 | 54 | if err := Generate(dir, args, Options{}); err != nil { 55 | fmt.Fprint(cmd.ErrOrStderr(), err) 56 | return 57 | } 58 | 59 | fmt.Printf("[generate] %s \n", time.Since(startTime).String()) 60 | } 61 | 62 | func init() { 63 | generate.Flags().BoolP("struct2interface", "s", true, "generate interface from struct.") 64 | generate.Flags().BoolP("verbose", "v", false, "verbose mode.") 65 | generate.Flags().BoolP("watch", "w", false, "watch the changes of the files and regenerate the codes.") 66 | rootCmd.AddCommand(generate) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/kod/internal/generate_file.go: -------------------------------------------------------------------------------- 1 | // Package files contains file-related utilities. 2 | package internal 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // Writer writes a sequence of bytes to a file. In case of errors, the old file 11 | // contents remain untouched since the writes are applied atomically when the 12 | // Writer is closed. 13 | type Writer struct { 14 | dst string // Name of destination file 15 | tmp *os.File // Temporary file to which data is written. 16 | tmpName string // Name of temporary file. 17 | err error 18 | } 19 | 20 | // NewWriter returns a writer that writes to the named files. 21 | // 22 | // The caller should eventually call Cleanup. A recommended pattern is: 23 | // 24 | // w := files.NewWriter(dst) 25 | // defer w.Cleanup() 26 | // ... write to w ... 27 | // err := w.Close() 28 | func NewWriter(file string) *Writer { 29 | w := &Writer{dst: file} 30 | dir, base := filepath.Dir(file), filepath.Base(file) 31 | w.tmp, w.err = os.CreateTemp(dir, base+".tmp*") 32 | if w.err == nil { 33 | w.tmpName = w.tmp.Name() 34 | } 35 | return w 36 | } 37 | 38 | // Write writes p and returns the number of bytes written, which will either be 39 | // len(p), or the returned error will be non-nil. 40 | func (w *Writer) Write(p []byte) (int, error) { 41 | if w.err != nil { 42 | return 0, w.err 43 | } 44 | if w.tmp == nil { 45 | return 0, fmt.Errorf("%s: already cleaned up", w.dst) 46 | } 47 | n, err := w.tmp.Write(p) 48 | if err != nil { 49 | w.err = err 50 | w.Cleanup() 51 | } 52 | return n, err 53 | } 54 | 55 | // Close saves the written bytes to the destination file and closes the writer. 56 | // Close returns an error if any errors were encountered, including during earlier 57 | // Write calls. 58 | func (w *Writer) Close() error { 59 | if w.err != nil { 60 | return w.err 61 | } 62 | if w.tmp == nil { 63 | return fmt.Errorf("%s: already cleaned up", w.dst) 64 | } 65 | err := w.tmp.Close() 66 | w.tmp = nil 67 | if err != nil { 68 | os.Remove(w.tmpName) 69 | return err 70 | } 71 | if err := os.Rename(w.tmpName, w.dst); err != nil { 72 | os.Remove(w.tmpName) 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | // Cleanup releases any resources in use by the writer, without attempting to write 79 | // collected bytes to the destination. It is safe to call Cleanup() even if Close or 80 | // Cleanup have already been called. 81 | func (w *Writer) Cleanup() { 82 | if w.tmp == nil { 83 | return 84 | } 85 | w.tmp.Close() 86 | w.tmp = nil 87 | os.Remove(w.tmpName) 88 | } 89 | -------------------------------------------------------------------------------- /cmd/kod/internal/generate_file_test.go: -------------------------------------------------------------------------------- 1 | // Package files contains file-related utilities. 2 | package internal 3 | 4 | import ( 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestWriter(t *testing.T) { 13 | file := filepath.Join(t.TempDir(), "test") 14 | w := NewWriter(file) 15 | 16 | _, _ = w.Write([]byte("hello")) 17 | 18 | data, err := os.ReadFile(w.tmpName) 19 | require.Nil(t, err) 20 | require.Equal(t, "hello", string(data)) 21 | 22 | require.Nil(t, w.Close()) 23 | 24 | data, err = os.ReadFile(file) 25 | require.Nil(t, err) 26 | require.Equal(t, "hello", string(data)) 27 | } 28 | 29 | func TestWriter1(t *testing.T) { 30 | file := filepath.Join(t.TempDir(), "test") 31 | w := NewWriter(file) 32 | 33 | _, _ = w.Write([]byte("hello")) 34 | 35 | data, err := os.ReadFile(w.tmpName) 36 | require.Nil(t, err) 37 | require.Equal(t, "hello", string(data)) 38 | 39 | w.Cleanup() 40 | } 41 | 42 | func TestWriter2(t *testing.T) { 43 | file := filepath.Join(t.TempDir(), "test") 44 | w := NewWriter(file) 45 | 46 | w.err = os.ErrExist 47 | 48 | _, _ = w.Write([]byte("hello")) 49 | 50 | data, err := os.ReadFile(w.tmpName) 51 | require.Nil(t, err) 52 | require.Equal(t, "", string(data)) 53 | 54 | w.Cleanup() 55 | } 56 | 57 | func TestWriter3(t *testing.T) { 58 | file := filepath.Join(t.TempDir(), "test") 59 | w := NewWriter(file) 60 | 61 | w.Cleanup() 62 | 63 | _, err := w.Write([]byte("hello")) 64 | 65 | require.NotNil(t, err) 66 | } 67 | 68 | func TestWriter4(t *testing.T) { 69 | file := filepath.Join(t.TempDir(), "test") 70 | w := NewWriter(file) 71 | 72 | w.err = os.ErrExist 73 | 74 | err := w.Close() 75 | 76 | require.NotNil(t, err) 77 | } 78 | 79 | func TestWriter5(t *testing.T) { 80 | file := filepath.Join(t.TempDir(), "test") 81 | w := NewWriter(file) 82 | 83 | w.Cleanup() 84 | 85 | err := w.Close() 86 | 87 | require.NotNil(t, err) 88 | } 89 | -------------------------------------------------------------------------------- /cmd/kod/internal/generate_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGenerate(t *testing.T) { 13 | t.Run("generate basic case", func(t *testing.T) { 14 | err := execute(t, "generate github.com/go-kod/kod/tests/graphcase/... -v") 15 | require.Empty(t, err) 16 | 17 | // Verify generated files exist 18 | require.FileExists(t, filepath.Join("../../../tests/graphcase", "kod_gen.go")) 19 | }) 20 | 21 | t.Run("generate with invalid path", func(t *testing.T) { 22 | err := execute(t, "generate invalid/path") 23 | require.NotEmpty(t, err) 24 | }) 25 | 26 | t.Run("generate without path", func(t *testing.T) { 27 | err := execute(t, "generate") 28 | require.Empty(t, err) 29 | }) 30 | } 31 | 32 | func TestGenerateWithStruct2Interface(t *testing.T) { 33 | t.Run("struct2interface basic case", func(t *testing.T) { 34 | err := execute(t, "generate -s github.com/go-kod/kod/tests/graphcase/... -v") 35 | require.Empty(t, err) 36 | }) 37 | 38 | t.Run("struct2interface with invalid path", func(t *testing.T) { 39 | err := execute(t, "generate -s invalid/path") 40 | require.NotEmpty(t, err) 41 | }) 42 | } 43 | 44 | func TestStartWatch(t *testing.T) { 45 | t.Run("watch with timeout", func(t *testing.T) { 46 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 47 | defer cancel() 48 | startWatcher(ctx, generate, []string{"github.com/go-kod/kod/tests/graphcase/..."}) 49 | }) 50 | 51 | t.Run("watch without path", func(t *testing.T) { 52 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 53 | defer cancel() 54 | startWatcher(ctx, generate, nil) 55 | }) 56 | 57 | t.Run("watch with invalid path", func(t *testing.T) { 58 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 59 | defer cancel() 60 | startWatcher(ctx, generate, []string{"invalid/path"}) 61 | }) 62 | } 63 | 64 | func TestGenerateOptions(t *testing.T) { 65 | t.Run("generate with custom warn function", func(t *testing.T) { 66 | var warnings []error 67 | opt := Options{ 68 | Warn: func(err error) { 69 | warnings = append(warnings, err) 70 | }, 71 | } 72 | 73 | err := Generate(".", []string{"github.com/go-kod/kod/tests/graphcase/..."}, opt) 74 | require.NoError(t, err) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/kod/internal/generate_types.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "sort" 7 | 8 | "golang.org/x/tools/go/packages" 9 | ) 10 | 11 | const kodPackagePath = "github.com/go-kod/kod" 12 | 13 | // typeSet holds type information needed by the code generator. 14 | type typeSet struct { 15 | pkg *packages.Package 16 | imported []importPkg // imported packages 17 | importedByPath map[string]importPkg // imported, indexed by path 18 | importedByName map[string]importPkg // imported, indexed by name 19 | } 20 | 21 | // importPkg is a package imported by the generated code. 22 | type importPkg struct { 23 | path string // e.g., "github.com/go-kod/kod" 24 | pkg string // e.g., "kod", "context", "time" 25 | alias string // e.g., foo in `import foo "context"` 26 | local bool // are we in this package? 27 | } 28 | 29 | // name returns the name by which the imported package should be referenced in 30 | // the generated code. If the package is imported without an alias, like this: 31 | // 32 | // import "context" 33 | // 34 | // then the name is the same as the package name (e.g., "context"). However, if 35 | // a package is imported with an alias, then the name is the alias: 36 | // 37 | // import thisIsAnAlias "context" 38 | // 39 | // If the package is local, an empty string is returned. 40 | func (i importPkg) name() string { 41 | if i.local { 42 | return "" 43 | } else if i.alias != "" { 44 | return i.alias 45 | } 46 | return i.pkg 47 | } 48 | 49 | // qualify returns the provided member of the package, qualified with the 50 | // package name. For example, the "Context" type inside the "context" package 51 | // is qualified "context.Context". The "Now" function inside the "time" package 52 | // is qualified "time.Now". Note that the package name is not prefixed when 53 | // qualifying members of the local package. 54 | func (i importPkg) qualify(member string) string { 55 | if i.local { 56 | return member 57 | } 58 | return fmt.Sprintf("%s.%s", i.name(), member) 59 | } 60 | 61 | // newTypeSet returns the container for types found in pkg. 62 | func newTypeSet(pkg *packages.Package) *typeSet { 63 | return &typeSet{ 64 | pkg: pkg, 65 | imported: []importPkg{}, 66 | importedByPath: map[string]importPkg{}, 67 | importedByName: map[string]importPkg{}, 68 | } 69 | } 70 | 71 | // importPackage imports a package with the provided path and package name. The 72 | // package is imported with an alias if there is a package name clash. 73 | func (tset *typeSet) importPackage(path, pkg string) importPkg { 74 | newImportPkg := func(path, pkg, alias string, local bool) importPkg { 75 | i := importPkg{path: path, pkg: pkg, alias: alias, local: local} 76 | tset.imported = append(tset.imported, i) 77 | tset.importedByPath[i.path] = i 78 | tset.importedByName[i.name()] = i 79 | return i 80 | } 81 | 82 | if imp, ok := tset.importedByPath[path]; ok { 83 | // This package has already been imported. 84 | return imp 85 | } 86 | 87 | if _, ok := tset.importedByName[pkg]; !ok { 88 | // Import the package without an alias. 89 | return newImportPkg(path, pkg, "", path == tset.pkg.PkgPath) 90 | } 91 | 92 | // Find an unused alias. 93 | var alias string 94 | counter := 1 95 | for { 96 | alias = fmt.Sprintf("%s%d", pkg, counter) 97 | if _, ok := tset.importedByName[alias]; !ok { 98 | break 99 | } 100 | counter++ 101 | } 102 | return newImportPkg(path, pkg, alias, path == tset.pkg.PkgPath) 103 | } 104 | 105 | // imports returns the list of packages to import in generated code. 106 | func (tset *typeSet) imports() []importPkg { 107 | sort.Slice(tset.imported, func(i, j int) bool { 108 | return tset.imported[i].path < tset.imported[j].path 109 | }) 110 | return tset.imported 111 | } 112 | 113 | // genTypeString returns the string representation of t as to be printed 114 | // in the generated code, updating import definitions to account for the 115 | // returned type string. 116 | // 117 | // Since this call has side-effects (i.e., updating import definitions), it 118 | // should only be called when the returned type string is written into 119 | // the generated file; otherwise, the generated code may end up with spurious 120 | // imports. 121 | func (tset *typeSet) genTypeString(t types.Type) string { 122 | // qualifier is passed to types.TypeString(Type, Qualifier) to determine 123 | // how packages are printed when pretty printing types. For this qualifier, 124 | // types in the root package are printed without their package name, while 125 | // types outside the root package are printed with their package name. For 126 | // example, if we're in root package foo, then the type foo.Bar is printed 127 | // as Bar, while the type io.Reader is printed as io.Reader. See [1] for 128 | // more information on qualifiers and pretty printing types. 129 | // 130 | // [1]: https://github.com/golang/example/tree/master/gotypes#formatting-support 131 | qualifier := func(pkg *types.Package) string { 132 | if pkg == tset.pkg.Types { 133 | return "" 134 | } 135 | return tset.importPackage(pkg.Path(), pkg.Name()).name() 136 | } 137 | return types.TypeString(t, qualifier) 138 | } 139 | 140 | // isKodType returns true iff t is a named type from the kod package with 141 | // the specified name and n type arguments. 142 | func isKodType(t types.Type, name string, n int) bool { 143 | named, ok := t.(*types.Named) 144 | return ok && 145 | named.Obj().Pkg() != nil && 146 | named.Obj().Pkg().Path() == kodPackagePath && 147 | named.Obj().Name() == name && 148 | named.TypeArgs().Len() == n 149 | } 150 | 151 | func isKodImplements(t types.Type) bool { 152 | return isKodType(t, "Implements", 1) 153 | } 154 | 155 | func isKodRef(t types.Type) bool { 156 | return isKodType(t, "Ref", 1) 157 | } 158 | 159 | func isKodMain(t types.Type) bool { 160 | return isKodType(t, "Main", 0) 161 | } 162 | 163 | func isContext(t types.Type) bool { 164 | n, ok := t.(*types.Named) 165 | if !ok { 166 | return false 167 | } 168 | return n.Obj().Pkg().Path() == "context" && n.Obj().Name() == "Context" 169 | } 170 | 171 | func isError(t types.Type) bool { 172 | n, ok := t.(*types.Named) 173 | if !ok { 174 | return false 175 | } 176 | return n.Obj().Name() == "error" 177 | } 178 | -------------------------------------------------------------------------------- /cmd/kod/internal/mock_watcher_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: watcher.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=watcher.go -destination=mock_watcher_test.go -package=internal 7 | // 8 | 9 | // Package internal is a generated GoMock package. 10 | package internal 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | fsnotify "github.com/fsnotify/fsnotify" 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockWatcher is a mock of Watcher interface. 21 | type MockWatcher struct { 22 | ctrl *gomock.Controller 23 | recorder *MockWatcherMockRecorder 24 | } 25 | 26 | // MockWatcherMockRecorder is the mock recorder for MockWatcher. 27 | type MockWatcherMockRecorder struct { 28 | mock *MockWatcher 29 | } 30 | 31 | // NewMockWatcher creates a new mock instance. 32 | func NewMockWatcher(ctrl *gomock.Controller) *MockWatcher { 33 | mock := &MockWatcher{ctrl: ctrl} 34 | mock.recorder = &MockWatcherMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockWatcher) EXPECT() *MockWatcherMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // Add mocks base method. 44 | func (m *MockWatcher) Add(arg0 string) error { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "Add", arg0) 47 | ret0, _ := ret[0].(error) 48 | return ret0 49 | } 50 | 51 | // Add indicates an expected call of Add. 52 | func (mr *MockWatcherMockRecorder) Add(arg0 any) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockWatcher)(nil).Add), arg0) 55 | } 56 | 57 | // Context mocks base method. 58 | func (m *MockWatcher) Context() context.Context { 59 | m.ctrl.T.Helper() 60 | ret := m.ctrl.Call(m, "Context") 61 | ret0, _ := ret[0].(context.Context) 62 | return ret0 63 | } 64 | 65 | // Context indicates an expected call of Context. 66 | func (mr *MockWatcherMockRecorder) Context() *gomock.Call { 67 | mr.mock.ctrl.T.Helper() 68 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockWatcher)(nil).Context)) 69 | } 70 | 71 | // Errors mocks base method. 72 | func (m *MockWatcher) Errors() chan error { 73 | m.ctrl.T.Helper() 74 | ret := m.ctrl.Call(m, "Errors") 75 | ret0, _ := ret[0].(chan error) 76 | return ret0 77 | } 78 | 79 | // Errors indicates an expected call of Errors. 80 | func (mr *MockWatcherMockRecorder) Errors() *gomock.Call { 81 | mr.mock.ctrl.T.Helper() 82 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errors", reflect.TypeOf((*MockWatcher)(nil).Errors)) 83 | } 84 | 85 | // Events mocks base method. 86 | func (m *MockWatcher) Events() chan fsnotify.Event { 87 | m.ctrl.T.Helper() 88 | ret := m.ctrl.Call(m, "Events") 89 | ret0, _ := ret[0].(chan fsnotify.Event) 90 | return ret0 91 | } 92 | 93 | // Events indicates an expected call of Events. 94 | func (mr *MockWatcherMockRecorder) Events() *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Events", reflect.TypeOf((*MockWatcher)(nil).Events)) 97 | } 98 | 99 | // Remove mocks base method. 100 | func (m *MockWatcher) Remove(arg0 string) error { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "Remove", arg0) 103 | ret0, _ := ret[0].(error) 104 | return ret0 105 | } 106 | 107 | // Remove indicates an expected call of Remove. 108 | func (mr *MockWatcherMockRecorder) Remove(arg0 any) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockWatcher)(nil).Remove), arg0) 111 | } 112 | -------------------------------------------------------------------------------- /cmd/kod/internal/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2023 NAME HERE 3 | */ 4 | package internal 5 | 6 | import ( 7 | "fmt" 8 | "runtime/debug" 9 | 10 | "github.com/samber/lo" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // rootCmd represents the base command when called without any subcommands 15 | var rootCmd = &cobra.Command{ 16 | Use: "kod", 17 | Short: "A powerful tool for writing kod applications.", 18 | // Uncomment the following line if your bare application 19 | // has an action associated with it: 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | if version, _ := cmd.Flags().GetBool("version"); version { 22 | info, ok := debug.ReadBuildInfo() 23 | if ok { 24 | fmt.Printf("%s %s\n", info.Main.Path, info.Main.Version) 25 | } 26 | } 27 | }, 28 | } 29 | 30 | // Execute adds all child commands to the root command and sets flags appropriately. 31 | // This is called by main.main(). It only needs to happen once to the rootCmd. 32 | func Execute() { 33 | lo.Must0(rootCmd.Execute()) 34 | } 35 | 36 | func init() { 37 | // Here you will define your flags and configuration settings. 38 | // Cobra supports persistent flags, which, if defined here, 39 | // will be global for your application. 40 | 41 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.kod.yaml)") 42 | 43 | // Cobra also supports local flags, which will only run 44 | // when this action is called directly. 45 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 46 | rootCmd.Flags().BoolP("version", "v", false, "Help message for toggle") 47 | } 48 | -------------------------------------------------------------------------------- /cmd/kod/internal/root_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestCmd(t *testing.T) { 10 | execute(t, "-v") 11 | } 12 | 13 | func execute(t *testing.T, args string) string { 14 | t.Helper() 15 | 16 | actual := new(bytes.Buffer) 17 | rootCmd.SetOut(actual) 18 | rootCmd.SetErr(actual) 19 | rootCmd.SetArgs(strings.Split(args, " ")) 20 | err := rootCmd.Execute() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | return actual.String() 26 | } 27 | -------------------------------------------------------------------------------- /cmd/kod/internal/struct2interface_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestStruct2Interface(t *testing.T) { 6 | execute(t, "struct2interface ../../../tests/case1 -v") 7 | } 8 | -------------------------------------------------------------------------------- /cmd/kod/internal/watcher.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/samber/lo" 13 | ) 14 | 15 | type Watcher interface { 16 | Add(string) error 17 | Remove(string) error 18 | Events() chan fsnotify.Event 19 | Errors() chan error 20 | Context() context.Context 21 | } 22 | 23 | type watcher struct { 24 | ctx context.Context 25 | w *fsnotify.Watcher 26 | } 27 | 28 | func (w *watcher) Add(name string) error { 29 | return w.w.Add(name) 30 | } 31 | 32 | func (w *watcher) Events() chan fsnotify.Event { 33 | return w.w.Events 34 | } 35 | 36 | func (w *watcher) Errors() chan error { 37 | return w.w.Errors 38 | } 39 | 40 | func (w *watcher) Context() context.Context { 41 | return w.ctx 42 | } 43 | 44 | func (w *watcher) Remove(name string) error { 45 | return w.w.Remove(name) 46 | } 47 | 48 | // Watch watches the directory and calls the callback function when a file is modified. 49 | func Watch(watcher Watcher, dir string, callback func(fsnotify.Event), verbose bool) { 50 | lo.Must0(filepath.Walk(dir, func(path string, info os.FileInfo, _ error) error { 51 | if info != nil && info.IsDir() { 52 | return addWatch(watcher, path) 53 | } 54 | 55 | return nil 56 | })) 57 | 58 | stop := make(chan struct{}, 1) 59 | // Start listening for events. 60 | go func() { 61 | for { 62 | select { 63 | 64 | case event, ok := <-watcher.Events(): 65 | if !ok { 66 | stop <- struct{}{} 67 | return 68 | } 69 | 70 | if verbose { 71 | fmt.Println("event1:", event) 72 | } 73 | 74 | if !validEvent(event) { 75 | continue 76 | } 77 | 78 | if isDir, _ := isDirectory(event.Name); isDir { 79 | if event.Op&fsnotify.Create == fsnotify.Create { 80 | _ = addWatch(watcher, event.Name) 81 | } else if event.Op&fsnotify.Remove == fsnotify.Remove { 82 | _ = watcher.Remove(event.Name) 83 | } 84 | continue 85 | } 86 | 87 | if strings.HasPrefix(filepath.Base(event.Name), "kod_gen") { 88 | continue 89 | } 90 | 91 | if !strings.HasSuffix(filepath.Base(event.Name), ".go") { 92 | continue 93 | } 94 | 95 | log.Println("modified file:", event.Name) 96 | callback(event) 97 | case err, ok := <-watcher.Errors(): 98 | if !ok { 99 | stop <- struct{}{} 100 | return 101 | } 102 | log.Println("error:", err) 103 | stop <- struct{}{} 104 | case <-watcher.Context().Done(): 105 | stop <- struct{}{} 106 | return 107 | } 108 | } 109 | }() 110 | 111 | // Block main goroutine forever. 112 | <-stop 113 | } 114 | 115 | func isHiddenDirectory(path string) bool { 116 | return len(path) > 1 && strings.HasPrefix(path, ".") && filepath.Base(path) != ".." 117 | } 118 | 119 | func validEvent(ev fsnotify.Event) bool { 120 | return ev.Op&fsnotify.Create == fsnotify.Create || 121 | ev.Op&fsnotify.Write == fsnotify.Write || 122 | ev.Op&fsnotify.Remove == fsnotify.Remove 123 | } 124 | 125 | // isDirectory determines if a file represented 126 | // by `path` is a directory or not 127 | func isDirectory(path string) (bool, error) { 128 | fileInfo, err := os.Stat(path) 129 | if err != nil { 130 | return false, err 131 | } 132 | 133 | return fileInfo.IsDir(), err 134 | } 135 | 136 | func addWatch(watcher Watcher, path string) error { 137 | if isHiddenDirectory(path) { 138 | return filepath.SkipDir 139 | } 140 | 141 | err := watcher.Add(path) 142 | if err != nil { 143 | return filepath.SkipDir 144 | } 145 | 146 | fmt.Println("watching", path) 147 | 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /cmd/kod/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import cmd "github.com/go-kod/kod/cmd/kod/internal" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey string 8 | 9 | const kodContextKey contextKey = "kodContext" 10 | 11 | // FromContext returns the Kod value stored in ctx, if any. 12 | func FromContext(ctx context.Context) *Kod { 13 | if v, ok := ctx.Value(kodContextKey).(*Kod); ok { 14 | return v 15 | } 16 | return nil 17 | } 18 | 19 | // newContext returns a new Context that carries value kod. 20 | func newContext(ctx context.Context, kod *Kod) context.Context { 21 | return context.WithValue(ctx, kodContextKey, kod) 22 | } 23 | -------------------------------------------------------------------------------- /examples/helloworld/config.toml: -------------------------------------------------------------------------------- 1 | name = "globalConfig" 2 | 3 | ["github.com/go-kod/kod/examples/helloworld/HelloWorld"] 4 | name = "config" 5 | -------------------------------------------------------------------------------- /examples/helloworld/helloworld.go: -------------------------------------------------------------------------------- 1 | package helloworld 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-kod/kod" 8 | "github.com/go-kod/kod/interceptor" 9 | ) 10 | 11 | type App struct { 12 | kod.Implements[kod.Main] 13 | kod.WithGlobalConfig[GlobalConfig] 14 | 15 | HelloWorld kod.Ref[HelloWorld] 16 | HelloWorldLazy kod.Ref[HelloWorldLazy] 17 | HelloWorldInterceptor kod.Ref[HelloWorldInterceptor] 18 | } 19 | 20 | type GlobalConfig struct { 21 | Name string `default:"kod"` 22 | } 23 | 24 | type Config struct { 25 | Name string `default:"-"` 26 | } 27 | 28 | // HelloWorld ... 29 | type helloWorld struct { 30 | kod.Implements[HelloWorld] 31 | kod.WithConfig[Config] 32 | } 33 | 34 | func (h *helloWorld) Init(_ context.Context) error { 35 | fmt.Println("helloWorld init") 36 | return nil 37 | } 38 | 39 | // SayHello ... 40 | // line two 41 | func (h *helloWorld) SayHello(ctx context.Context) { 42 | h.L(ctx).Info("Hello, World!") 43 | 44 | fmt.Println("Hello, World!" + h.Config().Name) 45 | } 46 | 47 | func (h *helloWorld) Shutdown(_ context.Context) error { 48 | fmt.Println("helloWorld shutdown") 49 | return nil 50 | } 51 | 52 | type lazyHelloWorld struct { 53 | kod.Implements[HelloWorldLazy] 54 | kod.LazyInit 55 | } 56 | 57 | func (h *lazyHelloWorld) Init(_ context.Context) error { 58 | fmt.Println("lazyHelloWorld init") 59 | return nil 60 | } 61 | 62 | // SayHello ... 63 | func (h *lazyHelloWorld) SayHello(_ context.Context) { 64 | fmt.Println("Hello, Lazy!") 65 | } 66 | 67 | func (h *lazyHelloWorld) Shutdown(_ context.Context) error { 68 | fmt.Println("lazyHelloWorld shutdown") 69 | return nil 70 | } 71 | 72 | // helloWorldInterceptor ... 73 | type helloWorldInterceptor struct { 74 | kod.Implements[HelloWorldInterceptor] 75 | } 76 | 77 | // SayHello ... 78 | func (h *helloWorldInterceptor) SayHello(_ context.Context) { 79 | fmt.Println("Hello, Interceptor!") 80 | } 81 | 82 | func (h *helloWorldInterceptor) Interceptors() []interceptor.Interceptor { 83 | return []interceptor.Interceptor{ 84 | func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 85 | fmt.Println("Before call") 86 | err := invoker(ctx, info, req, reply) 87 | fmt.Println("After call") 88 | return err 89 | }, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /examples/helloworld/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package helloworld 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // HelloWorld is implemented by [helloWorld], 10 | // which can be mocked with [NewMockHelloWorld]. 11 | // 12 | // HelloWorld ... 13 | type HelloWorld interface { 14 | // SayHello is implemented by [helloWorld.SayHello] 15 | // 16 | // SayHello ... 17 | // line two 18 | SayHello(ctx context.Context) 19 | } 20 | 21 | // HelloWorldLazy is implemented by [lazyHelloWorld], 22 | // which can be mocked with [NewMockHelloWorldLazy]. 23 | type HelloWorldLazy interface { 24 | // SayHello is implemented by [lazyHelloWorld.SayHello] 25 | // 26 | // SayHello ... 27 | SayHello(_ context.Context) 28 | } 29 | 30 | // HelloWorldInterceptor is implemented by [helloWorldInterceptor], 31 | // which can be mocked with [NewMockHelloWorldInterceptor]. 32 | // 33 | // helloWorldInterceptor ... 34 | type HelloWorldInterceptor interface { 35 | // SayHello is implemented by [helloWorldInterceptor.SayHello] 36 | // 37 | // SayHello ... 38 | SayHello(_ context.Context) 39 | } 40 | -------------------------------------------------------------------------------- /examples/helloworld/kod_gen_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !ignoreKodGen 2 | 3 | // Code generated by MockGen. DO NOT EDIT. 4 | // Source: examples/helloworld/kod_gen_interface.go 5 | // 6 | // Generated by this command: 7 | // 8 | // mockgen -source examples/helloworld/kod_gen_interface.go -destination examples/helloworld/kod_gen_mock.go -package helloworld -typed -build_constraint !ignoreKodGen 9 | // 10 | 11 | // Package helloworld is a generated GoMock package. 12 | package helloworld 13 | 14 | import ( 15 | context "context" 16 | reflect "reflect" 17 | 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockHelloWorld is a mock of HelloWorld interface. 22 | type MockHelloWorld struct { 23 | ctrl *gomock.Controller 24 | recorder *MockHelloWorldMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockHelloWorldMockRecorder is the mock recorder for MockHelloWorld. 29 | type MockHelloWorldMockRecorder struct { 30 | mock *MockHelloWorld 31 | } 32 | 33 | // NewMockHelloWorld creates a new mock instance. 34 | func NewMockHelloWorld(ctrl *gomock.Controller) *MockHelloWorld { 35 | mock := &MockHelloWorld{ctrl: ctrl} 36 | mock.recorder = &MockHelloWorldMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockHelloWorld) EXPECT() *MockHelloWorldMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // SayHello mocks base method. 46 | func (m *MockHelloWorld) SayHello(ctx context.Context) { 47 | m.ctrl.T.Helper() 48 | m.ctrl.Call(m, "SayHello", ctx) 49 | } 50 | 51 | // SayHello indicates an expected call of SayHello. 52 | func (mr *MockHelloWorldMockRecorder) SayHello(ctx any) *MockHelloWorldSayHelloCall { 53 | mr.mock.ctrl.T.Helper() 54 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SayHello", reflect.TypeOf((*MockHelloWorld)(nil).SayHello), ctx) 55 | return &MockHelloWorldSayHelloCall{Call: call} 56 | } 57 | 58 | // MockHelloWorldSayHelloCall wrap *gomock.Call 59 | type MockHelloWorldSayHelloCall struct { 60 | *gomock.Call 61 | } 62 | 63 | // Return rewrite *gomock.Call.Return 64 | func (c *MockHelloWorldSayHelloCall) Return() *MockHelloWorldSayHelloCall { 65 | c.Call = c.Call.Return() 66 | return c 67 | } 68 | 69 | // Do rewrite *gomock.Call.Do 70 | func (c *MockHelloWorldSayHelloCall) Do(f func(context.Context)) *MockHelloWorldSayHelloCall { 71 | c.Call = c.Call.Do(f) 72 | return c 73 | } 74 | 75 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 76 | func (c *MockHelloWorldSayHelloCall) DoAndReturn(f func(context.Context)) *MockHelloWorldSayHelloCall { 77 | c.Call = c.Call.DoAndReturn(f) 78 | return c 79 | } 80 | 81 | // MockHelloWorldLazy is a mock of HelloWorldLazy interface. 82 | type MockHelloWorldLazy struct { 83 | ctrl *gomock.Controller 84 | recorder *MockHelloWorldLazyMockRecorder 85 | isgomock struct{} 86 | } 87 | 88 | // MockHelloWorldLazyMockRecorder is the mock recorder for MockHelloWorldLazy. 89 | type MockHelloWorldLazyMockRecorder struct { 90 | mock *MockHelloWorldLazy 91 | } 92 | 93 | // NewMockHelloWorldLazy creates a new mock instance. 94 | func NewMockHelloWorldLazy(ctrl *gomock.Controller) *MockHelloWorldLazy { 95 | mock := &MockHelloWorldLazy{ctrl: ctrl} 96 | mock.recorder = &MockHelloWorldLazyMockRecorder{mock} 97 | return mock 98 | } 99 | 100 | // EXPECT returns an object that allows the caller to indicate expected use. 101 | func (m *MockHelloWorldLazy) EXPECT() *MockHelloWorldLazyMockRecorder { 102 | return m.recorder 103 | } 104 | 105 | // SayHello mocks base method. 106 | func (m *MockHelloWorldLazy) SayHello(arg0 context.Context) { 107 | m.ctrl.T.Helper() 108 | m.ctrl.Call(m, "SayHello", arg0) 109 | } 110 | 111 | // SayHello indicates an expected call of SayHello. 112 | func (mr *MockHelloWorldLazyMockRecorder) SayHello(arg0 any) *MockHelloWorldLazySayHelloCall { 113 | mr.mock.ctrl.T.Helper() 114 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SayHello", reflect.TypeOf((*MockHelloWorldLazy)(nil).SayHello), arg0) 115 | return &MockHelloWorldLazySayHelloCall{Call: call} 116 | } 117 | 118 | // MockHelloWorldLazySayHelloCall wrap *gomock.Call 119 | type MockHelloWorldLazySayHelloCall struct { 120 | *gomock.Call 121 | } 122 | 123 | // Return rewrite *gomock.Call.Return 124 | func (c *MockHelloWorldLazySayHelloCall) Return() *MockHelloWorldLazySayHelloCall { 125 | c.Call = c.Call.Return() 126 | return c 127 | } 128 | 129 | // Do rewrite *gomock.Call.Do 130 | func (c *MockHelloWorldLazySayHelloCall) Do(f func(context.Context)) *MockHelloWorldLazySayHelloCall { 131 | c.Call = c.Call.Do(f) 132 | return c 133 | } 134 | 135 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 136 | func (c *MockHelloWorldLazySayHelloCall) DoAndReturn(f func(context.Context)) *MockHelloWorldLazySayHelloCall { 137 | c.Call = c.Call.DoAndReturn(f) 138 | return c 139 | } 140 | 141 | // MockHelloWorldInterceptor is a mock of HelloWorldInterceptor interface. 142 | type MockHelloWorldInterceptor struct { 143 | ctrl *gomock.Controller 144 | recorder *MockHelloWorldInterceptorMockRecorder 145 | isgomock struct{} 146 | } 147 | 148 | // MockHelloWorldInterceptorMockRecorder is the mock recorder for MockHelloWorldInterceptor. 149 | type MockHelloWorldInterceptorMockRecorder struct { 150 | mock *MockHelloWorldInterceptor 151 | } 152 | 153 | // NewMockHelloWorldInterceptor creates a new mock instance. 154 | func NewMockHelloWorldInterceptor(ctrl *gomock.Controller) *MockHelloWorldInterceptor { 155 | mock := &MockHelloWorldInterceptor{ctrl: ctrl} 156 | mock.recorder = &MockHelloWorldInterceptorMockRecorder{mock} 157 | return mock 158 | } 159 | 160 | // EXPECT returns an object that allows the caller to indicate expected use. 161 | func (m *MockHelloWorldInterceptor) EXPECT() *MockHelloWorldInterceptorMockRecorder { 162 | return m.recorder 163 | } 164 | 165 | // SayHello mocks base method. 166 | func (m *MockHelloWorldInterceptor) SayHello(arg0 context.Context) { 167 | m.ctrl.T.Helper() 168 | m.ctrl.Call(m, "SayHello", arg0) 169 | } 170 | 171 | // SayHello indicates an expected call of SayHello. 172 | func (mr *MockHelloWorldInterceptorMockRecorder) SayHello(arg0 any) *MockHelloWorldInterceptorSayHelloCall { 173 | mr.mock.ctrl.T.Helper() 174 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SayHello", reflect.TypeOf((*MockHelloWorldInterceptor)(nil).SayHello), arg0) 175 | return &MockHelloWorldInterceptorSayHelloCall{Call: call} 176 | } 177 | 178 | // MockHelloWorldInterceptorSayHelloCall wrap *gomock.Call 179 | type MockHelloWorldInterceptorSayHelloCall struct { 180 | *gomock.Call 181 | } 182 | 183 | // Return rewrite *gomock.Call.Return 184 | func (c *MockHelloWorldInterceptorSayHelloCall) Return() *MockHelloWorldInterceptorSayHelloCall { 185 | c.Call = c.Call.Return() 186 | return c 187 | } 188 | 189 | // Do rewrite *gomock.Call.Do 190 | func (c *MockHelloWorldInterceptorSayHelloCall) Do(f func(context.Context)) *MockHelloWorldInterceptorSayHelloCall { 191 | c.Call = c.Call.Do(f) 192 | return c 193 | } 194 | 195 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 196 | func (c *MockHelloWorldInterceptorSayHelloCall) DoAndReturn(f func(context.Context)) *MockHelloWorldInterceptorSayHelloCall { 197 | c.Call = c.Call.DoAndReturn(f) 198 | return c 199 | } 200 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-kod/kod 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/avast/retry-go/v4 v4.6.1 9 | github.com/creasty/defaults v1.8.0 10 | github.com/dominikbraun/graph v0.23.0 11 | github.com/fatih/color v1.18.0 12 | github.com/fsnotify/fsnotify v1.9.0 13 | github.com/go-playground/validator/v10 v10.26.0 14 | github.com/knadh/koanf/parsers/json v1.0.0 15 | github.com/knadh/koanf/parsers/toml/v2 v2.2.0 16 | github.com/knadh/koanf/parsers/yaml v1.0.0 17 | github.com/knadh/koanf/providers/env v1.1.0 18 | github.com/knadh/koanf/providers/file v1.2.0 19 | github.com/knadh/koanf/v2 v2.2.0 20 | github.com/samber/lo v1.50.0 21 | github.com/shirou/gopsutil/v3 v3.24.5 22 | github.com/sony/gobreaker v1.0.0 23 | github.com/spf13/cobra v1.9.1 24 | github.com/stretchr/testify v1.10.0 25 | go.opentelemetry.io/otel v1.35.0 26 | go.opentelemetry.io/otel/metric v1.35.0 27 | go.opentelemetry.io/otel/sdk v1.35.0 28 | go.opentelemetry.io/otel/trace v1.35.0 29 | go.uber.org/goleak v1.3.0 30 | go.uber.org/mock v0.5.1 31 | golang.org/x/tools v0.32.0 32 | google.golang.org/grpc v1.72.0 33 | ) 34 | 35 | require ( 36 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 37 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 38 | github.com/go-logr/logr v1.4.2 // indirect 39 | github.com/go-logr/stdr v1.2.2 // indirect 40 | github.com/go-ole/go-ole v1.3.0 // indirect 41 | github.com/go-playground/locales v0.14.1 // indirect 42 | github.com/go-playground/universal-translator v0.18.1 // indirect 43 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 44 | github.com/google/uuid v1.6.0 // indirect 45 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 46 | github.com/knadh/koanf/maps v0.1.2 // indirect 47 | github.com/leodido/go-urn v1.4.0 // indirect 48 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 49 | github.com/mattn/go-colorable v0.1.13 // indirect 50 | github.com/mattn/go-isatty v0.0.20 // indirect 51 | github.com/mitchellh/copystructure v1.2.0 // indirect 52 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 53 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 55 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 56 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 57 | github.com/spf13/pflag v1.0.6 // indirect 58 | github.com/tklauser/go-sysconf v0.3.14 // indirect 59 | github.com/tklauser/numcpus v0.9.0 // indirect 60 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 61 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 62 | golang.org/x/crypto v0.37.0 // indirect 63 | golang.org/x/mod v0.24.0 // indirect 64 | golang.org/x/net v0.39.0 // indirect 65 | golang.org/x/sync v0.13.0 // indirect 66 | golang.org/x/sys v0.32.0 // indirect 67 | golang.org/x/text v0.24.0 // indirect 68 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 69 | google.golang.org/protobuf v1.36.5 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /interceptor/interceptor.go: -------------------------------------------------------------------------------- 1 | package interceptor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kod/kod/internal/singleton" 7 | ) 8 | 9 | // CallInfo contains information about the call. 10 | type CallInfo struct { 11 | // The impl of the called component. 12 | Impl any 13 | // The full name of the called method, in the format of "package/service.method". 14 | FullMethod string 15 | } 16 | 17 | // HandleFunc is the type of the function invoked by Components. 18 | type HandleFunc func(ctx context.Context, info CallInfo, req, reply []any) error 19 | 20 | // Interceptor is the type of the function used to intercept Components. 21 | type Interceptor func(ctx context.Context, info CallInfo, req, reply []any, invoker HandleFunc) error 22 | 23 | // Condition is the type of the function used to determine whether an interceptor should be used. 24 | type Condition func(ctx context.Context, info CallInfo) bool 25 | 26 | // pool is a singleton for interceptors. 27 | var pool = singleton.New[Interceptor]() 28 | 29 | // SingletonByFullMethod returns an Interceptor that is a singleton for the given method. 30 | func SingletonByFullMethod(initFn func() Interceptor) Interceptor { 31 | return func(ctx context.Context, info CallInfo, req, reply []any, invoker HandleFunc) error { 32 | interceptor := pool.Get(info.FullMethod, initFn) 33 | 34 | return interceptor(ctx, info, req, reply, invoker) 35 | } 36 | } 37 | 38 | // Chain converts a slice of Interceptors into a single Interceptor. 39 | func Chain(interceptors []Interceptor) Interceptor { 40 | if len(interceptors) == 0 { 41 | return nil 42 | } 43 | 44 | return func(ctx context.Context, info CallInfo, req, reply []any, invoker HandleFunc) error { 45 | // Build the interceptor chain. 46 | chain := buildInterceptorChain(invoker, interceptors, 0) 47 | return chain(ctx, info, req, reply) 48 | } 49 | } 50 | 51 | // buildInterceptorChain recursively constructs a chain of interceptors. 52 | func buildInterceptorChain(invoker HandleFunc, interceptors []Interceptor, current int) HandleFunc { 53 | if current == len(interceptors) { 54 | return invoker 55 | } 56 | 57 | return func(ctx context.Context, info CallInfo, req, reply []any) error { 58 | return interceptors[current](ctx, info, req, reply, buildInterceptorChain(invoker, interceptors, current+1)) 59 | } 60 | } 61 | 62 | // If returns an Interceptor that only invokes the given interceptor if the given condition is true. 63 | func If(interceptor Interceptor, condition Condition) Interceptor { 64 | return func(ctx context.Context, info CallInfo, req, reply []any, invoker HandleFunc) error { 65 | if condition(ctx, info) { 66 | return interceptor(ctx, info, req, reply, invoker) 67 | } 68 | 69 | return invoker(ctx, info, req, reply) 70 | } 71 | } 72 | 73 | // And groups conditions with the AND operator. 74 | func And(first, second Condition, conditions ...Condition) Condition { 75 | return func(ctx context.Context, info CallInfo) bool { 76 | if !first(ctx, info) || !second(ctx, info) { 77 | return false 78 | } 79 | for _, condition := range conditions { 80 | if !condition(ctx, info) { 81 | return false 82 | } 83 | } 84 | 85 | return true 86 | } 87 | } 88 | 89 | // Or groups conditions with the OR operator. 90 | func Or(first, second Condition, conditions ...Condition) Condition { 91 | return func(ctx context.Context, info CallInfo) bool { 92 | if first(ctx, info) || second(ctx, info) { 93 | return true 94 | } 95 | for _, condition := range conditions { 96 | if condition(ctx, info) { 97 | return true 98 | } 99 | } 100 | 101 | return false 102 | } 103 | } 104 | 105 | // Not negates the given condition. 106 | func Not(condition Condition) Condition { 107 | return func(ctx context.Context, info CallInfo) bool { 108 | return !condition(ctx, info) 109 | } 110 | } 111 | 112 | // IsMethod returns a condition that checks if the method name matches the given method. 113 | func IsMethod(method string) Condition { 114 | return func(_ context.Context, info CallInfo) bool { 115 | return info.FullMethod == method 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /interceptor/internal/circuitbreaker/circuitbreaker.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/sony/gobreaker" 10 | 11 | "github.com/go-kod/kod/interceptor/internal/kerror" 12 | ) 13 | 14 | type CircuitBreaker struct { 15 | breaker *gobreaker.TwoStepCircuitBreaker 16 | } 17 | 18 | // newCircuitBreaker creates TwoStepCircuitBreaker with suitable settings. 19 | // 20 | // Name is the name of the CircuitBreaker. 21 | // 22 | // MaxRequests is the maximum number of requests allowed to pass through 23 | // when the CircuitBreaker is half-open. 24 | // If MaxRequests is 0, the CircuitBreaker allows only 1 request. 25 | // 26 | // Interval is the cyclic period of the closed state 27 | // for the CircuitBreaker to clear the internal Counts. 28 | // If Interval is 0, the CircuitBreaker doesn't clear internal Counts during the closed state. 29 | // 30 | // Timeout is the period of the open state, 31 | // after which the state of the CircuitBreaker becomes half-open. 32 | // If Timeout is 0, the timeout value of the CircuitBreaker is set to 60 seconds. 33 | // 34 | // ReadyToTrip is called with a copy of Counts whenever a request fails in the closed state. 35 | // If ReadyToTrip returns true, the CircuitBreaker will be placed into the open state. 36 | // If ReadyToTrip is nil, default ReadyToTrip is used. 37 | // 38 | // Default settings: 39 | // MaxRequests: 3 40 | // Interval: 5 * time.Second 41 | // Timeout: 10 * time.Second 42 | // ReadyToTrip: DefaultReadyToTrip 43 | func NewCircuitBreaker(_ context.Context, name string) *CircuitBreaker { 44 | return &CircuitBreaker{ 45 | breaker: gobreaker.NewTwoStepCircuitBreaker(gobreaker.Settings{ 46 | Name: name, 47 | MaxRequests: 3, 48 | Interval: 5 * time.Second, 49 | Timeout: 10 * time.Second, 50 | ReadyToTrip: defaultReadyToTrip, 51 | OnStateChange: func(name string, from, to gobreaker.State) { 52 | fmt.Fprintf(os.Stderr, "circuit breaker state change: %s %s -> %s\n", name, from, to) 53 | }, 54 | }), 55 | } 56 | } 57 | 58 | func (c *CircuitBreaker) Allow() (func(error), error) { 59 | done, err := c.breaker.Allow() 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return func(err error) { 65 | done(!kerror.IsCritical(err)) 66 | }, nil 67 | } 68 | 69 | // DefaultReadyToTrip returns true when the number of consecutive failures is more than 3 and rate of failure is more than 60%. 70 | func defaultReadyToTrip(counts gobreaker.Counts) bool { 71 | failureRatio := float64(counts.TotalFailures) / float64(counts.Requests) 72 | return counts.ConsecutiveFailures >= 3 && failureRatio >= 0.6 73 | } 74 | -------------------------------------------------------------------------------- /interceptor/internal/circuitbreaker/circuitbreaker_test.go: -------------------------------------------------------------------------------- 1 | package circuitbreaker 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/sony/gobreaker" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCircuitBreaker(t *testing.T) { 13 | t.Run("case 1", func(t *testing.T) { 14 | cb := NewCircuitBreaker(context.Background(), "case1") 15 | done, err := cb.Allow() 16 | assert.Nil(t, err) 17 | done(context.Canceled) 18 | done, err = cb.Allow() 19 | assert.Nil(t, err) 20 | done(context.Canceled) 21 | done, err = cb.Allow() 22 | assert.Nil(t, err) 23 | done(context.Canceled) 24 | _, err = cb.Allow() 25 | assert.Equal(t, gobreaker.ErrOpenState, err) 26 | _, err = cb.Allow() 27 | assert.Equal(t, gobreaker.ErrOpenState, err) 28 | _, err = cb.Allow() 29 | assert.Equal(t, gobreaker.ErrOpenState, err) 30 | _, err = cb.Allow() 31 | assert.Equal(t, gobreaker.ErrOpenState, err) 32 | _, err = cb.Allow() 33 | assert.Equal(t, gobreaker.ErrOpenState, err) 34 | _, err = cb.Allow() 35 | assert.Equal(t, gobreaker.ErrOpenState, err) 36 | 37 | time.Sleep(time.Second) 38 | _, err = cb.Allow() 39 | assert.Equal(t, gobreaker.ErrOpenState, err) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /interceptor/internal/kerror/error.go: -------------------------------------------------------------------------------- 1 | package kerror 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | // IsCritical returns true if the error is critical. 12 | func IsCritical(err error) bool { 13 | if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { 14 | return true 15 | } 16 | 17 | switch status.Code(err) { 18 | case codes.Canceled, 19 | codes.DeadlineExceeded, 20 | codes.ResourceExhausted, 21 | codes.Aborted, 22 | codes.Internal, 23 | codes.Unavailable: 24 | return true 25 | } 26 | 27 | return false 28 | } 29 | 30 | // IsSystem returns true if the error is a system error. 31 | func IsSystem(err error) bool { 32 | if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { 33 | return true 34 | } 35 | 36 | switch status.Code(err) { 37 | case codes.Canceled, 38 | codes.DeadlineExceeded, 39 | codes.ResourceExhausted, 40 | codes.Aborted, 41 | codes.Internal, 42 | codes.Unavailable, 43 | codes.Unknown: 44 | return true 45 | } 46 | 47 | return false 48 | } 49 | 50 | // IsBusiness returns true if the error is a business error. 51 | func IsBusiness(err error) bool { 52 | return err != nil && !IsSystem(err) 53 | } 54 | -------------------------------------------------------------------------------- /interceptor/internal/kerror/error_test.go: -------------------------------------------------------------------------------- 1 | package kerror 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "google.golang.org/grpc/codes" 8 | "google.golang.org/grpc/status" 9 | ) 10 | 11 | func TestIsSuccessful(t *testing.T) { 12 | type args struct { 13 | err error 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want bool 19 | }{ 20 | {"case 1", args{nil}, true}, 21 | {"case 2", args{context.DeadlineExceeded}, false}, 22 | {"case 3", args{context.Canceled}, false}, 23 | {"case 4", args{status.Error(codes.DeadlineExceeded, "")}, false}, 24 | {"case 5", args{status.Error(codes.ResourceExhausted, "")}, false}, 25 | {"case 6", args{status.Error(codes.Canceled, "")}, false}, 26 | {"case 7", args{status.Error(codes.Aborted, "")}, false}, 27 | {"case 8", args{status.Error(codes.Internal, "")}, false}, 28 | {"case 9", args{status.Error(codes.Unavailable, "")}, false}, 29 | } 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | if got := !IsCritical(tt.args.err); got != tt.want { 33 | t.Errorf("IsSuccessful() = %v, want %v", got, tt.want) 34 | } 35 | }) 36 | } 37 | } 38 | 39 | func TestIsError(t *testing.T) { 40 | type args struct { 41 | err error 42 | } 43 | tests := []struct { 44 | name string 45 | args args 46 | want bool 47 | }{ 48 | {"case 1", args{nil}, true}, 49 | {"case 2", args{context.DeadlineExceeded}, false}, 50 | {"case 3", args{context.Canceled}, false}, 51 | {"case 4", args{status.Error(codes.DeadlineExceeded, "")}, false}, 52 | {"case 5", args{status.Error(codes.ResourceExhausted, "")}, false}, 53 | {"case 6", args{status.Error(codes.Canceled, "")}, false}, 54 | {"case 7", args{status.Error(codes.Aborted, "")}, false}, 55 | {"case 8", args{status.Error(codes.Internal, "")}, false}, 56 | {"case 9", args{status.Error(codes.Unavailable, "")}, false}, 57 | {"case 10", args{status.Error(codes.Unknown, "")}, false}, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | if got := !IsSystem(tt.args.err); got != tt.want { 62 | t.Errorf("IsError() = %v, want %v", got, tt.want) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestIsBusinessError(t *testing.T) { 69 | type args struct { 70 | err error 71 | } 72 | tests := []struct { 73 | name string 74 | args args 75 | want bool 76 | }{ 77 | {"case 1", args{nil}, false}, 78 | {"case 2", args{context.DeadlineExceeded}, false}, 79 | {"case 3", args{context.Canceled}, false}, 80 | {"case 4", args{status.Error(codes.DeadlineExceeded, "")}, false}, 81 | {"case 5", args{status.Error(codes.ResourceExhausted, "")}, false}, 82 | {"case 6", args{status.Error(codes.Canceled, "")}, false}, 83 | {"case 7", args{status.Error(codes.Aborted, "")}, false}, 84 | {"case 8", args{status.Error(codes.Internal, "")}, false}, 85 | {"case 9", args{status.Error(codes.Unavailable, "")}, false}, 86 | {"case 10", args{status.Error(codes.Unknown, "")}, false}, 87 | {"case 11", args{status.Error(codes.Unauthenticated+1, "")}, true}, 88 | } 89 | for _, tt := range tests { 90 | t.Run(tt.name, func(t *testing.T) { 91 | if got := IsBusiness(tt.args.err); got != tt.want { 92 | t.Errorf("IsBusinessError() = %v, want %v", got, tt.want) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /interceptor/internal/ratelimit/ratelimit_test.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestBBR(t *testing.T) { 10 | t.Run("case 1ms", func(t *testing.T) { 11 | bbr := NewLimiter(context.Background(), "bbr") 12 | bbr.opts.CPUThreshold = 0 13 | 14 | done, err := bbr.Allow() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | time.Sleep(time.Millisecond) 19 | done() 20 | 21 | _, _ = bbr.Allow() 22 | _, _ = bbr.Allow() 23 | 24 | _, err = bbr.Allow() 25 | if err != ErrLimitExceed { 26 | t.Fatal(err) 27 | } 28 | }) 29 | 30 | t.Run("case 1s", func(t *testing.T) { 31 | bbr := NewLimiter(context.Background(), "bbr") 32 | 33 | done, err := bbr.Allow() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | time.Sleep(time.Second) 38 | done() 39 | 40 | _, _ = bbr.Allow() 41 | _, _ = bbr.Allow() 42 | 43 | _, _ = bbr.Allow() 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | stat := bbr.Stat() 49 | if stat.InFlight != 3 { 50 | t.Fatal("inflight not expected") 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /interceptor/kaccesslog/accesslog.go: -------------------------------------------------------------------------------- 1 | package kaccesslog 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/go-kod/kod/interceptor" 9 | "github.com/go-kod/kod/interceptor/internal/kerror" 10 | ) 11 | 12 | // Interceptor returns a server interceptor that logs requests and responses. 13 | func Interceptor() interceptor.Interceptor { 14 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 15 | now := time.Now() 16 | 17 | err := invoker(ctx, info, req, reply) 18 | 19 | attrs := []slog.Attr{ 20 | slog.Any("req", req), 21 | slog.Any("reply", reply), 22 | slog.String("method", info.FullMethod), 23 | slog.Int64("cost", time.Since(now).Milliseconds()), 24 | } 25 | 26 | level := slog.LevelInfo 27 | if err != nil { 28 | level = slog.LevelError 29 | if kerror.IsBusiness(err) { 30 | level = slog.LevelWarn 31 | } 32 | 33 | attrs = append(attrs, slog.String("error", err.Error())) 34 | } 35 | 36 | // check if impl L(ctx context.Context) method 37 | if l, ok := info.Impl.(interface { 38 | L(context.Context) *slog.Logger 39 | }); ok { 40 | l.L(ctx).LogAttrs(ctx, level, "access_log", attrs...) 41 | } 42 | 43 | return err 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /interceptor/kcircuitbreaker/circuitbreaker.go: -------------------------------------------------------------------------------- 1 | package kcircuitbreaker 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/go-kod/kod/interceptor" 9 | "github.com/go-kod/kod/interceptor/internal/circuitbreaker" 10 | "github.com/go-kod/kod/internal/singleton" 11 | ) 12 | 13 | var ( 14 | once sync.Once 15 | pool *singleton.Singleton[*circuitbreaker.CircuitBreaker] 16 | ) 17 | 18 | // Interceptor returns an interceptor do circuit breaker. 19 | func Interceptor() interceptor.Interceptor { 20 | once.Do(func() { 21 | pool = singleton.New[*circuitbreaker.CircuitBreaker]() 22 | }) 23 | 24 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 25 | breaker := pool.Get(info.FullMethod, func() *circuitbreaker.CircuitBreaker { 26 | return circuitbreaker.NewCircuitBreaker(ctx, info.FullMethod) 27 | }) 28 | 29 | done, err := breaker.Allow() 30 | if err != nil { 31 | return fmt.Errorf("kcircuitbreaker: %w", err) 32 | } 33 | 34 | err = invoker(ctx, info, req, reply) 35 | 36 | done(err) 37 | 38 | return err 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /interceptor/kmetric/metric.go: -------------------------------------------------------------------------------- 1 | package kmetric 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/samber/lo" 8 | "go.opentelemetry.io/otel" 9 | "go.opentelemetry.io/otel/attribute" 10 | "go.opentelemetry.io/otel/metric" 11 | 12 | "github.com/go-kod/kod" 13 | "github.com/go-kod/kod/interceptor" 14 | ) 15 | 16 | var ( 17 | methodCounts = lo.Must(otel.Meter(kod.PkgPath).Int64Counter("kod.component.count", 18 | metric.WithDescription("Count of Kod component method invocations"), 19 | )) 20 | 21 | methodErrors = lo.Must(otel.Meter(kod.PkgPath).Int64Counter("kod.component.error", 22 | metric.WithDescription("Count of Kod component method invocations that result in an error"), 23 | )) 24 | 25 | methodDurations = lo.Must(otel.Meter(kod.PkgPath).Float64Histogram("kod.component.duration", 26 | metric.WithDescription("Duration, in microseconds, of Kod component method execution"), 27 | metric.WithUnit("ms"), 28 | )) 29 | ) 30 | 31 | // Interceptor returns an interceptor that adds OpenTelemetry metrics to the context. 32 | func Interceptor() interceptor.Interceptor { 33 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) (err error) { 34 | now := time.Now() 35 | 36 | err = invoker(ctx, info, req, reply) 37 | 38 | as := attribute.NewSet( 39 | attribute.String("method", info.FullMethod), 40 | ) 41 | 42 | if err != nil { 43 | methodErrors.Add(ctx, 1, metric.WithAttributeSet(as)) 44 | } 45 | 46 | methodCounts.Add(ctx, 1, metric.WithAttributeSet(as)) 47 | methodDurations.Record(ctx, float64(time.Since(now).Milliseconds()), metric.WithAttributeSet(as)) 48 | 49 | return err 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /interceptor/kratelimit/ratelimit.go: -------------------------------------------------------------------------------- 1 | package kratelimit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/go-kod/kod/interceptor" 9 | "github.com/go-kod/kod/interceptor/internal/ratelimit" 10 | "github.com/go-kod/kod/internal/singleton" 11 | ) 12 | 13 | var ( 14 | once sync.Once 15 | pool *singleton.Singleton[*ratelimit.Ratelimit] 16 | ) 17 | 18 | // Interceptor returns an interceptor do rate limit. 19 | func Interceptor() interceptor.Interceptor { 20 | once.Do(func() { 21 | pool = singleton.New[*ratelimit.Ratelimit]() 22 | }) 23 | 24 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 25 | limitor := pool.Get(info.FullMethod, func() *ratelimit.Ratelimit { 26 | return ratelimit.NewLimiter(ctx, info.FullMethod) 27 | }) 28 | 29 | done, err := limitor.Allow() 30 | if err != nil { 31 | return fmt.Errorf("kratelimit: %w", err) 32 | } 33 | 34 | err = invoker(ctx, info, req, reply) 35 | 36 | done() 37 | 38 | return err 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /interceptor/krecovery/recovery.go: -------------------------------------------------------------------------------- 1 | package krecovery 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | 9 | "github.com/go-kod/kod/interceptor" 10 | ) 11 | 12 | type panicError struct { 13 | Panic any 14 | Stack []byte 15 | } 16 | 17 | func (e *panicError) Error() string { 18 | return fmt.Sprintf("panic caught: %v\n\n%s", e.Panic, e.Stack) 19 | } 20 | 21 | func recoverFrom(p any) error { 22 | stack := make([]byte, 64<<10) 23 | stack = stack[:runtime.Stack(stack, false)] 24 | 25 | return &panicError{Panic: p, Stack: stack} 26 | } 27 | 28 | // Interceptor returns an interceptor that recovers from panics. 29 | func Interceptor() interceptor.Interceptor { 30 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) (err error) { 31 | normalReturn := false 32 | defer func() { 33 | if !normalReturn { 34 | if r := recover(); r != nil { 35 | err = recoverFrom(r) 36 | os.Stderr.Write([]byte(err.Error())) 37 | } 38 | } 39 | }() 40 | 41 | err = invoker(ctx, info, req, reply) 42 | normalReturn = true 43 | 44 | return err 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /interceptor/kretry/retry.go: -------------------------------------------------------------------------------- 1 | package kretry 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/avast/retry-go/v4" 8 | 9 | "github.com/go-kod/kod/interceptor" 10 | ) 11 | 12 | // Interceptor returns a interceptor that retries the call specified by info. 13 | func Interceptor(opts ...retry.Option) interceptor.Interceptor { 14 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 15 | err := retry.Do(func() error { 16 | return invoker(ctx, info, req, reply) 17 | }, opts...) 18 | if err != nil { 19 | return fmt.Errorf("retry failed: %w", err) 20 | } 21 | 22 | return nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /interceptor/ktimeout/timeout.go: -------------------------------------------------------------------------------- 1 | package ktimeout 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-kod/kod/interceptor" 8 | ) 9 | 10 | type Options struct { 11 | Timeout time.Duration 12 | } 13 | 14 | // WithTimeout sets the timeout for the interceptor. 15 | func WithTimeout(timeout time.Duration) func(*Options) { 16 | return func(options *Options) { 17 | options.Timeout = timeout 18 | } 19 | } 20 | 21 | const defaultTimeout = time.Second * 5 22 | 23 | // Interceptor returns an interceptor that adds OpenTelemetry tracing to the context. 24 | func Interceptor(opts ...func(*Options)) interceptor.Interceptor { 25 | options := Options{ 26 | Timeout: defaultTimeout, 27 | } 28 | 29 | for _, o := range opts { 30 | o(&options) 31 | } 32 | 33 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 34 | ctx, cancel := context.WithTimeout(ctx, options.Timeout) 35 | defer cancel() 36 | return invoker(ctx, info, req, reply) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /interceptor/ktrace/trace.go: -------------------------------------------------------------------------------- 1 | package ktrace 2 | 3 | import ( 4 | "context" 5 | 6 | "go.opentelemetry.io/otel" 7 | "go.opentelemetry.io/otel/attribute" 8 | "go.opentelemetry.io/otel/codes" 9 | "go.opentelemetry.io/otel/trace" 10 | 11 | "github.com/go-kod/kod" 12 | "github.com/go-kod/kod/interceptor" 13 | ) 14 | 15 | // Interceptor returns an interceptor that adds OpenTelemetry tracing to the context. 16 | func Interceptor() interceptor.Interceptor { 17 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 18 | span := trace.SpanFromContext(ctx) 19 | if span.SpanContext().IsValid() { 20 | // Create a child span for this method. 21 | ctx, span = otel.Tracer(kod.PkgPath).Start(ctx, 22 | info.FullMethod, 23 | trace.WithSpanKind(trace.SpanKindInternal), 24 | trace.WithAttributes( 25 | attribute.String("method", info.FullMethod), 26 | ), 27 | ) 28 | } 29 | 30 | err := invoker(ctx, info, req, reply) 31 | if err != nil { 32 | span.RecordError(err) 33 | span.SetStatus(codes.Error, err.Error()) 34 | } 35 | 36 | span.End() 37 | 38 | return err 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /interceptor/kvalidate/validate.go: -------------------------------------------------------------------------------- 1 | package kvalidate 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/go-playground/validator/v10" 8 | 9 | "github.com/go-kod/kod/interceptor" 10 | ) 11 | 12 | // Interceptor returns a interceptor that validates the call specified by info. 13 | func Interceptor() interceptor.Interceptor { 14 | validate := validator.New() 15 | 16 | return func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) error { 17 | for _, v := range req { 18 | if err := validate.Struct(v); err != nil { 19 | return fmt.Errorf("validate failed: %w", err) 20 | } 21 | } 22 | 23 | return invoker(ctx, info, req, reply) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/callgraph/bin.go: -------------------------------------------------------------------------------- 1 | package callgraph 2 | 3 | import ( 4 | "bytes" 5 | "debug/elf" 6 | "debug/macho" 7 | "debug/pe" 8 | "fmt" 9 | "os" 10 | "slices" 11 | 12 | "github.com/dominikbraun/graph" 13 | "github.com/samber/lo" 14 | ) 15 | 16 | // rodata returns the read-only data section of the provided binary. 17 | func rodata(file string) ([]byte, error) { 18 | f := lo.Must(os.Open(file)) 19 | 20 | defer f.Close() 21 | 22 | // Look at first few bytes to determine the file format. 23 | prefix := make([]byte, 4) 24 | lo.Must(f.ReadAt(prefix, 0)) 25 | 26 | // Handle the file formats we support. 27 | switch { 28 | case bytes.HasPrefix(prefix, []byte("\x7FELF")): // Linux 29 | f := lo.Must(elf.NewFile(f)) 30 | return f.Section(".rodata").Data() 31 | case bytes.HasPrefix(prefix, []byte("MZ")): // Windows 32 | f := lo.Must(pe.NewFile(f)) 33 | return f.Section(".rdata").Data() 34 | // case bytes.HasPrefix(prefix, []byte("\xFE\xED\xFA")): // MacOS 35 | // f := lo.Must(macho.NewFile(f)) 36 | // return f.Section("__rodata").Data() 37 | case bytes.HasPrefix(prefix[1:], []byte("\xFA\xED\xFE")): // MacOS 38 | f := lo.Must(macho.NewFile(f)) 39 | return f.Section("__rodata").Data() 40 | default: 41 | return nil, fmt.Errorf("unknown format") 42 | } 43 | } 44 | 45 | // ReadComponentGraph reads component graph information from the specified 46 | // binary. It returns a slice of components and a component graph whose nodes 47 | // are indices into that slice. 48 | func ReadComponentGraph(file string) (graph.Graph[string, string], error) { 49 | data := lo.Must(rodata(file)) 50 | 51 | es := ParseEdges(data) 52 | g := graph.New(graph.StringHash, graph.Directed()) 53 | 54 | // NOTE: initially, all node numbers are zero. 55 | nodeMap := map[string]int{} 56 | for _, e := range es { 57 | nodeMap[e[0]] = 0 58 | nodeMap[e[1]] = 0 59 | } 60 | // Assign node numbers. 61 | components := lo.Keys(nodeMap) 62 | slices.Sort(components) 63 | for _, c := range components { 64 | lo.Must0(g.AddVertex(c)) 65 | } 66 | 67 | // Convert component edges into graph edges. 68 | for _, e := range es { 69 | src := e[0] 70 | dst := e[1] 71 | lo.Must0(g.AddEdge(src, dst)) 72 | } 73 | 74 | return g, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/callgraph/graph.go: -------------------------------------------------------------------------------- 1 | package callgraph 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "regexp" 7 | "sort" 8 | ) 9 | 10 | // MakeEdgeString returns a string that represents an edge in the call graph. 11 | func MakeEdgeString(src, dst string) string { 12 | return fmt.Sprintf("⟦%s:KoDeDgE:%s→%s⟧", checksumEdge(src, dst), src, dst) 13 | } 14 | 15 | // ParseEdges returns a list of edges from the given data. 16 | func ParseEdges(data []byte) [][2]string { 17 | var result [][2]string 18 | re := regexp.MustCompile(`⟦([0-9a-fA-F]+):KoDeDgE:([a-zA-Z0-9\-.~_/]*?)→([a-zA-Z0-9\-.~_/]*?)⟧`) 19 | for _, m := range re.FindAllSubmatch(data, -1) { 20 | if len(m) != 4 { 21 | continue 22 | } 23 | sum, src, dst := string(m[1]), string(m[2]), string(m[3]) 24 | if sum != checksumEdge(src, dst) { 25 | continue 26 | } 27 | result = append(result, [2]string{src, dst}) 28 | } 29 | sort.Slice(result, func(i, j int) bool { 30 | if a, b := result[i][0], result[j][0]; a != b { 31 | return a < b 32 | } 33 | return result[i][1] < result[j][1] 34 | }) 35 | return result 36 | } 37 | 38 | // checksumEdge returns a checksum for the given edge. 39 | func checksumEdge(src, dst string) string { 40 | edge := fmt.Sprintf("KoDeDgE:%s→%s", src, dst) 41 | sum := fmt.Sprintf("%0x", sha256.Sum256([]byte(edge)))[:8] 42 | return sum 43 | } 44 | -------------------------------------------------------------------------------- /internal/hooks/defer.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type HookFunc struct { 9 | Name string 10 | Fn func(context.Context) error 11 | } 12 | 13 | type Hooker struct { 14 | mux sync.Mutex 15 | hooks []HookFunc 16 | } 17 | 18 | func New() *Hooker { 19 | return &Hooker{ 20 | mux: sync.Mutex{}, 21 | hooks: make([]HookFunc, 0), 22 | } 23 | } 24 | 25 | // Add adds a hook function to the Kod instance. 26 | func (k *Hooker) Add(d HookFunc) { 27 | k.mux.Lock() 28 | defer k.mux.Unlock() 29 | k.hooks = append(k.hooks, d) 30 | } 31 | 32 | // Do runs the hook functions in reverse order. 33 | func (k *Hooker) Do(ctx context.Context) { 34 | 35 | k.mux.Lock() 36 | defer k.mux.Unlock() 37 | for i := len(k.hooks) - 1; i >= 0; i-- { 38 | _ = k.hooks[i].Fn(ctx) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/kslog/slog.go: -------------------------------------------------------------------------------- 1 | package kslog 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log/slog" 7 | "strings" 8 | 9 | "github.com/samber/lo" 10 | ) 11 | 12 | // NewTestLogger returns a new test logger. 13 | func NewTestLogger() (*slog.Logger, *observer) { 14 | observer := &observer{ 15 | buf: new(bytes.Buffer), 16 | } 17 | log := slog.New(slog.NewJSONHandler(observer.buf, nil)) 18 | slog.SetDefault(log) 19 | 20 | return log, observer 21 | } 22 | 23 | type observer struct { 24 | buf *bytes.Buffer 25 | } 26 | 27 | func (b *observer) parse() []map[string]any { 28 | lines := strings.Split(b.buf.String(), "\n") 29 | 30 | data := make([]map[string]any, 0) 31 | for _, line := range lines { 32 | if len(line) == 0 { 33 | continue 34 | } 35 | 36 | var m map[string]any 37 | lo.Must0(json.Unmarshal([]byte(line), &m)) 38 | 39 | data = append(data, m) 40 | } 41 | 42 | return data 43 | } 44 | 45 | // String returns the observed logs as a string. 46 | func (b *observer) String() string { 47 | return b.buf.String() 48 | } 49 | 50 | // Len returns the number of observed logs. 51 | func (b *observer) Len() int { 52 | return len(b.parse()) 53 | } 54 | 55 | // ErrorCount returns the number of observed logs with level error. 56 | func (b *observer) ErrorCount() int { 57 | return b.Filter(func(r map[string]any) bool { 58 | return r["level"] == slog.LevelError.String() 59 | }).Len() 60 | } 61 | 62 | // Filter returns a new observed logs with the provided filter applied. 63 | func (b *observer) Filter(filter func(map[string]any) bool) *observer { 64 | var filtered []map[string]any 65 | for _, line := range b.parse() { 66 | if filter(line) { 67 | filtered = append(filtered, line) 68 | } 69 | } 70 | 71 | return &observer{ 72 | buf: b.encode(filtered), 73 | } 74 | } 75 | 76 | // RemoveKeys removes the provided keys from the observed logs. 77 | func (b *observer) RemoveKeys(keys ...string) *observer { 78 | filtered := make([]map[string]any, 0) 79 | for _, line := range b.parse() { 80 | for _, key := range keys { 81 | delete(line, key) 82 | } 83 | 84 | filtered = append(filtered, line) 85 | } 86 | 87 | return &observer{ 88 | buf: b.encode(filtered), 89 | } 90 | } 91 | 92 | // encode encodes the provided value to a buffer. 93 | func (b *observer) encode(lines []map[string]any) *bytes.Buffer { 94 | buf := new(bytes.Buffer) 95 | for _, v := range lines { 96 | lo.Must0(json.NewEncoder(buf).Encode(v)) 97 | } 98 | return buf 99 | } 100 | 101 | // Clean clears the observed logs. 102 | func (b *observer) Clean() *observer { 103 | b.buf.Reset() 104 | 105 | return b 106 | } 107 | -------------------------------------------------------------------------------- /internal/mock/testing.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type mockTestingT struct { 12 | FailNowCalled bool 13 | testing.TB 14 | } 15 | 16 | var _ testing.TB = (*mockTestingT)(nil) 17 | 18 | func (m *mockTestingT) FailNow() { 19 | // register the method is called 20 | m.FailNowCalled = true 21 | // exit, as normal behavior 22 | runtime.Goexit() 23 | } 24 | 25 | func ExpectFailure(tb testing.TB, fn func(tb testing.TB)) { 26 | tb.Helper() 27 | 28 | var wg sync.WaitGroup 29 | 30 | // create a mock structure for TestingT 31 | mockT := &mockTestingT{TB: tb} 32 | // setup the barrier 33 | wg.Add(1) 34 | // start a co-routine to execute the test function f 35 | // and release the barrier at its end 36 | go func() { 37 | defer wg.Done() 38 | fn(mockT) 39 | }() 40 | 41 | // wait for the barrier. 42 | wg.Wait() 43 | // verify fail now is invoked 44 | require.True(tb, mockT.FailNowCalled) 45 | } 46 | -------------------------------------------------------------------------------- /internal/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | 7 | "github.com/go-kod/kod/interceptor" 8 | ) 9 | 10 | // LocalStubFnInfo is the information passed to LocalStubFn. 11 | type LocalStubFnInfo struct { 12 | Impl any 13 | Interceptor interceptor.Interceptor 14 | } 15 | 16 | // Registration is the registration information for a component. 17 | type Registration struct { 18 | Name string // full package-prefixed component name 19 | Interface reflect.Type // interface type for the component 20 | Impl reflect.Type // implementation type (struct) 21 | Refs string 22 | LocalStubFn func(context.Context, *LocalStubFnInfo) any 23 | } 24 | 25 | // regs is the list of registered components. 26 | var regs = make([]*Registration, 0) 27 | 28 | // Register registers the given component implementations. 29 | func Register(reg *Registration) { 30 | regs = append(regs, reg) 31 | } 32 | 33 | func All() []*Registration { 34 | return regs 35 | } 36 | -------------------------------------------------------------------------------- /internal/rolling/point.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | import "sync" 4 | 5 | // PointPolicy is a rolling window policy that tracks the last N 6 | // values inserted regardless of insertion time. 7 | type PointPolicy struct { 8 | windowSize int 9 | window Window 10 | offset int 11 | lock *sync.RWMutex 12 | } 13 | 14 | // NewPointPolicy generates a Policy that operates on a rolling set of 15 | // input points. The number of points is determined by the size of the given 16 | // window. Each bucket will contain, at most, one data point when the window 17 | // is full. 18 | func NewPointPolicy(window Window) *PointPolicy { 19 | var p = &PointPolicy{ 20 | windowSize: len(window), 21 | window: window, 22 | lock: &sync.RWMutex{}, 23 | } 24 | for offset, bucket := range window { 25 | if len(bucket) < 1 { 26 | window[offset] = make([]float64, 1) 27 | } 28 | } 29 | return p 30 | } 31 | 32 | // Append a value to the window. 33 | func (w *PointPolicy) Append(value float64) { 34 | w.lock.Lock() 35 | defer w.lock.Unlock() 36 | 37 | w.window[w.offset][0] = value 38 | w.offset = (w.offset + 1) % w.windowSize 39 | } 40 | 41 | // Reduce the window to a single value using a reduction function. 42 | func (w *PointPolicy) Reduce(f func(Window) float64) float64 { 43 | w.lock.Lock() 44 | defer w.lock.Unlock() 45 | 46 | return f(w.window) 47 | } 48 | -------------------------------------------------------------------------------- /internal/rolling/point_test.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestPointWindow(t *testing.T) { 11 | var numberOfPoints = 100 12 | var w = NewWindow(numberOfPoints) 13 | var p = NewPointPolicy(w) 14 | for x := 0; x < numberOfPoints; x = x + 1 { 15 | p.Append(1) 16 | } 17 | var final = p.Reduce(func(w Window) float64 { 18 | var result float64 19 | for _, bucket := range w { 20 | for _, p := range bucket { 21 | result = result + p 22 | } 23 | } 24 | return result 25 | }) 26 | if final != float64(numberOfPoints) { 27 | t.Fatal(final) 28 | } 29 | } 30 | 31 | func TestPointWindowDataRace(t *testing.T) { 32 | var numberOfPoints = 100 33 | var w = NewWindow(numberOfPoints) 34 | var p = NewPointPolicy(w) 35 | var stop = make(chan bool) 36 | go func() { 37 | for { 38 | select { 39 | case <-stop: 40 | return 41 | default: 42 | p.Append(1) 43 | time.Sleep(time.Millisecond) 44 | } 45 | } 46 | }() 47 | go func() { 48 | var v float64 49 | for { 50 | select { 51 | case <-stop: 52 | return 53 | default: 54 | _ = p.Reduce(func(w Window) float64 { 55 | for _, bucket := range w { 56 | for _, p := range bucket { 57 | v = v + p 58 | v = math.Mod(v, float64(numberOfPoints)) 59 | } 60 | } 61 | return 0 62 | }) 63 | } 64 | } 65 | }() 66 | time.Sleep(time.Second) 67 | close(stop) 68 | } 69 | 70 | func BenchmarkPointWindow(b *testing.B) { 71 | var bucketSizes = []int{1, 10, 100, 1000, 10000} 72 | var insertions = []int{1, 1000, 10000} 73 | for _, size := range bucketSizes { 74 | for _, insertion := range insertions { 75 | b.Run(fmt.Sprintf("Window Size:%d | Insertions:%d", size, insertion), func(bt *testing.B) { 76 | var w = NewWindow(size) 77 | var p = NewPointPolicy(w) 78 | bt.ResetTimer() 79 | for n := 0; n < bt.N; n = n + 1 { 80 | for x := 0; x < insertion; x = x + 1 { 81 | p.Append(1) 82 | } 83 | } 84 | }) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/rolling/reduce.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | "sync" 7 | ) 8 | 9 | // Count returns the number of elements in a window. 10 | func Count(w Window) float64 { 11 | result := 0 12 | for _, bucket := range w { 13 | result += len(bucket) 14 | } 15 | return float64(result) 16 | } 17 | 18 | // Sum the values within the window. 19 | func Sum(w Window) float64 { 20 | var result = 0.0 21 | for _, bucket := range w { 22 | for _, p := range bucket { 23 | result = result + p 24 | } 25 | } 26 | return result 27 | } 28 | 29 | // Avg the values within the window. 30 | func Avg(w Window) float64 { 31 | var result = 0.0 32 | var count = 0.0 33 | for _, bucket := range w { 34 | for _, p := range bucket { 35 | result = result + p 36 | count = count + 1 37 | } 38 | } 39 | return result / count 40 | } 41 | 42 | // Min the values within the window. 43 | func Min(w Window) float64 { 44 | var result = 0.0 45 | var started = true 46 | for _, bucket := range w { 47 | for _, p := range bucket { 48 | if started { 49 | result = p 50 | started = false 51 | continue 52 | } 53 | if p < result { 54 | result = p 55 | } 56 | } 57 | } 58 | return result 59 | } 60 | 61 | // Max the values within the window. 62 | func Max(w Window) float64 { 63 | var result = 0.0 64 | var started = true 65 | for _, bucket := range w { 66 | for _, p := range bucket { 67 | if started { 68 | result = p 69 | started = false 70 | continue 71 | } 72 | if p > result { 73 | result = p 74 | } 75 | } 76 | } 77 | return result 78 | } 79 | 80 | // Percentile returns an aggregating function that computes the 81 | // given percentile calculation for a window. 82 | func Percentile(perc float64) func(w Window) float64 { 83 | var values []float64 84 | var lock = &sync.Mutex{} 85 | return func(w Window) float64 { 86 | lock.Lock() 87 | defer lock.Unlock() 88 | 89 | values = values[:0] 90 | for _, bucket := range w { 91 | values = append(values, bucket...) 92 | } 93 | if len(values) < 1 { 94 | return 0.0 95 | } 96 | sort.Float64s(values) 97 | var position = (float64(len(values))*(perc/100) + .5) - 1 98 | var k = int(math.Floor(position)) 99 | var f = math.Mod(position, 1) 100 | if f == 0.0 { 101 | return values[k] 102 | } 103 | var plusOne = k + 1 104 | if plusOne > len(values)-1 { 105 | plusOne = k 106 | } 107 | return ((1 - f) * values[k]) + (f * values[plusOne]) 108 | } 109 | } 110 | 111 | // FastPercentile implements the pSquare percentile estimation 112 | // algorithm for calculating percentiles from streams of data 113 | // using fixed memory allocations. 114 | func FastPercentile(perc float64) func(w Window) float64 { 115 | perc = perc / 100.0 116 | return func(w Window) float64 { 117 | var initialObservations = make([]float64, 0, 5) 118 | var q [5]float64 119 | var n [5]int 120 | var nPrime [5]float64 121 | var dnPrime [5]float64 122 | var observations uint64 123 | for _, bucket := range w { 124 | for _, v := range bucket { 125 | 126 | observations = observations + 1 127 | // Record first five observations 128 | if observations < 6 { 129 | initialObservations = append(initialObservations, v) 130 | continue 131 | } 132 | // Before proceeding beyond the first five, process them. 133 | if observations == 6 { 134 | bubbleSort(initialObservations) 135 | for offset := range q { 136 | q[offset] = initialObservations[offset] 137 | n[offset] = offset 138 | } 139 | nPrime[0] = 0 140 | nPrime[1] = 2 * perc 141 | nPrime[2] = 4 * perc 142 | nPrime[3] = 2 + 2*perc 143 | nPrime[4] = 4 144 | dnPrime[0] = 0 145 | dnPrime[1] = perc / 2 146 | dnPrime[2] = perc 147 | dnPrime[3] = (1 + perc) / 2 148 | dnPrime[4] = 1 149 | } 150 | var k int // k is the target cell to increment 151 | switch { 152 | case v < q[0]: 153 | q[0] = v 154 | k = 0 155 | case q[0] <= v && v < q[1]: 156 | k = 0 157 | case q[1] <= v && v < q[2]: 158 | k = 1 159 | case q[2] <= v && v < q[3]: 160 | k = 2 161 | case q[3] <= v && v <= q[4]: 162 | k = 3 163 | case v > q[4]: 164 | q[4] = v 165 | k = 3 166 | } 167 | for x := k + 1; x < 5; x = x + 1 { 168 | n[x] = n[x] + 1 169 | } 170 | nPrime[0] = nPrime[0] + dnPrime[0] 171 | nPrime[1] = nPrime[1] + dnPrime[1] 172 | nPrime[2] = nPrime[2] + dnPrime[2] 173 | nPrime[3] = nPrime[3] + dnPrime[3] 174 | nPrime[4] = nPrime[4] + dnPrime[4] 175 | for x := 1; x < 4; x = x + 1 { 176 | var d = nPrime[x] - float64(n[x]) 177 | if (d >= 1 && (n[x+1]-n[x]) > 1) || 178 | (d <= -1 && (n[x-1]-n[x]) < -1) { 179 | var s = sign(d) 180 | var si = int(s) 181 | var nx = float64(n[x]) 182 | var nxPlusOne = float64(n[x+1]) 183 | var nxMinusOne = float64(n[x-1]) 184 | var qx = q[x] 185 | var qxPlusOne = q[x+1] 186 | var qxMinusOne = q[x-1] 187 | var parab = q[x] + (s/(nxPlusOne-nxMinusOne))*((nx-nxMinusOne+s)*(qxPlusOne-qx)/(nxPlusOne-nx)+(nxPlusOne-nx-s)*(qx-qxMinusOne)/(nx-nxMinusOne)) 188 | if qxMinusOne < parab && parab < qxPlusOne { 189 | q[x] = parab 190 | } else { 191 | q[x] = q[x] + s*((q[x+si]-q[x])/float64(n[x+si]-n[x])) 192 | } 193 | n[x] = n[x] + si 194 | } 195 | } 196 | 197 | } 198 | } 199 | 200 | if observations < 1 { 201 | return 0.0 202 | } 203 | // If we have less than five values then degenerate into a max function. 204 | // This is a reasonable value for data sets this small. 205 | if observations < 5 { 206 | bubbleSort(initialObservations) 207 | return initialObservations[len(initialObservations)-1] 208 | } 209 | return q[2] 210 | } 211 | } 212 | 213 | func sign(v float64) float64 { 214 | if v < 0 { 215 | return -1 216 | } 217 | return 1 218 | } 219 | 220 | // using bubblesort because we're only working with datasets of 5 or fewer 221 | // elements. 222 | func bubbleSort(s []float64) { 223 | for range s { 224 | for x := 0; x < len(s)-1; x = x + 1 { 225 | if s[x] > s[x+1] { 226 | s[x], s[x+1] = s[x+1], s[x] 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /internal/rolling/time.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // TimePolicy is a window Accumulator implementation that uses some 9 | // duration of time to determine the content of the window. 10 | type TimePolicy struct { 11 | bucketSize time.Duration 12 | bucketSizeNano int64 13 | numberOfBuckets int 14 | numberOfBuckets64 int64 15 | window [][]float64 16 | lastWindowOffset int 17 | lastWindowTime int64 18 | lock *sync.Mutex 19 | } 20 | 21 | // NewTimePolicy manages a window with rolling time duratinos. 22 | // The given duration will be used to bucket data within the window. If data 23 | // points are received entire windows aparts then the window will only contain 24 | // a single data point. If one or more durations of the window are missed then 25 | // they are zeroed out to keep the window consistent. 26 | func NewTimePolicy(window Window, bucketDuration time.Duration) *TimePolicy { 27 | return &TimePolicy{ 28 | bucketSize: bucketDuration, 29 | bucketSizeNano: bucketDuration.Nanoseconds(), 30 | numberOfBuckets: len(window), 31 | numberOfBuckets64: int64(len(window)), 32 | window: window, 33 | lock: &sync.Mutex{}, 34 | } 35 | } 36 | 37 | func (w *TimePolicy) resetWindow() { 38 | for offset := range w.window { 39 | w.window[offset] = w.window[offset][:0] 40 | } 41 | } 42 | 43 | func (w *TimePolicy) resetBuckets(windowOffset int) { 44 | var distance = windowOffset - w.lastWindowOffset 45 | // If the distance between current and last is negative then we've wrapped 46 | // around the ring. Recalculate the distance. 47 | if distance < 0 { 48 | distance = (w.numberOfBuckets - w.lastWindowOffset) + windowOffset 49 | } 50 | for counter := 1; counter < distance; counter = counter + 1 { 51 | var offset = (counter + w.lastWindowOffset) % w.numberOfBuckets 52 | w.window[offset] = w.window[offset][:0] 53 | } 54 | } 55 | 56 | func (w *TimePolicy) keepConsistent(adjustedTime int64, windowOffset int) { 57 | // If we've waiting longer than a full window for data then we need to clear 58 | // the internal state completely. 59 | if adjustedTime-w.lastWindowTime > w.numberOfBuckets64 { 60 | w.resetWindow() 61 | } 62 | 63 | // When one or more buckets are missed we need to zero them out. 64 | if adjustedTime != w.lastWindowTime && adjustedTime-w.lastWindowTime < w.numberOfBuckets64 { 65 | w.resetBuckets(windowOffset) 66 | } 67 | } 68 | 69 | func (w *TimePolicy) selectBucket(currentTime time.Time) (int64, int) { 70 | var adjustedTime = currentTime.UnixNano() / w.bucketSizeNano 71 | var windowOffset = int(adjustedTime % w.numberOfBuckets64) 72 | return adjustedTime, windowOffset 73 | } 74 | 75 | // AppendWithTimestamp same as Append but with timestamp as parameter 76 | func (w *TimePolicy) AppendWithTimestamp(value float64, timestamp time.Time) { 77 | w.lock.Lock() 78 | defer w.lock.Unlock() 79 | 80 | var adjustedTime, windowOffset = w.selectBucket(timestamp) 81 | w.keepConsistent(adjustedTime, windowOffset) 82 | if w.lastWindowOffset != windowOffset { 83 | w.window[windowOffset] = []float64{value} 84 | } else { 85 | w.window[windowOffset] = append(w.window[windowOffset], value) 86 | } 87 | w.lastWindowTime = adjustedTime 88 | w.lastWindowOffset = windowOffset 89 | } 90 | 91 | // Append a value to the window using a time bucketing strategy. 92 | func (w *TimePolicy) Append(value float64) { 93 | w.AppendWithTimestamp(value, time.Now()) 94 | } 95 | 96 | // Reduce the window to a single value using a reduction function. 97 | func (w *TimePolicy) Reduce(f func(Window) float64) float64 { 98 | w.lock.Lock() 99 | defer w.lock.Unlock() 100 | 101 | var adjustedTime, windowOffset = w.selectBucket(time.Now()) 102 | w.keepConsistent(adjustedTime, windowOffset) 103 | return f(w.window) 104 | } 105 | -------------------------------------------------------------------------------- /internal/rolling/time_test.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestTimeWindow(t *testing.T) { 11 | bucketSize := time.Millisecond * 100 12 | numberBuckets := 10 13 | w := NewWindow(numberBuckets) 14 | p := NewTimePolicy(w, bucketSize) 15 | for x := 0; x < numberBuckets; x = x + 1 { 16 | p.Append(1) 17 | time.Sleep(bucketSize) 18 | } 19 | final := p.Reduce(func(w Window) float64 { 20 | var result float64 21 | for _, bucket := range w { 22 | for _, point := range bucket { 23 | result = result + point 24 | } 25 | } 26 | return result 27 | }) 28 | if final != float64(numberBuckets) && final != float64(numberBuckets-1) { 29 | t.Fatal(final) 30 | } 31 | 32 | for x := 0; x < numberBuckets; x = x + 1 { 33 | p.Append(2) 34 | time.Sleep(bucketSize) 35 | } 36 | 37 | final = p.Reduce(func(w Window) float64 { 38 | var result float64 39 | for _, bucket := range w { 40 | for _, point := range bucket { 41 | result = result + point 42 | } 43 | } 44 | return result 45 | }) 46 | if final != 2*float64(numberBuckets) && final != 2*float64(numberBuckets-1) { 47 | t.Fatal("got", final, "expected", 2*float64(numberBuckets)) 48 | } 49 | } 50 | 51 | func TestTimeWindowSelectBucket(t *testing.T) { 52 | bucketSize := time.Millisecond * 50 53 | numberBuckets := 10 54 | w := NewWindow(numberBuckets) 55 | p := NewTimePolicy(w, bucketSize) 56 | target := time.Unix(0, 0) 57 | adjustedTime, bucket := p.selectBucket(target) 58 | if bucket != 0 { 59 | t.Fatalf("expected bucket 0 but got %d %v", bucket, adjustedTime) 60 | } 61 | target = time.Unix(0, int64(50*time.Millisecond)) 62 | _, bucket = p.selectBucket(target) 63 | if bucket != 1 { 64 | t.Fatalf("expected bucket 1 but got %d %v", bucket, target) 65 | } 66 | target = time.Unix(0, int64(50*time.Millisecond)*10) 67 | _, bucket = p.selectBucket(target) 68 | if bucket != 0 { 69 | t.Fatalf("expected bucket 10 but got %d %v", bucket, target) 70 | } 71 | target = time.Unix(0, int64(50*time.Millisecond)*11) 72 | _, bucket = p.selectBucket(target) 73 | if bucket != 1 { 74 | t.Fatalf("expected bucket 0 but got %d %v", bucket, target) 75 | } 76 | } 77 | 78 | func TestTimeWindowConsistency(t *testing.T) { 79 | bucketSize := time.Millisecond * 50 80 | numberBuckets := 10 81 | w := NewWindow(numberBuckets) 82 | p := NewTimePolicy(w, bucketSize) 83 | for offset := range p.window { 84 | p.window[offset] = append(p.window[offset], 1) 85 | } 86 | p.lastWindowTime = time.Now().UnixNano() 87 | p.lastWindowOffset = 0 88 | target := time.Unix(1, 0) 89 | adjustedTime, bucket := p.selectBucket(target) 90 | p.keepConsistent(adjustedTime, bucket) 91 | if len(p.window[0]) != 1 { 92 | t.Fatal("data loss while adjusting internal state") 93 | } 94 | target = time.Unix(1, int64(50*time.Millisecond)) 95 | adjustedTime, bucket = p.selectBucket(target) 96 | p.keepConsistent(adjustedTime, bucket) 97 | if len(p.window[0]) != 1 { 98 | t.Fatal("data loss while adjusting internal state") 99 | } 100 | target = time.Unix(1, int64(5*50*time.Millisecond)) 101 | adjustedTime, bucket = p.selectBucket(target) 102 | p.keepConsistent(adjustedTime, bucket) 103 | if len(p.window[0]) != 1 { 104 | t.Fatal("data loss while adjusting internal state") 105 | } 106 | for x := 1; x < 5; x = x + 1 { 107 | if len(p.window[x]) != 0 { 108 | t.Fatal("internal state not kept consistent during time gap") 109 | } 110 | } 111 | } 112 | 113 | func TestTimeWindowDataRace(t *testing.T) { 114 | bucketSize := time.Millisecond 115 | numberBuckets := 1000 116 | w := NewWindow(numberBuckets) 117 | p := NewTimePolicy(w, bucketSize) 118 | stop := make(chan bool) 119 | go func() { 120 | for { 121 | select { 122 | case <-stop: 123 | return 124 | default: 125 | p.Append(1) 126 | time.Sleep(time.Millisecond) 127 | } 128 | } 129 | }() 130 | go func() { 131 | var v float64 132 | for { 133 | select { 134 | case <-stop: 135 | return 136 | default: 137 | _ = p.Reduce(func(w Window) float64 { 138 | for _, bucket := range w { 139 | for _, p := range bucket { 140 | v = v + p 141 | v = math.Mod(v, float64(numberBuckets)) 142 | } 143 | } 144 | return 0 145 | }) 146 | } 147 | } 148 | }() 149 | time.Sleep(time.Second) 150 | close(stop) 151 | } 152 | 153 | type timeWindowOptions struct { 154 | name string 155 | bucketSize time.Duration 156 | numberBuckets int 157 | insertions int 158 | } 159 | 160 | func BenchmarkTimeWindow(b *testing.B) { 161 | durations := []time.Duration{time.Millisecond} 162 | bucketSizes := []int{1, 10, 100, 1000} 163 | insertions := []int{1, 1000, 10000} 164 | options := make([]timeWindowOptions, 0, len(durations)*len(bucketSizes)*len(insertions)) 165 | for _, d := range durations { 166 | for _, s := range bucketSizes { 167 | for _, i := range insertions { 168 | options = append( 169 | options, 170 | timeWindowOptions{ 171 | name: fmt.Sprintf("Duration:%v | Buckets:%d | Insertions:%d", d, s, i), 172 | bucketSize: d, 173 | numberBuckets: s, 174 | insertions: i, 175 | }, 176 | ) 177 | } 178 | } 179 | } 180 | b.ResetTimer() 181 | for _, option := range options { 182 | b.Run(option.name, func(bt *testing.B) { 183 | w := NewWindow(option.numberBuckets) 184 | p := NewTimePolicy(w, option.bucketSize) 185 | bt.ResetTimer() 186 | for n := 0; n < bt.N; n = n + 1 { 187 | for x := 0; x < option.insertions; x = x + 1 { 188 | p.Append(1) 189 | } 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /internal/rolling/window.go: -------------------------------------------------------------------------------- 1 | package rolling 2 | 3 | // Window represents a bucketed set of data. It should be used in conjunction 4 | // with a Policy to populate it with data using some windowing policy. 5 | type Window [][]float64 6 | 7 | // NewWindow creates a Window with the given number of buckets. The number of 8 | // buckets is meaningful to each Policy. The Policy implementations 9 | // will describe their use of buckets. 10 | func NewWindow(buckets int) Window { 11 | return make([][]float64, buckets) 12 | } 13 | 14 | // NewPreallocatedWindow creates a Window both with the given number of buckets 15 | // and with a preallocated bucket size. This constructor may be used when the 16 | // number of data points per-bucket can be estimated and/or when the desire is 17 | // to allocate a large slice so that allocations do not happen as the Window 18 | // is populated by a Policy. 19 | func NewPreallocatedWindow(buckets int, bucketSize int) Window { 20 | var w = NewWindow(buckets) 21 | for offset := range w { 22 | w[offset] = make([]float64, 0, bucketSize) 23 | } 24 | return w 25 | } 26 | -------------------------------------------------------------------------------- /internal/signals/signals.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | ) 9 | 10 | var shutdownSignals = []os.Signal{syscall.SIGQUIT, os.Interrupt, syscall.SIGTERM} 11 | 12 | // Shutdown support twice signal must exit 13 | // first signal: graceful shutdown 14 | // second signal: exit directly 15 | func Shutdown(ctx context.Context, sig chan os.Signal, stop func(grace bool)) { 16 | signal.Notify( 17 | sig, 18 | shutdownSignals..., 19 | ) 20 | go func() { 21 | select { 22 | case <-ctx.Done(): 23 | return 24 | case s := <-sig: 25 | go stop(s != syscall.SIGQUIT) 26 | <-sig 27 | os.Exit(128 + int(s.(syscall.Signal))) // second signal. Exit directly. 28 | } 29 | }() 30 | } 31 | -------------------------------------------------------------------------------- /internal/signals/signals_test.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestShutdown(t *testing.T) { 11 | sig := make(chan os.Signal, 1) 12 | 13 | time.AfterFunc(time.Millisecond, func() { 14 | sig <- os.Interrupt 15 | }) 16 | 17 | Shutdown(context.Background(), sig, func(grace bool) { 18 | }) 19 | 20 | time.Sleep(time.Second) 21 | } 22 | -------------------------------------------------------------------------------- /internal/singleton/singleton.go: -------------------------------------------------------------------------------- 1 | package singleton 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Singleton[T any] provides a common structure for components, 8 | type Singleton[T any] struct { 9 | instances map[string]T 10 | mu sync.RWMutex 11 | } 12 | 13 | // New creates a new Singleton[T]. 14 | func New[T any]() *Singleton[T] { 15 | return &Singleton[T]{ 16 | instances: make(map[string]T), 17 | } 18 | } 19 | 20 | // Get returns the instance of the component with the given name. 21 | func (s *Singleton[T]) Get(name string, initFn func() T) T { 22 | s.mu.RLock() 23 | 24 | if instance, exists := s.instances[name]; exists { 25 | s.mu.RUnlock() 26 | return instance 27 | } 28 | 29 | s.mu.RUnlock() 30 | 31 | instance := initFn() 32 | 33 | s.mu.Lock() 34 | s.instances[name] = instance 35 | s.mu.Unlock() 36 | 37 | return instance 38 | } 39 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime/debug" 6 | 7 | "github.com/samber/lo" 8 | ) 9 | 10 | const ( 11 | // CodeGenMajor is the major version of the generated code. 12 | CodeGenMajor = 0 13 | // CodeGenMinor is the minor version of the generated code. 14 | CodeGenMinor = 1 15 | // codeGenPatch is the patch version of the generated code. 16 | codeGenPatch = 0 17 | ) 18 | 19 | // CodeGenSemVersion is the version of the generated code. 20 | var CodeGenSemVersion = SemVer{Major: CodeGenMajor, Minor: CodeGenMinor, Patch: codeGenPatch} 21 | 22 | // SemVer represents a semantic version. 23 | type SemVer struct { 24 | Major int 25 | Minor int 26 | Patch int 27 | } 28 | 29 | // String returns the string representation of the semantic version. 30 | func (v SemVer) String() string { 31 | return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) 32 | } 33 | 34 | // SelfVersion returns the version of the running tool binary. 35 | func SelfVersion() string { 36 | info := lo.Must(debug.ReadBuildInfo()) 37 | return info.Main.Version 38 | } 39 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestVersion(t *testing.T) { 10 | t.Run("version", func(t *testing.T) { 11 | require.Equal(t, "v1.0.0", SemVer{Major: 1}.String()) 12 | }) 13 | 14 | t.Run("self version", func(t *testing.T) { 15 | require.Equal(t, "(devel)", SelfVersion()) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /kod_test.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/knadh/koanf/v2" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "go.uber.org/goleak" 12 | ) 13 | 14 | func TestMain(m *testing.M) { 15 | goleak.VerifyTestMain(m, 16 | goleak.IgnoreAnyFunction("github.com/go-kod/kod/interceptor/internal/ratelimit.cpuproc"), 17 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/metric.(*PeriodicReader).run"), 18 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/trace.(*batchSpanProcessor).processQueue"), 19 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/log.exportSync.func1"), 20 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/log.(*BatchProcessor).poll.func1"), 21 | ) 22 | } 23 | 24 | func TestConfigNoSuffix(t *testing.T) { 25 | k, err := newKod(context.Background()) 26 | assert.Nil(t, err) 27 | 28 | assert.EqualError(t, k.parseConfig("nosuffix"), "read config file: Unsupported Config Type \"\"") 29 | } 30 | 31 | func TestConfigNoFile(t *testing.T) { 32 | k, err := newKod(context.Background()) 33 | assert.Nil(t, err) 34 | 35 | assert.EqualError(t, k.parseConfig("notfound.yaml"), "read config file: open notfound.yaml: no such file or directory") 36 | } 37 | 38 | func TestConfigEnv(t *testing.T) { 39 | k, err := newKod(context.Background()) 40 | assert.Nil(t, err) 41 | 42 | assert.Equal(t, k.config.Name, "kod.test") 43 | assert.Equal(t, k.config.Version, "") 44 | assert.Equal(t, k.config.Env, "local") 45 | 46 | t.Setenv("KOD_NAME", "test") 47 | t.Setenv("KOD_VERSION", "1.0.0") 48 | t.Setenv("KOD_ENV", "dev") 49 | 50 | k, err = newKod(context.Background()) 51 | assert.Nil(t, err) 52 | 53 | assert.Equal(t, k.config.Name, "test") 54 | assert.Equal(t, k.config.Version, "1.0.0") 55 | assert.Equal(t, k.config.Env, "dev") 56 | } 57 | 58 | type testComponent struct { 59 | Implements[testInterface] 60 | WithConfig[testConfig] 61 | initialized bool 62 | initErr error 63 | shutdown bool 64 | shutdownErr error 65 | } 66 | 67 | type testConfig struct { 68 | Value string `default:"default"` 69 | } 70 | 71 | type testInterface interface { 72 | IsInitialized() bool 73 | } 74 | 75 | func (c *testComponent) Init(context.Context) error { 76 | c.initialized = true 77 | return c.initErr 78 | } 79 | 80 | func (c *testComponent) Shutdown(context.Context) error { 81 | c.shutdown = true 82 | return c.shutdownErr 83 | } 84 | 85 | func (c *testComponent) IsInitialized() bool { 86 | return c.initialized 87 | } 88 | 89 | func (c *testComponent) implements(testInterface) {} 90 | 91 | func TestConfigurationLoading(t *testing.T) { 92 | tests := []struct { 93 | name string 94 | koanf *koanf.Koanf 95 | filename string 96 | wantErr bool 97 | }{ 98 | { 99 | name: "custom koanf", 100 | koanf: koanf.New("."), // 使用 koanf.New() 替代空实例 101 | }, 102 | { 103 | name: "invalid file extension", 104 | filename: "config.invalid", 105 | wantErr: true, 106 | }, 107 | { 108 | name: "missing file", 109 | filename: "notexist.yaml", 110 | wantErr: true, // Should use defaults 111 | }, 112 | } 113 | 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | opts := []func(*options){ 117 | WithConfigFile(tt.filename), 118 | } 119 | if tt.koanf != nil { 120 | opts = append(opts, WithKoanf(tt.koanf)) 121 | } 122 | 123 | k, err := newKod(context.Background(), opts...) 124 | if tt.wantErr { 125 | assert.Error(t, err) 126 | return 127 | } 128 | require.NoError(t, err) 129 | 130 | cfg := k.Config() 131 | assert.NotEmpty(t, cfg.Name) 132 | assert.NotEmpty(t, cfg.Env) 133 | assert.Equal(t, 5*time.Second, cfg.ShutdownTimeout) 134 | }) 135 | } 136 | } 137 | 138 | func TestDeferHooks(t *testing.T) { 139 | k, err := newKod(context.Background()) 140 | require.NoError(t, err) 141 | 142 | executed := false 143 | k.Defer("test", func(context.Context) error { 144 | executed = true 145 | return nil 146 | }) 147 | 148 | ctx, cancel := context.WithCancel(context.Background()) 149 | defer cancel() 150 | 151 | k.hooker.Do(ctx) 152 | assert.True(t, executed) 153 | } 154 | -------------------------------------------------------------------------------- /registry_test.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/go-kod/kod/internal/registry" 14 | ) 15 | 16 | func TestFill(t *testing.T) { 17 | t.Run("case 2", func(t *testing.T) { 18 | assert.NotNil(t, fillRefs(nil, nil, nil)) 19 | }) 20 | 21 | t.Run("case 3", func(t *testing.T) { 22 | i := 0 23 | assert.NotNil(t, fillRefs(&i, nil, nil)) 24 | }) 25 | } 26 | 27 | func TestValidateUnregisteredRef(t *testing.T) { 28 | type foo interface{} 29 | type fooImpl struct{ Ref[io.Reader] } 30 | regs := []*registry.Registration{ 31 | { 32 | Name: "foo", 33 | Interface: reflect.TypeFor[foo](), 34 | Impl: reflect.TypeFor[fooImpl](), 35 | }, 36 | } 37 | _, err := processRegistrations(regs) 38 | if err == nil { 39 | t.Fatal("unexpected validateRegistrations success") 40 | } 41 | const want = "component io.Reader was not registered" 42 | if !strings.Contains(err.Error(), want) { 43 | t.Fatalf("validateRegistrations: got %q, want %q", err, want) 44 | } 45 | } 46 | 47 | // TestValidateNoRegistrations tests that validateRegistrations succeeds on an 48 | // empty set of registrations. 49 | func TestValidateNoRegistrations(t *testing.T) { 50 | if _, err := processRegistrations(nil); err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | func TestMultipleRegistrations(t *testing.T) { 56 | type foo interface{} 57 | type fooImpl struct{ Ref[io.Reader] } 58 | regs := []*Registration{ 59 | { 60 | Name: "github.com/go-kod/kod/Main", 61 | Interface: reflect.TypeOf((*Main)(nil)).Elem(), 62 | Impl: reflect.TypeOf(fooImpl{}), 63 | Refs: `⟦48699770:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/kod/tests/graphcase/test1Controller⟧`, 64 | }, 65 | { 66 | Name: "github.com/go-kod/kod/Main", 67 | Interface: reflect.TypeOf((*foo)(nil)).Elem(), 68 | Impl: reflect.TypeOf(fooImpl{}), 69 | Refs: `⟦48699770:KoDeDgE:github.com/go-kod/kod/tests/graphcase/test1Controller→github.com/go-kod/kod/Main⟧`, 70 | }, 71 | } 72 | err := checkCircularDependency(regs) 73 | if err == nil { 74 | t.Fatal("unexpected checkCircularDependency success") 75 | } 76 | const want = "components [github.com/go-kod/kod/Main], error vertex already exists" 77 | if !strings.Contains(err.Error(), want) { 78 | t.Fatalf("checkCircularDependency: got %q, want %q", err, want) 79 | } 80 | } 81 | 82 | func TestCycleRegistrations(t *testing.T) { 83 | type test1Controller interface{} 84 | type test1ControllerImpl struct{ Ref[io.Reader] } 85 | type mainImpl struct{ Ref[test1Controller] } 86 | regs := []*Registration{ 87 | { 88 | Name: "github.com/go-kod/kod/Main", 89 | Interface: reflect.TypeOf((*Main)(nil)).Elem(), 90 | Impl: reflect.TypeOf(mainImpl{}), 91 | Refs: `⟦48699770:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/kod/test1Controller⟧`, 92 | }, 93 | { 94 | Name: "github.com/go-kod/kod/test1Controller", 95 | Interface: reflect.TypeOf((*test1Controller)(nil)).Elem(), 96 | Impl: reflect.TypeOf(test1ControllerImpl{}), 97 | Refs: `⟦b8422d0e:KoDeDgE:github.com/go-kod/kod/test1Controller→github.com/go-kod/kod/Main⟧`, 98 | }, 99 | } 100 | err := checkCircularDependency(regs) 101 | if err == nil { 102 | t.Fatal("unexpected checkCircularDependency success") 103 | } 104 | const want = "components [github.com/go-kod/kod/test1Controller] and [github.com/go-kod/kod/Main] have cycle Ref" 105 | if !strings.Contains(err.Error(), want) { 106 | t.Fatalf("checkCircularDependency: got %q, want %q", err, want) 107 | } 108 | } 109 | 110 | func TestGetImpl(t *testing.T) { 111 | k, err := newKod(context.Background()) 112 | require.NoError(t, err) 113 | 114 | _, err = k.getImpl(context.Background(), reflect.TypeOf(struct{}{})) 115 | assert.Error(t, err) // Should fail for unregistered type 116 | } 117 | 118 | func TestGetIntf(t *testing.T) { 119 | k, err := newKod(context.Background()) 120 | require.NoError(t, err) 121 | 122 | _, err = k.getIntf(context.Background(), reflect.TypeOf((*interface{})(nil)).Elem()) 123 | assert.Error(t, err) // Should fail for unregistered interface 124 | } 125 | -------------------------------------------------------------------------------- /testing.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/samber/lo" 10 | 11 | "github.com/go-kod/kod/internal/kslog" 12 | ) 13 | 14 | // NewTestLogger returns a new test logger. 15 | var NewTestLogger = kslog.NewTestLogger 16 | 17 | // fakeComponent is a fake component. 18 | type fakeComponent struct { 19 | intf reflect.Type 20 | impl any 21 | } 22 | 23 | // Fake returns a fake component. 24 | func Fake[T any](impl any) fakeComponent { 25 | t := reflect.TypeFor[T]() 26 | if _, ok := impl.(T); !ok { 27 | panic(fmt.Sprintf("%T does not implement %v", impl, t)) 28 | } 29 | return fakeComponent{intf: t, impl: impl} 30 | } 31 | 32 | // options contains options for the runner. 33 | type runner struct { 34 | options []func(*options) 35 | } 36 | 37 | // RunTest runs a test function with one component. 38 | func RunTest[T any](tb testing.TB, body func(context.Context, T), opts ...func(*options)) { 39 | tb.Helper() 40 | 41 | runTest(tb, body, opts...) 42 | } 43 | 44 | // RunTest2 runs a test function with two components. 45 | func RunTest2[T1, T2 any](tb testing.TB, body func(context.Context, T1, T2), opts ...func(*options)) { 46 | tb.Helper() 47 | 48 | runTest(tb, body, opts...) 49 | } 50 | 51 | // RunTest3 runs a test function with three components. 52 | func RunTest3[T1, T2, T3 any](tb testing.TB, body func(context.Context, T1, T2, T3), opts ...func(*options)) { 53 | tb.Helper() 54 | 55 | runTest(tb, body, opts...) 56 | } 57 | 58 | // runTest runs a test function. 59 | func runTest(tb testing.TB, testBody any, opts ...func(*options)) { 60 | tb.Helper() 61 | 62 | err := runner{options: opts}.sub(tb, testBody) 63 | if err != nil { 64 | tb.Logf("runTest failed: %v", err) 65 | tb.FailNow() 66 | } 67 | } 68 | 69 | // sub runs a test function. 70 | func (r runner) sub(tb testing.TB, testBody any) error { 71 | tb.Helper() 72 | 73 | ctx, cancelFn := context.WithCancel(context.Background()) 74 | defer func() { 75 | // Cancel the context so background activity will stop. 76 | cancelFn() 77 | }() 78 | 79 | runner, err := newKod(ctx, r.options...) 80 | if err != nil { 81 | return fmt.Errorf("newKod: %v", err) 82 | } 83 | defer runner.hooker.Do(ctx) 84 | 85 | ctx = newContext(ctx, runner) 86 | 87 | tb.Helper() 88 | body, intfs, err := checkRunFunc(ctx, testBody) 89 | if err != nil { 90 | return fmt.Errorf("kod.Run argument: %v", err) 91 | } 92 | 93 | // Assume a component Foo implementing struct foo. We disallow tests 94 | // like the one below where the user provides a fake and a component 95 | // implementation pointer for the same component. 96 | // 97 | // runner.Fakes = append(runner.Fakes, kod.Fake[Foo](...)) 98 | // runner.Test(t, func(t *testing.T, f *foo) {...}) 99 | for _, intf := range intfs { 100 | if _, ok := runner.opts.fakes[intf]; ok { 101 | return fmt.Errorf("component %v has both fake and component implementation pointer", intf) 102 | } 103 | } 104 | 105 | if err := body(ctx, runner); err != nil { 106 | return fmt.Errorf("kod.Run body: %v", err) 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func checkRunFunc(ctx context.Context, fn any) (func(context.Context, *Kod) error, []reflect.Type, error) { 113 | fnType := reflect.TypeOf(fn) 114 | if fnType == nil || fnType.Kind() != reflect.Func { 115 | return nil, nil, fmt.Errorf("not a func") 116 | } 117 | if fnType.IsVariadic() { 118 | return nil, nil, fmt.Errorf("must not be variadic") 119 | } 120 | n := fnType.NumIn() 121 | if n < 2 { 122 | return nil, nil, fmt.Errorf("must have at least two args") 123 | } 124 | if fnType.NumOut() > 0 { 125 | return nil, nil, fmt.Errorf("must have no return outputs") 126 | } 127 | if fnType.In(0) != reflect.TypeOf(&ctx).Elem() { 128 | return nil, nil, fmt.Errorf("function first argument type %v does not match first kod.Run argument %v", fnType.In(0), reflect.TypeOf(&ctx).Elem()) 129 | } 130 | var intfs []reflect.Type 131 | for i := 1; i < n; i++ { 132 | switch fnType.In(i).Kind() { 133 | case reflect.Interface: 134 | // Do nothing. 135 | case reflect.Pointer: 136 | intf, err := extractComponentInterfaceType(fnType.In(i).Elem()) 137 | if err != nil { 138 | return nil, nil, err 139 | } 140 | intfs = append(intfs, intf) 141 | default: 142 | return nil, nil, fmt.Errorf("function argument %d type %v must be a component interface or pointer to component implementation", i, fnType.In(i)) 143 | } 144 | } 145 | 146 | return func(ctx context.Context, runner *Kod) error { 147 | args := make([]any, n) 148 | args[0] = ctx 149 | for i := 1; i < n; i++ { 150 | argType := fnType.In(i) 151 | switch argType.Kind() { 152 | case reflect.Interface: 153 | comp, err := runner.getIntf(ctx, argType) 154 | if err != nil { 155 | return err 156 | } 157 | args[i] = comp 158 | case reflect.Pointer: 159 | comp, err := runner.getImpl(ctx, argType.Elem()) 160 | if err != nil { 161 | return err 162 | } 163 | args[i] = comp 164 | default: 165 | return fmt.Errorf("argument %v has unexpected type %v", i, argType) 166 | } 167 | } 168 | 169 | reflect.ValueOf(fn).Call(lo.Map(args, func(item any, _ int) reflect.Value { return reflect.ValueOf(item) })) 170 | return nil 171 | }, intfs, nil 172 | } 173 | 174 | func extractComponentInterfaceType(t reflect.Type) (reflect.Type, error) { 175 | if t.Kind() != reflect.Struct { 176 | return nil, fmt.Errorf("type %v is not a struct", t) 177 | } 178 | // See the definition of kod.Implements. 179 | f, ok := t.FieldByName("component_interface_type") 180 | if !ok { 181 | return nil, fmt.Errorf("type %v does not embed kod.Implements", t) 182 | } 183 | return f.Type, nil 184 | } 185 | -------------------------------------------------------------------------------- /testing_test.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/go-kod/kod/internal/mock" 11 | ) 12 | 13 | func Test_testRunner_sub(t *testing.T) { 14 | t.Run("failure", func(t *testing.T) { 15 | mock.ExpectFailure(t, func(tb testing.TB) { 16 | tb.Helper() 17 | 18 | r := &runner{} 19 | err := r.sub(tb, nil) 20 | if err != nil { 21 | tb.FailNow() 22 | } 23 | }) 24 | }) 25 | } 26 | 27 | func Test_checkRunFunc(t *testing.T) { 28 | t.Run("not a func", func(t *testing.T) { 29 | _, _, err := checkRunFunc(context.Background(), 0) 30 | assert.EqualError(t, err, "not a func") 31 | }) 32 | 33 | t.Run("must not be variadic", func(t *testing.T) { 34 | _, _, err := checkRunFunc(context.Background(), func(t *testing.T, a ...int) { 35 | }) 36 | assert.EqualError(t, err, "must not be variadic") 37 | }) 38 | 39 | // must have no return outputs 40 | t.Run("must have no return outputs", func(t *testing.T) { 41 | _, _, err := checkRunFunc(context.Background(), func(t *testing.T, a int) int { 42 | return 0 43 | }) 44 | assert.EqualError(t, err, "must have no return outputs") 45 | }) 46 | 47 | t.Run("must have at least two args", func(t *testing.T) { 48 | _, _, err := checkRunFunc(context.Background(), func() { 49 | }) 50 | assert.EqualError(t, err, "must have at least two args") 51 | }) 52 | 53 | // function first argument type *testing.T does not match first kod.Run argument context.Context 54 | t.Run("function first argument type *testing.T does not match first kod.Run argument context.Context", func(t *testing.T) { 55 | _, _, err := checkRunFunc(context.Background(), func(t *testing.T, a *testing.T, b *testing.T) { 56 | }) 57 | assert.EqualError(t, err, "function first argument type *testing.T does not match first kod.Run argument context.Context") 58 | }) 59 | 60 | t.Run("function argument %d type %v must be a component interface or pointer to component implementation", func(t *testing.T) { 61 | _, _, err := checkRunFunc(context.Background(), func(ctx context.Context, t int) { 62 | }) 63 | assert.EqualError(t, err, "function argument 1 type int must be a component interface or pointer to component implementation") 64 | }) 65 | 66 | t.Run("ok", func(t *testing.T) { 67 | _, _, err := checkRunFunc(context.Background(), func(ctx context.Context, a *testComponent) { 68 | }) 69 | assert.Nil(t, err) 70 | }) 71 | } 72 | 73 | // extractComponentInterfaceType 74 | func Test_extractComponentInterfaceType(t *testing.T) { 75 | t.Run("not a pointer", func(t *testing.T) { 76 | _, err := extractComponentInterfaceType(reflect.TypeOf(0)) 77 | assert.EqualError(t, err, "type int is not a struct") 78 | }) 79 | 80 | t.Run("not a struct pointer", func(t *testing.T) { 81 | _, err := extractComponentInterfaceType(reflect.TypeOf(&testing.T{})) 82 | assert.EqualError(t, err, "type *testing.T is not a struct") 83 | }) 84 | 85 | t.Run("not a component interface", func(t *testing.T) { 86 | _, err := extractComponentInterfaceType(reflect.TypeOf(&struct{}{})) 87 | assert.EqualError(t, err, "type *struct {} is not a struct") 88 | }) 89 | 90 | t.Run("not a struct", func(t *testing.T) { 91 | _, err := extractComponentInterfaceType(reflect.TypeOf(&struct { 92 | Implements[testing.T] 93 | }{})) 94 | assert.EqualError(t, err, "type *struct { kod.Implements[testing.T] } is not a struct") 95 | }) 96 | 97 | t.Run("type struct {} does not embed kod.Implements", func(t *testing.T) { 98 | _, err := extractComponentInterfaceType(reflect.TypeOf(struct{}{})) 99 | assert.EqualError(t, err, "type struct {} does not embed kod.Implements") 100 | }) 101 | 102 | t.Run("ok", func(t *testing.T) { 103 | _, err := extractComponentInterfaceType(reflect.TypeOf(struct { 104 | Implements[testing.T] 105 | }{})) 106 | assert.Nil(t, err) 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /tests/case1/case.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/samber/lo" 12 | "go.opentelemetry.io/otel/baggage" 13 | 14 | "github.com/go-kod/kod" 15 | "github.com/go-kod/kod/interceptor" 16 | "github.com/go-kod/kod/interceptor/kaccesslog" 17 | "github.com/go-kod/kod/interceptor/kcircuitbreaker" 18 | "github.com/go-kod/kod/interceptor/kmetric" 19 | "github.com/go-kod/kod/interceptor/kratelimit" 20 | "github.com/go-kod/kod/interceptor/krecovery" 21 | "github.com/go-kod/kod/interceptor/ktimeout" 22 | "github.com/go-kod/kod/interceptor/ktrace" 23 | "github.com/go-kod/kod/interceptor/kvalidate" 24 | ) 25 | 26 | type test1Config struct { 27 | A string `default:"a"` 28 | Redis struct { 29 | Addr string `default:"localhost:6379"` 30 | Timeout time.Duration `default:"1s"` 31 | } 32 | } 33 | 34 | type test1ControllerImpl struct { 35 | kod.Implements[test1Controller] 36 | 37 | test1Component kod.Ref[Test1Component] 38 | } 39 | 40 | type serviceImpl struct { 41 | kod.Implements[testService] 42 | } 43 | 44 | func (t *serviceImpl) Foo(ctx context.Context) error { 45 | return nil 46 | } 47 | 48 | type modelImpl struct { 49 | kod.Implements[testRepository] 50 | } 51 | 52 | func (t *modelImpl) Foo(ctx context.Context) error { 53 | return nil 54 | } 55 | 56 | type test1Component struct { 57 | kod.Implements[Test1Component] 58 | kod.WithConfig[test1Config] 59 | } 60 | 61 | func (t *test1Component) Init(ctx context.Context) error { 62 | kod := kod.FromContext(ctx) 63 | t.L(ctx).InfoContext(ctx, "Init test1Component"+kod.Config().Name) 64 | 65 | return nil 66 | } 67 | 68 | func (t *test1Component) Interceptors() []interceptor.Interceptor { 69 | return []interceptor.Interceptor{ 70 | ktrace.Interceptor(), 71 | kmetric.Interceptor(), 72 | krecovery.Interceptor(), 73 | kratelimit.Interceptor(), 74 | kaccesslog.Interceptor(), 75 | kcircuitbreaker.Interceptor(), 76 | kvalidate.Interceptor(), 77 | ktimeout.Interceptor(ktimeout.WithTimeout(time.Second)), 78 | } 79 | } 80 | 81 | func (t *test1Component) Shutdown(ctx context.Context) error { 82 | return nil 83 | } 84 | 85 | type FooReq struct { 86 | Id int `validate:"lt=100"` 87 | Panic bool 88 | } 89 | 90 | type FooRes struct { 91 | Id int 92 | } 93 | 94 | func (t *test1Component) Foo(ctx context.Context, req *FooReq) (*FooRes, error) { 95 | if req.Panic { 96 | panic("test panic") 97 | } 98 | 99 | ctx = baggage.ContextWithBaggage(ctx, lo.Must(baggage.New(lo.Must(baggage.NewMember("b1", "v1"))))) 100 | t.L(ctx).InfoContext(ctx, "Foo info ", slog.Any("config", t.Config())) 101 | t.L(ctx).ErrorContext(ctx, "Foo error:") 102 | t.L(ctx).DebugContext(ctx, "Foo debug:") 103 | t.L(ctx).WithGroup("test group").InfoContext(ctx, "Foo info with group") 104 | 105 | return &FooRes{Id: req.Id}, errors.New("test1:" + t.Config().A) 106 | } 107 | 108 | type fakeTest1Component struct { 109 | A string 110 | } 111 | 112 | var _ Test1Component = (*fakeTest1Component)(nil) 113 | 114 | func (f *fakeTest1Component) Foo(ctx context.Context, req *FooReq) (*FooRes, error) { 115 | fmt.Println(f.A) 116 | return nil, errors.New("A:" + f.A) 117 | } 118 | 119 | type test2Component struct { 120 | kod.Implements[Test2Component] 121 | kod.WithConfig[test1Config] 122 | } 123 | 124 | func (t *test2Component) GetClient() *http.Client { 125 | slog.Info("Foo info ", "config", t.Config()) 126 | slog.Debug("Foo debug:") 127 | fmt.Println(errors.New("test1")) 128 | return &http.Client{} 129 | } 130 | 131 | type App struct { 132 | kod.Implements[kod.Main] 133 | test1 kod.Ref[Test1Component] 134 | } 135 | 136 | func Run(ctx context.Context, app *App) error { 137 | _, err := app.test1.Get().Foo(ctx, &FooReq{}) 138 | return err 139 | } 140 | 141 | // func StartTrace(ctx context.Context) context.Context { 142 | // var opts []sdktrace.TracerProviderOption 143 | 144 | // provider := sdktrace.NewTracerProvider(opts...) 145 | // otel.SetTracerProvider(provider) 146 | 147 | // exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) 148 | // if err != nil { 149 | // panic(err) 150 | // } else { 151 | // provider.RegisterSpanProcessor(sdktrace.NewSimpleSpanProcessor(exporter)) 152 | // } 153 | 154 | // ctx, span := otel.Tracer("").Start(ctx, "Run") 155 | // defer func() { 156 | // span.End() 157 | // fmt.Println("!!!!!!") 158 | // }() 159 | 160 | // return ctx 161 | // } 162 | -------------------------------------------------------------------------------- /tests/case1/case_context.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-kod/kod" 8 | "github.com/go-kod/kod/interceptor" 9 | "github.com/go-kod/kod/interceptor/kaccesslog" 10 | "github.com/go-kod/kod/interceptor/kcircuitbreaker" 11 | "github.com/go-kod/kod/interceptor/kmetric" 12 | "github.com/go-kod/kod/interceptor/kratelimit" 13 | "github.com/go-kod/kod/interceptor/krecovery" 14 | "github.com/go-kod/kod/interceptor/ktimeout" 15 | "github.com/go-kod/kod/interceptor/ktrace" 16 | ) 17 | 18 | type ctxImpl struct { 19 | kod.Implements[ctxInterface] 20 | } 21 | 22 | // Foo is a http handler 23 | func (t *ctxImpl) Foo(ctx context.Context) { 24 | _, ok := ctx.Deadline() 25 | if !ok { 26 | panic("no deadline") 27 | } 28 | } 29 | 30 | func (t *ctxImpl) Interceptors() []interceptor.Interceptor { 31 | return []interceptor.Interceptor{ 32 | krecovery.Interceptor(), 33 | kaccesslog.Interceptor(), 34 | ktimeout.Interceptor(ktimeout.WithTimeout(time.Second)), 35 | kmetric.Interceptor(), 36 | ktrace.Interceptor(), 37 | kcircuitbreaker.Interceptor(), 38 | kratelimit.Interceptor(), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/case1/case_context_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | func TestCtxImpl(t *testing.T) { 11 | t.Parallel() 12 | kod.RunTest(t, func(ctx context.Context, k ctxInterface) { 13 | k.Foo(ctx) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /tests/case1/case_default_config.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "github.com/go-kod/kod" 5 | ) 6 | 7 | type errorConfig struct { 8 | A int `default:"sss"` 9 | } 10 | 11 | type test1ComponentDefaultErrorImpl struct { 12 | kod.Implements[test1ComponentDefaultError] 13 | kod.WithConfig[*errorConfig] 14 | } 15 | 16 | type test1ComponentGlobalDefaultErrorImpl struct { 17 | kod.Implements[test1ComponentGlobalDefaultError] 18 | kod.WithGlobalConfig[*errorConfig] 19 | } 20 | -------------------------------------------------------------------------------- /tests/case1/case_default_config_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/internal/mock" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestDefaultConfig(t *testing.T) { 14 | log, observer := kod.NewTestLogger() 15 | slog.SetDefault(log) 16 | 17 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 18 | observer.Clean() 19 | 20 | k.L(ctx).Info("hello", "config", k.Config()) 21 | 22 | assert.Equal(t, 1, observer.Len()) 23 | assert.Equal(t, "{\"component\":\"github.com/go-kod/kod/tests/case1/Test1Component\",\"config\":{\"A\":\"B\",\"Redis\":{\"Addr\":\"localhost:6379\",\"Timeout\":2000000000}},\"level\":\"INFO\",\"msg\":\"hello\"}\n", 24 | observer.RemoveKeys("time").String()) 25 | }) 26 | } 27 | 28 | func TestDefaultConfig2(t *testing.T) { 29 | log, observer := kod.NewTestLogger() 30 | slog.SetDefault(log) 31 | 32 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 33 | observer.Clean() 34 | 35 | k.L(ctx).Info("hello", "config", k.Config()) 36 | 37 | assert.Equal(t, 1, observer.Len()) 38 | assert.Equal(t, "{\"component\":\"github.com/go-kod/kod/tests/case1/Test1Component\",\"config\":{\"A\":\"B2\",\"Redis\":{\"Addr\":\"localhost:6379\",\"Timeout\":1000000000}},\"level\":\"INFO\",\"msg\":\"hello\"}\n", 39 | observer.RemoveKeys("time").String()) 40 | }, kod.WithConfigFile("./kod2.toml")) 41 | } 42 | 43 | func TestDefaultConfigError(t *testing.T) { 44 | mock.ExpectFailure(t, func(tb testing.TB) { 45 | kod.RunTest(tb, func(ctx context.Context, k *test1ComponentDefaultErrorImpl) { 46 | k.L(ctx).Info("hello", "config", k.Config()) 47 | }, kod.WithConfigFile("./kod2.toml")) 48 | }) 49 | } 50 | 51 | func TestDefaultGlobalConfigError(t *testing.T) { 52 | mock.ExpectFailure(t, func(tb testing.TB) { 53 | kod.RunTest(tb, func(ctx context.Context, k *test1ComponentGlobalDefaultErrorImpl) { 54 | k.L(ctx).Info("hello", "config", k.Config()) 55 | }, kod.WithConfigFile("./kod2.toml")) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /tests/case1/case_echo.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/avast/retry-go/v4" 8 | "github.com/labstack/echo/v4" 9 | 10 | "github.com/go-kod/kod" 11 | "github.com/go-kod/kod/interceptor" 12 | "github.com/go-kod/kod/interceptor/kretry" 13 | ) 14 | 15 | type testEchoControllerImpl struct { 16 | kod.Implements[testEchoController] 17 | 18 | retry int 19 | } 20 | 21 | // Hello is a method of testEchoControllerImpl 22 | func (t *testEchoControllerImpl) Hello(c echo.Context) error { 23 | return c.String(200, "Hello, World!") 24 | } 25 | 26 | // Error is a method of testEchoControllerImpl 27 | func (t *testEchoControllerImpl) Error(c echo.Context) error { 28 | t.retry++ 29 | return errors.New("!!!") 30 | } 31 | 32 | func (t *testEchoControllerImpl) Interceptors() []interceptor.Interceptor { 33 | return []interceptor.Interceptor{ 34 | func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) (err error) { 35 | return invoker(ctx, info, req, reply) 36 | }, 37 | kretry.Interceptor(retry.Attempts(2)), 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/case1/case_echo_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/labstack/echo/v4" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/go-kod/kod" 13 | ) 14 | 15 | func Test_testEchoControllerImpl_Hello(t *testing.T) { 16 | kod.RunTest(t, func(ctx context.Context, controller testEchoController) { 17 | e := echo.New() 18 | req := httptest.NewRequest(http.MethodGet, "/", nil) 19 | rec := httptest.NewRecorder() 20 | c := e.NewContext(req, rec) 21 | require.Nil(t, controller.Hello(c)) 22 | require.Equal(t, http.StatusOK, rec.Code) 23 | require.Equal(t, "Hello, World!", rec.Body.String()) 24 | }) 25 | } 26 | 27 | func Test_testEchoControllerImpl_Panic(t *testing.T) { 28 | kod.RunTest(t, func(ctx context.Context, controller testEchoController) { 29 | e := echo.New() 30 | req := httptest.NewRequest(http.MethodGet, "/", nil) 31 | rec := httptest.NewRecorder() 32 | c := e.NewContext(req, rec) 33 | controller.Error(c) 34 | require.Equal(t, controller.(testEchoController_local_stub).impl.(*testEchoControllerImpl).retry, 1) 35 | require.Equal(t, "", rec.Body.String()) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /tests/case1/case_gin.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | ) 11 | 12 | type testGinControllerImpl struct { 13 | kod.Implements[testGinController] 14 | } 15 | 16 | // Hello is a method of testGinControllerImpl 17 | func (t *testGinControllerImpl) Hello(c *gin.Context) { 18 | c.String(200, "Hello, World!") 19 | } 20 | 21 | func (t *testGinControllerImpl) Interceptors() []interceptor.Interceptor { 22 | return []interceptor.Interceptor{ 23 | func(ctx context.Context, info interceptor.CallInfo, req, reply []any, invoker interceptor.HandleFunc) (err error) { 24 | return invoker(ctx, info, req, reply) 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/case1/case_gin_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/go-kod/kod" 13 | ) 14 | 15 | func Test_testGinControllerImpl_Hello(t *testing.T) { 16 | kod.RunTest(t, func(ctx context.Context, controller testGinController) { 17 | rec := httptest.NewRecorder() 18 | c := gin.CreateTestContextOnly(rec, gin.New()) 19 | c.Request, _ = http.NewRequest(http.MethodGet, "/hello/gin", nil) 20 | controller.Hello(c) 21 | require.Equal(t, http.StatusOK, rec.Code) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /tests/case1/case_http.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-kod/kod" 7 | ) 8 | 9 | type httpControllerImpl struct { 10 | kod.Implements[HTTPController] 11 | } 12 | 13 | // Foo is a http handler 14 | func (t *httpControllerImpl) Foo(w http.ResponseWriter, r *http.Request) { 15 | w.Write([]byte("Hello, World!")) 16 | } 17 | -------------------------------------------------------------------------------- /tests/case1/case_http_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/go-kod/kod" 12 | ) 13 | 14 | func TestHttpHandler(t *testing.T) { 15 | t.Parallel() 16 | kod.RunTest(t, func(ctx context.Context, k HTTPController) { 17 | record := httptest.NewRecorder() 18 | 19 | r, _ := http.NewRequest(http.MethodGet, "/hello/gin", nil) 20 | r = r.WithContext(ctx) 21 | 22 | k.Foo(record, r) 23 | 24 | require.Equal(t, http.StatusOK, record.Code) 25 | require.Equal(t, "Hello, World!", record.Body.String()) 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /tests/case1/case_interceptor_retry.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/avast/retry-go/v4" 8 | 9 | "github.com/go-kod/kod" 10 | "github.com/go-kod/kod/interceptor" 11 | "github.com/go-kod/kod/interceptor/kretry" 12 | ) 13 | 14 | type interceptorRetry struct { 15 | kod.Implements[InterceptorRetry] 16 | } 17 | 18 | func (t *interceptorRetry) Init(ctx context.Context) error { 19 | t.L(ctx).Info("interceptorRetry init...") 20 | 21 | return nil 22 | } 23 | 24 | func (t *interceptorRetry) TestError(ctx context.Context) error { 25 | return errors.New("test error") 26 | } 27 | 28 | func (t *interceptorRetry) TestNormal(ctx context.Context) error { 29 | return nil 30 | } 31 | 32 | func (t *interceptorRetry) Shutdown(ctx context.Context) error { 33 | t.L(ctx).Info("interceptorRetry shutdown...") 34 | 35 | return nil 36 | } 37 | 38 | func (t *interceptorRetry) Interceptors() []interceptor.Interceptor { 39 | return []interceptor.Interceptor{ 40 | kretry.Interceptor( 41 | retry.Attempts(2), 42 | ), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/case1/case_interceptor_retry_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/go-kod/kod" 10 | ) 11 | 12 | func TestInterceptorRetry(t *testing.T) { 13 | kod.RunTest(t, func(ctx context.Context, k InterceptorRetry) { 14 | require.ErrorContains(t, k.TestError(ctx), "retry fail") 15 | }) 16 | } 17 | 18 | func TestInterceptorRetry1(t *testing.T) { 19 | kod.RunTest(t, func(ctx context.Context, k InterceptorRetry) { 20 | require.Nil(t, k.TestNormal(ctx)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /tests/case1/case_lazy_init.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kod/kod" 7 | ) 8 | 9 | type lazyInitImpl struct { 10 | kod.Implements[LazyInitImpl] 11 | test kod.Ref[LazyInitComponent] 12 | } 13 | 14 | func (t *lazyInitImpl) Init(ctx context.Context) error { 15 | t.L(ctx).Info("lazyInitImpl init...") 16 | 17 | return nil 18 | } 19 | 20 | func (t *lazyInitImpl) Try(ctx context.Context) { 21 | t.L(ctx).Info("Hello, World!") 22 | } 23 | 24 | func (t *lazyInitImpl) Shutdown(ctx context.Context) error { 25 | t.L(ctx).Info("lazyInitImpl shutdown...") 26 | 27 | return nil 28 | } 29 | 30 | type lazyInitComponent struct { 31 | kod.Implements[LazyInitComponent] 32 | kod.LazyInit 33 | } 34 | 35 | func (t *lazyInitComponent) Init(ctx context.Context) error { 36 | t.L(ctx).Info("lazyInitComponent init...") 37 | 38 | return nil 39 | } 40 | 41 | func (t *lazyInitComponent) Try(ctx context.Context) error { 42 | t.L(ctx).Info("Just do it!") 43 | 44 | return nil 45 | } 46 | 47 | func (t *lazyInitComponent) Shutdown(ctx context.Context) error { 48 | t.L(ctx).Info("lazyInitComponent shutdown...") 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /tests/case1/case_lazy_init_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/go-kod/kod" 11 | ) 12 | 13 | func TestLazyInit(t *testing.T) { 14 | log, observer := kod.NewTestLogger() 15 | slog.SetDefault(log) 16 | 17 | kod.RunTest(t, func(ctx context.Context, k *lazyInitImpl) { 18 | require.Equal(t, 1, observer.Len(), observer.String()) 19 | 20 | k.Try(ctx) 21 | 22 | require.Equal(t, 2, observer.Len(), observer.String()) 23 | 24 | k.test.Get() 25 | require.Equal(t, 3, observer.Len(), observer.String()) 26 | 27 | require.Nil(t, k.test.Get().Try(ctx)) 28 | require.Equal(t, 4, observer.Len(), observer.String()) 29 | }) 30 | } 31 | 32 | func TestLazyInitTest(t *testing.T) { 33 | log, observer := kod.NewTestLogger() 34 | slog.SetDefault(log) 35 | 36 | kod.RunTest2(t, func(ctx context.Context, k *lazyInitImpl, comp *lazyInitComponent) { 37 | k.Try(ctx) 38 | 39 | require.Equal(t, 3, observer.Len(), observer.String()) 40 | 41 | require.Equal(t, k.test.Get(), k.test.Get()) 42 | 43 | require.Equal(t, 3, observer.Len(), observer.String()) 44 | }) 45 | } 46 | 47 | func TestLazyInitTest2(t *testing.T) { 48 | log, observer := kod.NewTestLogger() 49 | slog.SetDefault(log) 50 | 51 | kod.RunTest2(t, func(ctx context.Context, k LazyInitImpl, comp LazyInitComponent) { 52 | require.Equal(t, 2, observer.Len(), observer.String()) 53 | 54 | k.Try(ctx) 55 | 56 | require.Equal(t, 3, observer.Len(), observer.String()) 57 | }) 58 | } 59 | 60 | func TestLazyInitTest3(t *testing.T) { 61 | log, observer := kod.NewTestLogger() 62 | slog.SetDefault(log) 63 | 64 | kod.RunTest2(t, func(ctx context.Context, k *lazyInitImpl, comp LazyInitComponent) { 65 | k.Try(ctx) 66 | 67 | require.Equal(t, 3, observer.Len(), observer.String()) 68 | 69 | // require.Equal(t, comp, k.test.Get()) 70 | require.Equal(t, 3, observer.Len(), observer.String()) 71 | 72 | require.Nil(t, k.test.Get().Try(ctx)) 73 | require.Equal(t, 4, observer.Len(), observer.String()) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /tests/case1/case_log_file_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | "github.com/go-kod/kod" 13 | ) 14 | 15 | func TestLogFile(t *testing.T) { 16 | log, observer := kod.NewTestLogger() 17 | slog.SetDefault(log) 18 | 19 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 20 | _, err := k.Foo(ctx, &FooReq{Id: 1}) 21 | fmt.Println(observer.String()) 22 | require.Equal(t, "test1:B", err.Error()) 23 | require.Equal(t, 5, observer.Len()) 24 | require.Equal(t, 2, observer.Filter(func(r map[string]any) bool { 25 | return r["level"] == slog.LevelError.String() 26 | }).Len()) 27 | require.Equal(t, 0, observer.Clean().Len()) 28 | slog.Info("test") 29 | require.Equal(t, 1, observer.Len()) 30 | os.Remove("./testapp.json") 31 | }, kod.WithConfigFile("./kod-logfile.toml")) 32 | } 33 | -------------------------------------------------------------------------------- /tests/case1/case_log_level_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestLogLevel(t *testing.T) { 13 | log, observer := kod.NewTestLogger() 14 | slog.SetDefault(log) 15 | 16 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 17 | observer.Clean() 18 | 19 | k.L(ctx).Debug("debug") 20 | k.L(ctx).WithGroup("group").Info("info") 21 | 22 | assert.Equal(t, 1, observer.Len()) 23 | assert.Equal(t, 0, observer.Filter(func(r map[string]any) bool { 24 | return r["level"] == slog.LevelDebug.String() 25 | }).Len()) 26 | }) 27 | 28 | t.Setenv("KOD_LOG_LEVEL", "debug") 29 | 30 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 31 | observer.Clean() 32 | 33 | k.L(ctx).Debug("debug") 34 | k.L(ctx).WithGroup("group").Info("info") 35 | 36 | assert.Equal(t, 1, observer.Len()) 37 | assert.Equal(t, 0, observer.Filter(func(r map[string]any) bool { 38 | return r["level"] == slog.LevelDebug.String() 39 | }).Len()) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /tests/case1/case_log_mock_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/go-kod/kod" 11 | ) 12 | 13 | func TestMockLog(t *testing.T) { 14 | log, observer := kod.NewTestLogger() 15 | slog.SetDefault(log) 16 | t.Setenv("KOD_LOG_LEVEL", "error") 17 | 18 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 19 | _, err := k.Foo(ctx, &FooReq{Id: 1}) 20 | require.Equal(t, "test1:B", err.Error()) 21 | require.Equal(t, 5, observer.Len(), observer.String()) 22 | require.Equal(t, 2, observer.Filter(func(r map[string]any) bool { 23 | return r["level"] == slog.LevelError.String() 24 | }).Len()) 25 | require.Equal(t, 2, observer.ErrorCount()) 26 | require.Equal(t, 0, observer.Clean().Len()) 27 | slog.Info("test") 28 | require.Equal(t, 1, observer.Len()) 29 | require.Equal(t, 0, observer.ErrorCount()) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /tests/case1/case_runtest_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/go-kod/kod" 11 | ) 12 | 13 | func TestTest(t *testing.T) { 14 | t.Parallel() 15 | 16 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 17 | _, err := k.Foo(ctx, &FooReq{}) 18 | fmt.Println(err) 19 | require.Equal(t, "test1:B", err.Error()) 20 | }) 21 | } 22 | 23 | func TestTest2(t *testing.T) { 24 | t.Parallel() 25 | 26 | kod.RunTest2(t, func(ctx context.Context, k *test1Component, k2 Test2Component) { 27 | _, err := k.Foo(ctx, &FooReq{}) 28 | fmt.Println(err) 29 | require.Equal(t, "test1:B", err.Error()) 30 | }) 31 | } 32 | 33 | func TestTest3(t *testing.T) { 34 | t.Parallel() 35 | 36 | require.Panics(t, func() { 37 | kod.RunTest3(t, func(ctx context.Context, k *test1Component, k2 panicNoRecvoeryCaseInterface, k3 test1Controller) { 38 | _, err := k.Foo(ctx, &FooReq{}) 39 | fmt.Println(err) 40 | require.Equal(t, "test1:B", err.Error()) 41 | 42 | k2.TestPanic(ctx) 43 | }) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /tests/case1/case_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "syscall" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/trace" 13 | "go.uber.org/goleak" 14 | "go.uber.org/mock/gomock" 15 | 16 | "github.com/go-kod/kod" 17 | "github.com/go-kod/kod/internal/mock" 18 | ) 19 | 20 | func TestMain(m *testing.M) { 21 | goleak.VerifyTestMain(m, 22 | goleak.IgnoreAnyFunction("github.com/go-kod/kod/interceptor/internal/ratelimit.cpuproc"), 23 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/metric.(*PeriodicReader).run"), 24 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/trace.(*batchSpanProcessor).processQueue"), 25 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/log.exportSync.func1"), 26 | goleak.IgnoreAnyFunction("go.opentelemetry.io/otel/sdk/log.(*BatchProcessor).poll.func1"), 27 | goleak.IgnoreAnyFunction("github.com/go-kod/kod/internal/signals.Shutdown.func1"), 28 | ) 29 | } 30 | 31 | func TestRun(t *testing.T) { 32 | t.Parallel() 33 | 34 | t.Run("case1", func(t *testing.T) { 35 | err := kod.Run(context.Background(), Run) 36 | require.Equal(t, "test1:B", err.Error()) 37 | }) 38 | } 39 | 40 | func TestImpl(t *testing.T) { 41 | t.Parallel() 42 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 43 | _, err := k.Foo(ctx, &FooReq{}) 44 | fmt.Println(err) 45 | require.Equal(t, "test1:B", err.Error()) 46 | }) 47 | } 48 | 49 | func TestInterface(t *testing.T) { 50 | // t.Parallel() 51 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 52 | // ctx = StartTrace(ctx) 53 | 54 | ctx, span := otel.Tracer("").Start(ctx, "Run", trace.WithSpanKind(trace.SpanKindInternal)) 55 | defer func() { 56 | span.End() 57 | fmt.Println("!!!!!!") 58 | }() 59 | 60 | _, err := k.Foo(ctx, &FooReq{Id: 1}) 61 | res, err := k.Foo(ctx, &FooReq{Id: 2}) 62 | fmt.Println(err) 63 | require.Equal(t, "test1:B", err.Error()) 64 | require.False(t, span.SpanContext().IsValid()) 65 | require.Equal(t, 2, res.Id) 66 | }) 67 | 68 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 69 | // ctx = StartTrace(ctx) 70 | 71 | ctx, span := otel.Tracer("").Start(ctx, "Run", trace.WithSpanKind(trace.SpanKindInternal)) 72 | defer func() { 73 | span.End() 74 | fmt.Println("!!!!!!") 75 | }() 76 | 77 | _, err := k.Foo(ctx, &FooReq{Id: 1}) 78 | res, err := k.Foo(ctx, &FooReq{Id: 2}) 79 | fmt.Println(err) 80 | require.Equal(t, "test1:B", err.Error()) 81 | require.False(t, span.SpanContext().IsValid()) 82 | require.Equal(t, 2, res.Id) 83 | }) 84 | } 85 | 86 | func TestInterfacePanic(t *testing.T) { 87 | t.Parallel() 88 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 89 | _, err := k.Foo(ctx, &FooReq{ 90 | Panic: true, 91 | }) 92 | require.Contains(t, err.Error(), "panic caught: test panic") 93 | }) 94 | } 95 | 96 | func TestInterfacValidate(t *testing.T) { 97 | t.Parallel() 98 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 99 | _, err := k.Foo(ctx, &FooReq{ 100 | Id: 101, 101 | }) 102 | require.Contains(t, err.Error(), "validate failed: Key: 'FooReq.Id' Error:Field validation for 'Id' failed on the 'lt' tag") 103 | }) 104 | } 105 | 106 | func TestFake(t *testing.T) { 107 | t.Parallel() 108 | fakeTest1 := &fakeTest1Component{"B"} 109 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 110 | _, err := k.Foo(ctx, &FooReq{}) 111 | fmt.Println(err) 112 | require.Equal(t, errors.New("A:B"), err) 113 | }, kod.WithFakes(kod.Fake[Test1Component](fakeTest1))) 114 | } 115 | 116 | func TestFakeWithMock(t *testing.T) { 117 | t.Parallel() 118 | fakeTest1 := NewMockTest1Component(gomock.NewController(t)) 119 | fakeTest1.EXPECT().Foo(gomock.Any(), gomock.Any()).Return(&FooRes{}, errors.New("A:B")) 120 | kod.RunTest(t, func(ctx context.Context, k Test1Component) { 121 | _, err := k.Foo(ctx, &FooReq{}) 122 | fmt.Println(err) 123 | require.Equal(t, errors.New("A:B"), err) 124 | }, kod.WithFakes(kod.Fake[Test1Component](fakeTest1))) 125 | } 126 | 127 | func TestConflictFake(t *testing.T) { 128 | t.Parallel() 129 | fakeTest1 := &fakeTest1Component{"B"} 130 | mock.ExpectFailure(t, func(tt testing.TB) { 131 | kod.RunTest(tt, func(ctx context.Context, k *test1Component) { 132 | _, err := k.Foo(ctx, &FooReq{}) 133 | fmt.Println(err) 134 | require.Equal(t, errors.New("A:B"), err) 135 | }, kod.WithFakes(kod.Fake[Test1Component](fakeTest1))) 136 | }) 137 | } 138 | 139 | func TestConfigFile1(t *testing.T) { 140 | t.Parallel() 141 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 142 | _, err := k.Foo(ctx, &FooReq{}) 143 | fmt.Println(err) 144 | require.Equal(t, "B", k.Config().A) 145 | require.Equal(t, "test1:B", err.Error()) 146 | }, kod.WithConfigFile("kod.toml")) 147 | } 148 | 149 | func TestConfigFileYaml(t *testing.T) { 150 | t.Parallel() 151 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 152 | _, err := k.Foo(ctx, &FooReq{}) 153 | fmt.Println(err) 154 | require.Equal(t, "B", k.Config().A) 155 | require.Equal(t, "test1:B", err.Error()) 156 | }, kod.WithConfigFile("kod.yaml")) 157 | } 158 | 159 | func TestConfigFileJSON(t *testing.T) { 160 | t.Parallel() 161 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 162 | _, err := k.Foo(ctx, &FooReq{}) 163 | fmt.Println(err) 164 | require.Equal(t, "B", k.Config().A) 165 | require.Equal(t, "test1:B", err.Error()) 166 | }, kod.WithConfigFile("kod.json")) 167 | } 168 | 169 | func TestConfigFile2(t *testing.T) { 170 | t.Parallel() 171 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 172 | _, err := k.Foo(ctx, &FooReq{}) 173 | fmt.Println(err) 174 | require.Equal(t, "test1:B2", err.Error()) 175 | }, kod.WithConfigFile("kod2.toml")) 176 | } 177 | 178 | func TestRunKill(t *testing.T) { 179 | t.Run("case1", func(t *testing.T) { 180 | err := kod.Run(context.Background(), Run) 181 | 182 | require.Nil(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT)) 183 | 184 | require.Equal(t, "test1:B", err.Error()) 185 | }) 186 | } 187 | 188 | func TestPanicKod(t *testing.T) { 189 | kod.RunTest(t, func(ctx context.Context, k *test1Component) { 190 | require.Panics(t, func() { 191 | kod := kod.FromContext(context.Background()) 192 | kod.Config() 193 | }) 194 | }) 195 | } 196 | 197 | func BenchmarkCase1(b *testing.B) { 198 | b.Run("case1", func(b *testing.B) { 199 | kod.RunTest(b, func(ctx context.Context, k *test1Component) { 200 | for i := 0; i < b.N; i++ { 201 | _, err := k.Foo(ctx, &FooReq{}) 202 | require.Equal(b, "B", k.Config().A) 203 | require.Equal(b, "test1:B", err.Error()) 204 | } 205 | }) 206 | }) 207 | } 208 | -------------------------------------------------------------------------------- /tests/case1/kod-logfile.toml: -------------------------------------------------------------------------------- 1 | [kod] 2 | name = "testapp" 3 | log.file = "testapp.json" 4 | log.level = "info" 5 | 6 | ["github.com/go-kod/kod/tests/case1/Test1Component"] 7 | A="B" -------------------------------------------------------------------------------- /tests/case1/kod.json: -------------------------------------------------------------------------------- 1 | { 2 | "kod": { 3 | "name": "testapp", 4 | "trace": { 5 | "debug": true 6 | } 7 | }, 8 | "github.com/go-kod/kod/tests/case1/Test1Component": { 9 | "A": "B", 10 | "redis": { 11 | "addr": "localhost:6379", 12 | "timeout": "2s" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tests/case1/kod.toml: -------------------------------------------------------------------------------- 1 | [kod] 2 | name = "testapp" 3 | [kod.trace] 4 | debug = true 5 | 6 | ["github.com/go-kod/kod/tests/case1/Test1Component"] 7 | A="B" 8 | redis = { addr = "localhost:6379", timeout = "2s" } 9 | -------------------------------------------------------------------------------- /tests/case1/kod.yaml: -------------------------------------------------------------------------------- 1 | kod: 2 | name: testapp 3 | trace: 4 | debug: true 5 | 6 | github.com/go-kod/kod/tests/case1/Test1Component: 7 | A: "B" 8 | redis: 9 | addr: localhost:6379 10 | timeout: 2s 11 | -------------------------------------------------------------------------------- /tests/case1/kod2.toml: -------------------------------------------------------------------------------- 1 | [kod] 2 | name = "testapp" 3 | [kod.trace] 4 | debug = true 5 | 6 | ["github.com/go-kod/kod/tests/case1/Test1Component"] 7 | A="B2" 8 | redis = { addr = "localhost:6379" } 9 | -------------------------------------------------------------------------------- /tests/case1/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package case1 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/labstack/echo/v4" 11 | ) 12 | 13 | // test1Controller is implemented by [test1ControllerImpl], 14 | // which can be mocked with [NewMocktest1Controller]. 15 | type test1Controller interface { 16 | } 17 | 18 | // testService is implemented by [serviceImpl], 19 | // which can be mocked with [NewMocktestService]. 20 | type testService interface { 21 | // Foo is implemented by [serviceImpl.Foo] 22 | Foo(ctx context.Context) error 23 | } 24 | 25 | // testRepository is implemented by [modelImpl], 26 | // which can be mocked with [NewMocktestRepository]. 27 | type testRepository interface { 28 | // Foo is implemented by [modelImpl.Foo] 29 | Foo(ctx context.Context) error 30 | } 31 | 32 | // Test1Component is implemented by [test1Component], 33 | // which can be mocked with [NewMockTest1Component]. 34 | type Test1Component interface { 35 | // Foo is implemented by [test1Component.Foo] 36 | Foo(ctx context.Context, req *FooReq) (*FooRes, error) 37 | } 38 | 39 | // Test2Component is implemented by [test2Component], 40 | // which can be mocked with [NewMockTest2Component]. 41 | type Test2Component interface { 42 | // GetClient is implemented by [test2Component.GetClient] 43 | GetClient() *http.Client 44 | } 45 | 46 | // ctxInterface is implemented by [ctxImpl], 47 | // which can be mocked with [NewMockctxInterface]. 48 | type ctxInterface interface { 49 | // Foo is implemented by [ctxImpl.Foo] 50 | // 51 | // Foo is a http handler 52 | Foo(ctx context.Context) 53 | } 54 | 55 | // test1ComponentDefaultError is implemented by [test1ComponentDefaultErrorImpl], 56 | // which can be mocked with [NewMocktest1ComponentDefaultError]. 57 | type test1ComponentDefaultError interface { 58 | } 59 | 60 | // test1ComponentGlobalDefaultError is implemented by [test1ComponentGlobalDefaultErrorImpl], 61 | // which can be mocked with [NewMocktest1ComponentGlobalDefaultError]. 62 | type test1ComponentGlobalDefaultError interface { 63 | } 64 | 65 | // testEchoController is implemented by [testEchoControllerImpl], 66 | // which can be mocked with [NewMocktestEchoController]. 67 | type testEchoController interface { 68 | // Hello is implemented by [testEchoControllerImpl.Hello] 69 | // 70 | // Hello is a method of testEchoControllerImpl 71 | Hello(c echo.Context) error 72 | // Error is implemented by [testEchoControllerImpl.Error] 73 | // 74 | // Error is a method of testEchoControllerImpl 75 | Error(c echo.Context) error 76 | } 77 | 78 | // testGinController is implemented by [testGinControllerImpl], 79 | // which can be mocked with [NewMocktestGinController]. 80 | type testGinController interface { 81 | // Hello is implemented by [testGinControllerImpl.Hello] 82 | // 83 | // Hello is a method of testGinControllerImpl 84 | Hello(c *gin.Context) 85 | } 86 | 87 | // HTTPController is implemented by [httpControllerImpl], 88 | // which can be mocked with [NewMockHTTPController]. 89 | type HTTPController interface { 90 | // Foo is implemented by [httpControllerImpl.Foo] 91 | // 92 | // Foo is a http handler 93 | Foo(w http.ResponseWriter, r *http.Request) 94 | } 95 | 96 | // InterceptorRetry is implemented by [interceptorRetry], 97 | // which can be mocked with [NewMockInterceptorRetry]. 98 | type InterceptorRetry interface { 99 | // TestError is implemented by [interceptorRetry.TestError] 100 | TestError(ctx context.Context) error 101 | // TestNormal is implemented by [interceptorRetry.TestNormal] 102 | TestNormal(ctx context.Context) error 103 | } 104 | 105 | // LazyInitImpl is implemented by [lazyInitImpl], 106 | // which can be mocked with [NewMockLazyInitImpl]. 107 | type LazyInitImpl interface { 108 | // Try is implemented by [lazyInitImpl.Try] 109 | Try(ctx context.Context) 110 | } 111 | 112 | // LazyInitComponent is implemented by [lazyInitComponent], 113 | // which can be mocked with [NewMockLazyInitComponent]. 114 | type LazyInitComponent interface { 115 | // Try is implemented by [lazyInitComponent.Try] 116 | Try(ctx context.Context) error 117 | } 118 | 119 | // panicCaseInterface is implemented by [panicCase], 120 | // which can be mocked with [NewMockpanicCaseInterface]. 121 | type panicCaseInterface interface { 122 | // TestPanic is implemented by [panicCase.TestPanic] 123 | TestPanic(ctx context.Context) 124 | } 125 | 126 | // panicNoRecvoeryCaseInterface is implemented by [panicNoRecvoeryCase], 127 | // which can be mocked with [NewMockpanicNoRecvoeryCaseInterface]. 128 | type panicNoRecvoeryCaseInterface interface { 129 | // TestPanic is implemented by [panicNoRecvoeryCase.TestPanic] 130 | TestPanic(ctx context.Context) 131 | } 132 | -------------------------------------------------------------------------------- /tests/case1/panic.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/go-kod/kod" 7 | "github.com/go-kod/kod/interceptor" 8 | "github.com/go-kod/kod/interceptor/krecovery" 9 | ) 10 | 11 | type panicCase struct { 12 | kod.Implements[panicCaseInterface] 13 | } 14 | 15 | func (t *panicCase) TestPanic(ctx context.Context) { 16 | panic("panic") 17 | } 18 | 19 | func (t *panicCase) Interceptors() []interceptor.Interceptor { 20 | return []interceptor.Interceptor{ 21 | krecovery.Interceptor(), 22 | } 23 | } 24 | 25 | type panicNoRecvoeryCase struct { 26 | kod.Implements[panicNoRecvoeryCaseInterface] 27 | } 28 | 29 | func (t *panicNoRecvoeryCase) TestPanic(ctx context.Context) { 30 | panic("panic") 31 | } 32 | -------------------------------------------------------------------------------- /tests/case1/panic_test.go: -------------------------------------------------------------------------------- 1 | package case1 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-kod/kod" 8 | "github.com/go-kod/kod/interceptor/krecovery" 9 | ) 10 | 11 | func TestPanicRecovery(t *testing.T) { 12 | t.Parallel() 13 | 14 | kod.RunTest(t, func(ctx context.Context, t panicCaseInterface) { 15 | t.TestPanic(ctx) 16 | }) 17 | } 18 | 19 | func TestRunSetInterceptor(t *testing.T) { 20 | t.Parallel() 21 | 22 | t.Run("panicNoRecvoeryCase with interceptor", func(t *testing.T) { 23 | kod.RunTest(t, func(ctx context.Context, t panicNoRecvoeryCaseInterface) { 24 | kod.FromContext(ctx).SetInterceptors(krecovery.Interceptor()) 25 | 26 | t.TestPanic(ctx) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /tests/case2/case.go: -------------------------------------------------------------------------------- 1 | package case2 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | type test1Component struct { 11 | kod.Implements[Test1Component] 12 | // nolint 13 | test2 kod.Ref[Test2Component] 14 | } 15 | 16 | type FooReq struct { 17 | Id int 18 | } 19 | 20 | func (t *test1Component) Foo(ctx context.Context, req *FooReq) error { 21 | return errors.New("test1") 22 | } 23 | 24 | type test2Component struct { 25 | kod.Implements[Test2Component] 26 | // nolint 27 | test1 kod.Ref[Test1Component] 28 | } 29 | 30 | func (t *test2Component) Foo(ctx context.Context, req *FooReq) error { 31 | return errors.New("test2") 32 | } 33 | 34 | type App struct { 35 | kod.Implements[kod.Main] 36 | test1 kod.Ref[Test1Component] 37 | } 38 | 39 | func (app *App) Run(ctx context.Context) error { 40 | return app.test1.Get().Foo(ctx, &FooReq{}) 41 | } 42 | 43 | func Run(ctx context.Context, app *App) error { 44 | return app.test1.Get().Foo(ctx, &FooReq{}) 45 | } 46 | -------------------------------------------------------------------------------- /tests/case2/case_test.go: -------------------------------------------------------------------------------- 1 | package case2 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | ) 11 | 12 | func TestRun(t *testing.T) { 13 | t.Parallel() 14 | t.Run("case1", func(t *testing.T) { 15 | err := kod.Run(context.Background(), func(ctx context.Context, t *App) error { 16 | return t.Run(ctx) 17 | }) 18 | if err.Error() != "components [github.com/go-kod/kod/tests/case2/Test2Component] and [github.com/go-kod/kod/tests/case2/Test1Component] have cycle Ref" { 19 | panic(err) 20 | } 21 | }) 22 | 23 | t.Run("case2", func(t *testing.T) { 24 | err := kod.Run(context.Background(), func(ctx context.Context, t *App) error { 25 | return t.Run(ctx) 26 | }, kod.WithRegistrations( 27 | &kod.Registration{ 28 | Name: "github.com/go-kod/kod/Main", 29 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 30 | Impl: reflect.TypeOf(App{}), 31 | Refs: `⟦73dc6a0b:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/kod/tests/case2/Test1Component⟧`, 32 | LocalStubFn: nil, 33 | }, 34 | &kod.Registration{ 35 | Name: "github.com/go-kod/kod/tests/case2/Test1Component", 36 | Interface: reflect.TypeOf((*Test1Component)(nil)).Elem(), 37 | Impl: reflect.TypeOf(test1Component{}), 38 | Refs: `⟦3dc9f060:KoDeDgE:github.com/go-kod/kod/tests/case2/Test1Component→github.com/go-kod/kod/tests/case2/Test2Component⟧`, 39 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 40 | var interceptors []interceptor.Interceptor 41 | if h, ok := info.Impl.(interface { 42 | Interceptors() []interceptor.Interceptor 43 | }); ok { 44 | interceptors = h.Interceptors() 45 | } 46 | 47 | return test1Component_local_stub{ 48 | impl: info.Impl.(Test1Component), 49 | interceptor: interceptor.Chain(interceptors), 50 | } 51 | }, 52 | }, 53 | &kod.Registration{ 54 | Name: "github.com/go-kod/kod/tests/case2/Test2Component", 55 | Interface: reflect.TypeOf((*Test2Component)(nil)).Elem(), 56 | Impl: reflect.TypeOf(test2Component{}), 57 | Refs: `⟦1767cee9:KoDeDgE:github.com/go-kod/kod/tests/case2/Test2Component→github.com/go-kod/kod/tests/case2/Test1Component⟧`, 58 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 59 | var interceptors []interceptor.Interceptor 60 | if h, ok := info.Impl.(interface { 61 | Interceptors() []interceptor.Interceptor 62 | }); ok { 63 | interceptors = h.Interceptors() 64 | } 65 | 66 | return test2Component_local_stub{ 67 | impl: info.Impl.(Test2Component), 68 | interceptor: interceptor.Chain(interceptors), 69 | } 70 | }, 71 | }, 72 | )) 73 | if err.Error() != "components [github.com/go-kod/kod/tests/case2/Test2Component] and [github.com/go-kod/kod/tests/case2/Test1Component] have cycle Ref" { 74 | panic(err) 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /tests/case2/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package case2 5 | 6 | import ( 7 | "context" 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | "reflect" 11 | ) 12 | 13 | // Full method names for components. 14 | const ( 15 | // Test1Component_ComponentName is the full name of the component [Test1Component]. 16 | Test1Component_ComponentName = "github.com/go-kod/kod/tests/case2/Test1Component" 17 | // Test1Component_Foo_FullMethodName is the full name of the method [test1Component.Foo]. 18 | Test1Component_Foo_FullMethodName = "github.com/go-kod/kod/tests/case2/Test1Component.Foo" 19 | // Test2Component_ComponentName is the full name of the component [Test2Component]. 20 | Test2Component_ComponentName = "github.com/go-kod/kod/tests/case2/Test2Component" 21 | // Test2Component_Foo_FullMethodName is the full name of the method [test2Component.Foo]. 22 | Test2Component_Foo_FullMethodName = "github.com/go-kod/kod/tests/case2/Test2Component.Foo" 23 | ) 24 | 25 | func init() { 26 | kod.Register(&kod.Registration{ 27 | Name: "github.com/go-kod/kod/tests/case2/Test1Component", 28 | Interface: reflect.TypeOf((*Test1Component)(nil)).Elem(), 29 | Impl: reflect.TypeOf(test1Component{}), 30 | Refs: `⟦3dc9f060:KoDeDgE:github.com/go-kod/kod/tests/case2/Test1Component→github.com/go-kod/kod/tests/case2/Test2Component⟧`, 31 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 32 | return test1Component_local_stub{ 33 | impl: info.Impl.(Test1Component), 34 | interceptor: info.Interceptor, 35 | } 36 | }, 37 | }) 38 | kod.Register(&kod.Registration{ 39 | Name: "github.com/go-kod/kod/tests/case2/Test2Component", 40 | Interface: reflect.TypeOf((*Test2Component)(nil)).Elem(), 41 | Impl: reflect.TypeOf(test2Component{}), 42 | Refs: `⟦1767cee9:KoDeDgE:github.com/go-kod/kod/tests/case2/Test2Component→github.com/go-kod/kod/tests/case2/Test1Component⟧`, 43 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 44 | return test2Component_local_stub{ 45 | impl: info.Impl.(Test2Component), 46 | interceptor: info.Interceptor, 47 | } 48 | }, 49 | }) 50 | kod.Register(&kod.Registration{ 51 | Name: "github.com/go-kod/kod/Main", 52 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 53 | Impl: reflect.TypeOf(App{}), 54 | Refs: `⟦73dc6a0b:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/kod/tests/case2/Test1Component⟧`, 55 | LocalStubFn: nil, 56 | }) 57 | } 58 | 59 | // CodeGen version check. 60 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 61 | ERROR: You generated this file with 'kod generate' (codegen 62 | version v0.1.0). The generated code is incompatible with the version of the 63 | github.com/go-kod/kod module that you're using. The kod module 64 | version can be found in your go.mod file or by running the following command. 65 | 66 | go list -m github.com/go-kod/kod 67 | 68 | We recommend updating the kod module and the 'kod generate' command by 69 | running the following. 70 | 71 | go get github.com/go-kod/kod@latest 72 | go install github.com/go-kod/kod/cmd/kod@latest 73 | 74 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 75 | please file an issue at https://github.com/go-kod/kod/issues. 76 | `) 77 | 78 | // kod.InstanceOf checks. 79 | var _ kod.InstanceOf[Test1Component] = (*test1Component)(nil) 80 | var _ kod.InstanceOf[Test2Component] = (*test2Component)(nil) 81 | var _ kod.InstanceOf[kod.Main] = (*App)(nil) 82 | 83 | // Local stub implementations. 84 | // test1Component_local_stub is a local stub implementation of [Test1Component]. 85 | type test1Component_local_stub struct { 86 | impl Test1Component 87 | interceptor interceptor.Interceptor 88 | } 89 | 90 | // Check that [test1Component_local_stub] implements the [Test1Component] interface. 91 | var _ Test1Component = (*test1Component_local_stub)(nil) 92 | 93 | // Foo wraps the method [test1Component.Foo]. 94 | func (s test1Component_local_stub) Foo(ctx context.Context, a1 *FooReq) (err error) { 95 | 96 | if s.interceptor == nil { 97 | err = s.impl.Foo(ctx, a1) 98 | return 99 | } 100 | 101 | call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { 102 | err = s.impl.Foo(ctx, a1) 103 | return 104 | } 105 | 106 | info := interceptor.CallInfo{ 107 | Impl: s.impl, 108 | FullMethod: Test1Component_Foo_FullMethodName, 109 | } 110 | 111 | err = s.interceptor(ctx, info, []any{a1}, []any{}, call) 112 | return 113 | } 114 | 115 | // test2Component_local_stub is a local stub implementation of [Test2Component]. 116 | type test2Component_local_stub struct { 117 | impl Test2Component 118 | interceptor interceptor.Interceptor 119 | } 120 | 121 | // Check that [test2Component_local_stub] implements the [Test2Component] interface. 122 | var _ Test2Component = (*test2Component_local_stub)(nil) 123 | 124 | // Foo wraps the method [test2Component.Foo]. 125 | func (s test2Component_local_stub) Foo(ctx context.Context, a1 *FooReq) (err error) { 126 | 127 | if s.interceptor == nil { 128 | err = s.impl.Foo(ctx, a1) 129 | return 130 | } 131 | 132 | call := func(ctx context.Context, info interceptor.CallInfo, req, res []any) (err error) { 133 | err = s.impl.Foo(ctx, a1) 134 | return 135 | } 136 | 137 | info := interceptor.CallInfo{ 138 | Impl: s.impl, 139 | FullMethod: Test2Component_Foo_FullMethodName, 140 | } 141 | 142 | err = s.interceptor(ctx, info, []any{a1}, []any{}, call) 143 | return 144 | } 145 | -------------------------------------------------------------------------------- /tests/case2/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package case2 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Test1Component is implemented by [test1Component], 10 | // which can be mocked with [NewMockTest1Component]. 11 | type Test1Component interface { 12 | // Foo is implemented by [test1Component.Foo] 13 | Foo(ctx context.Context, req *FooReq) error 14 | } 15 | 16 | // Test2Component is implemented by [test2Component], 17 | // which can be mocked with [NewMockTest2Component]. 18 | type Test2Component interface { 19 | // Foo is implemented by [test2Component.Foo] 20 | Foo(ctx context.Context, req *FooReq) error 21 | } 22 | -------------------------------------------------------------------------------- /tests/case2/kod_gen_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !ignoreKodGen 2 | 3 | // Code generated by MockGen. DO NOT EDIT. 4 | // Source: tests/case2/kod_gen_interface.go 5 | // 6 | // Generated by this command: 7 | // 8 | // mockgen -source tests/case2/kod_gen_interface.go -destination tests/case2/kod_gen_mock.go -package case2 -typed -build_constraint !ignoreKodGen 9 | // 10 | 11 | // Package case2 is a generated GoMock package. 12 | package case2 13 | 14 | import ( 15 | context "context" 16 | reflect "reflect" 17 | 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockTest1Component is a mock of Test1Component interface. 22 | type MockTest1Component struct { 23 | ctrl *gomock.Controller 24 | recorder *MockTest1ComponentMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockTest1ComponentMockRecorder is the mock recorder for MockTest1Component. 29 | type MockTest1ComponentMockRecorder struct { 30 | mock *MockTest1Component 31 | } 32 | 33 | // NewMockTest1Component creates a new mock instance. 34 | func NewMockTest1Component(ctrl *gomock.Controller) *MockTest1Component { 35 | mock := &MockTest1Component{ctrl: ctrl} 36 | mock.recorder = &MockTest1ComponentMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockTest1Component) EXPECT() *MockTest1ComponentMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // Foo mocks base method. 46 | func (m *MockTest1Component) Foo(ctx context.Context, req *FooReq) error { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "Foo", ctx, req) 49 | ret0, _ := ret[0].(error) 50 | return ret0 51 | } 52 | 53 | // Foo indicates an expected call of Foo. 54 | func (mr *MockTest1ComponentMockRecorder) Foo(ctx, req any) *MockTest1ComponentFooCall { 55 | mr.mock.ctrl.T.Helper() 56 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Foo", reflect.TypeOf((*MockTest1Component)(nil).Foo), ctx, req) 57 | return &MockTest1ComponentFooCall{Call: call} 58 | } 59 | 60 | // MockTest1ComponentFooCall wrap *gomock.Call 61 | type MockTest1ComponentFooCall struct { 62 | *gomock.Call 63 | } 64 | 65 | // Return rewrite *gomock.Call.Return 66 | func (c *MockTest1ComponentFooCall) Return(arg0 error) *MockTest1ComponentFooCall { 67 | c.Call = c.Call.Return(arg0) 68 | return c 69 | } 70 | 71 | // Do rewrite *gomock.Call.Do 72 | func (c *MockTest1ComponentFooCall) Do(f func(context.Context, *FooReq) error) *MockTest1ComponentFooCall { 73 | c.Call = c.Call.Do(f) 74 | return c 75 | } 76 | 77 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 78 | func (c *MockTest1ComponentFooCall) DoAndReturn(f func(context.Context, *FooReq) error) *MockTest1ComponentFooCall { 79 | c.Call = c.Call.DoAndReturn(f) 80 | return c 81 | } 82 | 83 | // MockTest2Component is a mock of Test2Component interface. 84 | type MockTest2Component struct { 85 | ctrl *gomock.Controller 86 | recorder *MockTest2ComponentMockRecorder 87 | isgomock struct{} 88 | } 89 | 90 | // MockTest2ComponentMockRecorder is the mock recorder for MockTest2Component. 91 | type MockTest2ComponentMockRecorder struct { 92 | mock *MockTest2Component 93 | } 94 | 95 | // NewMockTest2Component creates a new mock instance. 96 | func NewMockTest2Component(ctrl *gomock.Controller) *MockTest2Component { 97 | mock := &MockTest2Component{ctrl: ctrl} 98 | mock.recorder = &MockTest2ComponentMockRecorder{mock} 99 | return mock 100 | } 101 | 102 | // EXPECT returns an object that allows the caller to indicate expected use. 103 | func (m *MockTest2Component) EXPECT() *MockTest2ComponentMockRecorder { 104 | return m.recorder 105 | } 106 | 107 | // Foo mocks base method. 108 | func (m *MockTest2Component) Foo(ctx context.Context, req *FooReq) error { 109 | m.ctrl.T.Helper() 110 | ret := m.ctrl.Call(m, "Foo", ctx, req) 111 | ret0, _ := ret[0].(error) 112 | return ret0 113 | } 114 | 115 | // Foo indicates an expected call of Foo. 116 | func (mr *MockTest2ComponentMockRecorder) Foo(ctx, req any) *MockTest2ComponentFooCall { 117 | mr.mock.ctrl.T.Helper() 118 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Foo", reflect.TypeOf((*MockTest2Component)(nil).Foo), ctx, req) 119 | return &MockTest2ComponentFooCall{Call: call} 120 | } 121 | 122 | // MockTest2ComponentFooCall wrap *gomock.Call 123 | type MockTest2ComponentFooCall struct { 124 | *gomock.Call 125 | } 126 | 127 | // Return rewrite *gomock.Call.Return 128 | func (c *MockTest2ComponentFooCall) Return(arg0 error) *MockTest2ComponentFooCall { 129 | c.Call = c.Call.Return(arg0) 130 | return c 131 | } 132 | 133 | // Do rewrite *gomock.Call.Do 134 | func (c *MockTest2ComponentFooCall) Do(f func(context.Context, *FooReq) error) *MockTest2ComponentFooCall { 135 | c.Call = c.Call.Do(f) 136 | return c 137 | } 138 | 139 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 140 | func (c *MockTest2ComponentFooCall) DoAndReturn(f func(context.Context, *FooReq) error) *MockTest2ComponentFooCall { 141 | c.Call = c.Call.DoAndReturn(f) 142 | return c 143 | } 144 | -------------------------------------------------------------------------------- /tests/case3/case.go: -------------------------------------------------------------------------------- 1 | package case3 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | type test1Component struct { 11 | kod.Implements[Test1Component] 12 | // nolint 13 | test2 kod.Ref[Test2Component] 14 | } 15 | 16 | func (t *test1Component) Foo(ctx context.Context, req *FooReq) error { 17 | return errors.New("test1") 18 | } 19 | 20 | type test2Component struct { 21 | kod.Implements[Test2Component] 22 | // nolint 23 | test1 kod.Ref[Test3Component] 24 | } 25 | 26 | type FooReq struct { 27 | Id int 28 | } 29 | 30 | func (t *test2Component) Foo(ctx context.Context, req *FooReq) error { 31 | return errors.New("test2") 32 | } 33 | 34 | type test3Component struct { 35 | kod.Implements[Test3Component] 36 | // nolint 37 | test2 kod.Ref[Test1Component] 38 | } 39 | 40 | func (t *test3Component) Foo(ctx context.Context, req *FooReq) error { 41 | return errors.New("test3") 42 | } 43 | 44 | type App struct { 45 | kod.Implements[kod.Main] 46 | test1 kod.Ref[Test1Component] 47 | _ kod.Ref[Test2Component] 48 | } 49 | 50 | func (app *App) Run(ctx context.Context) error { 51 | return app.test1.Get().Foo(ctx, &FooReq{}) 52 | } 53 | -------------------------------------------------------------------------------- /tests/case3/case_test.go: -------------------------------------------------------------------------------- 1 | package case3 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | func TestRun(t *testing.T) { 11 | t.Parallel() 12 | t.Run("Main依赖1,2依赖3,3依赖1,有循环依赖", func(t *testing.T) { 13 | err := kod.Run(context.Background(), func(ctx context.Context, t *App) error { 14 | return t.Run(ctx) 15 | }) 16 | if err.Error() != "components [github.com/go-kod/kod/tests/case3/Test3Component] and [github.com/go-kod/kod/tests/case3/Test1Component] have cycle Ref" { 17 | panic(err) 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /tests/case3/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package case3 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Test1Component is implemented by [test1Component], 10 | // which can be mocked with [NewMockTest1Component]. 11 | type Test1Component interface { 12 | // Foo is implemented by [test1Component.Foo] 13 | Foo(ctx context.Context, req *FooReq) error 14 | } 15 | 16 | // Test2Component is implemented by [test2Component], 17 | // which can be mocked with [NewMockTest2Component]. 18 | type Test2Component interface { 19 | // Foo is implemented by [test2Component.Foo] 20 | Foo(ctx context.Context, req *FooReq) error 21 | } 22 | 23 | // Test3Component is implemented by [test3Component], 24 | // which can be mocked with [NewMockTest3Component]. 25 | type Test3Component interface { 26 | // Foo is implemented by [test3Component.Foo] 27 | Foo(ctx context.Context, req *FooReq) error 28 | } 29 | -------------------------------------------------------------------------------- /tests/case3/kod_gen_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !ignoreKodGen 2 | 3 | // Code generated by MockGen. DO NOT EDIT. 4 | // Source: tests/case3/kod_gen_interface.go 5 | // 6 | // Generated by this command: 7 | // 8 | // mockgen -source tests/case3/kod_gen_interface.go -destination tests/case3/kod_gen_mock.go -package case3 -typed -build_constraint !ignoreKodGen 9 | // 10 | 11 | // Package case3 is a generated GoMock package. 12 | package case3 13 | 14 | import ( 15 | context "context" 16 | reflect "reflect" 17 | 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockTest1Component is a mock of Test1Component interface. 22 | type MockTest1Component struct { 23 | ctrl *gomock.Controller 24 | recorder *MockTest1ComponentMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockTest1ComponentMockRecorder is the mock recorder for MockTest1Component. 29 | type MockTest1ComponentMockRecorder struct { 30 | mock *MockTest1Component 31 | } 32 | 33 | // NewMockTest1Component creates a new mock instance. 34 | func NewMockTest1Component(ctrl *gomock.Controller) *MockTest1Component { 35 | mock := &MockTest1Component{ctrl: ctrl} 36 | mock.recorder = &MockTest1ComponentMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockTest1Component) EXPECT() *MockTest1ComponentMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // Foo mocks base method. 46 | func (m *MockTest1Component) Foo(ctx context.Context, req *FooReq) error { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "Foo", ctx, req) 49 | ret0, _ := ret[0].(error) 50 | return ret0 51 | } 52 | 53 | // Foo indicates an expected call of Foo. 54 | func (mr *MockTest1ComponentMockRecorder) Foo(ctx, req any) *MockTest1ComponentFooCall { 55 | mr.mock.ctrl.T.Helper() 56 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Foo", reflect.TypeOf((*MockTest1Component)(nil).Foo), ctx, req) 57 | return &MockTest1ComponentFooCall{Call: call} 58 | } 59 | 60 | // MockTest1ComponentFooCall wrap *gomock.Call 61 | type MockTest1ComponentFooCall struct { 62 | *gomock.Call 63 | } 64 | 65 | // Return rewrite *gomock.Call.Return 66 | func (c *MockTest1ComponentFooCall) Return(arg0 error) *MockTest1ComponentFooCall { 67 | c.Call = c.Call.Return(arg0) 68 | return c 69 | } 70 | 71 | // Do rewrite *gomock.Call.Do 72 | func (c *MockTest1ComponentFooCall) Do(f func(context.Context, *FooReq) error) *MockTest1ComponentFooCall { 73 | c.Call = c.Call.Do(f) 74 | return c 75 | } 76 | 77 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 78 | func (c *MockTest1ComponentFooCall) DoAndReturn(f func(context.Context, *FooReq) error) *MockTest1ComponentFooCall { 79 | c.Call = c.Call.DoAndReturn(f) 80 | return c 81 | } 82 | 83 | // MockTest2Component is a mock of Test2Component interface. 84 | type MockTest2Component struct { 85 | ctrl *gomock.Controller 86 | recorder *MockTest2ComponentMockRecorder 87 | isgomock struct{} 88 | } 89 | 90 | // MockTest2ComponentMockRecorder is the mock recorder for MockTest2Component. 91 | type MockTest2ComponentMockRecorder struct { 92 | mock *MockTest2Component 93 | } 94 | 95 | // NewMockTest2Component creates a new mock instance. 96 | func NewMockTest2Component(ctrl *gomock.Controller) *MockTest2Component { 97 | mock := &MockTest2Component{ctrl: ctrl} 98 | mock.recorder = &MockTest2ComponentMockRecorder{mock} 99 | return mock 100 | } 101 | 102 | // EXPECT returns an object that allows the caller to indicate expected use. 103 | func (m *MockTest2Component) EXPECT() *MockTest2ComponentMockRecorder { 104 | return m.recorder 105 | } 106 | 107 | // Foo mocks base method. 108 | func (m *MockTest2Component) Foo(ctx context.Context, req *FooReq) error { 109 | m.ctrl.T.Helper() 110 | ret := m.ctrl.Call(m, "Foo", ctx, req) 111 | ret0, _ := ret[0].(error) 112 | return ret0 113 | } 114 | 115 | // Foo indicates an expected call of Foo. 116 | func (mr *MockTest2ComponentMockRecorder) Foo(ctx, req any) *MockTest2ComponentFooCall { 117 | mr.mock.ctrl.T.Helper() 118 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Foo", reflect.TypeOf((*MockTest2Component)(nil).Foo), ctx, req) 119 | return &MockTest2ComponentFooCall{Call: call} 120 | } 121 | 122 | // MockTest2ComponentFooCall wrap *gomock.Call 123 | type MockTest2ComponentFooCall struct { 124 | *gomock.Call 125 | } 126 | 127 | // Return rewrite *gomock.Call.Return 128 | func (c *MockTest2ComponentFooCall) Return(arg0 error) *MockTest2ComponentFooCall { 129 | c.Call = c.Call.Return(arg0) 130 | return c 131 | } 132 | 133 | // Do rewrite *gomock.Call.Do 134 | func (c *MockTest2ComponentFooCall) Do(f func(context.Context, *FooReq) error) *MockTest2ComponentFooCall { 135 | c.Call = c.Call.Do(f) 136 | return c 137 | } 138 | 139 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 140 | func (c *MockTest2ComponentFooCall) DoAndReturn(f func(context.Context, *FooReq) error) *MockTest2ComponentFooCall { 141 | c.Call = c.Call.DoAndReturn(f) 142 | return c 143 | } 144 | 145 | // MockTest3Component is a mock of Test3Component interface. 146 | type MockTest3Component struct { 147 | ctrl *gomock.Controller 148 | recorder *MockTest3ComponentMockRecorder 149 | isgomock struct{} 150 | } 151 | 152 | // MockTest3ComponentMockRecorder is the mock recorder for MockTest3Component. 153 | type MockTest3ComponentMockRecorder struct { 154 | mock *MockTest3Component 155 | } 156 | 157 | // NewMockTest3Component creates a new mock instance. 158 | func NewMockTest3Component(ctrl *gomock.Controller) *MockTest3Component { 159 | mock := &MockTest3Component{ctrl: ctrl} 160 | mock.recorder = &MockTest3ComponentMockRecorder{mock} 161 | return mock 162 | } 163 | 164 | // EXPECT returns an object that allows the caller to indicate expected use. 165 | func (m *MockTest3Component) EXPECT() *MockTest3ComponentMockRecorder { 166 | return m.recorder 167 | } 168 | 169 | // Foo mocks base method. 170 | func (m *MockTest3Component) Foo(ctx context.Context, req *FooReq) error { 171 | m.ctrl.T.Helper() 172 | ret := m.ctrl.Call(m, "Foo", ctx, req) 173 | ret0, _ := ret[0].(error) 174 | return ret0 175 | } 176 | 177 | // Foo indicates an expected call of Foo. 178 | func (mr *MockTest3ComponentMockRecorder) Foo(ctx, req any) *MockTest3ComponentFooCall { 179 | mr.mock.ctrl.T.Helper() 180 | call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Foo", reflect.TypeOf((*MockTest3Component)(nil).Foo), ctx, req) 181 | return &MockTest3ComponentFooCall{Call: call} 182 | } 183 | 184 | // MockTest3ComponentFooCall wrap *gomock.Call 185 | type MockTest3ComponentFooCall struct { 186 | *gomock.Call 187 | } 188 | 189 | // Return rewrite *gomock.Call.Return 190 | func (c *MockTest3ComponentFooCall) Return(arg0 error) *MockTest3ComponentFooCall { 191 | c.Call = c.Call.Return(arg0) 192 | return c 193 | } 194 | 195 | // Do rewrite *gomock.Call.Do 196 | func (c *MockTest3ComponentFooCall) Do(f func(context.Context, *FooReq) error) *MockTest3ComponentFooCall { 197 | c.Call = c.Call.Do(f) 198 | return c 199 | } 200 | 201 | // DoAndReturn rewrite *gomock.Call.DoAndReturn 202 | func (c *MockTest3ComponentFooCall) DoAndReturn(f func(context.Context, *FooReq) error) *MockTest3ComponentFooCall { 203 | c.Call = c.Call.DoAndReturn(f) 204 | return c 205 | } 206 | -------------------------------------------------------------------------------- /tests/case4/case.go: -------------------------------------------------------------------------------- 1 | package case4 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | type test1Component struct { 11 | kod.Implements[Test1Component] 12 | // nolint 13 | test2 kod.Ref[Test3Component] 14 | } 15 | 16 | type FooReq struct { 17 | Id int 18 | } 19 | 20 | func (t *test1Component) Foo(ctx context.Context, req *FooReq) error { 21 | return errors.New("test1") 22 | } 23 | 24 | type test2Component struct { 25 | kod.Implements[Test2Component] 26 | // nolint 27 | test1 kod.Ref[Test3Component] 28 | } 29 | 30 | func (t *test2Component) Foo(ctx context.Context, req *FooReq) error { 31 | return errors.New("test2") 32 | } 33 | 34 | type test3Component struct { 35 | kod.Implements[Test3Component] 36 | } 37 | 38 | func (t *test3Component) Foo(ctx context.Context, req *FooReq) error { 39 | return errors.New("test3") 40 | } 41 | 42 | type App struct { 43 | kod.Implements[kod.Main] 44 | test1 kod.Ref[Test1Component] 45 | _ kod.Ref[Test2Component] 46 | } 47 | 48 | func (app *App) Run(ctx context.Context) error { 49 | return app.test1.Get().Foo(ctx, &FooReq{}) 50 | } 51 | -------------------------------------------------------------------------------- /tests/case4/case_test.go: -------------------------------------------------------------------------------- 1 | package case4 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-kod/kod" 8 | ) 9 | 10 | func TestRun(t *testing.T) { 11 | t.Parallel() 12 | 13 | t.Run("Main依赖1和2,1依赖3,2也依赖3,没有循环依赖", func(t *testing.T) { 14 | err := kod.Run(context.Background(), func(ctx context.Context, t *App) error { 15 | return t.Run(ctx) 16 | }) 17 | if err.Error() != "test1" { 18 | panic(err) 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /tests/case4/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package case4 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | // Test1Component is implemented by [test1Component], 10 | // which can be mocked with [NewMockTest1Component]. 11 | type Test1Component interface { 12 | // Foo is implemented by [test1Component.Foo] 13 | Foo(ctx context.Context, req *FooReq) error 14 | } 15 | 16 | // Test2Component is implemented by [test2Component], 17 | // which can be mocked with [NewMockTest2Component]. 18 | type Test2Component interface { 19 | // Foo is implemented by [test2Component.Foo] 20 | Foo(ctx context.Context, req *FooReq) error 21 | } 22 | 23 | // Test3Component is implemented by [test3Component], 24 | // which can be mocked with [NewMockTest3Component]. 25 | type Test3Component interface { 26 | // Foo is implemented by [test3Component.Foo] 27 | Foo(ctx context.Context, req *FooReq) error 28 | } 29 | -------------------------------------------------------------------------------- /tests/case5/case_ref_struct.go: -------------------------------------------------------------------------------- 1 | package case5 2 | 3 | import "github.com/go-kod/kod" 4 | 5 | type refStructImpl struct { 6 | kod.Implements[kod.Main] 7 | 8 | _ kod.Ref[testRefStruct1] 9 | } 10 | 11 | func (t *refStructImpl) Hello() string { 12 | return "Hello, World!" 13 | } 14 | 15 | type testRefStruct1 struct { 16 | kod.Implements[TestRefStruct1] 17 | } 18 | -------------------------------------------------------------------------------- /tests/case5/case_ref_struct_test.go: -------------------------------------------------------------------------------- 1 | package case5 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | 9 | "github.com/go-kod/kod" 10 | ) 11 | 12 | func TestRefStruct(t *testing.T) { 13 | err := kod.Run(context.Background(), func(ctx context.Context, comp *refStructImpl) error { 14 | return nil 15 | }) 16 | require.EqualError(t, err, "component implementation struct case5.refStructImpl has field kod.Ref[github.com/go-kod/kod/tests/case5.testRefStruct1], but field type case5.testRefStruct1 is not an interface") 17 | } 18 | -------------------------------------------------------------------------------- /tests/case5/kod_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate". DO NOT EDIT. 2 | //go:build !ignoreKodGen 3 | 4 | package case5 5 | 6 | import ( 7 | "context" 8 | "github.com/go-kod/kod" 9 | "github.com/go-kod/kod/interceptor" 10 | "reflect" 11 | ) 12 | 13 | // Full method names for components. 14 | const ( 15 | // TestRefStruct1_ComponentName is the full name of the component [TestRefStruct1]. 16 | TestRefStruct1_ComponentName = "github.com/go-kod/kod/tests/case5/TestRefStruct1" 17 | ) 18 | 19 | func init() { 20 | kod.Register(&kod.Registration{ 21 | Name: "github.com/go-kod/kod/Main", 22 | Interface: reflect.TypeOf((*kod.Main)(nil)).Elem(), 23 | Impl: reflect.TypeOf(refStructImpl{}), 24 | Refs: `⟦b915993d:KoDeDgE:github.com/go-kod/kod/Main→github.com/go-kod/kod/tests/case5/testRefStruct1⟧`, 25 | LocalStubFn: nil, 26 | }) 27 | kod.Register(&kod.Registration{ 28 | Name: "github.com/go-kod/kod/tests/case5/TestRefStruct1", 29 | Interface: reflect.TypeOf((*TestRefStruct1)(nil)).Elem(), 30 | Impl: reflect.TypeOf(testRefStruct1{}), 31 | Refs: ``, 32 | LocalStubFn: func(ctx context.Context, info *kod.LocalStubFnInfo) any { 33 | return testRefStruct1_local_stub{ 34 | impl: info.Impl.(TestRefStruct1), 35 | interceptor: info.Interceptor, 36 | } 37 | }, 38 | }) 39 | } 40 | 41 | // CodeGen version check. 42 | var _ kod.CodeGenLatestVersion = kod.CodeGenVersion[[0][1]struct{}](` 43 | ERROR: You generated this file with 'kod generate' (codegen 44 | version v0.1.0). The generated code is incompatible with the version of the 45 | github.com/go-kod/kod module that you're using. The kod module 46 | version can be found in your go.mod file or by running the following command. 47 | 48 | go list -m github.com/go-kod/kod 49 | 50 | We recommend updating the kod module and the 'kod generate' command by 51 | running the following. 52 | 53 | go get github.com/go-kod/kod@latest 54 | go install github.com/go-kod/kod/cmd/kod@latest 55 | 56 | Then, re-run 'kod generate' and re-build your code. If the problem persists, 57 | please file an issue at https://github.com/go-kod/kod/issues. 58 | `) 59 | 60 | // kod.InstanceOf checks. 61 | var _ kod.InstanceOf[kod.Main] = (*refStructImpl)(nil) 62 | var _ kod.InstanceOf[TestRefStruct1] = (*testRefStruct1)(nil) 63 | 64 | // Local stub implementations. 65 | // testRefStruct1_local_stub is a local stub implementation of [TestRefStruct1]. 66 | type testRefStruct1_local_stub struct { 67 | impl TestRefStruct1 68 | interceptor interceptor.Interceptor 69 | } 70 | 71 | // Check that [testRefStruct1_local_stub] implements the [TestRefStruct1] interface. 72 | var _ TestRefStruct1 = (*testRefStruct1_local_stub)(nil) 73 | 74 | -------------------------------------------------------------------------------- /tests/case5/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package case5 4 | 5 | // TestRefStruct1 is implemented by [testRefStruct1], 6 | // which can be mocked with [NewMockTestRefStruct1]. 7 | type TestRefStruct1 interface { 8 | } 9 | -------------------------------------------------------------------------------- /tests/case5/kod_gen_mock.go: -------------------------------------------------------------------------------- 1 | //go:build !ignoreKodGen 2 | 3 | // Code generated by MockGen. DO NOT EDIT. 4 | // Source: tests/case5/kod_gen_interface.go 5 | // 6 | // Generated by this command: 7 | // 8 | // mockgen -source tests/case5/kod_gen_interface.go -destination tests/case5/kod_gen_mock.go -package case5 -typed -build_constraint !ignoreKodGen 9 | // 10 | 11 | // Package case5 is a generated GoMock package. 12 | package case5 13 | 14 | import ( 15 | gomock "go.uber.org/mock/gomock" 16 | ) 17 | 18 | // MockTestRefStruct1 is a mock of TestRefStruct1 interface. 19 | type MockTestRefStruct1 struct { 20 | ctrl *gomock.Controller 21 | recorder *MockTestRefStruct1MockRecorder 22 | isgomock struct{} 23 | } 24 | 25 | // MockTestRefStruct1MockRecorder is the mock recorder for MockTestRefStruct1. 26 | type MockTestRefStruct1MockRecorder struct { 27 | mock *MockTestRefStruct1 28 | } 29 | 30 | // NewMockTestRefStruct1 creates a new mock instance. 31 | func NewMockTestRefStruct1(ctrl *gomock.Controller) *MockTestRefStruct1 { 32 | mock := &MockTestRefStruct1{ctrl: ctrl} 33 | mock.recorder = &MockTestRefStruct1MockRecorder{mock} 34 | return mock 35 | } 36 | 37 | // EXPECT returns an object that allows the caller to indicate expected use. 38 | func (m *MockTestRefStruct1) EXPECT() *MockTestRefStruct1MockRecorder { 39 | return m.recorder 40 | } 41 | -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-kod/kod/tests 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | replace github.com/go-kod/kod => ../ 8 | 9 | require ( 10 | github.com/avast/retry-go/v4 v4.6.1 11 | github.com/gin-gonic/gin v1.10.0 12 | github.com/go-kod/kod v0.0.0-00010101000000-000000000000 13 | github.com/labstack/echo/v4 v4.13.3 14 | github.com/samber/lo v1.50.0 15 | github.com/stretchr/testify v1.10.0 16 | go.opentelemetry.io/otel v1.35.0 17 | go.opentelemetry.io/otel/trace v1.35.0 18 | go.uber.org/goleak v1.3.0 19 | go.uber.org/mock v0.5.1 20 | ) 21 | 22 | require ( 23 | github.com/bytedance/sonic v1.12.5 // indirect 24 | github.com/bytedance/sonic/loader v0.2.1 // indirect 25 | github.com/cloudwego/base64x v0.1.4 // indirect 26 | github.com/cloudwego/iasm v0.2.0 // indirect 27 | github.com/creasty/defaults v1.8.0 // indirect 28 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 29 | github.com/dominikbraun/graph v0.23.0 // indirect 30 | github.com/fsnotify/fsnotify v1.9.0 // indirect 31 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 32 | github.com/gin-contrib/sse v0.1.0 // indirect 33 | github.com/go-logr/logr v1.4.2 // indirect 34 | github.com/go-logr/stdr v1.2.2 // indirect 35 | github.com/go-ole/go-ole v1.3.0 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/go-playground/validator/v10 v10.26.0 // indirect 39 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 40 | github.com/goccy/go-json v0.10.4 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 43 | github.com/knadh/koanf/maps v0.1.2 // indirect 44 | github.com/knadh/koanf/parsers/json v1.0.0 // indirect 45 | github.com/knadh/koanf/parsers/toml/v2 v2.2.0 // indirect 46 | github.com/knadh/koanf/parsers/yaml v1.0.0 // indirect 47 | github.com/knadh/koanf/providers/env v1.1.0 // indirect 48 | github.com/knadh/koanf/providers/file v1.2.0 // indirect 49 | github.com/knadh/koanf/v2 v2.2.0 // indirect 50 | github.com/labstack/gommon v0.4.2 // indirect 51 | github.com/leodido/go-urn v1.4.0 // indirect 52 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 53 | github.com/mattn/go-colorable v0.1.13 // indirect 54 | github.com/mattn/go-isatty v0.0.20 // indirect 55 | github.com/mitchellh/copystructure v1.2.0 // indirect 56 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 60 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 61 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 62 | github.com/shirou/gopsutil/v3 v3.24.5 // indirect 63 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 64 | github.com/sony/gobreaker v1.0.0 // indirect 65 | github.com/tklauser/go-sysconf v0.3.14 // indirect 66 | github.com/tklauser/numcpus v0.9.0 // indirect 67 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 68 | github.com/ugorji/go/codec v1.2.12 // indirect 69 | github.com/valyala/bytebufferpool v1.0.0 // indirect 70 | github.com/valyala/fasttemplate v1.2.2 // indirect 71 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 72 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 74 | golang.org/x/arch v0.12.0 // indirect 75 | golang.org/x/crypto v0.37.0 // indirect 76 | golang.org/x/net v0.39.0 // indirect 77 | golang.org/x/sys v0.32.0 // indirect 78 | golang.org/x/text v0.24.0 // indirect 79 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 80 | google.golang.org/grpc v1.72.0 // indirect 81 | google.golang.org/protobuf v1.36.5 // indirect 82 | gopkg.in/yaml.v3 v3.0.1 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /tests/graphcase/case.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.opentelemetry.io/otel" 12 | "go.opentelemetry.io/otel/trace" 13 | 14 | "github.com/go-kod/kod" 15 | ) 16 | 17 | type test1Config struct { 18 | A string 19 | Redis struct { 20 | Addr string 21 | Timeout time.Duration 22 | } 23 | } 24 | 25 | type test1ControllerImpl struct { 26 | kod.Implements[test1Controller] 27 | 28 | httpControllerImpl kod.Ref[HTTPController] 29 | test1Component kod.Ref[Test1Component] 30 | } 31 | 32 | func (t *test1ControllerImpl) Foo(cccccc *gin.Context) { 33 | _ = t.test1Component.Get().Foo(cccccc, &FooReq{}) 34 | } 35 | 36 | type httpControllerImpl struct { 37 | kod.Implements[HTTPController] 38 | kod.Ref[testService] 39 | } 40 | 41 | func (t *httpControllerImpl) Foo(w http.ResponseWriter, r http.Request) { 42 | } 43 | 44 | type serviceImpl struct { 45 | kod.Implements[testService] 46 | kod.Ref[testModel] 47 | } 48 | 49 | func (t *serviceImpl) Foo(ctx context.Context) error { 50 | return nil 51 | } 52 | 53 | type modelImpl struct { 54 | kod.Implements[testModel] 55 | } 56 | 57 | func (t *modelImpl) Foo(ctx context.Context) error { 58 | return nil 59 | } 60 | 61 | type test1Component struct { 62 | kod.Implements[Test1Component] 63 | kod.WithConfig[test1Config] 64 | } 65 | 66 | func (t *test1Component) Init(ctx context.Context) error { 67 | return nil 68 | } 69 | 70 | func (t *test1Component) Shutdown(ctx context.Context) error { 71 | return nil 72 | } 73 | 74 | type FooReq struct { 75 | Id int 76 | } 77 | 78 | func (t *test1Component) Foo(ctx context.Context, req *FooReq) error { 79 | t.L(ctx).InfoContext(ctx, "Foo info ", "config", t.Config()) 80 | t.L(ctx).Debug("Foo debug:") 81 | fmt.Println(errors.New("test1")) 82 | return errors.New("test1:" + t.Config().A) 83 | } 84 | 85 | type fakeTest1Component struct { 86 | A string 87 | } 88 | 89 | func (f *fakeTest1Component) Foo(ctx context.Context, req *FooReq) error { 90 | fmt.Println(f.A) 91 | return errors.New("A:" + f.A) 92 | } 93 | 94 | type App struct { 95 | kod.Implements[kod.Main] 96 | test1ControllerImpl kod.Ref[test1Controller] 97 | test1 kod.Ref[Test1Component] 98 | } 99 | 100 | func (app *App) Run(ctx context.Context) error { 101 | ctx, span := otel.Tracer("").Start(ctx, "Run", trace.WithSpanKind(trace.SpanKindInternal)) 102 | defer span.End() 103 | 104 | return app.test1.Get().Foo(ctx, &FooReq{0}) 105 | } 106 | 107 | func Run(ctx context.Context, app *App) error { 108 | ctx, span := otel.Tracer("").Start(ctx, "Run", trace.WithSpanKind(trace.SpanKindInternal)) 109 | defer span.End() 110 | 111 | return app.test1.Get().Foo(ctx, &FooReq{0}) 112 | } 113 | 114 | func main() { 115 | Run(context.TODO(), new(App)) 116 | } 117 | -------------------------------------------------------------------------------- /tests/graphcase/kod_gen_interface.go: -------------------------------------------------------------------------------- 1 | // Code generated by "kod generate"; DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // test1Controller is implemented by [test1ControllerImpl], 13 | // which can be mocked with [NewMocktest1Controller]. 14 | type test1Controller interface { 15 | // Foo is implemented by [test1ControllerImpl.Foo] 16 | Foo(cccccc *gin.Context) 17 | } 18 | 19 | // HTTPController is implemented by [httpControllerImpl], 20 | // which can be mocked with [NewMockHTTPController]. 21 | type HTTPController interface { 22 | // Foo is implemented by [httpControllerImpl.Foo] 23 | Foo(w http.ResponseWriter, r http.Request) 24 | } 25 | 26 | // testService is implemented by [serviceImpl], 27 | // which can be mocked with [NewMocktestService]. 28 | type testService interface { 29 | // Foo is implemented by [serviceImpl.Foo] 30 | Foo(ctx context.Context) error 31 | } 32 | 33 | // testModel is implemented by [modelImpl], 34 | // which can be mocked with [NewMocktestModel]. 35 | type testModel interface { 36 | // Foo is implemented by [modelImpl.Foo] 37 | Foo(ctx context.Context) error 38 | } 39 | 40 | // Test1Component is implemented by [test1Component], 41 | // which can be mocked with [NewMockTest1Component]. 42 | type Test1Component interface { 43 | // Foo is implemented by [test1Component.Foo] 44 | Foo(ctx context.Context, req *FooReq) error 45 | } 46 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package kod 2 | 3 | import ( 4 | "github.com/go-kod/kod/internal/version" 5 | ) 6 | 7 | // The following types are used to check, at compile time, that every 8 | // kod_gen.go file uses the codegen API version that is linked into the binary. 9 | type ( 10 | // CodeGenVersion is the version of the codegen API. 11 | CodeGenVersion[_ any] string 12 | // CodeGenLatestVersion is the latest version of the codegen API. 13 | CodeGenLatestVersion = CodeGenVersion[[version.CodeGenMajor][version.CodeGenMinor]struct{}] 14 | ) 15 | --------------------------------------------------------------------------------