diff --git a/.editorconfig b/.editorconfig index 3693f0c..2ed5dbb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,12 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file root = true [*] charset = utf-8 -end_of_line = lf indent_size = 2 indent_style = tab insert_final_newline = true trim_trailing_whitespace = true +end_of_line = lf -[*.{yaml,yml}] +[*.{yaml,yml,md,hcl}] indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1ba1f3b..59e1daf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,15 +1,40 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - +# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/refs/heads/master/src/schemas/json/dependabot-2.0.json +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference version: 2 + updates: - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: github-actions + open-pull-requests-limit: 1 + commit-message: { prefix: chore(deps) } + directory: '/' schedule: - interval: "weekly" - - package-ecosystem: "gomod" - directory: "/" + day: sunday + interval: weekly + timezone: Europe/Berlin + groups: + github-actions: + patterns: [ '*' ] + cooldown: + default-days: 1 + include: [ '*' ] + + - package-ecosystem: gomod + open-pull-requests-limit: 1 + commit-message: { prefix: sec(deps) } + directories: [ '**/*' ] schedule: - interval: "weekly" + interval: daily + groups: + security: + applies-to: security-updates + update-types: [ minor, patch ] + patterns: [ '*' ] + ignore: + - dependency-name: '*' + update-types: [ 'version-update:semver-major' ] + cooldown: + default-days: 1 + semver-major-days: 14 + semver-minor-days: 7 + semver-patch-days: 1 + include: [ '*' ] diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml deleted file mode 100644 index f9d55e3..0000000 --- a/.github/workflows/pull-requests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Pull Requests - -on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - branches: [ main ] - pull_request: - branches: [ main ] - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - GOFLAGS: -mod=readonly - GOPROXY: https://proxy.golang.org - -jobs: - pull-request: - name: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 - with: - check-latest: true - go-version-file: 'go.mod' - - name: Run go mod tidy - run: | - go mod tidy - git diff --exit-code - - uses: golangci/golangci-lint-action@v3 - - name: Run go test - run: | - go test -v ./... - diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..f6f784c --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,31 @@ +name: Release Tag + +on: + push: + tags: + - v*.*.* + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: jdx/mise-action@v3 + + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - uses: goreleaser/goreleaser-action@v7 + with: + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/releases.yaml b/.github/workflows/releases.yaml deleted file mode 100644 index 8a69ac9..0000000 --- a/.github/workflows/releases.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: Releases - -on: - push: - tags: - - v*.*.* - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -env: - GOFLAGS: -mod=readonly - GOPROXY: https://proxy.golang.org - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - run: git fetch --force --tags - - uses: actions/setup-go@v4 - with: - check-latest: true - go-version-file: 'go.mod' - - uses: goreleaser/goreleaser-action@v5 - with: - version: latest - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..e02a763 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: Test Branch + +on: + push: + branches: [ main ] + pull_request: + merge_group: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: jdx/mise-action@v3 + + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: 'go.mod' + + - name: Setup golangci-lint cache + uses: actions/cache@v5 + with: + path: /home/runner/.cache/golangci-lint + key: golangci-lint-${{ runner.os }}-${{ hashFiles('**/go.mod') }} + restore-keys: | + golangci-lint-${{ runner.os }}- + + - name: Run go mod tidy + run: make tidy && git diff --exit-code + + - name: Run go generate + run: make generate && git diff --exit-code + + - name: Run go lint + run: make lint + + - name: Run go tests + run: make test diff --git a/.gitignore b/.gitignore index b1e259c..f977040 100644 --- a/.gitignore +++ b/.gitignore @@ -1,64 +1,43 @@ -# Created by https://www.toptal.com/developers/gitignore/api/jetbrains,go,visualstudiocode,macos -# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains,go,visualstudiocode,macos - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Go ### -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -### Go Patch ### -/vendor/ -/Godeps/ - -### JetBrains ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 -.idea/ - -### VisualStudioCode ### -.vscode/ - -# End of https://www.toptal.com/developers/gitignore/api/jetbrains,go,visualstudiocode,macos - -/tmp/ +.* *.log +*.pid +*.retry +*.out +*.tgz +*.zip +*.pem +bin/ +tmp/ + +## Git +!.gitkeep +!.gitignore +!.gitattributes + +## GitHub +!.github/ + +## Mise +!.mise.toml + +## Lefthook +!.lefthook.yaml + +## VSCode +!.vscode/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.default.json + +## Editorconfig +!.editorconfig + +## Golang +go.work +go.work.sum +!.golangci.yaml +!.goreleaser.yaml + +## Vitepress +node_modules/ +!docs/.vitepress diff --git a/.golangci.yml b/.golangci.yml index d152d93..a518c8a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,113 +1,103 @@ -linters-settings: - revive: - rules: - - name: indent-error-flow - disabled: true - gocritic: - disabled-checks: - - ifElseChain - # https://golangci-lint.run/usage/linters/#gosec - gosec: - config: - G306: "0700" - excludes: - - G101 # Potential hardcoded credentials - - G102 # Bind to all interfaces - - G112 # Potential slowloris attack - - G401 # Detect the usage of DES, RC4, MD5 or SHA1 - - G402 # Look for bad TLS connection settings - - G404 # Insecure random number source (rand) - - G501 # Import blocklist: crypto/md5 - - G505 # Import blocklist: crypto/sha1 +# https://golangci-lint.run/usage/configuration/ +# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json +version: "2" -linters: - enable: - # Enabled by default linters: - - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] - - gosimple # (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false] - - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - - staticcheck # (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false] - - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] - - unused # (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] +run: + build-tags: [ safe ] + modules-download-mode: readonly - # Disabled by default linters: - - asasalint # check for pass []any as any in variadic func(...any) [fast: false, auto-fix: false] - - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] - - bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false] - - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - #- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false] - #- contextcheck # check the function whether to use a non-inherited context [fast: false, auto-fix: false] - #- cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] - - decorder # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false] - #- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] +linters: + default: all + disable: + # Project specific linters + - containedctx # containedctx is a linter that detects struct contained context.Context field [fast: false, auto-fix: false] + # Discouraged linters + - noinlineerr # Disallows inline error handling (`if err := ...; err != nil {`). + - embeddedstructfieldcheck # Embedded types should be at the top of the field list of a struct, and there must be an empty line separating embedded fields from regular fields. [fast] + - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] + - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] - #- dupl # Tool for code clone detection [fast: true, auto-fix: false] - - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] - - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. [fast: false, auto-fix: false] - - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] - - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds [fast: false, auto-fix: false] - - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - #- exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false] - - exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] - #- forbidigo # Forbids identifiers [fast: true, auto-fix: false] - - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] - #- funlen # Tool for detection of long functions [fast: true, auto-fix: false] - #- gci # Gci controls golang package import order and makes it always deterministic. [fast: true, auto-fix: false] - #- gochecknoglobals # check that no global variables exist [fast: true, auto-fix: false] - #- gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - #- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] - - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - - gocritic # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] - #- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - #- godot # Check if comments end in a period [fast: true, auto-fix: true] - #- godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] - # - goerr113 # Golang linter to check the errors handling expressions [fast: false, auto-fix: false] - - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - #- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] - - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false] - - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] - #- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] - #- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] - - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] - - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] - - gosec # (gas): Inspects source code for security problems [fast: false, auto-fix: false] - - grouper # An analyzer to analyze expression groups. [fast: true, auto-fix: false] - - importas # Enforces consistent import aliases [fast: false, auto-fix: false] - #- ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false] - #- lll # Reports long lines [fast: true, auto-fix: false] + - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - dupword # checks for duplicate words in the source code [fast: true, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - err113 # Go linter to check the errors handling expressions [fast: false, auto-fix: false] + - exhaustruct # Checks if all structure fields are initialized [fast: false, auto-fix: false] + - funlen # Tool for detection of long functions [fast: true, auto-fix: false] + - ginkgolinter # enforces standards of using ginkgo and gomega [fast: false, auto-fix: false] + - gochecknoglobals # Check that no global variables exist. [fast: false, auto-fix: false] + - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - godot # Check if comments end in a period [fast: true, auto-fix: true] + - godox # Tool for detection of comment keywords [fast: true, auto-fix: false] + - interfacebloat # A linter that checks the number of methods inside an interface. [fast: true, auto-fix: false] + - intrange # (go >= 1.22) intrange is a linter to find places where for loops could make use of an integer range. [fast: true, auto-fix: false] + - ireturn # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false] + - lll # Reports long lines [fast: true, auto-fix: false] - maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] - - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] - - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - #- nestif # Reports deeply nested if statements [fast: true, auto-fix: false] - - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] - #- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] - #- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] - - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false] - - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] - - nonamedreturns # Reports all named returns [fast: false, auto-fix: false] - - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. [fast: true, auto-fix: false] - #- paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: false, auto-fix: false] + - nestif # Reports deeply nested if statements [fast: true, auto-fix: false] + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] + - mnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] + - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. [fast: false, auto-fix: false] - prealloc # Finds slice declarations that could potentially be pre-allocated [fast: true, auto-fix: false] - - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] - - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] - - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] - - rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false] - - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] - - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] + - protogetter # Reports direct reads from proto message fields when getters should be used [fast: false, auto-fix: true] + - sloglint # ensure consistent code style when using log/slog [fast: false, auto-fix: false] + - tagalign # check that struct tags are well aligned [fast: true, auto-fix: true] - tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] - - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false] - - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] - - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] - - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] - - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] - unparam # Reports unused function parameters [fast: false, auto-fix: false] - - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. [fast: true, auto-fix: false] - #- varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] - - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] - - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] - #- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] - #- wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] + - varnamelen # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] + - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] + - zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg` [fast: false, auto-fix: false] + # Deprected linters + - wsl + # https://golangci-lint.run/docs/linters/ + settings: + # https://golangci-lint.run/docs/linters/configuration/#testifylint + testifylint: + disable: [ float-compare ] + # https://golangci-lint.run/docs/linters/configuration/#exhaustive + exhaustive: + default-signifies-exhaustive: true + # https://golangci-lint.run/docs/linters/configuration/#gocritic + gocritic: + disabled-checks: + - assignOp + - ifElseChain + - deprecatedComment + # https://golangci-lint.run/docs/linters/configuration/#gomoddirectives + gomoddirectives: + replace-local: true + # https://golangci-lint.run/docs/linters/configuration/#gosec + gosec: + severity: medium + confidence: medium + # https://golangci-lint.run/docs/linters/configuration/#predeclared + predeclared: + ignore: [ min, max ] + # https://golangci-lint.run/docs/linters/configuration/#revive + revive: + rules: + - name: exported + disabled: true + - name: package-comments + disabled: true + exclusions: + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - path: _test\.go + linters: + - forbidigo + - unused + - gosec + paths: + - tmp + - examples +formatters: + enable: + - gofmt + - goimports diff --git a/.goreleaser.yml b/.goreleaser.yml index 2bb7412..cdb4a40 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,2 +1,24 @@ +version: 2 + +project_name: fender + +release: + github: + owner: foomo + name: fender + prerelease: auto + builds: - skip: true + +archives: + - format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - LICENSE + - README.md + +changelog: + use: github-native diff --git a/.husky.yaml b/.husky.yaml deleted file mode 100644 index d235c6a..0000000 --- a/.husky.yaml +++ /dev/null @@ -1,16 +0,0 @@ -hooks: - pre-commit: - - golangci-lint run --fast - - husky lint-staged - commit-msg: - - husky lint-commit - -lint-staged: - '*.go': - - goimports -l -w - - gofmt -l -w - -lint-commit: - email: '^(.+@bestbytes.com)$' - types: '^(feat|fix|build|chore|docs|perf|refactor|revert|style|test|wip)$' - header: '^(?P\w+)(\((?P[\w/.-]+)\))?(?P!)?:( +)?(?P
.+)' diff --git a/.lefthook.yaml b/.lefthook.yaml new file mode 100644 index 0000000..3eff260 --- /dev/null +++ b/.lefthook.yaml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/evilmartians/lefthook/refs/heads/master/schema.json +# https://lefthook.dev/configuration/ +output: [ summary ] + +pre-commit: + skip: [ merge, rebase ] + parallel: true + commands: + branch-name: + run: | + branch_name=$(git rev-parse --abbrev-ref HEAD) + if ! [[ "$branch_name" =~ ^(feature|fix)/ ]]; then + echo "Error: Branch name must start with 'feature/' or 'fix/'. Your branch: $branch_name" + exit 1 + fi + golangci-fmt: + glob: "*.go" + run: golangci-lint fmt {staged_files} + stage_fixed: true + golangci-lint: + glob: "*.go" + run: golangci-lint run --new --fast-only + stage_fixed: false + +commit-msg: + skip: [ merge, rebase ] + commands: + commit: + run: | + commit_msg_file={1} + commit_msg=$(cat "$commit_msg_file") + # Regex for conventional commits (type(scope?): subject) + regex="^(build|chore|ci|docs|feat|fix|perf|refactor|style|test|sec|wip|revert)(\([a-z0-9\-]+\))?(!)?: .{1,50}" + if ! echo "$commit_msg" | grep -qE "$regex"; then + echo "Error: Commit message does not follow Conventional Commits format." + echo "Format: type(scope?): subject" + echo "Example: feat(login): add remember me option" + exit 1 + fi + +post-checkout: + commands: + mise: + run: mise -q install diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..b3013f6 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,5 @@ +[tools] +# https://mise-tools.jdx.dev/tools/lefthook +lefthook = "2.1.1" +# https://mise-tools.jdx.dev/tools/golangci-lint +golangci-lint = "2.10.1" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0de36ab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,38 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is Fender + +Fender is a Go validation library that provides a unified, composable way to validate data. It uses generics to build type-safe validation pipelines with three layers: **fender** (orchestrator), **fend** (field-level validators), and **rule** (individual validation rules). + +## Commands + +All commands go through `make`. Run `make help` for the full list. + +- `make check` — full pipeline: tidy, generate, lint, test +- `make lint` / `make lint.fix` — run golangci-lint (config in `.golangci.yml`) +- `make test` — run tests with `-tags=safe` +- `make test.race` — run tests with race detector +- `make test.update` — run tests with `-update` flag +- Single test: `go test -tags=safe -v -run TestName ./path/to/package/...` +- Tool setup: `make .mise` (installs mise dependencies), `make .lefthook` (configures git hooks) + +Build tag `-tags=safe` is required for all builds and tests. + +## Architecture + +Three-layer validation pipeline, each layer wrapping the one below: + +**`rule/`** — Atomic validation rules. A `rule.Rule[T]` is `func(ctx context.Context, v T) error`. Returns `*rule.Error` on validation failure or `rule.ErrBreak` to short-circuit. Includes built-in rules: `RequiredString`, `MinString`, `Email`, `Match`, `Range`, `UUID`, etc. Type aliases (`StringRule`, `IntRule`, etc.) provide convenience. + +**`fend/`** — Field-level validators. A `fend.Fend` is `func(ctx context.Context, mode Mode) error`. Created via `fend.Field(name, value, rules...)`, `fend.Var(value, rules...)`, or `fend.DynamicField`/`fend.DynamicVar` for runtime-typed rules. `fend.Union` implements OR-logic across rule sets. `fend.Rules[T]` allows prepending shared rules to fields. + +**`fender.go` (root)** — Orchestrator with three validation modes: +- `fender.All` — runs all fends, collects all errors from all rules +- `fender.First` — stops at first fend with an error, returns only its first rule error +- `fender.AllFirst` — runs all fends but returns only the first fend's errors + +**`config/`** — Shared configuration: a `go-playground/validator` instance and delimiter/regex constants. + +**Error hierarchy**: `fender.Error` contains `[]*fend.Error`, each containing `[]*rule.Error`. Use `fender.AsError()`, `fend.AsError()`, `rule.AsError()` to unwrap. String format uses `;` between rule errors and `:` between fend errors. diff --git a/LICENSE b/LICENSE index df625f9..2b3a107 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +MIT License -Copyright (c) 2021 bestbytes +Copyright (c) foomo by bestbytes Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 6641efd..0c6c6b2 100644 --- a/Makefile +++ b/Makefile @@ -1,71 +1,101 @@ .DEFAULT_GOAL:=help +-include .makerc -## === Tasks === +# --- Config ------------------------------------------------------------------ -.PHONY: gomod -## Run go mod tidy -tidy: - go mod tidy +# Newline hack for error output +define br -.PHONY: outdated -## Show outdated direct dependencies -outdated: - go list -u -m -json all | go-mod-outdated -update -direct -.PHONY: check -## Run tests and linters -check: test lint +endef -.PHONY: test -## Run tests -test: - gotestsum --format dots-v2 ./... +# --- Targets ----------------------------------------------------------------- + +# This allows us to accept extra arguments +%: .mise .lefthook + @: + +.PHONY: .mise +# Install dependencies +.mise: +ifeq (, $(shell command -v mise)) + $(error $(br)$(br)Please ensure you have 'mise' installed and activated!$(br)$(br) $$ brew update$(br) $$ brew install mise$(br)$(br)See the documentation: https://mise.jdx.dev/getting-started.html) +endif + @mise install + +# Configure git hooks for lefthook +.lefthook: + @lefthook install --reset-hooks-path + +### Tasks + +.PHONY: check +## Run lint & tests +check: tidy generate lint test -.PHONY: bench -## Run benchmarks -bench: - go test -run ^$$ -bench . | prettybench +.PHONY: tidy +## Run go mod tidy +tidy: + @echo "〉go mod tidy" + @go mod tidy .PHONY: lint ## Run linter lint: - golangci-lint run + @echo "〉go lint" + @golangci-lint run .PHONY: lint.fix -## Fix lint violations +## Run linter and fix violations lint.fix: - golangci-lint run --fix + @echo "〉go lint with --fix" + @golangci-lint run --fix + +.PHONY: generate +## Run go generate +generate: + @echo "〉go generate" + @go generate ./... + +.PHONY: test +## Run tests +test: + @echo "〉go test" + @GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out ./... + +.PHONY: test.race +## Run tests with -race +test.race: + @GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -race ./... + +.PHONY: test.update +## Run tests with -update +test.update: + @GO_TEST_TAGS=-skip go test -tags=safe -coverprofile=coverage.out -update ./... + +.PHONY: outdated +## Show outdated direct dependencies +outdated: + @echo "〉go mod outdated" + @go list -u -m -json all | go-mod-outdated -update -direct -## === Utils === +### Utils +.PHONY: help ## Show help text help: + @echo "Keel\n" + @echo "Usage:\n make [task]" @awk '{ \ - if ($$0 ~ /^.PHONY: [a-zA-Z\-\_0-9]+$$/) { \ - helpCommand = substr($$0, index($$0, ":") + 2); \ - if (helpMessage) { \ - printf "\033[36m%-23s\033[0m %s\n", \ - helpCommand, helpMessage; \ - helpMessage = ""; \ - } \ - } else if ($$0 ~ /^[a-zA-Z\-\_0-9.]+:/) { \ - helpCommand = substr($$0, 0, index($$0, ":")); \ - if (helpMessage) { \ - printf "\033[36m%-23s\033[0m %s\n", \ - helpCommand, helpMessage"\n"; \ - helpMessage = ""; \ - } \ - } else if ($$0 ~ /^##/) { \ - if (helpMessage) { \ - helpMessage = helpMessage"\n "substr($$0, 3); \ - } else { \ - helpMessage = substr($$0, 3); \ - } \ - } else { \ - if (helpMessage) { \ - print "\n "helpMessage"\n" \ - } \ - helpMessage = ""; \ - } \ - }' \ - $(MAKEFILE_LIST) + if($$0 ~ /^### /){ \ + if(help) printf "%-23s %s\n\n", cmd, help; help=""; \ + printf "\n%s:\n", substr($$0,5); \ + } else if($$0 ~ /^[a-zA-Z0-9._-]+:/){ \ + cmd = substr($$0, 1, index($$0, ":")-1); \ + if(help) printf " %-23s %s\n", cmd, help; help=""; \ + } else if($$0 ~ /^##/){ \ + help = help ? help "\n " substr($$0,3) : substr($$0,3); \ + } else if(help){ \ + print "\n " help "\n"; help=""; \ + } \ + }' $(MAKEFILE_LIST) diff --git a/README.md b/README.md index 44c105a..458d3e8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +[![Build Status](https://github.com/foomo/fender/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/foomo/fender/actions/workflows/test.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/foomo/fender)](https://goreportcard.com/report/github.com/foomo/fender) +[![GoDoc](https://godoc.org/github.com/foomo/fender?status.svg)](https://godoc.org/github.com/foomo/fender) + +

