diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..086e5bb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + assignees: + - mehdihadeli \ No newline at end of file diff --git a/.github/multi-labeler.yml b/.github/multi-labeler.yml new file mode 100644 index 0000000..f4a4e21 --- /dev/null +++ b/.github/multi-labeler.yml @@ -0,0 +1,87 @@ +# https://github.com/fuxingloh/multi-labeler +# https://stackoverflow.com/questions/58899999/regexp-to-match-conventional-commit-syntax +version: v1 + +labels: + - label: "feature" + matcher: + title: '^(feat)(\([a-z ]+\))?: .' + commits: '^(feat)(\([a-z ]+\))?: .' + branch: '(feat)(\([a-z ]+\))?\/.' + + - label: "bug" + matcher: + title: '^(fix)(\([a-z ]+\))?: .' + commits: '^(fix)(\([a-z ]+\))?: .' + branch: '(fix)(\([a-z ]+\))?\/.' + + - label: "chore" + matcher: + title: '^(chore)(\([a-z ]+\))?: .' + commits: '^(chore)(\([a-z ]+\))?: .' + branch: '(chore)(\([a-z ]+\))?\/.' + + - label: "documentation" + matcher: + title: '^(docs)(\([a-z ]+\))?: .' + commits: '^(docs)(\([a-z ]+\))?: .' + branch: '(docs)(\([a-z ]+\))?\/.' + + - label: "devops" + matcher: + title: '^(ci)(\([a-z ]+\))?: .' + commits: '^(ci)(\([a-z ]+\))?: .' + branch: '(ci)(\([a-z ]+\))?\/.' + + - label: "ci/cd" + matcher: + title: '^(ci)(\([a-z ]+\))?: .' + commits: '^(ci)(\([a-z ]+\))?: .' + branch: '(ci)(\([a-z ]+\))?\/.' + + - label: "enhancement" + matcher: + title: '^(refactor)(\([a-z ]+\))?: .' + commits: '^(refactor)(\([a-z ]+\))?: .' + branch: '(refactor)(\([a-z ]+\))?\/.' + + - label: "formatting" + matcher: + title: '^(style)(\([a-z ]+\))?: .' + commits: '^(style)(\([a-z ]+\))?: .' + branch: '(style)(\([a-z ]+\))?\/.' + + - label: "performance" + matcher: + title: '^(perf)(\([a-z ]+\))?: .' + commits: '^(perf)(\([a-z ]+\))?: .' + branch: '(perf)(\([a-z ]+\))?\/.' + + - label: "build" + matcher: + title: '^(build)(\([a-z ]+\))?: .' + commits: '^(build)(\([a-z ]+\))?: .' + branch: '(build)(\([a-z ]+\))?\/.' + + - label: "test" + matcher: + title: '^(test)(\([a-z ]+\))?: .' + commits: '^(test)(\([a-z ]+\))?: .' + branch: '(test)(\([a-z ]+\))?\/.' + + - label: "dependencies" + matcher: + title: '^build\(deps\): .' + commits: '^build\(deps\): .' + + - label: "minor" + matcher: + title: '^(feat)(\([a-z ]+\))?: .' + commits: '^(feat)(\([a-z ]+\))?: .' + branch: '(feat)(\([a-z ]+\))?\/.' + + - label: "patch" + matcher: + title: '^(fix)(\([a-z ]+\))?: .' + commits: '^(fix)(\([a-z ]+\))?: .' + branch: '(fix)(\([a-z ]+\))?\/.' \ No newline at end of file diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml deleted file mode 100644 index 4e0dd3b..0000000 --- a/.github/pr-labeler.yml +++ /dev/null @@ -1,5 +0,0 @@ -# https://github.com/TimonVS/pr-labeler-action -feature: ['feature/*', 'feat/*', 'feat:*'] -fix: ['fix/*','fix:*'] -chore :hammer:: chore/* -misc: '*' \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..848fe93 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,39 @@ + + +## What does this PR do? + + + +## Why is it important? + + + +## Related issues + + +- + + + + \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index ba00db4..d372adf 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -4,25 +4,34 @@ # This release drafter follows the conventions # from https://keepachangelog.com +# https://github.com/release-drafter/release-drafter/issues/551 +# https://github.com/release-drafter/release-drafter/pull/1013 +# https://github.com/release-drafter/release-drafter/issues/139 +# https://github.com/atk4/data/blob/develop/.github/release-drafter.yml name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' template: | ## What Changed ๐Ÿ‘€ - $CHANGES **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION categories: - title: ๐Ÿš€ Features labels: - feature + - title: โ™ป๏ธ Enhancement + labels: - enhancement + - refactor - title: ๐Ÿ› Bug Fixes labels: - fix - bug - - title: โš ๏ธ Changes + - title: ๐Ÿ‘ท CI + labels: + - ci + - title: โš ๏ธ Breaking Changes labels: - - changed + - breaking-changes - title: โ›”๏ธ Deprecated labels: - deprecated @@ -35,7 +44,7 @@ categories: - title: ๐Ÿ“„ Documentation labels: - docs - - documentation + - documentation - title: ๐Ÿงฉ Dependency Updates labels: - deps @@ -44,14 +53,56 @@ categories: label: 'chore' - title: ๐Ÿงบ Miscellaneous #Everything except ABAP label: misc - collapse-after: 10 + - title: ๐Ÿšฉ Other changes +## putting no labels pr to `Other Changes` category with no label - https://github.com/release-drafter/release-drafter/issues/139#issuecomment-480473934 + + +# https://www.trywilco.com/post/wilco-ci-cd-github-heroku +# https://github.com/release-drafter/release-drafter#autolabeler +# https://github.com/fuxingloh/multi-labeler + +# Using regex for defining rules - https://regexr.com/ +# https://stackoverflow.com/questions/58899999/regexp-to-match-conventional-commit-syntax +autolabeler: + - label: 'chore' + branch: + - '(chore)(\([a-z ]+\))?\/.' + title: + - '^(chore)(\([a-z ]+\))?: .' + - label: 'bug' + branch: + - '(fix)(\([a-z ]+\))?\/.' + title: + - '^(fix)(\([a-z ]+\))?: .' + - label: 'feature' + branch: + - '(feat)(\([a-z ]+\))?\/.' + title: + - '^(feat)(\([a-z ]+\))?: .' + - label: 'ci/cd' + branch: + - '(ci)(\([a-z ]+\))?\/.' + title: + - '^(ci)(\([a-z ]+\))?: .' + - label: 'minor' + branch: + - '(feat)(\([a-z ]+\))?\/.' + title: + - '^(feat)(\([a-z ]+\))?: .' + - label: 'patch' + branch: + - '(fix)(\([a-z ]+\))?\/.' + - '(ci)(\([a-z ]+\))?\/.' + title: + - '^(fix)(\([a-z ]+\))?: .' + - '^(ci)(\([a-z ]+\))?: .' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. version-resolver: major: labels: - - major + - breaking-changes minor: labels: - minor @@ -59,6 +110,6 @@ version-resolver: labels: - patch default: patch - + exclude-labels: - - skip-changelog \ No newline at end of file + - skip-changelog diff --git a/.github/release.yml b/.github/release.yml index 931847b..b209cf1 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,23 +1,47 @@ # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes # https://github.com/bcoe/conventional-release-labels # https://dev.to/github/how-to-automatically-generate-release-notes-for-your-project-2ng8 +# https://www.conventionalcommits.org/en/v1.0.0/ + +# github release pre-defined template changelog: exclude: labels: - ignore-for-release categories: - - title: Features + - title: ๐Ÿš€ Features labels: - feature - - title: Fixes + - title: ๐Ÿ› Bug Fixes labels: - - bug - fix - - enhancement - - title: Enhancement + - bug + - title: โ™ป๏ธ Changes labels: - - refactor + - changed - enhancement + - refactor + - title: โ›”๏ธ Deprecated + labels: + - deprecated + - title: ๐Ÿ—‘ Removed + labels: + - removed + - title: ๐Ÿ” Security + labels: + - security + - title: ๐Ÿ“„ Documentation + labels: + - docs + - documentation + - title: ๐Ÿงฉ Dependency Updates + labels: + - deps + - dependencies + - title: ๐Ÿงฐ Maintenance + label: 'chore' + - title: ๐Ÿงบ Miscellaneous #Everything except ABAP + label: misc - title: Other Changes labels: - - "*" \ No newline at end of file + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b43936..b83129e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: [ "main, dev" ] pull_request: - branches: [ "main" ] + branches: [ "main, dev" ] jobs: diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..21670e4 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,53 @@ +# https://github.com/amannn/action-semantic-pull-request +# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/configuring-commit-squashing-for-pull-requests +# Linting workflow: https://github.com/rhysd/actionlint + +name: Conventional Commits + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - labeled + - unlabeled + - edited + - ready_for_review + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + conventional-commits: + runs-on: ubuntu-latest + steps: + - name: Check Pull Request Title Conventional Commits + uses: amannn/action-semantic-pull-request@v5 + if: always() + id: check-pull-request-title-conventional-commits + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Comments the error message from the above lint_pr_title action + - if: ${{ always() && steps.check-pull-request-title-conventional-commits.outputs.error_message != null}} + name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + message: | + We require all PRs to follow [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). + More details ๐Ÿ‘‡๐Ÿผ + ``` + ${{ steps.check-pull-request-title-conventional-commits.outputs.error_message}} + ``` + # deletes the error comment if the title is correct + - if: ${{ steps.check-pull-request-title-conventional-commits.outputs.error_message == null }} + name: delete the comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr-title-lint-error + delete: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/conventional-release-labels.yml b/.github/workflows/conventional-release-labels.yml deleted file mode 100644 index e8a5d94..0000000 --- a/.github/workflows/conventional-release-labels.yml +++ /dev/null @@ -1,14 +0,0 @@ -#https://dev.to/github/how-to-automatically-generate-release-notes-for-your-project-2ng8 -#https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes -#https://github.com/bcoe/conventional-release-labels - -# add label to pul request based on convectional commit -on: - pull_request_target: - types: [ opened, edited ] -name: conventional-release-labels -jobs: - label: - runs-on: ubuntu-latest - steps: - - uses: bcoe/conventional-release-labels@v1 \ No newline at end of file diff --git a/.github/workflows/first-interaction.yml b/.github/workflows/first-interaction.yml new file mode 100644 index 0000000..9e9db9e --- /dev/null +++ b/.github/workflows/first-interaction.yml @@ -0,0 +1,19 @@ +name: First interaction +on: + pull_request_target: + types: + - opened + issues: + types: + - opened + +jobs: + first-interaction: + runs-on: ubuntu-latest + name: First interaction + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Welcome to ecommerece-microservice. Thank you ${{ github.event.pull_request.user.login }} for reporting your first issue. Please check out our [contributor guide](https://github.com/mehdihadeli/ecommerce-microservices/blob/develop/CONTRIBUTION.md).' + pr-message: 'Thank you ${{ github.event.pull_request.user.login }} for your first pull request to ecommerece-microservice repository. Please check out our [contributors guide](https://github.com/mehdihadeli/ecommerce-microservices/blob/develop/CONTRIBUTION.md).' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..8711c92 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,40 @@ +name: "Pull Request Labeler" + +on: + pull_request: + types: [ opened, edited, synchronize, ready_for_review ] + +jobs: + auto-labeling: + runs-on: ubuntu-latest + permissions: + # Setting up permissions in the workflow to limit the scope of what it can do. Optional! + contents: read + pull-requests: write + statuses: write + checks: write + + steps: + - uses: fuxingloh/multi-labeler@v1 + name: conventional-commits-pull-request-labeler + with: + github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}' + config-path: .github/multi-labeler.yml # optional, default to '.github/labeler.yml' + + # - uses: release-drafter/release-drafter@v5 + # name: release-drafter auto labeler + # with: + # config-name: release-drafter.yml + # disable-releaser: true # only run auto-labeler for PRs + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # https://docs.github.com/en/actions/using-workflows/about-workflows#creating-dependent-jobs + # https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow + - name: check-conventional-commits-labels + uses: docker://agilepathway/pull-request-label-checker:latest + if: success() + with: + any_of : feature,bug,enhancement,refactor,deprecated,security,documentation,build,ci/cd,chore,performance,formatting,dependencies + repo_token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml deleted file mode 100644 index 195dc98..0000000 --- a/.github/workflows/pr-labeler.yml +++ /dev/null @@ -1,20 +0,0 @@ -# https://github.com/TimonVS/pr-labeler-action -name: PR Labeler -on: - pull_request: - types: [opened] - -permissions: - contents: read - -jobs: - pr-labeler: - permissions: - contents: read # for TimonVS/pr-labeler-action to read config file - pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR - runs-on: ubuntu-latest - steps: - - uses: TimonVS/pr-labeler-action@v4 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/pr-labeler.yml # optional, .github/pr-labeler.yml is the default value \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 09bf5a8..98a7f25 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,19 +1,34 @@ -# # https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ -# # https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually -# name: Release Drafter +# https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ +# https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually +name: Release Drafter -# on: -# push: -# branches: -# - main +on: + push: + branches: + - main -# jobs: -# update_release_draft: -# name: Release drafter -# runs-on: ubuntu-latest +jobs: + update_release_draft: + name: Release drafter + runs-on: ubuntu-latest -# steps: -# - name: Update Release Draft -# uses: release-drafter/release-drafter@v5 -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + # write permission is required to create a github release + contents: write + + steps: + - name: Update Release Draft + uses: release-drafter/release-drafter@v5 + id: semantic + with: + config-name: release-drafter.yml + ## Default versioning just increase the path version as default. but the can use minor, patch and breaking-changes labels to apply semver + # version: 1.29.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Do something when a new release published + # run: | + # echo ${{ $RESOLVED_VERSION steps.semantic.outputs }} + + \ No newline at end of file diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml deleted file mode 100644 index aa7ea02..0000000 --- a/.github/workflows/release-please.yml +++ /dev/null @@ -1,15 +0,0 @@ -# https://github.com/google-github-actions/release-please-action#how-release-please-works -# https://www.conventionalcommits.org/en/v1.0.0/ -on: - push: - branches: - - main -name: release-please -jobs: - release-please: - runs-on: ubuntu-latest - steps: - - uses: google-github-actions/release-please-action@v3 - with: - release-type: go - package-name: release-please-action diff --git a/mediatr.go b/mediatr.go index db93573..76b846d 100644 --- a/mediatr.go +++ b/mediatr.go @@ -20,18 +20,21 @@ type RequestHandler[TRequest any, TResponse any] interface { Handle(ctx context.Context, request TRequest) (TResponse, error) } +type RequestHandlerFactory[TRequest any, TResponse any] func() RequestHandler[TRequest, TResponse] + type NotificationHandler[TNotification any] interface { Handle(ctx context.Context, notification TNotification) error } +type NotificationHandlerFactory[TNotification any] func() NotificationHandler[TNotification] + var requestHandlersRegistrations = map[reflect.Type]interface{}{} var notificationHandlersRegistrations = map[reflect.Type][]interface{}{} var pipelineBehaviours []interface{} = []interface{}{} type Unit struct{} -// RegisterRequestHandler register the request handler to mediatr registry. -func RegisterRequestHandler[TRequest any, TResponse any](handler RequestHandler[TRequest, TResponse]) error { +func registerRequestHandler[TRequest any, TResponse any](handler any) error { var request TRequest requestType := reflect.TypeOf(request) @@ -46,6 +49,16 @@ func RegisterRequestHandler[TRequest any, TResponse any](handler RequestHandler[ return nil } +// RegisterRequestHandler register the request handler to mediatr registry. +func RegisterRequestHandler[TRequest any, TResponse any](handler RequestHandler[TRequest, TResponse]) error { + return registerRequestHandler[TRequest, TResponse](handler) +} + +// RegisterRequestHandlerFactory register the request handler factory to mediatr registry. +func RegisterRequestHandlerFactory[TRequest any, TResponse any](factory RequestHandlerFactory[TRequest, TResponse]) error { + return registerRequestHandler[TRequest, TResponse](factory) +} + // RegisterRequestPipelineBehaviors register the request behaviors to mediatr registry. func RegisterRequestPipelineBehaviors(behaviours ...PipelineBehavior) error { for _, behavior := range behaviours { @@ -62,8 +75,7 @@ func RegisterRequestPipelineBehaviors(behaviours ...PipelineBehavior) error { return nil } -// RegisterNotificationHandler register the notification handler to mediatr registry. -func RegisterNotificationHandler[TEvent any](handler NotificationHandler[TEvent]) error { +func registerNotificationHandler[TEvent any](handler any) error { var event TEvent eventType := reflect.TypeOf(event) @@ -78,6 +90,16 @@ func RegisterNotificationHandler[TEvent any](handler NotificationHandler[TEvent] return nil } +// RegisterNotificationHandler register the notification handler to mediatr registry. +func RegisterNotificationHandler[TEvent any](handler NotificationHandler[TEvent]) error { + return registerNotificationHandler[TEvent](handler) +} + +// RegisterNotificationHandlerFactory register the notification handler factory to mediatr registry. +func RegisterNotificationHandlerFactory[TEvent any](factory NotificationHandlerFactory[TEvent]) error { + return registerNotificationHandler[TEvent](factory) +} + // RegisterNotificationHandlers register the notification handlers to mediatr registry. func RegisterNotificationHandlers[TEvent any](handlers ...NotificationHandler[TEvent]) error { if len(handlers) == 0 { @@ -85,7 +107,23 @@ func RegisterNotificationHandlers[TEvent any](handlers ...NotificationHandler[TE } for _, handler := range handlers { - err := RegisterNotificationHandler[TEvent](handler) + err := RegisterNotificationHandler(handler) + if err != nil { + return err + } + } + + return nil +} + +// RegisterNotificationHandlers register the notification handlers factories to mediatr registry. +func RegisterNotificationHandlersFactories[TEvent any](factories ...NotificationHandlerFactory[TEvent]) error { + if len(factories) == 0 { + return errors.New("no handlers provided") + } + + for _, handler := range factories { + err := RegisterNotificationHandlerFactory[TEvent](handler) if err != nil { return err } @@ -102,6 +140,20 @@ func ClearNotificationRegistrations() { notificationHandlersRegistrations = map[reflect.Type][]interface{}{} } +func buildRequestHandler[TRequest any, TResponse any](handler any) (RequestHandler[TRequest, TResponse], bool) { + handlerValue, ok := handler.(RequestHandler[TRequest, TResponse]) + if !ok { + factory, ok := handler.(RequestHandlerFactory[TRequest, TResponse]) + if !ok { + return nil, false + } + + return factory(), true + } + + return handlerValue, true +} + // Send the request to its corresponding request handler. func Send[TRequest any, TResponse any](ctx context.Context, request TRequest) (TResponse, error) { requestType := reflect.TypeOf(request) @@ -112,7 +164,7 @@ func Send[TRequest any, TResponse any](ctx context.Context, request TRequest) (T return *new(TResponse), errors.Errorf("no handler for request %T", request) } - handlerValue, ok := handler.(RequestHandler[TRequest, TResponse]) + handlerValue, ok := buildRequestHandler[TRequest, TResponse](handler) if !ok { return *new(TResponse), errors.Errorf("handler for request %T is not a Handler", request) } @@ -155,6 +207,20 @@ func Send[TRequest any, TResponse any](ctx context.Context, request TRequest) (T return response, nil } +func buildNotificationHandler[TNotification any](handler any) (NotificationHandler[TNotification], bool) { + handlerValue, ok := handler.(NotificationHandler[TNotification]) + if !ok { + factory, ok := handler.(NotificationHandlerFactory[TNotification]) + if !ok { + return nil, false + } + + return factory(), true + } + + return handlerValue, true +} + // Publish the notification event to its corresponding notification handler. func Publish[TNotification any](ctx context.Context, notification TNotification) error { eventType := reflect.TypeOf(notification) @@ -166,7 +232,8 @@ func Publish[TNotification any](ctx context.Context, notification TNotification) } for _, handler := range handlers { - handlerValue, ok := handler.(NotificationHandler[TNotification]) + handlerValue, ok := buildNotificationHandler[TNotification](handler) + if !ok { return errors.Errorf("handler for notification %T is not a Handler", notification) } diff --git a/mediatr_test.go b/mediatr_test.go index f5ded62..e9e8360 100644 --- a/mediatr_test.go +++ b/mediatr_test.go @@ -3,11 +3,12 @@ package mediatr import ( "context" "fmt" + "testing" + "github.com/goccy/go-reflect" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) var testData []string @@ -22,6 +23,10 @@ func TestRunner(t *testing.T) { test.Test_Send_Should_Return_Error_If_Handler_Returns_Error() test.Test_Send_Should_Dispatch_Request_To_Handler_And_Get_Response_Without_Pipeline() test.Test_Clear_Request_Registrations() + + test.Test_RegisterRequestHandlerFactory_Should_Return_Error_If_Handler_Already_Registered_For_Request() + test.Test_RegisterRequestHandlerFactory_Should_Register_All_Handlers_For_Different_Requests() + test.Test_Send_Should_Dispatch_Request_To_Factory() }) t.Run("B=notifications", func(t *testing.T) { @@ -30,6 +35,8 @@ func TestRunner(t *testing.T) { test.Test_Publish_Should_Return_Error_If_Handler_Returns_Error() test.Test_Publish_Should_Dispatch_Notification_To_All_Handlers_Without_Any_Response_And_Error() test.Test_Clear_Notifications_Registrations() + + test.Test_Publish_Should_Dispatch_Notification_To_All_Handlers_Factories_Without_Any_Response_And_Error() }) t.Run("C=pipeline-behaviours", func(t *testing.T) { @@ -44,6 +51,68 @@ type MediatRTests struct { *testing.T } +func (t *MediatRTests) Test_Send_Should_Dispatch_Request_To_Factory() { + defer cleanup() + var factory1 RequestHandlerFactory[*RequestTest, *ResponseTest] = func() RequestHandler[*RequestTest, *ResponseTest] { + return &RequestTestHandler{} + } + errRegister := RegisterRequestHandlerFactory(factory1) + if errRegister != nil { + t.Error(errRegister) + } + + response, err := Send[*RequestTest, *ResponseTest](context.Background(), &RequestTest{Data: "test"}) + assert.Nil(t, err) + assert.IsType(t, &ResponseTest{}, response) + assert.Equal(t, "test", response.Data) +} + +func (t *MediatRTests) Test_RegisterRequestHandlerFactory_Should_Return_Error_If_Handler_Already_Registered_For_Request() { + defer cleanup() + + expectedErr := fmt.Sprintf("registered handler already exists in the registry for message %s", "*mediatr.RequestTest") + + var factory1 RequestHandlerFactory[*RequestTest, *ResponseTest] = func() RequestHandler[*RequestTest, *ResponseTest] { + return &RequestTestHandler{} + } + var factory2 RequestHandlerFactory[*RequestTest, *ResponseTest] = func() RequestHandler[*RequestTest, *ResponseTest] { + return &RequestTestHandler{} + } + + err1 := RegisterRequestHandlerFactory(factory1) + err2 := RegisterRequestHandlerFactory(factory2) + + assert.Nil(t, err1) + assert.Containsf(t, err2.Error(), expectedErr, "expected error containing %q, got %s", expectedErr, err2) + + count := len(requestHandlersRegistrations) + assert.Equal(t, 1, count) +} + +func (t *MediatRTests) Test_RegisterRequestHandlerFactory_Should_Register_All_Handlers_For_Different_Requests() { + defer cleanup() + var factory1 RequestHandlerFactory[*RequestTest, *ResponseTest] = func() RequestHandler[*RequestTest, *ResponseTest] { + return &RequestTestHandler{} + } + var factory2 RequestHandlerFactory[*RequestTest2, *ResponseTest2] = func() RequestHandler[*RequestTest2, *ResponseTest2] { + return &RequestTestHandler2{} + } + + err1 := RegisterRequestHandlerFactory(factory1) + err2 := RegisterRequestHandlerFactory(factory2) + + if err1 != nil { + t.Errorf("error registering request handler: %s", err1) + } + + if err2 != nil { + t.Errorf("error registering request handler: %s", err2) + } + + count := len(requestHandlersRegistrations) + assert.Equal(t, 2, count) +} + // Each request should have exactly one handler func (t *MediatRTests) Test_RegisterRequestHandler_Should_Return_Error_If_Handler_Already_Registered_For_Request() { defer cleanup() @@ -186,10 +255,31 @@ func (t *MediatRTests) Test_Publish_Should_Return_Error_If_Handler_Returns_Error if errRegister != nil { t.Error(errRegister) } + err := Publish[*NotificationTest](context.Background(), &NotificationTest{}) assert.Containsf(t, err.Error(), expectedErr, "expected error containing %q, got %s", expectedErr, err) } +func (t *MediatRTests) Test_Publish_Should_Dispatch_Notification_To_All_Handlers_Factories_Without_Any_Response_And_Error() { + defer cleanup() + var factory1 NotificationHandlerFactory[*NotificationTest] = func() NotificationHandler[*NotificationTest] { + return &NotificationTestHandler{} + } + var factory2 NotificationHandlerFactory[*NotificationTest] = func() NotificationHandler[*NotificationTest] { + return &NotificationTestHandler4{} + } + + errRegister := RegisterNotificationHandlersFactories(factory1, factory2) + if errRegister != nil { + t.Error(errRegister) + } + + notification := &NotificationTest{} + err := Publish[*NotificationTest](context.Background(), notification) + assert.Nil(t, err) + assert.True(t, notification.Processed) +} + func (t *MediatRTests) Test_Publish_Should_Dispatch_Notification_To_All_Handlers_Without_Any_Response_And_Error() { defer cleanup() handler1 := &NotificationTestHandler{} @@ -251,7 +341,7 @@ func (t *MediatRTests) Test_Clear_Notifications_Registrations() { require.NoError(t, errRegister) ClearNotificationRegistrations() - + count := len(notificationHandlersRegistrations) assert.Equal(t, 0, count) }