diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b268335..19eca38 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,14 +8,14 @@ on: jobs: test: name: Test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Setup Golang with cache uses: magnetikonline/action-golang-cache@v5 with: - go-version: 1.20.x + go-version: 1.22.x - name: Install dependencies run: | @@ -27,7 +27,7 @@ jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: - name: Setup Golang with cache uses: magnetikonline/action-golang-cache@v5 with: - go-version: 1.20.x + go-version: 1.22.x - name: Install dependencies run: go mod download @@ -50,7 +50,7 @@ jobs: config: revive.toml commit_lint: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout code uses: actions/checkout@v4 @@ -61,7 +61,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - name: Install Commitizen run: pip install -U commitizen diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 847f90d..011b88b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ permissions: jobs: bump_version: if: "!startsWith(github.event.head_commit.message, 'bump:')" - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: "Bump version" steps: - name: Check out @@ -25,59 +25,21 @@ jobs: token: "${{ secrets.ACCESS_TOKEN }}" ref: "main" - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Config Git User - run: | - git config --local user.email "$GIT_USER_EMAIL" - git config --local user.name "$GIT_USER_NAME" - git config --local pull.ff only - - - id: cz - name: Create bump and changelog - run: | - python -m pip install -U commitizen - cz bump --changelog --yes - export REV=`cz version --project` - echo "version=$REV" >> $GITHUB_OUTPUT - - - name: Push changes - uses: Woile/github-push-action@master + - name: Create bump and changelog + uses: commitizen-tools/commitizen-action@master with: github_token: ${{ secrets.ACCESS_TOKEN }} - tags: "true" - branch: "main" - - - name: Print Version - run: echo "Bumped to version ${{ steps.cz.outputs.version }}" - - release: - runs-on: ubuntu-latest - name: "Release service" - needs: - - bump_version - steps: - - name: Check out - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: "${{ secrets.ACCESS_TOKEN }}" - ref: "main" + changelog_increment_filename: body.md + git_name: ${{ secrets.GIT_NAME }} + git_email: ${{ secrets.GIT_EMAIL }} - - name: Setup Golang with cache - uses: magnetikonline/action-golang-cache@v5 + - name: Release + uses: softprops/action-gh-release@v2 with: - go-version: 1.20.x - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser - version: latest - args: release --rm-dist + body_path: "body.md" + tag_name: "v${{ env.REVISION }}" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SLACK_WEBHOOK: ${{ secrets.SLACK_RELEASE_WEBHOOK }} + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + + - name: Print Version + run: echo "Bumped to version v${{ env.REVISION }}" diff --git a/.gitignore b/.gitignore index 173fa2d..076e21d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ node_modules/ config.yml go.work.sum dist/ + +flake.lock \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..4853f24 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,88 @@ +version: "2" + +run: + timeout: 2m + allow-parallel-runners: true + go: '1.24' +# build-tags: +# - local + +formatters: + exclusions: + paths: + - '\\.data/.*' + - 'node_modules/.*' + - 'vendor/.*' + - '.git/.*' + - '.husky/.*' + - '.github/.*' + - '.idea/.*' + - '.vscode/.*' + - '\\.bin/.*' + enable: + - gci + settings: + golines: + max-len: 120 + tab-len: 4 + gci: + sections: + - standard + - default + - blank + - dot + - alias + - localmodule + - prefix(github.com/Drafteame/docker-api) + no-inline-comments: true + no-prefix-comments: true + custom-order: true + no-lex-order: true + +linters: + exclusions: + paths: + - '\\.data/.*' + - 'node_modules/.*' + - 'vendor/.*' + - '.git/.*' + - '.husky/.*' + - '.github/.*' + - '.idea/.*' + - '.vscode/.*' + - '\\.bin/.*' + rules: + - path: _test\.go$ + linters: + - gocognit + - errcheck + - path: .*/test/.* + linters: + - gocognit + - errcheck + enable: + - exptostd + - errcheck + - govet + - gocognit + - goconst + settings: + errcheck: + check-blank: true + govet: + disable-all: true + enable: + - buildtag + - copylocks + - defers + - loopclosure + - lostcancel + - slog + - tests + gocognit: + min-complexity: 15 + goconst: + min-len: 2 + min-occurrences: 2 + + diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 414e9a7..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,47 +0,0 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com -before: - hooks: - # You may remove this if you don't use go modules. - - go mod tidy - -builds: - - env: - - CGO_ENABLED=0 - main: cmd/main.go - goos: - - linux - -archives: - - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - -checksum: - name_template: 'checksums.txt' - -snapshot: - name_template: "{{ incpatch .Version }}-next" - -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - -announce: - slack: - # Whether its enabled or not. - # Defaults to false. - enabled: true - - # Message template to use while publishing. - # Defaults to `{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}` - message_template: ' Package github.com/Drafteame/container@{{.Tag}} is out!' - - # The name of the channel that the user selected as a destination for webhook messages. - channel: '#engineering' diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..3ef4e01 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,18 @@ +#!/usr/bin/env sh + +. "$(dirname "$0")/_/husky.sh" + +# check if the env variable GIT_NV is equal to 1, if so, skipp the script +if [ "$GIT_NV" = "1" ]; then + echo "[husky] skipping commit-msg hooks" + exit 0 +fi + + +sh ./.husky/commit-msg-scripts/commitizen.sh + +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + echo "[husky] commit validation error" + exit 1 +fi diff --git a/.husky/commit-msg-scripts/commitizen.sh b/.husky/commit-msg-scripts/commitizen.sh new file mode 100644 index 0000000..9e76328 --- /dev/null +++ b/.husky/commit-msg-scripts/commitizen.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +echo "[commitizen] checking commit message with commitizen" + +cz check --commit-msg-file .git/COMMIT_EDITMSG + +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + echo "[commitizen] found issues in commit message" + exit 1 +fi + +exit 0 \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..59500d5 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +. "$(dirname "$0")/_/husky.sh" + +if [ "$GIT_NV" = "1" ]; then + echo "[husky] skipping pre-commit hooks" + exit 0 +fi + +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) +GO_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\.go$|go\.(mod|sum|work|work\.sum)$" || true) + +if [ -n "$GO_FILES" ]; then + sh ./.husky/pre-commit-scripts/golangci-lint.sh || exit 1 + echo "Running task format..." + task format + if [ $? -ne 0 ]; then + echo "Error: task format failed" + exit 1 + fi +fi + +sh ./.husky/pre-commit-scripts/goimports-reviser.sh && \ +sh ./.husky/pre-commit-scripts/golangci-lint.sh + +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + echo "[husky] commit validation error" + exit 1 +fi diff --git a/.husky/pre-commit-scripts/go-mod-tidy.sh b/.husky/pre-commit-scripts/go-mod-tidy.sh new file mode 100644 index 0000000..3a7aa19 --- /dev/null +++ b/.husky/pre-commit-scripts/go-mod-tidy.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +echo "[go-mod-tidy] checking go.mod files" + +function get_base_dir() { + local dir + + local file="$1" + dir=$(dirname "$file") + + echo "$dir" +} + +# Find and process go.mod files +go_mod_files=$(fd 'go.mod' --glob) +go_sum_files=$(fd 'go.sum' --glob) + +if [ -n "$go_mod_files" ]; then + for file in $go_mod_files; do + echo "[go-mod-tidy] tidying $file" + + cd "$(get_base_dir "$file")" && go mod tidy -v + + # shellcheck disable=SC2181 + if [ $? -ne 0 ]; then + exit 2 + fi + + # shellcheck disable=SC2164 + cd - > /dev/null + done +fi + +# add files to git +if [ -n "$go_mod_files" ]; then + git add $go_mod_files +fi + +if [ -n "$go_sum_files" ]; then + git add $go_sum_files +fi + +git commit -m "deps: tidy go.mod files" +exit 0 diff --git a/.husky/pre-commit-scripts/goimports-reviser.sh b/.husky/pre-commit-scripts/goimports-reviser.sh new file mode 100644 index 0000000..f6e1b4b --- /dev/null +++ b/.husky/pre-commit-scripts/goimports-reviser.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +files=("$@") +if [ ${#files[@]} -eq 0 ]; then + echo "[goimports-reviser] no go files to check" + exit 0 +fi + +echo "[goimports-reviser] formatting ${#files[@]} go files" + +goimports-reviser -format ${files[@]} &> /dev/null + +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + echo "[goimports-reviser] found issues formatting go files" + exit 1 +fi + +git add --all +exit 0 diff --git a/.husky/pre-commit-scripts/golangci-lint.sh b/.husky/pre-commit-scripts/golangci-lint.sh new file mode 100755 index 0000000..38ab5da --- /dev/null +++ b/.husky/pre-commit-scripts/golangci-lint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +echo "[golangci-lint] checking go files" + +golangci-lint run ./... + +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + echo "[golangci-lint] found issues that needs to be fixed" + exit 1 +fi + + diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..841576e --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,23 @@ +#!/usr/bin/env sh + +. "$(dirname "$0")/_/husky.sh" + +echo "Running pre-push hooks" + +# Get only the staged files +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) + +# Get Go files that are staged +GO_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E "\.go$|go\.(mod|sum|work|work\.sum)$" || true) + +if [ "$GIT_NV" = "1" ]; then + echo "[husky] skipping pre-push hooks" + exit 0 +fi + +sh ./.husky/pre-commit-scripts/go-mod-tidy.sh + +if [ $? -ne 0 ]; then + echo "[husky] push validation error" + exit 1 +fi \ No newline at end of file diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..45a93f5 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,29 @@ +issue-845-fix: True + +disable-version-string: True + +resolve-type-alias: False + +dir: '{{.InterfaceDir}}/mocks' + +outpkg: mocks + +with-expecter: true + +recursive: false + + +exclude: + - node_modules + - vendor + - .git + - .husky + - .github + - .idea + - .vscode + +packages: + github.com/Drafteame/container/dependency: + interfaces: + Builder: {} + Container: {} \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index eb9da0f..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -repos: - - repo: https://github.com/Bahjat/pre-commit-golang - rev: v1.0.3 - hooks: - - id: go-vet - - - repo: https://github.com/Drafteame/pre-commit-golang - rev: 0.10.1 - hooks: - - id: go-mod-tidy - - id: goimports-reviser - args: [ "-excludes=bin,node_modules,tmp,.git" ] - - id: revive - args: [ "-config=revive.toml", "-formatter=friendly" ] - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: check-yaml - - id: check-added-large-files - - - repo: https://github.com/commitizen-tools/commitizen - rev: v3.24.0 - hooks: - - id: commitizen - stages: [commit-msg] diff --git a/README.md b/README.md index 9e60896..e4f416a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ # container -![gopher-container](https://user-images.githubusercontent.com/9085902/216511169-b28f488e-1f9c-4a8e-8ec9-4cb90db1c25a.png) - -Flexible runtime dependency container inspired on go.uber.org/dig and based on reflection. It applies the dependency tree -concept to make flexible injections. +Flexible and opinionated IoC container for dependency injection with black magic ## Require -- Go >= 1.20 +- Go >= 1.22 ## Install @@ -56,7 +53,8 @@ func someConstructor(p *param) *someType { } func regularInstance() *someType { - err := container.Register("someName", someConstructor, container.Inject("someParam")) + // Register a regular dependency with options + err := container.Register("someName", someConstructor, container.WithArgs(container.MustGet[*param]("someParam"))) if err != nil { panic(err) } @@ -65,7 +63,8 @@ func regularInstance() *someType { } func singletonInstance() mainInterface { - err := container.Singleton("someNameSingleton", someConstructor, container.Inject("someParam")) + // Register a singleton dependency with options + err := container.Singleton("someNameSingleton", someConstructor, container.WithArgs(container.MustGet[*param]("someParam"))) if err != nil { panic(err) } @@ -147,11 +146,12 @@ func getSingletonAsSubInterface() subInterface { ## Dependencies -There two types of dependencies, regular dependencies and singleton dependencies. +There are two types of dependencies: regular dependencies and singleton dependencies. Regular dependencies are instances that each time that are required to be injected or retrieved, they will create a new instance from the provided factory each time. This means that with this type of dependencies, you will have multiple -instances of the same type and this will not share any context. Basically is a fresh new instance each time we inject it. +instances of the same type and this will not share any context. Basically, there is a fresh new instance each time we +inject it. ```go package main @@ -176,9 +176,9 @@ func newUser(name string, age int) *User { func main() { depName := "someDep" - dep := dependency.New(newUser, "John", 21) - - if err := container.Register(depName, dep); err != nil { + + // Register a regular dependency with options + if err := container.Register(depName, newUser, container.WithArgs("John", 21)); err != nil { panic(err) } @@ -192,10 +192,10 @@ func main() { ``` Singleton dependencies are pretty much the same as a regular dependency with the particularity that the container will -keep the result obtained from the factory internally and if a new instance of the same dependency is called to be -injected, instead of create a new one from the factory will inject the previous created instance. +keep the result obtained from the factory internally. They if a new instance of the same dependency is called to be +injected, instead of creating a new one from the factory will inject the previously created instance. -Keep in mind that this can not work as a real singleton if the returned value of the factory is not a pointer or +Keep in mind that this cannot work as a real singleton if the returned value of the factory is not a pointer or interface. ```go @@ -205,7 +205,6 @@ import ( "fmt" "github.com/Drafteame/container" - "github.com/Drafteame/container/dependency" ) type User struct { @@ -222,9 +221,9 @@ func newUser(name string, age int) *User { func main() { depName := "someDep" - dep := dependency.NewSingleton(newUser, "John", 21) - - if err := container.Register(depName, dep); err != nil { + + // Register a singleton dependency with options + if err := container.Singleton(depName, newUser, container.WithArgs("John", 21)); err != nil { panic(err) } @@ -244,223 +243,264 @@ func main() { } ``` -Arguments of the regular and singleton dependencies can be plain values, other `dependency.Dependency` objects or -`dependency.Injectable` instances. This last type of argument are objects that make reference to a dependency that was -registered in the container previously. This is specially helpful if you do not want to redefine a dependency many -times, and just reuse same specification of the dependency. +## Must Methods + +The container package provides a set of "Must" methods that are variants of their regular counterparts. These methods panic instead of returning errors, which can be useful in scenarios where you want to fail fast if something goes wrong, such as during application initialization. + +### MustGet -Example of plain values as dependency arguments: +`MustGet` retrieves a dependency from the container and panics if it cannot be found or cast to the requested type: ```go package main -import "github.com/Drafteame/container/dependency" +import ( + "github.com/Drafteame/container" +) func main() { - name := "foo" - age := 21 - - // Regular dependency - depName := "test" - dep := dependency.New(newUser, name, age) - - // Singleton dependency - depName2 := "test2" - dep2 := dependency.NewSingleton(newUser, name, age) + // Register a dependency + if err := container.Register("config", newConfig, container.WithArgs("production")); err != nil { + panic(err) + } + + // Get the dependency with MustGet - will panic if not found or cannot be cast + config := container.MustGet[*Config]("config") + + // Use the dependency directly without error checking + fmt.Println(config.Environment) } ``` -Example of dependency instances as arguments: +### MustRegister + +`MustRegister` registers a dependency and panics if registration fails: ```go package main import ( - "os" - - "github.com/Drafteame/container/dependency" + "github.com/Drafteame/container" ) func main() { - driver := dependency.New(newDB, os.Getenv("DB_URL")) - - // Regular dependency - dep := dependency.New(newUser, driver) - - // Singleton dependency - dep2 := dependency.NewSingleton(newUser, driver) + // Register a dependency with MustRegister - will panic if registration fails + container.MustRegister("logger", newLogger, container.WithArgs("debug")) + + // Use the registered dependency + logger := container.MustGet[Logger]("logger") + logger.Info("Application started") } ``` -Example of Injectable dependency as argument: +### MustSingleton + +`MustSingleton` registers a singleton dependency and panics if registration fails: ```go package main import ( - "os" - "github.com/Drafteame/container" - "github.com/Drafteame/container/dependency" ) func main() { - driverName := "database" - driver := dependency.New(dbConstructor, os.Getenv("DB_URL")) - - if err := container.Register(driverName, driver); err != nil { - panic(err) - } + // Register a singleton with MustSingleton - will panic if registration fails + container.MustSingleton("database", newDatabase, container.WithArgs("connection-string")) - // Regular dependency - dep := dependency.New(userConstructor, dependency.Inject(driverName)) - - // Singleton dependency - dep2 := dependency.NewSingleton(userConstructor, dependency.Inject(driverName)) + // Use the registered singleton + db := container.MustGet[Database]("database") + db.Connect() } ``` -### Invoke - -There is a method that can help you to bring some extra functionality to the container and obtain more than one instance -at a time. +### MustOverride -This method will receive a callback that can or not return an error and can or not receive multiple arguments. This -arguments should be structs, defining on his fields the instances that the container should inject to it. +`MustOverride` overrides an existing dependency and panics if the operation fails: ```go package main import ( - "errors" - "fmt" - "os" - "github.com/Drafteame/container" - "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) -type args struct { - types.In - User *user `inject:"name=user"` +func main() { + // Register the original dependency + container.MustRegister("emailService", newRealEmailService) + + // Later, override it with a mock for testing + container.MustOverride("emailService", newMockEmailService) + + // Use the overridden dependency + emailService := container.MustGet[EmailService]("emailService") + emailService.SendEmail("user@example.com", "Test Subject", "Test Body") } +``` -func invoker(in args) error { - if in.User == nil { - return errors.New("empty instance of user") - } +### When to Use Must Methods - fmt.Println("Hello ", in.User.GetName()) - return nil -} +Must methods are particularly useful in the following scenarios: -func main() { - driverName := "database" - driver := dependency.New(newDB, os.Getenv("DB_URL")) +1. **Application Initialization**: When setting up your application, you often want to fail fast if a critical dependency cannot be registered or retrieved. - if err := container.Register(driverName, driver); err != nil { - panic(err) - } +2. **Testing**: In tests, you may want to simplify error handling and focus on the test logic. + +3. **Simple Applications**: In small applications or scripts where comprehensive error handling is not necessary. + +However, be cautious when using Must methods in production code, especially in request handlers or other code that should gracefully handle errors, as panics can crash your application if not properly recovered. + +## Options + +The container package provides a flexible options pattern for configuring dependency registration and retrieval. - depName := "user" - dep := dependency.New(newUser, dependency.Inject(driverName)) +### WithArgs - if err := container.Register(depName, dep); err != nil { +The `WithArgs` option allows you to provide arguments to the factory function when registering a dependency: + +```go +package main + +import ( + "github.com/Drafteame/container" +) + +func main() { + // Register with direct arguments + if err := container.Register("user", newUser, container.WithArgs("John", 21)); err != nil { panic(err) } - if err := container.Invoke(invoker); err != nil { + // Register with a mix of direct arguments and dependencies + if err := container.Register("service", newService, container.WithArgs( + container.MustGet[*User]("user"), + "api-key-123", + )); err != nil { panic(err) - } + } } ``` -Also you can use interface segregation to define the arguments: +### WithContainer + +The `WithContainer` option allows you to specify a custom container instance to use instead of the global container: ```go package main import ( - "errors" - "fmt" - "os" - - // ... - "github.com/Drafteame/container" ) -type namer interface{ - GetName() +func main() { + // Create a custom container + customContainer := container.New() + + // Register a dependency in the custom container + if err := container.Register("user", newUser, + container.WithContainer(customContainer), + container.WithArgs("John", 21), + ); err != nil { + panic(err) + } + + // Get the dependency from the custom container + user, err := container.Get[*User]("user", container.WithContainer(customContainer)) + if err != nil { + panic(err) + } } +``` -type args struct { - types.In - User namer `inject:"name=user"` -} +## Override Dependencies -func invoker(in args) error { - if in.User == nil { - return errors.New("empty instance of user") - } +The container package provides a way to override existing dependencies, which is particularly useful for testing: - fmt.Println("Hello ", in.User.GetName()) - return nil -} +```go +package main -func main() { - // ..... +import ( + "github.com/Drafteame/container" +) - if err := container.Invoke(invoker); err != nil { +func main() { + // Register the original dependency + if err := container.Register("database", newRealDatabase, container.WithArgs("connection-string")); err != nil { + panic(err) + } + + // Later, override it with a mock for testing + if err := container.Override("database", newMockDatabase, container.WithArgs()); err != nil { + panic(err) + } + + // The container will now return the mock implementation + db, err := container.Get[Database]("database") + if err != nil { panic(err) } } ``` -#### Optional arguments +## Using Injectable Dependencies -When you define In structs to be used with the `Invoke` method you can mark optional fields if you expect that some -fields can or not be filled by the injector and avoid an error if there's no dependency registered with the required -name. +You can use the `Inject` function to reference dependencies that are already registered in the container: ```go package main import ( - "errors" - "fmt" - "os" - - // ... - - "github.com/Drafteame/container/types" + "github.com/Drafteame/container" + "github.com/Drafteame/container/dependency" ) -type namer interface{ - GetName() +func main() { + // Register a database dependency + if err := container.Register("database", newDatabase, container.WithArgs("connection-string")); err != nil { + panic(err) + } + + // Register a user repository that depends on the database + if err := container.Register("userRepo", newUserRepository, container.WithArgs(container.Inject("database"))); err != nil { + panic(err) + } + + // Get the user repository + repo, err := container.Get[*UserRepository]("userRepo") + if err != nil { + panic(err) + } } +``` -type args struct { - types.In - User namer `inject:"name=notExist,optional"` -} +## Custom Containers -func invoker(in args) error { - if in.User != nil { - fmt.Println("Hello ", in.User.GetName()) - } else { - fmt.Println("Ups no namer instance found") - } - - return nil -} +You can create and use custom containers instead of the global container: -func main() { - // ..... +```go +package main + +import ( + "github.com/Drafteame/container" + "github.com/Drafteame/container/dependency" +) - if err := container.Invoke(invoker); err != nil { +func main() { + // Create a custom container + customContainer := container.New() + + // Register dependencies in the custom container + if err := customContainer.Provide("user", dependency.New(newUser, "John", 21)); err != nil { panic(err) } + + // Get dependencies from the custom container + user, err := customContainer.Get("user") + if err != nil { + panic(err) + } + + // Type assertion is needed when using the container directly + typedUser := user.(*User) } -``` +``` \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..73cf2ee --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,23 @@ +version: '3' + +tasks: + format: + desc: Format all code + cmds: + - goimports-reviser ./... + - golangci-lint fmt ./... + silent: true + + lint: + desc: Lint all code + cmds: + - golangci-lint run ./... + silent: true + + test: + desc: Run all tests + cmds: + - go test -v -race ./... + silent: true + + diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 0754eab..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,6 +0,0 @@ -package main - -// nolint -func main() { - // nothing to do -} diff --git a/container.go b/container.go index a02b0b3..9325a0a 100644 --- a/container.go +++ b/container.go @@ -3,28 +3,21 @@ package container import ( "github.com/Drafteame/container/dependency" "github.com/Drafteame/container/injector" - "github.com/Drafteame/container/types" ) var depContainer Container -type symbolName interface { - string | types.Symbol -} - // Container represents a dependency container that should register factory methods and its dependency threes to be // injected when type Container interface { - Provide(name types.Symbol, dep dependency.Dependency) error - Override(name types.Symbol, dep dependency.Dependency) error - Invoke(construct any) error - Get(name types.Symbol) (any, error) + Override(name string, dep dependency.Dependency) error + Provide(name string, dep dependency.Dependency) error + Get(name string) (any, error) Flush() - Remove(name types.Symbol) - TestMode() + Remove(name string) } -// get return a global instance for the dependency injection container. If the container is nil, then it will initialize +// get returns a global instance for the dependency injection container. If the container is nil, then it will initialize // a new instance before returning the container. func get() Container { if depContainer == nil { @@ -35,33 +28,19 @@ func get() Container { } // New Return a new isolated instance for the dependency injection container. This instance is totally different from -// the global container and do not share any saved dependency three between each other. +// the global container and does not share any saved dependency three between each other. func New() Container { return injector.New() } -// Invoke Is the entry point to execute dependency injection resolution. It calls an invoker function that can -// receive or not a struct that embeds inject.In struct as input, and return an error or not (any other return field or -// type will be ignored on resolution). When invoker is called it will resolve the dependency threes of each field from -// the previously provided resources on Container. -func Invoke(construct any) error { - return get().Invoke(construct) -} - // Flush WARNING: This function will delete all saved instances, solved and registered factories from the container. -// Do not use this method on production, and just use it for testing purposes. +// Do not use this method on production and just use it for testing purposes. func Flush() { get().Flush() } -// Remove WARNING: This function will remove a specific factory and its solve dependency from the container. Do not use -// this method on production, and just use it on testing purposes. -func Remove[T symbolName](name T) { - get().Remove(types.Symbol(name)) -} - -// TestMode WARNING: Sets testMode flag to true, bypassing singleton instance generation to avoid race conditions when -// container is used on test cases. -func TestMode() { - get().TestMode() +// Remove WARNING: This function will remove a specific factory and its solved dependency from the container. Do not use +// this method on production and just use it for testing purposes. +func Remove(name string) { + get().Remove(name) } diff --git a/container_test.go b/container_test.go index d04e5be..273a565 100644 --- a/container_test.go +++ b/container_test.go @@ -4,19 +4,17 @@ import ( "database/sql" "errors" "fmt" - "math/rand" "testing" "github.com/stretchr/testify/assert" "github.com/Drafteame/container/dependency" "github.com/Drafteame/container/injector" - "github.com/Drafteame/container/types" ) const name = "John" - const age = 21 +const factoryName = "test" type user struct { name string @@ -50,53 +48,17 @@ func TestNew(t *testing.T) { assert.Implements(t, new(Container), ic) } -func TestInvoke(t *testing.T) { - defer Flush() - - depName := types.Symbol("userTest") - dep := dependency.New(newUser, name, age) - - if err := Register(depName, dep); err != nil { - t.Fatal(err) - } - - type args struct { - types.In - User *user `inject:"name=userTest"` - } - - called := false - - invoker := func(in args) { - if assert.NotNil(t, in.User) { - assert.Equal(t, in.User.age, age) - assert.Equal(t, in.User.name, name) - } - - called = true - } - - err := Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) -} - func TestSingleton(t *testing.T) { t.Run("should register a raw factory singleton instance", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) - err := Singleton(factoryName, newUser, name, age) + err := Singleton(factoryName, newUser, WithArgs(name, age)) assert.NoError(t, err) }) t.Run("should register a singleton from singleton dependency", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) dep := dependency.NewSingleton(newUser, name, age) @@ -106,9 +68,7 @@ func TestSingleton(t *testing.T) { }) t.Run("should register a singleton from dependency", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) dep := dependency.New(newUser, name, age) @@ -118,23 +78,19 @@ func TestSingleton(t *testing.T) { }) t.Run("should register a singleton from raw function and nested dependencies", func(t *testing.T) { - defer Flush() - - depName := "db" + t.Cleanup(Flush) - if err := Singleton(depName, newDB); err != nil { + if err := Singleton("db", newDB); err != nil { t.Fatal(err) } - factoryName := "test" - - if err := Singleton(factoryName, newUserWithDB, Inject(depName)); err != nil { + if err := Singleton(factoryName, newUserWithDB, WithArgs(Inject(depName))); err != nil { t.Fatal(err) } }) t.Run("error when no dependency.Depdndendency instance or raw function is registered", func(t *testing.T) { - defer Flush() + t.Cleanup(Flush) err := Singleton("name", dependency.Injectable{}) @@ -148,19 +104,15 @@ func TestSingleton(t *testing.T) { func TestRegister(t *testing.T) { t.Run("should register a raw factory instance", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) - err := Register(factoryName, newUser, name, age) + err := Register(factoryName, newUser, WithArgs(name, age)) assert.NoError(t, err) }) t.Run("should register a singleton from singleton dependency", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) dep := dependency.NewSingleton(newUser, name, age) @@ -170,9 +122,7 @@ func TestRegister(t *testing.T) { }) t.Run("should register a singleton from dependency", func(t *testing.T) { - defer Flush() - - factoryName := "test" + t.Cleanup(Flush) dep := dependency.New(newUser, name, age) @@ -182,23 +132,19 @@ func TestRegister(t *testing.T) { }) t.Run("should register a singleton from raw function and nested dependencies", func(t *testing.T) { - defer Flush() - - depName := "db" + t.Cleanup(Flush) - if err := Register(depName, newDB); err != nil { + if err := Register("db", newDB); err != nil { t.Fatal(err) } - factoryName := "test" - - if err := Register(factoryName, newUserWithDB, Inject(depName)); err != nil { + if err := Register(factoryName, newUserWithDB, WithArgs(Inject(depName))); err != nil { t.Fatal(err) } }) t.Run("error when no dependency.Dependency instance or raw function is registered", func(t *testing.T) { - defer Flush() + t.Cleanup(Flush) err := Register("name", dependency.Injectable{}) @@ -212,7 +158,7 @@ func TestRegister(t *testing.T) { func TestFunctionalRegistration(t *testing.T) { t.Run("non singleton instance", func(t *testing.T) { - defer Flush() + t.Cleanup(Flush) defer func() { if r := recover(); r != nil { t.Fatal(r) @@ -243,13 +189,12 @@ func TestFunctionalRegistration(t *testing.T) { } func TestRemove(t *testing.T) { - const depName = "test" - - if err := Register(depName, newDB); err != nil { + c := New() + if err := Register(depName, newDB, WithContainer(c)); err != nil { t.Fatal(err) } - _, err := Get[any](depName) + _, err := Get[any](depName, WithContainer(c)) assert.NoError(t, err) @@ -261,8 +206,7 @@ func TestRemove(t *testing.T) { } func TestOverride(t *testing.T) { - const depName = "test" - defer Flush() + t.Cleanup(Flush) if err := Register(depName, func() int { return 10 }); err != nil { t.Fatal(err) @@ -282,35 +226,3 @@ func TestOverride(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 20, v) } - -func TestTestMode(t *testing.T) { - t.Run("get singleton instance on test mode", func(t *testing.T) { - TestMode() - - type test struct { - number int - } - - sym := types.Symbol("test") - - err := Singleton(sym, func() test { - return test{number: rand.Int()} - }) - - if err != nil { - t.Fatal(err) - } - - obj1, errGet := Get[test](sym) - if errGet != nil { - t.Fatal(errGet) - } - - obj2, errGet2 := Get[test](sym) - if errGet2 != nil { - t.Fatal(errGet2) - } - - assert.NotEqual(t, obj1.number, obj2.number) - }) -} diff --git a/dependency/build_func.go b/dependency/build_func.go deleted file mode 100644 index 9f32568..0000000 --- a/dependency/build_func.go +++ /dev/null @@ -1 +0,0 @@ -package dependency diff --git a/dependency/dependency.go b/dependency/dependency.go index c13c2ab..7778590 100644 --- a/dependency/dependency.go +++ b/dependency/dependency.go @@ -4,23 +4,8 @@ import ( "errors" "fmt" "reflect" - - "github.com/Drafteame/container/types" ) -//go:generate mockery --name=Builder --filename=builder.go --structname=Builder --output=mocks --outpkg=mocks -//go:generate mockery --name=Container --filename=container.go --structname=Container --output=mocks --outpkg=mocks - -// Builder definition for a dependency that should be build on injection time. -type Builder interface { - Build() (any, error) -} - -// Container is a container that holds global dependencies. -type Container interface { - Get(name types.Symbol) (any, error) -} - // Dependency implementation of dependency. type Dependency struct { Factory any @@ -53,17 +38,17 @@ func NewSingleton(constructor any, args ...any) Dependency { } } -// IsSingleton returns true if the current dependency will be treated as a shared dependency. +// IsSingleton returns true if the current dependency is treated as a shared dependency. func (d Dependency) IsSingleton() bool { return d.Singleton } -// SetContainer add shared container to the dependency object in order to resolve shared arguments in the +// SetContainer add a shared container to the dependency object to resolve shared arguments in // dependency three. func (d Dependency) SetContainer(sc Container) Dependency { d.container = sc return d } -// Build It validates the constructor and gets its type. It gets the arguments values for the constructor. It calls the +// Build It validates the constructor and gets its type. It gets the argument values for the constructor. It calls the // constructor with those arguments using reflection (`reflect` package). Finally, it returns a value and an error if // any of them is not nil (the error can be returned by one of the dependencies). func (d Dependency) Build() (any, error) { @@ -192,7 +177,7 @@ func (d Dependency) resolveArgument(index int, builder Builder, ctype reflect.Ty // getValueAndError If the constructor returns no values, we return `nil` and `nil`. If the constructor returns one // value, we return that value and `nil`. If the constructor returns more than one value, we take the first as a result -// and last as an error. We check if last argument is an error (if it's not nil). We return result and error (or just +// and last as an error. We check if the last argument is an error (if it's not nil). We return result and error (or just // nil if there was no error). func (d Dependency) getValueAndError(res []reflect.Value) (any, error) { if len(res) == 0 { diff --git a/dependency/dependency_test.go b/dependency/dependency_test.go index 8d32547..4c63de8 100644 --- a/dependency/dependency_test.go +++ b/dependency/dependency_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/Drafteame/container/dependency/mocks" - "github.com/Drafteame/container/types" ) // nolint @@ -15,19 +14,6 @@ type db interface { client() } -// nolint -type database struct { - dbname string -} - -// nolint -func newDatabase(dbname string) *database { - return &database{dbname: dbname} -} - -// nolint -func (db *database) client() {} - type namer interface { getName() string } @@ -55,11 +41,6 @@ func newUser(name string, age int) *user { return &user{name: name, age: age} } -// nolint -func newUserConn(conn db) *user { - return &user{conn: conn} -} - // nolint func (u *user) getName() string { return u.name @@ -427,11 +408,11 @@ func TestDependency_Build(t *testing.T) { }) t.Run("with injectable dependency", func(t *testing.T) { - injectDepName := types.Symbol("inject") + injectDepName := string("inject") injectDep := Inject(injectDepName) injectDepValue := "some" - ic := mocks.NewContainer(t) + ic := mocks.NewMockContainer(t) ic.On("Get", injectDepName).Return(injectDepValue, nil) injectedValue := "" diff --git a/dependency/injectable.go b/dependency/injectable.go index f9a47f3..80e6074 100644 --- a/dependency/injectable.go +++ b/dependency/injectable.go @@ -2,21 +2,19 @@ package dependency import ( "fmt" - - "github.com/Drafteame/container/types" ) // Injectable is a type of dependency that is not a dependency three itself, but also is a reference to other dependency // three, stored on the container. This Dependency will be accessed by his associated name on the container. type Injectable struct { - name types.Symbol + name string container Container } var _ Builder = &Injectable{} // Inject return an instance of Injectable dependency. -func Inject(name types.Symbol) Injectable { +func Inject(name string) Injectable { return Injectable{ name: name, } diff --git a/dependency/injectable_test.go b/dependency/injectable_test.go index 2190653..a0f89c4 100644 --- a/dependency/injectable_test.go +++ b/dependency/injectable_test.go @@ -7,11 +7,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/Drafteame/container/dependency/mocks" - "github.com/Drafteame/container/types" ) +const name = "test" + func TestInject(t *testing.T) { - name := types.Symbol("test") + i := Inject(name) assert.IsType(t, Injectable{}, i) @@ -19,16 +20,14 @@ func TestInject(t *testing.T) { } func TestInjectable_IsSingleton(t *testing.T) { - name := types.Symbol("test") i := Inject(name) assert.False(t, i.IsSingleton()) } func TestInjectable_SetContainer(t *testing.T) { - ic := mocks.NewContainer(t) + ic := mocks.NewMockContainer(t) - name := types.Symbol("test") i := Inject(name).SetContainer(ic).(Injectable) assert.NotNil(t, i.container) @@ -37,11 +36,10 @@ func TestInjectable_SetContainer(t *testing.T) { func TestInjectable_Build(t *testing.T) { t.Run("resolve build from container", func(t *testing.T) { - depName := types.Symbol("test") - dep := Inject(depName) + dep := Inject(name) - ic := mocks.NewContainer(t) - ic.On("Get", depName).Return("some", nil) + ic := mocks.NewMockContainer(t) + ic.On("Get", name).Return("some", nil) res, err := dep.SetContainer(ic).Build() @@ -50,8 +48,7 @@ func TestInjectable_Build(t *testing.T) { }) t.Run("error by empty container", func(t *testing.T) { - depName := types.Symbol("test") - dep := Inject(depName) + dep := Inject(name) _, err := dep.Build() expErr := errors.New("inject: [internal-error] no container provided") diff --git a/dependency/interfaces.go b/dependency/interfaces.go new file mode 100644 index 0000000..d0a40e1 --- /dev/null +++ b/dependency/interfaces.go @@ -0,0 +1,11 @@ +package dependency + +// Builder definition for a dependency that should be build on injection time. +type Builder interface { + Build() (any, error) +} + +// Container is a container that holds global dependencies. +type Container interface { + Get(name string) (any, error) +} diff --git a/dependency/mocks/builder.go b/dependency/mocks/builder.go deleted file mode 100644 index d9150d2..0000000 --- a/dependency/mocks/builder.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by mockery v2.16.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// Builder is an autogenerated mock type for the Builder type -type Builder struct { - mock.Mock -} - -// Build provides a mock function with given fields: -func (_m *Builder) Build() (interface{}, error) { - ret := _m.Called() - - var r0 interface{} - if rf, ok := ret.Get(0).(func() interface{}); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(interface{}) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewBuilder interface { - mock.TestingT - Cleanup(func()) -} - -// NewBuilder creates a new instance of Builder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewBuilder(t mockConstructorTestingTNewBuilder) *Builder { - mock := &Builder{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/dependency/mocks/container.go b/dependency/mocks/container.go deleted file mode 100644 index 5a66316..0000000 --- a/dependency/mocks/container.go +++ /dev/null @@ -1,51 +0,0 @@ -// Code generated by mockery v2.16.0. DO NOT EDIT. - -package mocks - -import ( - types "github.com/Drafteame/container/types" - mock "github.com/stretchr/testify/mock" -) - -// Container is an autogenerated mock type for the Container type -type Container struct { - mock.Mock -} - -// Get provides a mock function with given fields: name -func (_m *Container) Get(name types.Symbol) (interface{}, error) { - ret := _m.Called(name) - - var r0 interface{} - if rf, ok := ret.Get(0).(func(types.Symbol) interface{}); ok { - r0 = rf(name) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(interface{}) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(types.Symbol) error); ok { - r1 = rf(name) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewContainer interface { - mock.TestingT - Cleanup(func()) -} - -// NewContainer creates a new instance of Container. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewContainer(t mockConstructorTestingTNewContainer) *Container { - mock := &Container{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/dependency/mocks/mock_Builder.go b/dependency/mocks/mock_Builder.go new file mode 100644 index 0000000..ee310d3 --- /dev/null +++ b/dependency/mocks/mock_Builder.go @@ -0,0 +1,89 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// MockBuilder is an autogenerated mock type for the Builder type +type MockBuilder struct { + mock.Mock +} + +type MockBuilder_Expecter struct { + mock *mock.Mock +} + +func (_m *MockBuilder) EXPECT() *MockBuilder_Expecter { + return &MockBuilder_Expecter{mock: &_m.Mock} +} + +// Build provides a mock function with no fields +func (_m *MockBuilder) Build() (any, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Build") + } + + var r0 any + var r1 error + if rf, ok := ret.Get(0).(func() (any, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() any); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(any) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBuilder_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build' +type MockBuilder_Build_Call struct { + *mock.Call +} + +// Build is a helper method to define mock.On call +func (_e *MockBuilder_Expecter) Build() *MockBuilder_Build_Call { + return &MockBuilder_Build_Call{Call: _e.mock.On("Build")} +} + +func (_c *MockBuilder_Build_Call) Run(run func()) *MockBuilder_Build_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockBuilder_Build_Call) Return(_a0 any, _a1 error) *MockBuilder_Build_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBuilder_Build_Call) RunAndReturn(run func() (any, error)) *MockBuilder_Build_Call { + _c.Call.Return(run) + return _c +} + +// NewMockBuilder creates a new instance of MockBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBuilder(t interface { + mock.TestingT + Cleanup(func()) +}) *MockBuilder { + mock := &MockBuilder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/dependency/mocks/mock_Container.go b/dependency/mocks/mock_Container.go new file mode 100644 index 0000000..0517814 --- /dev/null +++ b/dependency/mocks/mock_Container.go @@ -0,0 +1,90 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// MockContainer is an autogenerated mock type for the Container type +type MockContainer struct { + mock.Mock +} + +type MockContainer_Expecter struct { + mock *mock.Mock +} + +func (_m *MockContainer) EXPECT() *MockContainer_Expecter { + return &MockContainer_Expecter{mock: &_m.Mock} +} + +// Get provides a mock function with given fields: name +func (_m *MockContainer) Get(name string) (any, error) { + ret := _m.Called(name) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 any + var r1 error + if rf, ok := ret.Get(0).(func(string) (any, error)); ok { + return rf(name) + } + if rf, ok := ret.Get(0).(func(string) any); ok { + r0 = rf(name) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(any) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(name) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockContainer_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockContainer_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - name string +func (_e *MockContainer_Expecter) Get(name interface{}) *MockContainer_Get_Call { + return &MockContainer_Get_Call{Call: _e.mock.On("Get", name)} +} + +func (_c *MockContainer_Get_Call) Run(run func(name string)) *MockContainer_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockContainer_Get_Call) Return(_a0 any, _a1 error) *MockContainer_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockContainer_Get_Call) RunAndReturn(run func(string) (any, error)) *MockContainer_Get_Call { + _c.Call.Return(run) + return _c +} + +// NewMockContainer creates a new instance of MockContainer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockContainer(t interface { + mock.TestingT + Cleanup(func()) +}) *MockContainer { + mock := &MockContainer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e932785 --- /dev/null +++ b/flake.nix @@ -0,0 +1,38 @@ +{ + description = "Development tools"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-25.05"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem(system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShell = pkgs.mkShell { + buildInputs = with pkgs; [ + husky + commitizen + + go + goimports-reviser + golangci-lint + go-task + go-mockery + golines + gci + + sd + fd + ]; + + shellHook = '' + export GOROOT="${pkgs.go}/share/go" + ''; + }; + } + ); +} \ No newline at end of file diff --git a/get.go b/get.go index e47f952..14f1877 100644 --- a/get.go +++ b/get.go @@ -3,15 +3,17 @@ package container import ( "fmt" "reflect" - - "github.com/Drafteame/container/types" ) -// Get is a wrapper over the Get function attached to the global container. This function modify the return type of the -// resolved dependency, returned as `any` to the provided generic type `T`. If it can't be casted it will return an +// Get is a wrapper over the Get function attached to the global container. This function modifies the return type of the +// resolved dependency, returned as `any` to the provided generic type `T`. If it can't be cast it will return an // error. -func Get[T any, K symbolName](name K) (T, error) { - instance, err := get().Get(types.Symbol(name)) +func Get[T any](name string, opts ...Option) (T, error) { + depOpts := buildOptions(opts...) + + c := depOpts.container + + instance, err := c.Get(name) if err != nil { aux := new(T) return *aux, err @@ -27,9 +29,13 @@ func Get[T any, K symbolName](name K) (T, error) { return cast, nil } -// MustGet Same functionality that Get function but instead of returning error, it panics. -func MustGet[T any, K symbolName](name K) T { - instance, err := get().Get(types.Symbol(name)) +// MustGet Same functionality that Get function, but instead of returning an error, it panics. +func MustGet[T any](name string, opts ...Option) T { + depOpts := buildOptions(opts...) + + c := depOpts.container + + instance, err := c.Get(name) if err != nil { panic(err) } diff --git a/get_test.go b/get_test.go index 6c2c03e..e9f50f0 100644 --- a/get_test.go +++ b/get_test.go @@ -6,24 +6,23 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) +const depName = "userTest" + func TestGet(t *testing.T) { t.Run("get instance of specific type by name", func(t *testing.T) { - defer Flush() + c := New() - depName := types.Symbol("userTest1") dep := dependency.New(newUser, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - ui, err := Get[*user](depName) + ui, err := Get[*user](depName, WithContainer(c)) assert.NoError(t, err) assert.NotEmpty(t, ui) @@ -32,17 +31,14 @@ func TestGet(t *testing.T) { }) t.Run("get instance from container error", func(t *testing.T) { - defer Flush() + c := New() - depName := types.Symbol("userTest2") dep := dependency.New(newUserError, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - ui, err := Get[*user](string(depName)) + ui, err := Get[*user](depName, WithContainer(c)) expErr := errors.New("inject: error building dependency instance: inject: error constructing `func(string, int) (*container.user, error)`: some error") assert.Error(t, err) @@ -51,18 +47,15 @@ func TestGet(t *testing.T) { }) t.Run("cast type error", func(t *testing.T) { - defer Flush() + c := New() - depName := types.Symbol("userTest3") dep := dependency.New(newUser, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - ui, err := Get[string](depName) - expErr := errors.New("inject: error casting instance of `userTest3` dependency to `string`") + ui, err := Get[string](depName, WithContainer(c)) + expErr := errors.New("inject: error casting instance of `userTest` dependency to `string`") assert.Error(t, err) assert.Empty(t, ui) @@ -72,22 +65,20 @@ func TestGet(t *testing.T) { func TestMustGet(t *testing.T) { t.Run("get instance of specific type by name", func(t *testing.T) { - defer Flush() + c := New() + defer func() { if r := recover(); r != nil { t.Error(r) } }() - depName := types.Symbol("userTest1") dep := dependency.New(newUser, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - ui := MustGet[*user](depName) + ui := MustGet[*user](depName, WithContainer(c)) assert.NotEmpty(t, ui) assert.Equal(t, ui.age, age) @@ -95,7 +86,7 @@ func TestMustGet(t *testing.T) { }) t.Run("get instance from container error", func(t *testing.T) { - defer Flush() + c := New() defer func() { r := recover() expErr := errors.New("inject: error building dependency instance: inject: error constructing `func(string, int) (*container.user, error)`: some error") @@ -103,34 +94,32 @@ func TestMustGet(t *testing.T) { assert.Equal(t, expErr, fmt.Errorf("%v", r)) }() - depName := types.Symbol("userTest2") dep := dependency.New(newUserError, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - _ = MustGet[*user](string(depName)) + val := MustGet[*user](depName, WithContainer(c)) + assert.NotEmpty(t, val) + assert.Equal(t, val.age, age) + assert.Equal(t, val.name, name) }) t.Run("cast type error", func(t *testing.T) { - defer Flush() + c := New() + defer func() { r := recover() - expErr := errors.New("inject: error casting instance of `userTest3` dependency to `string`") + expErr := errors.New("inject: error casting instance of `userTest` dependency to `string`") assert.Equal(t, expErr, fmt.Errorf("%v", r)) }() - depName := types.Symbol("userTest3") dep := dependency.New(newUser, name, age) - if err := Register(depName, dep); err != nil { - t.Error(err) - return - } + err := Register(depName, dep, WithContainer(c)) + require.NoError(t, err) - _ = MustGet[string](depName) + _ = MustGet[string](depName, WithContainer(c)) }) } diff --git a/go.mod b/go.mod index a580fa6..29e114b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Drafteame/container -go 1.20 +go 1.22 require github.com/stretchr/testify v1.10.0 diff --git a/go.work b/go.work deleted file mode 100644 index 74da45b..0000000 --- a/go.work +++ /dev/null @@ -1,6 +0,0 @@ -go 1.20 - -use ( - . - ./magefiles -) diff --git a/injector/container.go b/injector/container.go index f2ec1b3..aaef6d3 100644 --- a/injector/container.go +++ b/injector/container.go @@ -2,49 +2,40 @@ package injector import ( "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) // Container is a dependency injection Container implementation type Container struct { - solvedDeps map[types.Symbol]any - deps map[types.Symbol]dependency.Dependency - testMode bool + solvedDeps map[string]any + deps map[string]dependency.Dependency } // New creates a new instance of a Container. func New() *Container { return &Container{ - solvedDeps: make(map[types.Symbol]any), - deps: make(map[types.Symbol]dependency.Dependency), - testMode: false, + solvedDeps: make(map[string]any), + deps: make(map[string]dependency.Dependency), } } -// TestMode WARNING: Sets testMode flag to true, bypassing singleton instance generation to avoid race conditions when -// container is used on test cases. -func (c *Container) TestMode() { - c.testMode = true -} - // Flush WARNING: This function will delete all saved instances, solved and registered factories from the container. -// Do not use this method on production, and just use it on testing purposes. +// Do not use this method on production and just use it for testing purposes. func (c *Container) Flush() { - c.solvedDeps = make(map[types.Symbol]any) - c.deps = make(map[types.Symbol]dependency.Dependency) + c.solvedDeps = make(map[string]any) + c.deps = make(map[string]dependency.Dependency) } -// Remove WARNING: This function will remove a specific factory and its solve dependency from the container. Do not use -// this method on production, and just use it on testing purposes. -func (c *Container) Remove(name types.Symbol) { +// Remove WARNING: This function will remove a specific factory and its solved dependency from the container. Do not use +// this method on production and use it for testing purposes. +func (c *Container) Remove(name string) { delete(c.solvedDeps, name) delete(c.deps, name) } // Override Set a new dependency that replaces the old one to change behavior on runtime. -// WARNING: This function will remove a specific factory and its solve dependency from the container. Do not use -// this method on production, and just use it on testing purposes. -func (c *Container) Override(name types.Symbol, dep dependency.Dependency) error { +// WARNING: This function will remove a specific factory and its solved dependency from the container. Do not use +// this method on production and use it for testing purposes. +func (c *Container) Override(name string, dep dependency.Dependency) error { previous, ok := c.deps[name] if !ok { return c.Provide(name, dep) diff --git a/injector/container_test.go b/injector/container_test.go index c66484d..c812bfc 100644 --- a/injector/container_test.go +++ b/injector/container_test.go @@ -6,24 +6,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) -func TestContainer_EnableTestMode(t *testing.T) { - ic := New() - assert.False(t, ic.testMode) - - ic.TestMode() - - assert.True(t, ic.testMode) -} - func TestContainer_Flush(t *testing.T) { - depName := "test" - ic := New() - if err := ic.Provide(types.Symbol(depName), dependency.New(func() int { return 10 })); err != nil { + if err := ic.Provide(depName, dependency.New(func() int { return 10 })); err != nil { t.Error(err) return } @@ -35,8 +23,7 @@ func TestContainer_Flush(t *testing.T) { } func TestContainer_Remove(t *testing.T) { - depName := types.Symbol("test") - depName2 := types.Symbol("test2") + depName2 := "test2" c := New() @@ -66,8 +53,6 @@ func TestContainer_Remove(t *testing.T) { } func TestContainer_Override(t *testing.T) { - depName := types.Symbol("test") - c := New() if err := c.Provide(depName, dependency.NewSingleton(func() int { return 10 })); err != nil { diff --git a/injector/data_test.go b/injector/data_test.go deleted file mode 100644 index 0b600b4..0000000 --- a/injector/data_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package injector - -type todo struct { - db database -} - -func newTodo(db database) *todo { - return &todo{db: db} -} - -type database interface { - client() string -} - -type driver struct { - dbanme string -} - -func (d driver) client() string { - return d.dbanme -} - -var _ database = &driver{} - -func newDriver(dbanme string) *driver { - return &driver{dbanme: dbanme} -} - -type namer interface { - getName() string -} - -type ager interface { - getAge() int -} - -type driverer interface { - getDb() database -} - -type userer interface { - namer - ager - driverer -} - -type user struct { - name string - age int - db database -} - -func newUser(name string, age int) *user { - return &user{name: name, age: age} -} - -func newUserWithDriver(db database) *user { - return &user{db: db} -} - -func (u *user) getName() string { - return u.name -} - -func (u *user) getAge() int { - return u.age -} - -func (u *user) getDb() database { - return u.db -} diff --git a/injector/get.go b/injector/get.go index e9ae76f..87b176a 100644 --- a/injector/get.go +++ b/injector/get.go @@ -4,26 +4,25 @@ import ( "fmt" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) // Get returns a dependency instance and a possible build error by the associated name on that dependency. The instance -// type will depend on the dependency configuration, if it was marked as a singleton or not. If it was, the builder will -// try to return a previously created instance of that dependency instead of just create a new instance. -func (c *Container) Get(name types.Symbol) (any, error) { +// type will depend on the dependency configuration if it was marked as a singleton or not. If it was, the builder will +// try to return a previously created instance of that dependency instead of creating a new instance. +func (c *Container) Get(name string) (any, error) { dep, ok := c.deps[name] if !ok { - return nil, fmt.Errorf("inject: no provided dependency of name `%s`", name) + return dependency.Dependency{}, fmt.Errorf("inject: no provided dependency of name `%s`", name) } - if dep.IsSingleton() && !c.testMode { + if dep.IsSingleton() { return c.getSingleton(name, dep) } return c.getInstance(dep) } -func (c *Container) getSingleton(name types.Symbol, dep dependency.Dependency) (any, error) { +func (c *Container) getSingleton(name string, dep dependency.Dependency) (any, error) { val, ok := c.solvedDeps[name] if ok { return val, nil diff --git a/injector/get_test.go b/injector/get_test.go index 6031980..0b8ead3 100644 --- a/injector/get_test.go +++ b/injector/get_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) func TestContainer_Get(t *testing.T) { @@ -19,87 +19,41 @@ func TestContainer_Get(t *testing.T) { name string } - sym := types.Symbol("test") - - err := c.Provide(sym, dependency.New(func() test { + err := c.Provide(depName, dependency.New(func() test { return test{name: "test"} })) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - obj, errGet := c.Get(sym) - if errGet != nil { - t.Fatal(errGet) - } + obj, errGet := c.Get(depName) + require.NoError(t, errGet) tobj, ok := obj.(test) - if !ok { - t.Fatal("cant cast obj to test") - } + require.True(t, ok) assert.Equal(t, "test", tobj.name) }) - t.Run("get singleton instance in regular mode", func(t *testing.T) { + t.Run("get singleton instance", func(t *testing.T) { c := New() type test struct { number int } - sym := types.Symbol("test") - - err := c.Provide(sym, dependency.NewSingleton(func() test { + err := c.Provide(depName, dependency.NewSingleton(func() test { return test{number: rand.Int()} })) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - obj1, errGet := c.Get(sym) - if errGet != nil { - t.Fatal(errGet) - } + obj1, errGet := c.Get(depName) + require.NoError(t, errGet) - obj2, errGet2 := c.Get(sym) - if errGet2 != nil { - t.Fatal(errGet2) - } + obj2, errGet2 := c.Get(depName) + require.NoError(t, errGet2) fmt.Printf("obj1: %p\n", obj1) fmt.Printf("obj2: %p\n", obj2) assert.Equal(t, obj1.(test).number, obj2.(test).number) }) - - t.Run("get singleton instance on test mode", func(t *testing.T) { - c := New() - c.TestMode() - - type test struct { - number int - } - - sym := types.Symbol("test") - - err := c.Provide(sym, dependency.NewSingleton(func() test { - return test{number: rand.Int()} - })) - if err != nil { - t.Fatal(err) - } - - obj1, errGet := c.Get(sym) - if errGet != nil { - t.Fatal(errGet) - } - - obj2, errGet2 := c.Get(sym) - if errGet2 != nil { - t.Fatal(errGet2) - } - - assert.NotEqual(t, obj1.(test).number, obj2.(test).number) - }) } diff --git a/injector/invoke.go b/injector/invoke.go deleted file mode 100644 index f30e8f1..0000000 --- a/injector/invoke.go +++ /dev/null @@ -1,75 +0,0 @@ -package injector - -import ( - "fmt" - "reflect" - - "github.com/Drafteame/container/types" - "github.com/Drafteame/container/utils" -) - -// Invoke Is the entry point to execute dependency injection resolution. It calls an invoker function that can -// receive or not a struct that embeds inject.In struct as input, and return an error or not (any other return field or -// type will be ignored on resolution). When invoker is called it will resolve the dependency threes of each field from -// the previously provided resources on Container. -func (c *Container) Invoke(construct any) error { - if construct == nil { - return fmt.Errorf("inject: can't invoke nil constructor") - } - - ctype := reflect.TypeOf(construct) - - if ctype.Kind() != reflect.Func { - return fmt.Errorf("inject: can't invoke a non-function constructor") - } - - args, err := c.getInDeps(ctype) - if err != nil { - return err - } - - res := reflect.ValueOf(construct).Call(args) - - return getResponseError(ctype, res) -} - -// getResponseError It gets the type of the function that is being called. It checks if the function has an error as -// output parameter. If it does, it creates a new instance of `definitions.Error` and sets its value to the error -// returned by the function call (the index is calculated in step 2). Finally, it returns this error as an interface -// that can be casted from `definitions.Error` to `error`. -func getResponseError(ctype reflect.Type, res []reflect.Value) error { - index, hasErr := utils.WhereErrorOut(ctype) - if !hasErr { - return nil - } - - errInt := reflect.TypeOf(new(types.Error)).Elem() - err := reflect.New(errInt) - err.Elem().Set(res[index]) - - return *err.Interface().(*types.Error) -} - -// getInDeps It creates a slice of reflect.Value with the size of the number of input parameters. For each input -// parameter, it creates a new `reflect.Value` using `reflect.New`. Then it calls `buildInStruct` to build the struct -// and set its fields. If the type or the input struct is not a pointer, we need to get its value using `Elem()` method. -// We add this value to our slice of values and return it at the end. -func (c *Container) getInDeps(ctype reflect.Type) ([]reflect.Value, error) { - values := make([]reflect.Value, ctype.NumIn()) - - for i := 0; i < ctype.NumIn(); i++ { - newArg := reflect.New(ctype.In(i)) - - if err := types.BuildIn(c, newArg); err != nil { - return nil, err - } - - if ctype.In(i).Kind() != reflect.Ptr { - newArg = newArg.Elem() - } - - values[i] = newArg - } - - return values, nil -} diff --git a/injector/invoke_test.go b/injector/invoke_test.go deleted file mode 100644 index 2565ec3..0000000 --- a/injector/invoke_test.go +++ /dev/null @@ -1,436 +0,0 @@ -package injector - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" -) - -func TestContainer_Invoke(t *testing.T) { - t.Run("invoke no dependency function", func(t *testing.T) { - inject := New() - called := false - - invoker := func() { - called = true - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with dependencies", func(t *testing.T) { - const name = "John Smith" - const age = 21 - - inject := New() - called := false - depName := types.Symbol("test") - - type args struct { - types.In - UserService *user `inject:"name=test"` - } - - invoker := func(in args) { - if assert.NotNil(t, in.UserService) { - assert.Equal(t, name, in.UserService.getName()) - assert.Equal(t, age, in.UserService.getAge()) - } - - called = true - } - - userDep := dependency.New(newUser, name, age) - - if err := inject.Provide(depName, userDep); err != nil { - t.Error(err) - return - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with dependency injected to interface", func(t *testing.T) { - const name = "John Smith" - const age = 21 - const depName = "usersService" - - inject := New() - called := false - - type args struct { - types.In - UserService userer `inject:"name=usersService"` - } - - invoker := func(in args) { - if assert.NotNil(t, in.UserService) { - assert.Equal(t, name, in.UserService.getName()) - assert.Equal(t, age, in.UserService.getAge()) - } - - called = true - } - - userDep := dependency.New(newUser, name, age) - - if err := inject.Provide(depName, userDep); err != nil { - t.Error(err) - return - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with dependency and optional injection", func(t *testing.T) { - const name = "John Smith" - const age = 21 - - inject := New() - called := false - - userDepName := types.Symbol("usersService") - userDep := dependency.New(newUser, name, age) - if err := inject.Provide(userDepName, userDep); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserService namer `inject:"name=test,optional"` - } - - invoker := func(in args) { - assert.Nil(t, in.UserService) - - called = true - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with empty inject tag dependency", func(t *testing.T) { - const name = "John Smith" - const age = 21 - - inject := New() - - depName := types.Symbol("test") - userDep := dependency.New(newUser, name, age) - if err := inject.Provide(depName, userDep); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserService userer `inject:""` - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - expErr := errors.New("inject: missing name tag of inject dependency on field `UserService`") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke with nil invoker", func(t *testing.T) { - inject := New() - - err := inject.Invoke(nil) - - expErr := fmt.Errorf("inject: can't invoke nil constructor") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke with non-function invoker", func(t *testing.T) { - inject := New() - - err := inject.Invoke(10) - - expErr := fmt.Errorf("inject: can't invoke a non-function constructor") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke with non In embedded struct", func(t *testing.T) { - inject := New() - - type args struct{} - - invoker := func(args) {} - - err := inject.Invoke(invoker) - - expErr := fmt.Errorf("inject: struct doesn't embed `inject.In` struct") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke with invoker returning non error value", func(t *testing.T) { - inject := New() - - type args struct { - types.In - } - - called := false - - invoker := func(args) bool { - called = true - return called - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with invoker returning nil error value", func(t *testing.T) { - inject := New() - - type args struct { - types.In - } - - called := false - - invoker := func(args) error { - called = true - return nil - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with invoker returning nil error value", func(t *testing.T) { - inject := New() - - type args struct { - types.In - } - - called := false - expErr := fmt.Errorf("invoke with invoker returning nil error value") - - invoker := func(args) error { - called = true - return expErr - } - - err := inject.Invoke(invoker) - - assert.Error(t, err) - assert.Equal(t, expErr, err) - assert.True(t, called) - }) - - t.Run("invoke with error providing field with no tag", func(t *testing.T) { - inject := New() - - type some interface { - someMethod() - } - - type args struct { - types.In - UserService some - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - - expErr := fmt.Errorf("inject: missing name tag of inject dependency on field `UserService`") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke with error providing named dependency", func(t *testing.T) { - inject := New() - - type args struct { - types.In - UserService *user `inject:"name=usersService"` - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - - expErr := fmt.Errorf("inject: no provided dependency of name `usersService`") - - assert.Error(t, err) - assert.Equal(t, expErr, err) - }) - - t.Run("invoke error resolving dependency three", func(t *testing.T) { - inject := New() - - depName := types.Symbol("depName") - dep := dependency.New(func() (*user, error) { return nil, errors.New("some") }) - if err := inject.Provide(depName, dep); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserService *user `inject:"name=depName"` - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - - expErr := fmt.Errorf("inject: error building dependency instance: inject: error constructing `func() (*injector.user, error)`: some") - - assert.Error(t, err) - assert.Equal(t, expErr.Error(), err.Error()) - }) - - t.Run("invoke with shared dependency on multiple targets as singleton", func(t *testing.T) { - inject := New() - - const driverName = "some" - - sharedDepName := types.Symbol("driver") - sharedDep := dependency.NewSingleton(newDriver, driverName) - if err := inject.Provide(sharedDepName, sharedDep); err != nil { - t.Error(err) - return - } - - userRepoName := types.Symbol("userRepo") - usersRepo := dependency.New(newUserWithDriver, dependency.Inject(sharedDepName)) - if err := inject.Provide(userRepoName, usersRepo); err != nil { - t.Error(err) - return - } - - todoRepoName := types.Symbol("todoRepo") - todoRepo := dependency.New(newTodo, dependency.Inject(sharedDepName)) - if err := inject.Provide(todoRepoName, todoRepo); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserRepo *user `inject:"name=userRepo"` - TodoRepo *todo `inject:"name=todoRepo"` - } - - called := false - - invoker := func(in args) { - assert.Equal(t, driverName, in.UserRepo.getDb().client()) - assert.Equal(t, driverName, in.TodoRepo.db.client()) - assert.Same(t, in.TodoRepo.db, in.UserRepo.getDb()) - called = true - } - - err := inject.Invoke(invoker) - - assert.NoError(t, err) - assert.True(t, called) - }) - - t.Run("invoke with shared dependency that return error", func(t *testing.T) { - inject := New() - - const dbName = "main" - - errBuild := errors.New("some") - - sharedDepName := types.Symbol("driver") - sharedDep := dependency.NewSingleton(func(string) (*driver, error) { return nil, errBuild }, dbName) - if err := inject.Provide(sharedDepName, sharedDep); err != nil { - t.Error(err) - return - } - - usersRepoName := types.Symbol("usersRepo") - usersRepo := dependency.New(newUserWithDriver, dependency.Inject(sharedDepName)) - if err := inject.Provide(usersRepoName, usersRepo); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserRepo *user `inject:"name=usersRepo"` - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - expErr := errors.New("inject: error building dependency instance: inject: error resolving argument 0 for constructor func(injector.database) *injector.user: inject: error building dependency instance: inject: error constructing `func(string) (*injector.driver, error)`: some") - - assert.Error(t, err) - assert.Equal(t, expErr.Error(), err.Error()) - }) - - t.Run("invoke with shared dependency that does not exist", func(t *testing.T) { - inject := New() - - const dbName = "main" - - sharedDepName := types.Symbol("driver") - sharedDep := dependency.NewSingleton(newDriver, dbName) - if err := inject.Provide(sharedDepName, sharedDep); err != nil { - t.Error(err) - return - } - - usersRepoName := types.Symbol("usersRepo") - usersRepo := dependency.New(newUserWithDriver, dependency.Inject("algo")) - if err := inject.Provide(usersRepoName, usersRepo); err != nil { - t.Error(err) - return - } - - type args struct { - types.In - UserRepo *user `inject:"name=usersRepo"` - } - - invoker := func(args) {} - - err := inject.Invoke(invoker) - - expErr := errors.New("inject: error building dependency instance: inject: error resolving argument 0 for constructor func(injector.database) *injector.user: inject: no provided dependency of name `algo`") - - assert.Error(t, err) - assert.Equal(t, expErr.Error(), err.Error()) - }) -} diff --git a/injector/provide.go b/injector/provide.go index c22f3e3..dd807cb 100644 --- a/injector/provide.go +++ b/injector/provide.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" "github.com/Drafteame/container/utils" ) @@ -14,7 +13,7 @@ import ( // inject.get().Provide(dependency.New(callback, arg1, arg2), inject.As(new(someInterface))) // // This injection will be resolved and built on execution time when the `inject.get().Invoke(...)` method is called. -func (c *Container) Provide(name types.Symbol, dep dependency.Dependency) error { +func (c *Container) Provide(name string, dep dependency.Dependency) error { var err error if rt := utils.GetFirstReturnType(dep.Factory); rt == nil { @@ -29,16 +28,16 @@ func (c *Container) Provide(name types.Symbol, dep dependency.Dependency) error return nil } -// provide If the name option is not set, it returns the Container and nil. If the Container is nil, it creates a +// Provide If the name option is not set, it returns the Container and nil. If the Container is nil, it creates a // new one. It checks if there's already a dependency with that name in the Container and returns an error if so. It -// adds the dependency to the Container using its name as key and returns it along with nil (no error). -func (c *Container) provide(container map[types.Symbol]dependency.Dependency, name types.Symbol, dep dependency.Dependency) (map[types.Symbol]dependency.Dependency, error) { +// adds the dependency to the Container using its name as a key and returns it along with nil (no error). +func (c *Container) provide(container map[string]dependency.Dependency, name string, dep dependency.Dependency) (map[string]dependency.Dependency, error) { if name == "" { return container, fmt.Errorf("inject: dependency name cannot be empty") } if container == nil { - container = make(map[types.Symbol]dependency.Dependency) + container = make(map[string]dependency.Dependency) } if _, ok := container[name]; ok { diff --git a/injector/provide_test.go b/injector/provide_test.go index b270dd2..1ac3fef 100644 --- a/injector/provide_test.go +++ b/injector/provide_test.go @@ -8,9 +8,20 @@ import ( "github.com/stretchr/testify/assert" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) +const depName = "test" +const userDepName = "usersService" + +type user struct { + name string + age int +} + +func newUser(name string, age int) (*user, error) { + return &user{name: name, age: age}, nil +} + func TestContainer_Provide(t *testing.T) { t.Run("provide simple dependency", func(t *testing.T) { const name = "John Smith" @@ -18,7 +29,6 @@ func TestContainer_Provide(t *testing.T) { ic := New() - userDepName := types.Symbol("userDep") userDep := dependency.New(newUser, name, age) err := ic.Provide(userDepName, userDep) @@ -38,7 +48,6 @@ func TestContainer_Provide(t *testing.T) { ic := New() - userDepName := types.Symbol("usersService") userDep := dependency.New(newUser, name, age) if err := ic.Provide(userDepName, userDep); err != nil { @@ -57,7 +66,6 @@ func TestContainer_Provide(t *testing.T) { t.Run("provide dependency with no return value constructor", func(t *testing.T) { ic := New() - depName := types.Symbol("test") dep := dependency.New(func() {}) err := ic.Provide(depName, dep) @@ -68,10 +76,12 @@ func TestContainer_Provide(t *testing.T) { }) t.Run("provide singleton dependency", func(t *testing.T) { + const name = "John Smith" + const age = 21 + ic := New() - depName := types.Symbol("test") - dep := dependency.NewSingleton(newDriver, "test") + dep := dependency.NewSingleton(newUser, name, age) err := ic.Provide(depName, dep) @@ -86,7 +96,6 @@ func TestContainer_Provide(t *testing.T) { ic := New() - userDepName := types.Symbol("usersService") userDep := dependency.NewSingleton(newUser, name, age) if err := ic.Provide(userDepName, userDep); err != nil { t.Error(err) @@ -104,7 +113,6 @@ func TestContainer_Provide(t *testing.T) { t.Run("provide singleton dependency with no return value constructor", func(t *testing.T) { ic := New() - depName := types.Symbol("test") dep := dependency.NewSingleton(func() {}) err := ic.Provide(depName, dep) @@ -120,10 +128,9 @@ func TestContainer_Provide(t *testing.T) { ic := New() - userDepName := types.Symbol("") userDep := dependency.New(newUser, name, age) - err := ic.Provide(userDepName, userDep) + err := ic.Provide("", userDep) expErr := errors.New("inject: dependency name cannot be empty") @@ -137,7 +144,6 @@ func TestContainer_Provide(t *testing.T) { ic := &Container{} - userDepName := types.Symbol("some") userDep := dependency.New(newUser, name, age) err := ic.Provide(userDepName, userDep) diff --git a/magefiles/go.mod b/magefiles/go.mod deleted file mode 100644 index 7131399..0000000 --- a/magefiles/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module magefiles - -go 1.20 - -require github.com/magefile/mage v1.15.0 diff --git a/magefiles/go.sum b/magefiles/go.sum deleted file mode 100644 index 4ee1b87..0000000 --- a/magefiles/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= diff --git a/magefiles/mage.format.go b/magefiles/mage.format.go deleted file mode 100644 index 55aeee5..0000000 --- a/magefiles/mage.format.go +++ /dev/null @@ -1,50 +0,0 @@ -// nolint -package main - -import ( - "fmt" - - "github.com/magefile/mage/mg" - "github.com/magefile/mage/sh" -) - -// Vet execute go vet test over the code. -func Vet() error { - command := "go" - args := []string{"vet", "./..."} - - out, err := sh.Output(command, args...) - - if out != "" { - fmt.Println(out) - } - - return err -} - -// Lint Runs revive checks over the code. -func Lint() error { - mg.Deps(Vet) - - command := "revive" - args := []string{"-config=revive.toml", "-formatter=friendly", "-exclude=magefiles/...", "./..."} - - out, err := sh.Output(command, args...) - - if out != "" { - fmt.Println(out) - } - - return err -} - -// Format Runs gofmt over the code. -func Format() error { - out, err := sh.Output("goimports-reviser", "-format", "./...") - - if out != "" { - fmt.Println(out) - } - - return err -} diff --git a/magefiles/mage.install.go b/magefiles/mage.install.go deleted file mode 100644 index 276543f..0000000 --- a/magefiles/mage.install.go +++ /dev/null @@ -1,24 +0,0 @@ -// nolint -package main - -import ( - "fmt" - - "github.com/magefile/mage/sh" -) - -// PreCommit Install pre-commit hooks -func PreCommit() { - preCommit := sh.OutCmd("pre-commit") - - out, _ := preCommit("install", "--hook-type", "commit-msg") - fmt.Println(out) - - out, _ = preCommit("install") - fmt.Println(out) -} - -// Install install dependencies -func Install() error { - return sh.Run("go", "mod", "download") -} diff --git a/magefiles/mage.test.go b/magefiles/mage.test.go deleted file mode 100644 index 3b7a1fd..0000000 --- a/magefiles/mage.test.go +++ /dev/null @@ -1,35 +0,0 @@ -// nolint -package main - -import ( - "fmt" - - "github.com/magefile/mage/mg" - "github.com/magefile/mage/sh" -) - -// Generate Execute automatic generation of code. -func Generate() error { - return sh.Run("go", "generate", "./...") -} - -// Test Execute unit testing. -func Test() error { - out, err := sh.Output("go", "test", "-v", "-race", "./...", "-covermode=atomic", "-coverprofile=coverage.out") - if out != "" { - fmt.Println(out) - } - - return err -} - -// CoverHtml Show HTML coverage output. -func CoverHtml() error { - mg.Deps(Test) - return sh.Run("go", "tool", "cover", "-html", "coverage.out") -} - -func CoverXml() error { - mg.Deps(Test) - return sh.Run("go", "tool", "cover", "-o", "coverage.xml") -} diff --git a/options.go b/options.go new file mode 100644 index 0000000..1b35975 --- /dev/null +++ b/options.go @@ -0,0 +1,32 @@ +package container + +type options struct { + args []any + container Container +} + +type Option func(*options) + +func WithContainer(c Container) Option { + return func(o *options) { + o.container = c + } +} + +func WithArgs(args ...any) Option { + return func(o *options) { + o.args = args + } +} + +func buildOptions(opts ...Option) options { + depOpts := options{ + container: get(), + } + + for _, opt := range opts { + opt(&depOpts) + } + + return depOpts +} diff --git a/register.go b/register.go index a58cb94..d2635f9 100644 --- a/register.go +++ b/register.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/Drafteame/container/dependency" - "github.com/Drafteame/container/types" ) var ( @@ -16,35 +15,68 @@ var ( // // This injection will be resolved and built on execution time when the `inject.Invoke(...)` or `inject.Get(name)` // methods are called. -func Register[T symbolName](name T, factory any, args ...any) error { - return registerDep(types.Symbol(name), false, factory, args...) +func Register(name string, factory any, opts ...Option) error { + return registerDep(name, false, factory, opts...) +} + +// MustRegister registers a dependency with the given name and factory. Panics if registration fails. +func MustRegister(name string, factory any, opts ...Option) { + err := registerDep(name, false, factory, opts...) + if err != nil { + panic(err) + } } // Singleton It adds a new injection dependency to the container, getting the first result type of the constructor to // associate the constructor on the injection dependency threes as a singleton instance. // -// This function also receive dependency arguments as variadic in case the factory were a function instead of a +// This function also receives dependency arguments as variadic in case the factory was a function instead of a // dependency.Dependency. -func Singleton[T symbolName](name T, factory any, args ...any) error { - return registerDep(types.Symbol(name), true, factory, args...) +func Singleton(name string, factory any, opts ...Option) error { + return registerDep(name, true, factory, opts...) +} + +// MustSingleton registers a dependency with the given name and factory as a singleton. Panics if registration fails. +func MustSingleton(name string, factory any, opts ...Option) { + err := registerDep(name, true, factory, opts...) + if err != nil { + panic(err) + } } // Override Set a new dependency that replaces the old one to change behavior on runtime. -// WARNING: This function will remove a specific factory and its solve dependency from the container. Do not use -// this method on production, and just use it on testing purposes. -func Override[T symbolName](name T, factory any, args ...any) error { - return get().Override(types.Symbol(name), dependency.New(factory, args...)) +// WARNING: This function will remove a specific factory and its solved dependency from the container. Do not use +// this method on production and use it for testing purposes. +func Override(name string, factory any, opts ...Option) error { + depOpts := buildOptions(opts...) + c := depOpts.container + + return c.Override(name, dependency.New(factory, depOpts.args...)) +} + +// MustOverride Set a new dependency that replaces the old one to change behavior on runtime. +// WARNING: This function will remove a specific factory and its solved dependency from the container. Do not use +// this method on production and use it for testing purposes. +func MustOverride(name string, factory any, opts ...Option) { + err := Override(name, factory, opts...) + if err != nil { + panic(err) + } } -// Inject is a Wrapper ver the dependency.Inject function to generify string symbol name. -func Inject[T symbolName](name T) dependency.Injectable { - return dependency.Inject(types.Symbol(name)) +// Inject is a Wrapper over the dependency.Inject function to generify string symbol name. +func Inject(name string) dependency.Injectable { + return dependency.Inject(name) } -func registerDep(name types.Symbol, singleton bool, factory any, args ...any) error { +func registerDep(name string, singleton bool, factory any, opts ...Option) error { + depOpts := buildOptions(opts...) + + c := depOpts.container + if dep, ok := factory.(dependency.Dependency); ok { dep.Singleton = singleton - return get().Provide(name, dep) + return c.Provide(name, dep) } if _, ok := factory.(dependency.Builder); ok { @@ -52,8 +84,8 @@ func registerDep(name types.Symbol, singleton bool, factory any, args ...any) er } if singleton { - return get().Provide(name, dependency.NewSingleton(factory, args...)) + return c.Provide(name, dependency.NewSingleton(factory, depOpts.args...)) } - return get().Provide(name, dependency.New(factory, args...)) + return c.Provide(name, dependency.New(factory, depOpts.args...)) } diff --git a/register_test.go b/register_test.go new file mode 100644 index 0000000..f75cbbb --- /dev/null +++ b/register_test.go @@ -0,0 +1,134 @@ +package container + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/Drafteame/container/dependency" +) + +func TestMustRegister(t *testing.T) { + t.Run("should register a raw factory instance without panic", func(t *testing.T) { + c := New() + + defer func() { + if r := recover(); r != nil { + t.Errorf("MustRegister panicked unexpectedly: %v", r) + } + }() + + MustRegister(factoryName, newUser, WithArgs(name, age), WithContainer(c)) + + // Verify registration was successful by getting the instance + instance, err := Get[*user](factoryName, WithContainer(c)) + assert.NoError(t, err) + assert.NotNil(t, instance) + assert.Equal(t, name, instance.name) + assert.Equal(t, age, instance.age) + }) + + t.Run("should panic when registration fails", func(t *testing.T) { + c := New() + + defer func() { + r := recover() + assert.NotNil(t, r) + + expErr := fmt.Errorf("factory parameter should be a function or a dependency.Dependency instance") + assert.Equal(t, expErr.Error(), fmt.Sprintf("%v", r)) + }() + + // This should panic because dependency.Injectable{} is not a valid factory + MustRegister("name", dependency.Injectable{}, WithContainer(c)) + }) +} + +func TestMustSingleton(t *testing.T) { + t.Run("should register a singleton without panic", func(t *testing.T) { + c := New() + + defer func() { + if r := recover(); r != nil { + t.Errorf("MustSingleton panicked unexpectedly: %v", r) + } + }() + + MustSingleton(factoryName, newUser, WithArgs(name, age), WithContainer(c)) + + // Verify registration was successful by getting the instance + instance1, err := Get[*user](factoryName, WithContainer(c)) + assert.NoError(t, err) + assert.NotNil(t, instance1) + + // Get a second instance to verify it's a singleton (same instance) + instance2, err := Get[*user](factoryName, WithContainer(c)) + assert.NoError(t, err) + assert.NotNil(t, instance2) + + // Both instances should be the same object (pointer equality) + assert.Same(t, instance1, instance2) + }) + + t.Run("should panic when registration fails", func(t *testing.T) { + c := New() + + defer func() { + r := recover() + assert.NotNil(t, r) + + expErr := fmt.Errorf("factory parameter should be a function or a dependency.Dependency instance") + assert.Equal(t, expErr.Error(), fmt.Sprintf("%v", r)) + }() + + // This should panic because dependency.Injectable{} is not a valid factory + MustSingleton("name", dependency.Injectable{}, WithContainer(c)) + }) +} + +func TestMustOverride(t *testing.T) { + t.Run("should override a dependency without panic", func(t *testing.T) { + c := New() + + // First, register a dependency + err := Register(depName, func() int { return 10 }, WithContainer(c)) + require.NoError(t, err) + + // Verify initial value + v, err := Get[int](depName, WithContainer(c)) + assert.NoError(t, err) + assert.Equal(t, 10, v) + + // Now override it without a panic + defer func() { + if r := recover(); r != nil { + t.Errorf("MustOverride panicked unexpectedly: %v", r) + } + }() + + MustOverride(depName, func() int { return 20 }, WithContainer(c)) + + // Verify the value was overridden + v, err = Get[int](depName, WithContainer(c)) + assert.NoError(t, err) + assert.Equal(t, 20, v) + }) + + t.Run("should panic when override fails with invalid factory", func(t *testing.T) { + c := New() + + defer func() { + r := recover() + assert.NotNil(t, r) + + // The error message will be about the factory not returning a value + expErr := fmt.Errorf("inject: dependency factory should return at least one return type: dependency.Dependency{Factory: func(), Args: []}") + assert.Equal(t, expErr.Error(), fmt.Sprintf("%v", r)) + }() + + // This should panic because the factory doesn't return a value + MustOverride("test_override", func() {}, WithContainer(c)) + }) +} diff --git a/types/error.go b/types/error.go deleted file mode 100644 index 9d75c7d..0000000 --- a/types/error.go +++ /dev/null @@ -1,3 +0,0 @@ -package types - -type Error error diff --git a/types/in.go b/types/in.go deleted file mode 100644 index 1f98cb5..0000000 --- a/types/in.go +++ /dev/null @@ -1,168 +0,0 @@ -package types - -import ( - "fmt" - "reflect" - "strings" - - "github.com/Drafteame/container/utils" -) - -const ( - tag = "inject" - nameOption = "name" - optionalOption = "optional" -) - -type Container interface { - Get(name Symbol) (any, error) -} - -// In is a struct that should be embedded to other struct to denote that is a valid input for an invoker function and -// his fields should be filled from the dependency container threes. -type In struct{} - -// injectInField is the configuration that each In struct fields should follow to be filled. -type injectInField struct { - fieldName string - injectName Symbol - optional bool - container Container -} - -func BuildIn(cont Container, in reflect.Value) error { - if !utils.EmbedsType(in.Type(), reflect.TypeOf(In{})) { - return fmt.Errorf("inject: struct doesn't embed `inject.In` struct") - } - - itype := in.Type() - if itype.Kind() == reflect.Ptr { - itype = itype.Elem() - } - - nfields := itype.NumField() - injectFields := make([]injectInField, 0) - - for i := 0; i < nfields; i++ { - if itype.Field(i).Anonymous { - continue - } - - injectField, err := buildInjectInField(itype.Field(i)) - if err != nil { - return err - } - - injectField.container = cont - - injectFields = append(injectFields, injectField) - } - - return fillInStruct(cont, in, injectFields) -} - -// fillInStruct It iterates over the `conf` array. For each element in the array, it calls a function based on the value -// of `inject.source`. The functions called are either `fillInStructFromDeps` or `fillInStructFromNamedDeps`. Both -// functions return an error if something goes wrong, and this error is returned by the caller (`fillInStruct`). If no -// errors occur, then nil is returned. -func fillInStruct(cont Container, in reflect.Value, conf []injectInField) error { - for _, inject := range conf { - if err := fillStructFieldFromBuilder(cont, in, inject); err != nil { - return err - } - } - - return nil -} - -// fillStructFieldFromBuilder It checks if the dependency exists. If it doesn't exist, it returns an error. It builds the -// dependency using `builder`. It sets the field of the struct with name `conf.fieldName` to be equal to `out`. Returns -// nil (no error). -func fillStructFieldFromBuilder(cont Container, in reflect.Value, conf injectInField) error { - val, err := cont.Get(conf.injectName) - if err != nil { - if conf.optional { - return nil - } - - return err - } - - invalue := in - - if invalue.Kind() == reflect.Ptr { - invalue = invalue.Elem() - } - - field := invalue.FieldByName(conf.fieldName) - - field.Set(reflect.ValueOf(val)) - return nil -} - -// buildInjectInField We get the tags of the field. If there is a `name` tag, we set the source to `sourceNamedDeps`. If -// there is an `optional` tag, we set it to true. We create a new injectInField struct and return it. -func buildInjectInField(field reflect.StructField) (injectInField, error) { - ftags := getFieldTags(field) - injectName := "" - optional := false - - val, ok := ftags[nameOption] - - if !ok { - return injectInField{}, fmt.Errorf("inject: missing name tag of inject dependency on field `%s`", field.Name) - } - - injectName = val - - if _, ok := ftags[optionalOption]; ok { - optional = true - } - - inject := injectInField{ - fieldName: field.Name, - injectName: Symbol(injectName), - optional: optional, - } - - return inject, nil -} - -// getFieldTags It gets the tag value from the field. If there is no tag, it returns an empty map. It splits the tag by -// commas and trims spaces from each part of the split result. It creates a map to store all tags and their values (if -// any). For each part of the split result: -// 1. It splits again by equal sign (`=`) and trims spaces from each part of this second split result too; if there is -// no equal sign, it will be treated as an empty string for that side of the split operation; so `"a=b"` will be -// splitted into `["a", "b"]`, but `"a="` will be splitted into `["a", ""]`. -// 2. The first element in this second split operation is considered to be a key for our map; if it's an empty string, -// we skip this iteration because we don't want to add keys with empty names to our map; otherwise, we add it as a -// key in our map with its value being either. -func getFieldTags(field reflect.StructField) map[string]string { - value, exists := field.Tag.Lookup(tag) - if !exists { - return map[string]string{} - } - - tags := strings.Split(value, ",") - - tagValues := make(map[string]string) - - for _, tagValue := range tags { - tagValue = strings.TrimSpace(tagValue) - aux := strings.Split(tagValue, "=") - - tagOptionName := strings.TrimSpace(aux[0]) - - if tagOptionName == "" { - continue - } - - if len(aux) > 1 { - tagValues[tagOptionName] = strings.TrimSpace(aux[1]) - } else { - tagValues[tagOptionName] = "" - } - } - - return tagValues -} diff --git a/types/symbol.go b/types/symbol.go deleted file mode 100644 index 64ccc68..0000000 --- a/types/symbol.go +++ /dev/null @@ -1,4 +0,0 @@ -package types - -// Symbol is used to replace name of dependencies when are being registered -type Symbol string diff --git a/utils/types.go b/utils/types.go index 6abb79b..6aca3e6 100644 --- a/utils/types.go +++ b/utils/types.go @@ -15,30 +15,3 @@ func GetFirstReturnType(construct any) reflect.Type { return ctype.Out(0) } - -// EmbedsType checks that the provided `elem` interface embeds the provided type `e` directly. If it does, return true, -// otherwise return false. -func EmbedsType(elem any, e reflect.Type) bool { - if elem == nil { - return false - } - - etype, ok := elem.(reflect.Type) - if !ok { - etype = reflect.TypeOf(elem) - } - - if etype.Kind() == reflect.Ptr { - etype = etype.Elem() - } - - for i := 0; i < etype.NumField(); i++ { - field := etype.Field(i) - - if field.Anonymous && e == field.Type { - return true - } - } - - return false -} diff --git a/utils/types_test.go b/utils/types_test.go index f901d64..5040aec 100644 --- a/utils/types_test.go +++ b/utils/types_test.go @@ -40,64 +40,3 @@ func TestGetFirstReturnType(t *testing.T) { assert.Nil(t, tfun) }) } - -// nolint -func TestEmbedsType(t *testing.T) { - type in struct{} - type out struct{} - - t.Run("struct embeds specified type", func(t *testing.T) { - type some struct { - in - } - - embed := EmbedsType(some{}, reflect.TypeOf(in{})) - - assert.True(t, embed) - }) - - t.Run("struct embeds specified type with reflected type as input", func(t *testing.T) { - type some struct { - in - } - - embed := EmbedsType(reflect.TypeOf(some{}), reflect.TypeOf(in{})) - - assert.True(t, embed) - }) - - t.Run("struct embeds specified type with pointer as input", func(t *testing.T) { - type some struct { - in - } - - embed := EmbedsType(&some{}, reflect.TypeOf(in{})) - - assert.True(t, embed) - }) - - t.Run("struct embeds specified type with multiple embedded types", func(t *testing.T) { - type some struct { - out - in - } - - embed := EmbedsType(some{}, reflect.TypeOf(in{})) - - assert.True(t, embed) - }) - - t.Run("check embed from nil input", func(t *testing.T) { - embed := EmbedsType(nil, reflect.TypeOf(in{})) - - assert.False(t, embed) - }) - - t.Run("struct do not embed specified type", func(t *testing.T) { - type some struct{} - - embed := EmbedsType(some{}, reflect.TypeOf(in{})) - - assert.False(t, embed) - }) -}