+ fender +

+ # fender > a piece of rope or a tyre that protects the side of a boat from knocks @@ -133,8 +141,12 @@ ok github.com/foomo/fender 5.619s ## How to Contribute -Make a pull request... +Contributions are welcome! Please read the [contributing guide](docs/CONTRIBUTING.md). + +![Contributors](https://contributors-table.vercel.app/image?repo=foomo/fender&width=50&columns=15) ## License -Distributed under MIT License, please see license file within the code for more details. +Distributed under MIT License, please read the [license file](LICENSE) for more details. + +_Made with ♥ [foomo](https://www.foomo.org) by [bestbytes](https://www.bestbytes.com)_ diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..06b2fcb --- /dev/null +++ b/docs/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +info@bestbytes.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..f5c9e52 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing + +Thank you for your interest in contributing to our project! This guide will help you get started with the development process. + +## Development Workflow + +1. Fork this repository +2. Create a new branch: `git checkout -b feature/your-feature-name` +3. Make your changes +4. Check and fix code style and formatting issues: `make lint` +5. Run checks: `make test` +6. Build the project: `make build` +7. Commit your changes using the conventions below +8. Push your branch to your fork +9. Open a pull request + +## Commit Message Conventions + +We follow [Conventional Commits](https://www.conventionalcommits.org/) for clear and structured commit messages: + +- `feat:` New features +- `fix:` Bug fixes +- `docs:` Documentation changes +- `style:` Code style changes (formatting, etc.) +- `refactor:` Code changes that neither fix bugs nor add features +- `perf:` Performance improvements +- `test:` Adding or updating tests +- `chore:` Maintenance tasks, dependencies, etc. + +## Pull Request Guidelines + +1. Update documentation if needed +2. Ensure all tests pass +3. Address any feedback from code reviews +4. Once approved, your PR will be merged + +## Code of Conduct & Security + +Please be respectful and constructive in all interactions within our community and follow the [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) and [SECURITY](SECURITY.md) guidelines. + +## Questions? + +If you have any questions, please [open an issue](https://github.com/foomo/fender/issues/new) for discussion. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..d9c15c1 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,45 @@ +# Security Guidelines + +## How security is managed on this project + +The foomo team and community take security seriously and wants to ensure that +we maintain a secure environment and provide secure solutions for the open +source community. To help us achieve these goals, please note the +following before using this software: + +- Review the software license to understand the contributor's obligations in + terms of warranties and suitability for purpose +- For any questions or concerns about security, you can + [create an issue][new-issue] or [report a vulnerability][new-sec-issue] +- We request that you work with our security team and opt for + responsible disclosure using the guidelines below +- All security related issues and pull requests you make should be tagged with + "security" for easy identification +- Please monitor this repository and update your environment in a timely manner + as we release patches and updates + +## Responsibly Disclosing Security Bugs + +If you find a security bug in this repository, please work with contributors +following responsible disclosure principles and these guidelines: + +- Do not submit a normal issue or pull request in our public repository, instead + [report it directly][new-sec-issue]. +- We will review your submission and may follow up for additional details +- If you have a patch, we will review it and approve it privately; once approved + for release you can submit it as a pull request publicly in the repository (we + give credit where credit is due) +- We will keep you informed during our investigation, feel free to check in for + a status update +- We will release the fix and publicly disclose the issue as soon as possible, + but want to ensure we due properly due diligence before releasing +- Please do not publicly blog or post about the security issue until after we + have updated the public repo so that other downstream users have an opportunity + to patch + +## Contact / Misc + +If you have any questions, please reach out directly by [creating an issue][new-issue]. + +[new-issue]: https://github.com/foomo/fender/issues/new/choose +[new-sec-issue]: https://github.com/foomo/fender/security/advisories/new diff --git a/docs/public/logo.png b/docs/public/logo.png new file mode 100644 index 0000000..9ab1603 Binary files /dev/null and b/docs/public/logo.png differ diff --git a/error.go b/error.go index fdafb63..0499907 100644 --- a/error.go +++ b/error.go @@ -32,6 +32,7 @@ func (e *Error) Error() string { for i, cause := range e.FendErrs { causes[i] = cause.Error() } + return strings.Join(causes, config.DelimiterFend) } @@ -39,6 +40,7 @@ func (e *Error) First() error { if errs := e.Errors(); len(errs) > 0 { return errs[0] } + return nil } @@ -47,6 +49,7 @@ func (e *Error) Errors() []error { for i, fendErr := range e.FendErrs { causes[i] = fendErr } + return causes } @@ -59,5 +62,6 @@ func AsError(err error) *Error { if errors.As(err, &fendErr) { return fendErr } + return nil } diff --git a/fend/error.go b/fend/error.go index d10acfc..ece0862 100644 --- a/fend/error.go +++ b/fend/error.go @@ -33,13 +33,16 @@ func (e *Error) Name() string { func (e *Error) Error() string { var ret string + causes := make([]string, len(e.RuleErrs)) for i, ruleErr := range e.RuleErrs { causes[i] = ruleErr.Error() } + if e.Path != "" { ret += e.Path + config.DelimiterFendName } + return ret + strings.Join(causes, config.DelimiterRule) } @@ -48,6 +51,7 @@ func (e *Error) Errors() []error { for i, ruleErr := range e.RuleErrs { causes[i] = ruleErr } + return causes } @@ -60,5 +64,6 @@ func AsError(err error) *Error { if errors.As(err, &fendErr) { return fendErr } + return nil } diff --git a/fend/fend.go b/fend/fend.go index 7ec203a..b792ac5 100644 --- a/fend/fend.go +++ b/fend/fend.go @@ -11,6 +11,7 @@ type Fend func(ctx context.Context, mode Mode) error func fend[T any](ctx context.Context, mode Mode, meta string, value T, rules ...rule.Rule[T]) error { var causes []*rule.Error + for _, r := range rules { err := r(ctx, value) if errors.Is(err, rule.ErrBreak) { @@ -25,14 +26,17 @@ func fend[T any](ctx context.Context, mode Mode, meta string, value T, rules ... return err } } + if causes != nil { return NewError(meta, causes...) } + return nil } func fendDynamic(ctx context.Context, mode Mode, meta string, rules ...rule.DynamicRule) error { var causes []*rule.Error + for _, r := range rules { err := r(ctx) if errors.Is(err, rule.ErrBreak) { @@ -47,8 +51,10 @@ func fendDynamic(ctx context.Context, mode Mode, meta string, rules ...rule.Dyna return err } } + if causes != nil { return NewError(meta, causes...) } + return nil } diff --git a/fend/union.go b/fend/union.go index f82c9c8..70d5c08 100644 --- a/fend/union.go +++ b/fend/union.go @@ -9,19 +9,23 @@ import ( func Union[T any](rules ...Rules[T]) rule.Rule[T] { return func(ctx context.Context, v T) error { var e error + for _, r := range rules { var e2 error + for _, r2 := range r { if err := r2(ctx, v); err != nil { e2 = err } } + if e2 != nil { e = e2 } else { return nil } } + return e } } diff --git a/fender.go b/fender.go index 30d4a9a..34f5b1f 100644 --- a/fender.go +++ b/fender.go @@ -20,6 +20,7 @@ func AllFirst(ctx context.Context, fends ...fend.Fend) error { func Mode(ctx context.Context, mode fend.Mode, fends ...fend.Fend) error { var cause []*fend.Error + for _, validator := range fends { err := validator(ctx, mode) if e, ok := err.(*fend.Error); ok { //nolint:errorlint @@ -33,8 +34,10 @@ func Mode(ctx context.Context, mode fend.Mode, fends ...fend.Fend) error { return err } } + if cause != nil { return NewError(cause...) } + return nil } diff --git a/fender_benchmarks_test.go b/fender_benchmarks_test.go index 8e8552f..257b533 100644 --- a/fender_benchmarks_test.go +++ b/fender_benchmarks_test.go @@ -29,20 +29,25 @@ func BenchmarkAll(b *testing.B) { type Test struct { Int int `validate:"required,min=1,max=5"` } + b.Run("playground", func(b *testing.B) { b.Run("v1", func(b *testing.B) { u := &Test{} v := validator.New() + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = v.Struct(u) } }) b.Run("v2", func(b *testing.B) { u := &Test{} + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = validator.New().Struct(u) } @@ -50,16 +55,20 @@ func BenchmarkAll(b *testing.B) { b.Run("v3", func(b *testing.B) { u := &Test{Int: 3} v := validator.New() + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = v.Struct(u) } }) b.Run("v4", func(b *testing.B) { u := &Test{Int: 3} + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = validator.New().Struct(u) } @@ -69,8 +78,10 @@ func BenchmarkAll(b *testing.B) { b.Run("fender", func(b *testing.B) { b.Run("v1", func(b *testing.B) { u := &Test{} + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = fender.All(context.TODO(), fend.Field("int", u.Int, rule.Required[int], rule.NumberMin[int](1), rule.NumberMax[int](5)), @@ -80,8 +91,10 @@ func BenchmarkAll(b *testing.B) { b.Run("v2", func(b *testing.B) { u := &Test{} rules := fend.NewRules(rule.Required[int], rule.NumberMin[int](1), rule.NumberMax[int](5)) + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = fender.All(context.TODO(), rules.Field("int", u.Int), @@ -90,8 +103,10 @@ func BenchmarkAll(b *testing.B) { }) b.Run("v3", func(b *testing.B) { u := &Test{Int: 3} + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = fender.All(context.TODO(), fend.Field("int", u.Int, rule.Required[int], rule.NumberMin[int](1), rule.NumberMax[int](5)), @@ -101,8 +116,10 @@ func BenchmarkAll(b *testing.B) { b.Run("v4", func(b *testing.B) { u := &Test{Int: 3} rules := fend.NewRules(rule.Required[int], rule.NumberMin[int](1), rule.NumberMax[int](5)) + b.ResetTimer() b.ReportAllocs() + for i := 0; i < b.N; i++ { _ = fender.All(context.TODO(), rules.Field("int", u.Int), diff --git a/fender_example_test.go b/fender_example_test.go index 50fb858..28406f9 100644 --- a/fender_example_test.go +++ b/fender_example_test.go @@ -55,38 +55,3 @@ func ExampleAllFirst() { } // Output: one:required:min=10 } - -func ExampleErrors() { //nolint:govet - err := fender.All( - context.Background(), - fend.Field("one", "", rule.Required[string], rule.StringMin(10)), - fend.Field("two", "", rule.Required[string], rule.StringMin(10)), - ) - - // cast fender error - if fenderErr := fender.AsError(err); fenderErr != nil { - // iterate fend errors - for _, err := range fenderErr.Errors() { - // cast fend error - if fendErr := fend.AsError(err); fendErr != nil { - fmt.Println(fendErr.Name()) - // iterate rule errors - for _, err := range fendErr.Errors() { - // cast rule error - if ruleErr := rule.AsError(err); ruleErr != nil { - fmt.Println(ruleErr.Error()) - } - } - } - } - } else if err != nil { - panic(err) - } - // Output: - // one - // required - // min=10 - // two - // required - // min=10 -} diff --git a/fender_test.go b/fender_test.go index e0d3a01..46fb318 100644 --- a/fender_test.go +++ b/fender_test.go @@ -55,6 +55,8 @@ var ( ) func TestAll(t *testing.T) { + t.Parallel() + tests := []struct { name string fends fend.Fends @@ -114,6 +116,8 @@ func TestAll(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := fender.All(context.TODO(), tt.fends...); tt.wantAllErr == "" { assert.NoError(t, err) } else { diff --git a/go.mod b/go.mod index 12d156d..fc240d4 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,22 @@ module github.com/foomo/fender -go 1.20 +go 1.26 require ( - github.com/go-playground/validator/v10 v10.16.0 - github.com/stretchr/testify v1.8.4 - golang.org/x/exp v0.0.0-20230307190834-24139beb5833 + github.com/go-playground/validator/v10 v10.30.1 + github.com/stretchr/testify v1.11.1 + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/leodido/go-urn v1.2.4 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9755296..b1b542a 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,30 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= -github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s= -golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/rule/bool.go b/rule/bool.go index 2a1ed1b..59d0c23 100644 --- a/rule/bool.go +++ b/rule/bool.go @@ -12,6 +12,7 @@ func Bool(expectd bool) BoolRule { if v != expectd { return NewError(NameBool, Meta('T', expectd)) } + return nil } } @@ -23,6 +24,7 @@ func StringBool(expectd bool) StringRule { } else if b != expectd { return NewError(NameBool, Meta('T', expectd)) } + return nil } } diff --git a/rule/constraints.go b/rule/constraints.go index 27f1e93..9dcdb07 100644 --- a/rule/constraints.go +++ b/rule/constraints.go @@ -1,12 +1,14 @@ package rule import ( - "golang.org/x/exp/constraints" + "cmp" + + "golang.org/x/exp/constraints" //nolint:exptostd // not all supported yet ) type ( Integer = constraints.Integer - Ordered = constraints.Ordered + Ordered = cmp.Ordered Float = constraints.Float Number interface { Integer | Float diff --git a/rule/contains.go b/rule/contains.go index 782c332..fddc1e0 100644 --- a/rule/contains.go +++ b/rule/contains.go @@ -12,6 +12,7 @@ func StringContains(expected string) StringRule { if !strings.Contains(v, expected) { return NewError(NameContains) } + return nil } } @@ -21,6 +22,7 @@ func StringNotContains(expected string) StringRule { if strings.Contains(v, expected) { return NewError(NameContains) } + return nil } } diff --git a/rule/email.go b/rule/email.go index d9ce9be..a70fcb2 100644 --- a/rule/email.go +++ b/rule/email.go @@ -16,6 +16,7 @@ func Email(ctx context.Context, v string) error { if _, err := mail.ParseAddress(v); err != nil { return NewError(NameEmail, "parse") } + return nil } @@ -24,6 +25,7 @@ func EmailWeak(ctx context.Context, v string) error { if !config.RegexEmailWeak.MatchString(v) { return NewError(NameEmail, "weak") } + return nil } @@ -33,11 +35,15 @@ func EmailLookup(ctx context.Context, v string) error { if at < 0 { return NewError(NameEmail, "lookup") } + host := v[at+1:] - if _, err := net.LookupMX(host); err != nil { - if _, err := net.LookupIP(host); err != nil { + + r := net.Resolver{} + if _, err := r.LookupMX(ctx, host); err != nil { + if _, err := r.LookupIPAddr(ctx, host); err != nil { return NewError(NameEmail, "lookup") } } + return nil } diff --git a/rule/email_test.go b/rule/email_test.go index 74dfae9..86d4b43 100644 --- a/rule/email_test.go +++ b/rule/email_test.go @@ -8,6 +8,8 @@ import ( ) func TestEmail(t *testing.T) { + t.Parallel() + testEmails := map[string]bool{ `email@example.com`: true, `firstname.lastname@example.com`: true, @@ -27,7 +29,7 @@ func TestEmail(t *testing.T) { `email@-example.com`: true, `email@example`: true, `email@example@example.com`: false, - `email@[123.123.123.123]`: false, + `email@[123.123.123.123]`: true, `email.@example.com`: false, `email..email@example.com`: false, `#@%^%#$@#$@#.com`: false, @@ -44,6 +46,8 @@ func TestEmail(t *testing.T) { } for email, valid := range testEmails { t.Run(email, func(t *testing.T) { + t.Parallel() + if err := rule.Email(context.TODO(), email); (err == nil) != valid { t.Errorf("Email() error = %v, wantErr %v", err, !valid) t.Log() @@ -53,6 +57,8 @@ func TestEmail(t *testing.T) { } func TestEmailWeak(t *testing.T) { + t.Parallel() + testEmails := map[string]bool{ `email@example.com`: true, `firstname.lastname@example.com`: true, @@ -89,6 +95,8 @@ func TestEmailWeak(t *testing.T) { } for email, valid := range testEmails { t.Run(email, func(t *testing.T) { + t.Parallel() + if err := rule.EmailWeak(context.TODO(), email); (err == nil) != valid { t.Errorf("Email() error = %v, wantErr %v", err, !valid) } @@ -97,9 +105,12 @@ func TestEmailWeak(t *testing.T) { } func Test_emailHostLookup(t *testing.T) { + t.Parallel() + type args struct { v string } + tests := []struct { name string args args @@ -122,6 +133,8 @@ func Test_emailHostLookup(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := rule.EmailLookup(context.TODO(), tt.args.v); (err != nil) != tt.wantErr { t.Errorf("emailHostLookup() error = %v, wantErr %v", err, tt.wantErr) } diff --git a/rule/equal.go b/rule/equal.go index b7a04e2..b9e4f55 100644 --- a/rule/equal.go +++ b/rule/equal.go @@ -11,6 +11,7 @@ func Equal[T comparable](expected T) Rule[T] { if v != expected { return NewError(NameEqual, Meta('s', expected)) } + return nil } } @@ -20,6 +21,7 @@ func NotEqual[T comparable](expected T) Rule[T] { if v == expected { return NewError(NameEqual, Meta('s', expected)) } + return nil } } diff --git a/rule/error.go b/rule/error.go index d831486..4c2c458 100644 --- a/rule/error.go +++ b/rule/error.go @@ -27,9 +27,11 @@ func (e *Error) Error() string { if len(e.Meta) > 0 { s += config.DelimiterRuleMeta + e.Meta[0] } + if len(e.Meta) > 1 { s = fmt.Sprintf("%s[%s]", s, strings.Join(e.Meta[1:], ",")) } + return s } @@ -38,6 +40,7 @@ func AsError(err error) *Error { if errors.As(err, &fendErr) { return fendErr } + return nil } diff --git a/rule/hostname.go b/rule/hostname.go index e028ca4..997e0f9 100644 --- a/rule/hostname.go +++ b/rule/hostname.go @@ -12,5 +12,6 @@ func Hostname(ctx context.Context, v string) error { if !config.RegexHostname.MatchString(v) { return NewError(NameHostname) } + return nil } diff --git a/rule/match.go b/rule/match.go index c42b3f2..1877d14 100644 --- a/rule/match.go +++ b/rule/match.go @@ -12,10 +12,12 @@ func Match(alias string, regexp *regexp.Regexp) StringRule { if alias != "" { name = Name(alias) } + return func(ctx context.Context, v string) error { if !regexp.MatchString(v) { return NewError(name, regexp.String()) } + return nil } } @@ -25,10 +27,12 @@ func NotMatch(alias string, regexp *regexp.Regexp) StringRule { if alias != "" { name = Name(alias) } + return func(ctx context.Context, v string) error { if regexp.MatchString(v) { return NewError(name, regexp.String()) } + return nil } } diff --git a/rule/max.go b/rule/max.go index c96c60d..1151474 100644 --- a/rule/max.go +++ b/rule/max.go @@ -11,6 +11,7 @@ func StringMax(expected int) StringRule { if len(v) > expected { return NewError(NameMax, Meta('d', expected)) } + return nil } } @@ -20,6 +21,7 @@ func NumberMax[T Number](expected T) Rule[T] { if v > expected { return NewError(NameMax, Meta('d', expected)) } + return nil } } diff --git a/rule/md5.go b/rule/md5.go index 479cde6..25d60f1 100644 --- a/rule/md5.go +++ b/rule/md5.go @@ -12,5 +12,6 @@ func MD5(ctx context.Context, v string) error { if !config.RegexMD5.MatchString(v) { return NewError(NameMD5) } + return nil } diff --git a/rule/min.go b/rule/min.go index 0d95720..e3f4b28 100644 --- a/rule/min.go +++ b/rule/min.go @@ -11,6 +11,7 @@ func StringMin(expected int) StringRule { if len(v) < expected { return NewError(NameMin, Meta('d', expected)) } + return nil } } @@ -20,6 +21,7 @@ func NumberMin[T Number](expected T) Rule[T] { if v < expected { return NewError(NameMin, Meta('d', expected)) } + return nil } } diff --git a/rule/optional.go b/rule/optional.go index 52f961c..42adb37 100644 --- a/rule/optional.go +++ b/rule/optional.go @@ -9,5 +9,6 @@ func Optional[T any](ctx context.Context, v T) error { if reflect.ValueOf(v).IsZero() { return ErrBreak } + return nil } diff --git a/rule/prefix.go b/rule/prefix.go index 7c2e58d..4a72005 100644 --- a/rule/prefix.go +++ b/rule/prefix.go @@ -12,6 +12,7 @@ func Prefix(expected string) StringRule { if !strings.HasPrefix(v, expected) { return NewError(NamePrefix, Meta('d', expected)) } + return nil } } @@ -21,6 +22,7 @@ func NoPrefix(expected string) StringRule { if strings.HasPrefix(v, expected) { return NewError(NamePrefix, Meta('d', expected)) } + return nil } } diff --git a/rule/range.go b/rule/range.go index 52093ec..96a823e 100644 --- a/rule/range.go +++ b/rule/range.go @@ -11,6 +11,7 @@ func StringRange(min, max int) StringRule { if len(v) < min || len(v) > max { return NewError(NameRange, Meta('d', min), Meta('d', max)) } + return nil } } @@ -20,6 +21,7 @@ func StringNotRange(min, max int) StringRule { if len(v) >= min || len(v) <= max { return NewError(NameRange, Meta('d', min), Meta('d', max)) } + return nil } } @@ -29,6 +31,7 @@ func NumberRange[T Number](min, max T) Rule[T] { if v < min || v > max { return NewError(NameRange, Meta('d', min), Meta('d', max)) } + return nil } } @@ -38,6 +41,7 @@ func NumberNotRange[T Number](min, max T) Rule[T] { if v >= min || v <= max { return NewError(NameRange, Meta('d', min), Meta('d', max)) } + return nil } } diff --git a/rule/required.go b/rule/required.go index c836076..9254282 100644 --- a/rule/required.go +++ b/rule/required.go @@ -11,6 +11,7 @@ func Required[T any](ctx context.Context, v T) error { if reflect.ValueOf(v).IsZero() { return NewError(NameRequired) } + return nil } @@ -19,9 +20,11 @@ func IsRequired[T any](expected bool) Rule[T] { if !expected { return ErrBreak } + if reflect.ValueOf(v).IsZero() { return NewError(NameRequired) } + return nil } } diff --git a/rule/rule.go b/rule/rule.go index 5b0bbbc..8cd060d 100644 --- a/rule/rule.go +++ b/rule/rule.go @@ -19,6 +19,6 @@ type ( UInt64Rule = Rule[uint64] Float32Rule = Rule[float32] Float64Rule = Rule[float64] - InterfaceRule = Rule[interface{}] + InterfaceRule = Rule[any] DynamicRule func(ctx context.Context) error ) diff --git a/rule/rules.go b/rule/rules.go index fbb9ffa..dd043f0 100644 --- a/rule/rules.go +++ b/rule/rules.go @@ -15,7 +15,7 @@ type ( UInt64Rules = Rules[uint64] Float32Rules = Rules[float32] Float64Rules = Rules[float64] - InterfaceRules = Rules[interface{}] + InterfaceRules = Rules[any] ) func (r Rules[T]) Append(rules ...Rule[T]) Rules[T] { diff --git a/rule/size.go b/rule/size.go index 7060d6b..623d720 100644 --- a/rule/size.go +++ b/rule/size.go @@ -11,6 +11,7 @@ func StringSize(expected int) StringRule { if len(v) != expected { return NewError(NameSize, Meta('d', expected)) } + return nil } } @@ -20,6 +21,7 @@ func StringNotSize(expected int) StringRule { if len(v) == expected { return NewError(NameSize, Meta('d', expected)) } + return nil } } @@ -29,6 +31,7 @@ func NumberSize[T Number](expected T) Rule[T] { if v != expected { return NewError(NameSize, Meta('d', expected)) } + return nil } } @@ -38,6 +41,7 @@ func NumberNotSize[T Number](expected T) Rule[T] { if v == expected { return NewError(NameSize, Meta('d', expected)) } + return nil } } diff --git a/rule/suffix.go b/rule/suffix.go index 706b14b..9e08f85 100644 --- a/rule/suffix.go +++ b/rule/suffix.go @@ -12,6 +12,7 @@ func Suffix(expected string) StringRule { if !strings.HasSuffix(v, expected) { return NewError(NameSuffix, Meta('d', expected)) } + return nil } } @@ -21,6 +22,7 @@ func NoSuffix(expected string) StringRule { if strings.HasSuffix(v, expected) { return NewError(NameSuffix, Meta('d', expected)) } + return nil } } diff --git a/rule/union.go b/rule/union.go index 78fdfa0..de11c14 100644 --- a/rule/union.go +++ b/rule/union.go @@ -7,6 +7,7 @@ import ( func Union[T any](rules ...Rule[T]) Rule[T] { return func(ctx context.Context, v T) error { var e error + for _, r := range rules { if err := r(ctx, v); err != nil { e = err // TODO only return las t @@ -14,6 +15,7 @@ func Union[T any](rules ...Rule[T]) Rule[T] { return nil } } + return e } } diff --git a/rule/uri.go b/rule/uri.go index a0e1b5a..6247c35 100644 --- a/rule/uri.go +++ b/rule/uri.go @@ -12,8 +12,10 @@ func URI(ctx context.Context, v string) error { if i := strings.Index(v, "#"); i > -1 { v = v[:i] } + if _, err := url.ParseRequestURI(v); err != nil { return NewError(NameURI) } + return nil } diff --git a/rule/url.go b/rule/url.go index 37955da..592ea20 100644 --- a/rule/url.go +++ b/rule/url.go @@ -12,8 +12,10 @@ func URL(ctx context.Context, v string) error { if i := strings.Index(v, "#"); i > -1 { v = v[:i] } + if value, err := url.ParseRequestURI(v); err != nil || value.Scheme == "" { return NewError(NameURL) } + return nil } diff --git a/rule/uuid.go b/rule/uuid.go index 61055eb..de4c3a1 100644 --- a/rule/uuid.go +++ b/rule/uuid.go @@ -12,5 +12,6 @@ func UUID(ctx context.Context, v string) error { if !config.RegexUUID.MatchString(v) { return NewError(NameUUID) } + return nil } diff --git a/rule/valid.go b/rule/valid.go index 158b5ee..fd8d9f0 100644 --- a/rule/valid.go +++ b/rule/valid.go @@ -17,5 +17,6 @@ func Valid[T Validator](ctx context.Context, v T) error { if !v.Valid() { return NewError(NameValid) } + return nil }