diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 00000000..f830ad13 --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1,5 @@ +plans/ +skills/ +commands/ +agents/ +hooks/ diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..d580cb9e --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,85 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Go types modeling the [Swagger 2.0 / OpenAPI 2.0](https://swagger.io/specification/v2/) +specification. Every object in the spec --- `Swagger`, `Info`, `PathItem`, `Operation`, +`Parameter`, `Schema`, `Response`, `Header`, `SecurityScheme`, etc. --- has a corresponding +Go struct with JSON serialization (`encoding/json`) that round-trips through the spec's +JSON representation. + +This package is the **foundational data model** for the +[go-swagger](https://github.com/go-swagger/go-swagger) ecosystem. Higher-level packages +(`analysis`, `loads`, `validate`, `runtime`) consume these types to load, analyze, validate, +and serve Swagger specifications. Because it sits at the bottom of the dependency graph, +changes here ripple through the entire ecosystem. + +Key capabilities beyond plain structs: + +- **`$ref` resolution** --- the `Ref` type wraps JSON Reference pointers; the `expander` + resolves `$ref` nodes (local, remote, circular) into fully expanded specs. +- **Schema composition** --- `Schema` supports `allOf`, `additionalProperties`, + `additionalItems`, and JSON Schema validations (`minimum`, `pattern`, `enum`, etc.). +- **URL normalization** --- cross-platform path/URL normalization for `$ref` targets. +- **Embedded spec** --- a copy of the Swagger 2.0 JSON Schema is embedded via `go:embed` + for offline use. + +See [docs/MAINTAINERS.md](../docs/MAINTAINERS.md) for CI/CD, release process, and repo structure details. + +### Package layout (single package) + +| File | Contents | +|------|----------| +| `swagger.go` | Root `Swagger` type (top-level spec object) | +| `info.go` | `Info`, `ContactInfo`, `LicenseInfo` | +| `paths.go` | `Paths` (map of path patterns to `PathItem`) | +| `path_item.go` | `PathItem` (GET/PUT/POST/DELETE/... operations per path) | +| `operation.go` | `Operation` (single API operation) | +| `parameter.go` | `Parameter` (query, header, path, body, formData) | +| `header.go` | `Header` | +| `response.go`, `responses.go` | `Response`, `Responses` | +| `schema.go` | `Schema` (JSON Schema subset used by Swagger) | +| `security_scheme.go` | `SecurityScheme` | +| `items.go` | `Items` (non-body parameter schema) | +| `ref.go` | `Ref` type, JSON Reference (`$ref`) handling | +| `expander.go` | `$ref` expansion / resolution engine | +| `normalizer.go` | URL/path normalization (platform-specific variants) | +| `cache.go` | Resolution cache for expanded specs | +| `validations.go` | Common validation properties shared across types | +| `properties.go` | `SchemaProperties` ordered map | +| `embed.go` | Embedded Swagger 2.0 JSON Schema (`go:embed`) | +| `spec.go` | `MustLoadSwagger20Schema()` loader | +| `external_docs.go` | `ExternalDocumentation` | +| `tag.go` | `Tag` | +| `xml_object.go` | `XMLObject` | +| `debug.go` | Debug logging helpers | + +### Key API + +- `Swagger` --- root specification object; deserialize with `json.Unmarshal` +- `Schema` --- JSON Schema with Swagger extensions; supports `allOf`, `$ref`, validations +- `Ref` / `MustCreateRef(uri)` --- JSON Reference wrapper +- `ExpandSpec(spec, opts)` --- resolve all `$ref` nodes in a specification +- `ExpandSchema(schema, root, cache)` --- resolve `$ref` nodes in a single schema +- `ResolveRef(root, ref)` / `ResolveParameter` / `ResolveResponse` --- targeted resolution + +### Dependencies + +- `github.com/go-openapi/jsonpointer` --- JSON Pointer (RFC 6901) navigation +- `github.com/go-openapi/jsonreference` --- JSON Reference parsing +- `github.com/go-openapi/swag` --- JSON/YAML utilities, name mangling +- `github.com/go-openapi/testify/v2` --- test-only assertions (zero-dep testify fork) + +### Notable historical design decisions + +- **Mixin of spec types and `$ref`** --- many types embed both their data fields and a `Ref` + field. When `$ref` is present, the data fields are ignored per the Swagger specification. + This is modeled by custom `MarshalJSON`/`UnmarshalJSON` on each type. +- **`VendorExtensible`** --- most types embed `VendorExtensible` to capture `x-` extension + fields as `map[string]any`. +- **`SchemaProperties` as ordered slice** --- schema properties are stored as a slice of + key-value pairs (not a map) to preserve declaration order during round-trip serialization. +- **Platform-specific normalization** --- Windows path handling differs from Unix; separate + `normalizer_windows.go` / `normalizer_nonwindows.go` files handle this. diff --git a/.claude/rules/contributions.md b/.claude/rules/contributions.md new file mode 100644 index 00000000..58027b9c --- /dev/null +++ b/.claude/rules/contributions.md @@ -0,0 +1,52 @@ +--- +paths: + - "**/*" +--- + +# Contribution rules (go-openapi) + +Read `.github/CONTRIBUTING.md` before opening a pull request. + +## Commit hygiene + +- Every commit **must** be DCO signed-off (`git commit -s`) with a real email address. + PGP-signed commits are appreciated but not required. +- Agents may be listed as co-authors (`Co-Authored-By:`) but the commit **author must be the human sponsor**. + We do not accept commits solely authored by bots or agents. +- Squash commits into logical units of work before requesting review (`git rebase -i`). + +## Linting + +Before pushing, verify your changes pass linting against the base branch: + +```sh +golangci-lint run --new-from-rev master +``` + +Install the latest version if you don't have it: + +```sh +go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest +``` + +## Problem statement + +- Clearly describe the problem the PR solves, or reference an existing issue. +- PR descriptions must not be vague ("fix bug", "improve code") — explain *what* was wrong and *why* the change is correct. + +## Tests are mandatory + +- Every bug fix or feature **must** include tests that demonstrate the problem and verify the fix. +- The only exceptions are documentation changes and typo fixes. +- Aim for at least 80% coverage of your patch. +- Run the full test suite before submitting: + +For mono-repos: +```sh +go test work ./... +``` + +For single module repos: +```sh +go test ./... +``` diff --git a/.claude/rules/github-workflows-conventions.md b/.claude/rules/github-workflows-conventions.md new file mode 100644 index 00000000..33800d0e --- /dev/null +++ b/.claude/rules/github-workflows-conventions.md @@ -0,0 +1,297 @@ +--- +paths: + - ".github/workflows/**.yml" + - ".github/workflows/**.yaml" +--- + +# GitHub Actions Workflows Formatting and Style Conventions + +This rule captures YAML and bash formatting rules to provide a consistent maintainer's experience across CI workflows. + +## File Structure + +**REQUIRED**: All github action workflows are organized as a flat structure beneath `.github/workflows/`. + +> GitHub does not support a hierarchical organization for workflows yet. + +**REQUIRED**: YAML files are conventionally named `{workflow}.yml`, with the `.yml` extension. + +## Code Style & Formatting + +### Expression Spacing + +**REQUIRED**: All GitHub Actions expressions must have spaces inside the braces: + +```yaml +# ✅ CORRECT +env: + PR_URL: ${{ github.event.pull_request.html_url }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + +# ❌ WRONG +env: + PR_URL: ${{github.event.pull_request.html_url}} + TOKEN: ${{secrets.GITHUB_TOKEN}} +``` + +> Provides a consistent formatting rule. + +### Conditional Syntax + +**REQUIRED**: Always use `${{ }}` in `if:` conditions: + +```yaml +# ✅ CORRECT +if: ${{ inputs.enable-signing == 'true' }} +if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + +# ❌ WRONG (works but inconsistent) +if: inputs.enable-signing == 'true' +``` + +> Provides a consistent formatting rule. + +### GitHub Workflow Commands + +**REQUIRED**: Use workflow commands for status messages that should appear as annotations, with **double colon separator**: + +```bash +# ✅ CORRECT - Double colon (::) separator after title +echo "::notice title=build::Build completed successfully" +echo "::warning title=race-condition::Merge already in progress" +echo "::error title=deployment::Failed to deploy" + +# ❌ WRONG - Single colon separator (won't render as annotation) +echo "::notice title=build:Build completed" # Missing second ':' +echo "::warning title=x:message" # Won't display correctly +``` + +**Syntax pattern:** `::LEVEL title=TITLE::MESSAGE` +- `LEVEL`: notice, warning, or error +- Double `::` separator is required between title and message + +> Wrong syntax may raise untidy warnings and produce botched output. + +### YAML arrays formatting + +For steps, YAML arrays are formatted with the following indentation: + +```yaml +# ✅ CORRECT - Clear spacing between steps + steps: + - + name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + - + name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + +# ❌ WRONG - Dense format, more difficult to read + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + +# ❌ WRONG - YAML comment or blank line could be avoided + steps: + # + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 +``` + +## Security Best Practices + +### Version Pinning using SHAs + +**REQUIRED**: Always pin action versions to commit SHAs: + +> Runs must be repeatable with known pinned version. Automated updates are pushed frequently (e.g. daily or weekly) +> to keep pinned versions up-to-date. + +```yaml +# ✅ CORRECT - Pinned to commit SHA with version comment +uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 +uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + +# ❌ WRONG - Mutable tag reference +uses: actions/checkout@v6 +``` + +### Permission settings + +**REQUIRED**: Always set minimal permissions at the workflow level. + +```yaml +# ✅ CORRECT - Workflow level permissions set to minimum +permissions: + contents: read + +# ❌ WRONG - Workflow level permissions with undue privilege escalation +permissions: + contents: write + pull-requests: write +``` + +**REQUIRED**: Whenever a job needs elevated privileges, always raise required permissions at the job level. + +```yaml +# ✅ CORRECT - Job level permissions set to the specific requirements for that job +jobs: + dependabot: + permissions: + contents: write + pull-requests: write + uses: ./.github/workflows/auto-merge.yml + secrets: inherit + +# ❌ WRONG - Same permissions but set at workflow level instead of job level +permissions: + contents: write + pull-requests: write +``` + +> (Security best practice detected by CodeQL analysis) + +### Undue secret exposure + +**NEVER** use `secrets[inputs.name]` — always use explicit secret parameters. + +> Using keyed access to secrets forces the runner to expose ALL secrets to the job, which causes a security risk +> (caught and reported by CodeQL security analysis). + +```yaml +# ❌ SECURITY VULNERABILITY +# This exposes ALL organization and repository secrets to the runner +on: + workflow_call: + inputs: + secret-name: + type: string +jobs: + my-job: + steps: + - uses: some-action@v1 + with: + token: ${{ secrets[inputs.secret-name] }} # ❌ DANGEROUS! +``` + +**SOLUTION**: Use explicit secret parameters with fallback for defaults: + +```yaml +# ✅ SECURE +on: + workflow_call: + secrets: + gpg-private-key: + required: false +jobs: + my-job: + steps: + - uses: go-openapi/gh-actions/ci-jobs/bot-credentials@master + with: + # Falls back to go-openapi default if not explicitly passed + gpg-private-key: ${{ secrets.gpg-private-key || secrets.CI_BOT_GPG_PRIVATE_KEY }} +``` + +## Common Gotchas + +### Description fields containing parsable expressions + +**REQUIRED**: **DO NOT** use `${{ }}` expressions in description fields: + +> They may be parsed by the runner, wrongly interpreted or causing failure (e.g. "not defined in this context"). + +```yaml +# ❌ WRONG - Can cause YAML parsing errors +description: | + Pass it as: gpg-private-key: ${{ secrets.MY_KEY }} + +# ✅ CORRECT +description: | + Pass it as: secrets.MY_KEY +``` + +### Boolean inputs + +**Boolean inputs are forbidden**: NEVER use `type: boolean` for workflow inputs due to unpredictable type coercion + +> gh-action expressions using boolean job inputs are hard to predict and come with many quirks. + + ```yaml + # ❌ FORBIDDEN - Boolean inputs have type coercion issues + on: + workflow_call: + inputs: + enable-feature: + type: boolean # ❌ NEVER USE THIS + default: true + + # The pattern `x == 'true' || x == true` seems safe but fails when: + # - x is not a boolean: `x == true` evaluates to true if x != null + # - Type coercion is unpredictable and error-prone + + # ✅ CORRECT - Always use string type for boolean-like inputs + on: + workflow_call: + inputs: + enable-feature: + type: string # ✅ Use string instead + default: 'true' # String value + + jobs: + my-job: + # Simple, reliable comparison + if: ${{ inputs.enable-feature == 'true' }} + + # ✅ In bash, this works perfectly (inputs are always strings in bash): + if [[ '${{ inputs.enable-feature }}' == 'true' ]]; then + echo "Feature enabled" + fi + ``` + + **Rule**: Use `type: string` with values `'true'` or `'false'` for all boolean-like workflow inputs. + + **Note**: Step outputs and bash variables are always strings, so `x == 'true'` works fine for those. + +### YAML fold scalars in action inputs + +**NEVER** use `>` or `>-` (fold scalars) for `with:` input values: + +> The YAML spec says fold scalars replace newlines with spaces, but the GitHub Actions runner +> does not reliably honor this for action inputs. The action receives the literal multi-line string +> instead of a single folded line, which breaks flag parsing. + +```yaml +# ❌ BROKEN - Fold scalar, args received with embedded newlines +- uses: goreleaser/goreleaser-action@... + with: + args: >- + release + --clean + --release-notes /tmp/notes.md + +# ✅ CORRECT - Single line +- uses: goreleaser/goreleaser-action@... + with: + args: release --clean --release-notes /tmp/notes.md + +# ✅ CORRECT - Literal block scalar (|) is fine for run: scripts +- run: | + echo "line 1" + echo "line 2" +``` + +**Rule**: Use single-line strings for `with:` inputs. Only use `|` (literal block scalar) for `run:` scripts where multi-line is intentional. diff --git a/.claude/rules/go-conventions.md b/.claude/rules/go-conventions.md new file mode 100644 index 00000000..9c2c9240 --- /dev/null +++ b/.claude/rules/go-conventions.md @@ -0,0 +1,11 @@ +--- +paths: + - "**/*.go" +--- + +# Code conventions (go-openapi) + +- All files must have SPDX license headers (Apache-2.0). +- Go version policy: support the 2 latest stable Go minor versions. +- Commits require DCO sign-off (`git commit -s`). +- use `golangci-lint fmt` to format code (not `gofmt` or `gofumpt`) diff --git a/.claude/rules/linting.md b/.claude/rules/linting.md new file mode 100644 index 00000000..a4456d42 --- /dev/null +++ b/.claude/rules/linting.md @@ -0,0 +1,17 @@ +--- +paths: + - "**/*.go" +--- + +# Linting conventions (go-openapi) + +```sh +golangci-lint run +``` + +Config: `.golangci.yml` — posture is `default: all` with explicit disables. +See `docs/STYLE.md` for the rationale behind each disabled linter. + +Key rules: +- Every `//nolint` directive **must** have an inline comment explaining why. +- Prefer disabling a linter over scattering `//nolint` across the codebase. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 00000000..6974abaa --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/*_test.go" +--- + +# Testing conventions (go-openapi) + +## Running tests + +**Single module repos:** + +```sh +go test ./... +``` + +**Mono-repos (with `go.work`):** + +```sh +# All modules +go test work ./... + +# Single module +go test ./conv/... +``` + +Note: in mono-repos, plain `go test ./...` only tests the root module. +The `work` pattern expands to all modules listed in `go.work`. + +CI runs tests on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race` via `gotestsum`. + +## Fuzz tests + +```sh +# List all fuzz targets +go test -list Fuzz ./... + +# Run a specific target (go test -fuzz cannot span multiple packages) +go test -fuzz=Fuzz -run='FuzzTargetName$' -fuzztime=1m30s ./package +``` + +Fuzz corpus lives in `testdata/fuzz/` within each package. CI runs each fuzz target for 1m30s +with a 5m minimize timeout. + +## Test framework + +`github.com/go-openapi/testify/v2` — a zero-dep fork of `stretchr/testify`. +Because it's a fork, `testifylint` does not work. diff --git a/.cliff.toml b/.cliff.toml deleted file mode 100644 index 702629f5..00000000 --- a/.cliff.toml +++ /dev/null @@ -1,181 +0,0 @@ -# git-cliff ~ configuration file -# https://git-cliff.org/docs/configuration - -[changelog] -header = """ -""" - -footer = """ - ------ - -**[{{ remote.github.repo }}]({{ self::remote_url() }}) license terms** - -[![License][license-badge]][license-url] - -[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg -[license-url]: {{ self::remote_url() }}/?tab=Apache-2.0-1-ov-file#readme - -{%- macro remote_url() -%} - https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} -{%- endmacro -%} -""" - -body = """ -{%- if version %} -## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} -{%- else %} -## [unreleased] -{%- endif %} -{%- if message %} - {%- raw %}\n{% endraw %} -{{ message }} - {%- raw %}\n{% endraw %} -{%- endif %} -{%- if version %} - {%- if previous.version %} - -**Full Changelog**: <{{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}> - {%- endif %} -{%- else %} - {%- raw %}\n{% endraw %} -{%- endif %} - -{%- if statistics %}{% if statistics.commit_count %} - {%- raw %}\n{% endraw %} -{{ statistics.commit_count }} commits in this release. - {%- raw %}\n{% endraw %} -{%- endif %}{% endif %} ------ - -{%- for group, commits in commits | group_by(attribute="group") %} - {%- raw %}\n{% endraw %} -### {{ group | upper_first }} - {%- raw %}\n{% endraw %} - {%- for commit in commits %} - {%- if commit.remote.pr_title %} - {%- set commit_message = commit.remote.pr_title %} - {%- else %} - {%- set commit_message = commit.message %} - {%- endif %} -* {{ commit_message | split(pat="\n") | first | trim }} - {%- if commit.remote.username %} -{%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }}) - {%- endif %} - {%- if commit.remote.pr_number %} -{%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) - {%- endif %} -{%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }}) - {%- endfor %} -{%- endfor %} - -{%- if github %} -{%- raw %}\n{% endraw -%} - {%- set all_contributors = github.contributors | length %} - {%- if github.contributors | filter(attribute="username", value="dependabot[bot]") | length < all_contributors %} ------ - -### People who contributed to this release - {% endif %} - {%- for contributor in github.contributors | filter(attribute="username") | sort(attribute="username") %} - {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %} -* [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) - {%- endif %} - {%- endfor %} - - {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} ------ - {%- raw %}\n{% endraw %} - -### New Contributors - {%- endif %} - - {%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} - {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %} -* @{{ contributor.username }} made their first contribution - {%- if contributor.pr_number %} - in [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ - {%- endif %} - {%- endif %} - {%- endfor %} -{%- endif %} - -{%- raw %}\n{% endraw %} - -{%- macro remote_url() -%} - https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} -{%- endmacro -%} -""" -# Remove leading and trailing whitespaces from the changelog's body. -trim = true -# Render body even when there are no releases to process. -render_always = true -# An array of regex based postprocessors to modify the changelog. -postprocessors = [ - # Replace the placeholder with a URL. - #{ pattern = '', replace = "https://github.com/orhun/git-cliff" }, -] -# output file path -# output = "test.md" - -[git] -# Parse commits according to the conventional commits specification. -# See https://www.conventionalcommits.org -conventional_commits = false -# Exclude commits that do not match the conventional commits specification. -filter_unconventional = false -# Require all commits to be conventional. -# Takes precedence over filter_unconventional. -require_conventional = false -# Split commits on newlines, treating each line as an individual commit. -split_commits = false -# An array of regex based parsers to modify commit messages prior to further processing. -commit_preprocessors = [ - # Replace issue numbers with link templates to be updated in `changelog.postprocessors`. - #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, - # Check spelling of the commit message using https://github.com/crate-ci/typos. - # If the spelling is incorrect, it will be fixed automatically. - #{ pattern = '.*', replace_command = 'typos --write-changes -' } -] -# Prevent commits that are breaking from being excluded by commit parsers. -protect_breaking_commits = false -# An array of regex based parsers for extracting data from the commit message. -# Assigns commits to groups. -# Optionally sets the commit's scope and can decide to exclude commits from further processing. -commit_parsers = [ - { message = "^[Cc]hore\\([Rr]elease\\): prepare for", skip = true }, - { message = "(^[Mm]erge)|([Mm]erge conflict)", skip = true }, - { field = "author.name", pattern = "dependabot*", group = "Updates" }, - { message = "([Ss]ecurity)|([Vv]uln)", group = "Security" }, - { body = "(.*[Ss]ecurity)|([Vv]uln)", group = "Security" }, - { message = "([Cc]hore\\(lint\\))|(style)|(lint)|(codeql)|(golangci)", group = "Code quality" }, - { message = "(^[Dd]oc)|((?i)readme)|(badge)|(typo)|(documentation)", group = "Documentation" }, - { message = "(^[Ff]eat)|(^[Ee]nhancement)", group = "Implemented enhancements" }, - { message = "(^ci)|(\\(ci\\))|(fixup\\s+ci)|(fix\\s+ci)|(license)|(example)", group = "Miscellaneous tasks" }, - { message = "^test", group = "Testing" }, - { message = "(^fix)|(panic)", group = "Fixed bugs" }, - { message = "(^refact)|(rework)", group = "Refactor" }, - { message = "(^[Pp]erf)|(performance)", group = "Performance" }, - { message = "(^[Cc]hore)", group = "Miscellaneous tasks" }, - { message = "^[Rr]evert", group = "Reverted changes" }, - { message = "(upgrade.*?go)|(go\\s+version)", group = "Updates" }, - { message = ".*", group = "Other" }, -] -# Exclude commits that are not matched by any commit parser. -filter_commits = false -# An array of link parsers for extracting external references, and turning them into URLs, using regex. -link_parsers = [] -# Include only the tags that belong to the current branch. -use_branch_tags = false -# Order releases topologically instead of chronologically. -topo_order = false -# Order releases topologically instead of chronologically. -topo_order_commits = true -# Order of commits in each group/release within the changelog. -# Allowed values: newest, oldest -sort_commits = "newest" -# Process submodules commits -recurse_submodules = false - -#[remote.github] -#owner = "go-openapi" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 85707f7e..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,214 +0,0 @@ -## Contribution Guidelines - -You'll find below general guidelines, which mostly correspond to standard practices for open sourced repositories. - ->**TL;DR** -> -> If you're already an experienced go developer on github, then you should just feel at home with us -> and you may well skip the rest of this document. -> -> You'll essentially find the usual guideline for a go library project on github. - -These guidelines are general to all libraries published on github by the `go-openapi` organization. - -You'll find more detailed (or repo-specific) instructions in the [maintainer's docs](../docs). - -## How can I contribute? - -There are many ways in which you can contribute. Here are a few ideas: - - * Reporting Issues / Bugs - * Suggesting Improvements - * Code - * bug fixes and new features that are within the main project scope - * improving test coverage - * addressing code quality issues - * Documentation - * Art work that makes the project look great - -## Questions & issues - -### Asking questions - -You may inquire about anything about this library by reporting a "Question" issue on github. - -### Reporting issues - -Reporting a problem with our libraries _is_ a valuable contribution. - -You can do this on the github issues page of this repository. - -Please be as specific as possible when describing your issue. - -Whenever relevant, please provide information about your environment (go version, OS). - -Adding a code snippet to reproduce the issue is great, and as a big time saver for maintainers. - -### Triaging issues - -You can help triage issues which may include: - -* reproducing bug reports -* asking for important information, such as version numbers or reproduction instructions -* answering questions and sharing your insight in issue comments - -## Code contributions - -### Pull requests are always welcome - -We are always thrilled to receive pull requests, and we do our best to -process them as fast as possible. - -Not sure if that typo is worth a pull request? Do it! We will appreciate it. - -If your pull request is not accepted on the first try, don't be discouraged! -If there's a problem with the implementation, hopefully you received feedback on what to improve. - -If you have a lot of ideas or a lot of issues to solve, try to refrain a bit and post focused -pull requests. -Think that they must be reviewed by a maintainer and it is easy to lost track of things on big PRs. - -We're trying very hard to keep the go-openapi packages lean and focused. -These packages constitute a toolkit: it won't do everything for everybody out of the box, -but everybody can use it to do just about everything related to OpenAPI. - -This means that we might decide against incorporating a new feature. - -However, there might be a way to implement that feature *on top of* our libraries. - -### Environment - -You just need a `go` compiler to be installed. No special tools are needed to work with our libraries. - -The go compiler version required is always the old stable (latest minor go version - 1). - -If you're already used to work with `go` you should already have everything in place. - -Although not required, you'll be certainly more productive with a local installation of `golangci-lint`, -the meta-linter our CI uses. - -If you don't have it, you may install it like so: - -```sh -go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest -``` - -### Conventions - -#### Git flow - -Fork the repo and make changes to your fork in a feature branch. - -To submit a pull request, push your branch to your fork (e.g. `upstream` remote): -github will propose to open a pull request on the original repository. - -Typically you'd follow some common naming conventions: - -- if it's a bugfix branch, name it `fix/XXX-something`where XXX is the number of the - issue on github -- if it's a feature branch, create an enhancement issue to announce your - intentions, and name it `feature/XXX-something` where XXX is the number of the issue. - -> NOTE: we don't enforce naming conventions on branches: it's your fork after all. - -#### Tests - -Submit unit tests for your changes. - -Go has a great built-in test framework ; use it! - -Take a look at existing tests for inspiration, and run the full test suite on your branch -before submitting a pull request. - -Our CI measures test coverage and the test coverage of every patch. -Although not a blocking step - because there are so many special cases - -this is an indicator that maintainers consider when approving a PR. - -Please try your best to cover about 80% of your patch. - -#### Code style - -You may read our stance on code style [there](../docs/STYLE.md). - -#### Documentation - -Don't forget to update the documentation when creating or modifying features. - -Most documentation for this library is directly found in code as comments for godoc. - -The documentation for the go-openapi packages is published on the public go docs site: - - - -Check your documentation changes for clarity, concision, and correctness. - -If you want to assess the rendering of your changes when published to `pkg.go.dev`, you may -want to install the `pkgsite` tool proposed by `golang.org`. - -```sh -go install golang.org/x/pkgsite/cmd/pkgsite@latest -``` - -Then run on the repository folder: -```sh -pkgsite . -``` - -This wil run a godoc server locally where you may see the documentation generated from your local repository. - -#### Commit messages - -Pull requests descriptions should be as clear as possible and include a -reference to all the issues that they address. - -Pull requests must not contain commits from other users or branches. - -Commit messages are not required to follow the "conventional commit" rule, but it's certainly a good -thing to follow this guidelinea (e.g. "fix: blah blah", "ci: did this", "feat: did that" ...). - -The title in your commit message is used directly to produce our release notes: try to keep them neat. - -The commit message body should detail your changes. - -If an issue should be closed by a commit, please add this reference in the commit body: - -``` -* fixes #{issue number} -``` - -#### Code review - -Code review comments may be added to your pull request. - -Discuss, then make the suggested modifications and push additional commits to your feature branch. - -Be sure to post a comment after pushing. The new commits will show up in the pull -request automatically, but the reviewers will not be notified unless you comment. - -Before the pull request is merged, -**make sure that you squash your commits into logical units of work** -using `git rebase -i` and `git push -f`. - -After every commit the test suite should be passing. - -Include documentation changes in the same commit so that a revert would remove all traces of the feature or fix. - -#### Sign your work - -The sign-off is a simple line at the end of your commit message, -which certifies that you wrote it or otherwise have the right to -pass it on as an open-source patch. - -We require the simple DCO below with an email signing your commit. -PGP-signed commit are greatly appreciated but not required. - -The rules are pretty simple: - -* read our [DCO](./DCO.md) (from [developercertificate.org](http://developercertificate.org/)) -* if you agree with these terms, then you just add a line to every git commit message - - Signed-off-by: Joe Smith - -using your real name (sorry, no pseudonyms or anonymous contributions.) - -You can add the sign off when creating the git commit via `git commit -s`. diff --git a/.github/DCO.md b/.github/DCO.md deleted file mode 100644 index e168dc4c..00000000 --- a/.github/DCO.md +++ /dev/null @@ -1,40 +0,0 @@ - # Developer's Certificate of Origin - -``` -Developer Certificate of Origin -Version 1.1 - -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -660 York Street, Suite 102, -San Francisco, CA 94110 USA - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - - -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. -``` diff --git a/.github/copilot b/.github/copilot new file mode 120000 index 00000000..52694831 --- /dev/null +++ b/.github/copilot @@ -0,0 +1 @@ +../.claude/rules \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..9131b7ed --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,61 @@ +# Copilot Instructions — spec + +## Project Overview + +Go types modeling the Swagger 2.0 / OpenAPI 2.0 specification. This package is the +foundational data model for the go-swagger ecosystem — every specification object +(`Swagger`, `Schema`, `Operation`, `Parameter`, etc.) is a Go struct with JSON +round-trip serialization. It also includes a `$ref` expansion engine for resolving +JSON References across local and remote documents. + +Single module: `github.com/go-openapi/spec`. + +### Package layout (single package) + +| File | Contents | +|------|----------| +| `swagger.go` | Root `Swagger` type (top-level spec object) | +| `schema.go` | `Schema` (JSON Schema subset used by Swagger) | +| `operation.go` | `Operation` (single API operation) | +| `parameter.go` | `Parameter` (query, header, path, body, formData) | +| `response.go`, `responses.go` | `Response`, `Responses` | +| `ref.go` | `Ref` type, JSON Reference (`$ref`) handling | +| `expander.go` | `$ref` expansion / resolution engine | +| `normalizer.go` | URL/path normalization (platform-specific variants) | + +### Key API + +- `Swagger` — root specification object; deserialize with `json.Unmarshal` +- `Schema` — JSON Schema with Swagger extensions; supports `allOf`, `$ref`, validations +- `Ref` / `MustCreateRef(uri)` — JSON Reference wrapper +- `ExpandSpec(spec, opts)` — resolve all `$ref` nodes in a specification +- `ExpandSchema(schema, root, cache)` — resolve `$ref` nodes in a single schema +- `ResolveRef(root, ref)` / `ResolveParameter` / `ResolveResponse` — targeted resolution + +### Dependencies + +- `github.com/go-openapi/jsonpointer` — JSON Pointer (RFC 6901) navigation +- `github.com/go-openapi/jsonreference` — JSON Reference parsing +- `github.com/go-openapi/swag` — JSON/YAML utilities, name mangling +- `github.com/go-openapi/testify/v2` — test-only assertions (zero-dep testify fork) + +## Building & testing + +```sh +go test ./... +``` + +## Conventions + +Coding conventions are found beneath `.github/copilot` + +### Summary + +- All `.go` files must have SPDX license headers (Apache-2.0). +- Commits require DCO sign-off (`git commit -s`). +- Linting: `golangci-lint run` — config in `.golangci.yml` (posture: `default: all` with explicit disables). +- Every `//nolint` directive **must** have an inline comment explaining why. +- Tests: `go test ./...`. CI runs on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race`. +- Test framework: `github.com/go-openapi/testify/v2` (not `stretchr/testify`; `testifylint` does not work). + +See `.github/copilot/` (symlinked to `.claude/rules/`) for detailed rules on Go conventions, linting, testing, and contributions. diff --git a/.github/wordlist.txt b/.github/wordlist.txt new file mode 100644 index 00000000..6dc83168 --- /dev/null +++ b/.github/wordlist.txt @@ -0,0 +1,44 @@ +CodeFactor +CodeQL +DCO +GoDoc +JSON +Maintainer's +PR's +PRs +Repo +SPDX +TODOs +Triaging +UI +XYZ +YAML +agentic +ci +codebase +codecov +config +dependabot +dev +developercertificate +fka +github +godoc +golang +golangci +jsonpointer +jsonschema +linter's +linters +maintainer's +md +metalinter +monorepo +openapi +prepended +repos +semver +sexualized +unmarshal +unmarshaling +vuln diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index b4009db9..c3377a63 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -1,43 +1,15 @@ name: Dependabot auto-merge -on: pull_request permissions: - contents: write - pull-requests: write + contents: read + +on: + pull_request: jobs: dependabot: - runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v1 - - - name: Auto-approve all dependabot PRs - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Auto-merge dependabot PRs for development dependencies - if: contains(steps.metadata.outputs.dependency-group, 'development-dependencies') - run: gh pr merge --auto --rebase "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Auto-merge dependabot PRs for go-openapi patches - if: contains(steps.metadata.outputs.dependency-group, 'go-openapi-dependencies') && (steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch') - run: gh pr merge --auto --rebase "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Auto-merge dependabot PRs for golang.org updates - if: contains(steps.metadata.outputs.dependency-group, 'golang.org-dependencies') - run: gh pr merge --auto --rebase "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - + permissions: + contents: write + pull-requests: write + uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/bump-release.yml b/.github/workflows/bump-release.yml new file mode 100644 index 00000000..fa44d33e --- /dev/null +++ b/.github/workflows/bump-release.yml @@ -0,0 +1,38 @@ +name: Bump Release + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + bump-type: + description: Type of bump (patch, minor, major) + type: choice + options: + - patch + - minor + - major + default: patch + required: false + tag-message-title: + description: Tag message title to prepend to the release notes + required: false + type: string + tag-message-body: + description: | + Tag message body to prepend to the release notes. + (use "|" to replace end of line). + required: false + type: string + +jobs: + bump-release: + permissions: + contents: write + uses: go-openapi/ci-workflows/.github/workflows/bump-release.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + with: + bump-type: ${{ inputs.bump-type }} + tag-message-title: ${{ inputs.tag-message-title }} + tag-message-body: ${{ inputs.tag-message-body }} + secrets: inherit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..b48a31f9 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,22 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + paths-ignore: # remove this clause if CodeQL is a required check + - '**/*.md' + schedule: + - cron: '39 19 * * 5' + +permissions: + contents: read + +jobs: + codeql: + permissions: + contents: read + security-events: write + uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..6be321f1 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,18 @@ +name: Contributors + +on: + schedule: + - cron: '49 4 1 * *' + + workflow_dispatch: + +permissions: + contents: read + +jobs: + contributors: + permissions: + pull-requests: write + contents: write + uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 340ce07e..bdc4608d 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -1,58 +1,17 @@ name: go test +permissions: + pull-requests: read + contents: read + on: push: - tags: - - v* branches: - master pull_request: jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: 1.25.5 - check-latest: true - cache: true - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - version: v1.59.1 - only-new-issues: true - skip-cache: true - test: - name: Unit tests - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - go_version: ['oldstable', 'stable' ] - - steps: - - name: Run unit tests - - uses: actions/setup-go@v5 - with: - go-version: '${{ matrix.go_version }}' - check-latest: true - cache: true - - - uses: actions/checkout@v4 - - - run: go test -v -race -coverprofile="coverage-${{ matrix.os }}.${{ matrix.go_version }}.out" -covermode=atomic -coverpkg=$(go list)/... ./... - - name: Upload coverage to codecov - uses: codecov/codecov-action@v4 - - with: - files: './coverage-${{ matrix.os }}.${{ matrix.go_version }}.out' - flags: '${{ matrix.go_version }}' - os: '${{ matrix.os }}' - fail_ci_if_error: false - verbose: true + uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/monitor-bot-pr.yml b/.github/workflows/monitor-bot-pr.yml new file mode 100644 index 00000000..3e5e18ea --- /dev/null +++ b/.github/workflows/monitor-bot-pr.yml @@ -0,0 +1,18 @@ +name: Monitor bot PRs + +on: + workflow_dispatch: + schedule: + - cron: '18 6 * * *' + +permissions: + contents: read + +jobs: + monitor-pr: + permissions: + contents: write + pull-requests: write + statuses: read + uses: go-openapi/ci-workflows/.github/workflows/monitor-bot-pr.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/scanner.yml b/.github/workflows/scanner.yml new file mode 100644 index 00000000..15ad371c --- /dev/null +++ b/.github/workflows/scanner.yml @@ -0,0 +1,19 @@ +name: Vulnerability scans + +on: + branch_protection_rule: + push: + branches: [ "master" ] + schedule: + - cron: '18 4 * * 3' + +permissions: + contents: read + +jobs: + scanners: + permissions: + contents: read + security-events: write + uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + secrets: inherit diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml new file mode 100644 index 00000000..11736a8e --- /dev/null +++ b/.github/workflows/tag-release.yml @@ -0,0 +1,19 @@ +name: Release on tag + +permissions: + contents: read + +on: + push: + tags: + - v[0-9]+* + +jobs: + gh-release: + name: Create release + permissions: + contents: write + uses: go-openapi/ci-workflows/.github/workflows/release.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + with: + tag: ${{ github.ref_name }} + secrets: inherit diff --git a/.github/workflows/webhook-announcements.yml b/.github/workflows/webhook-announcements.yml new file mode 100644 index 00000000..a21bb820 --- /dev/null +++ b/.github/workflows/webhook-announcements.yml @@ -0,0 +1,60 @@ +name: Webhook Announcements + +# invoke the common webhook-announcements workflow, scanning README. +# +# Two modes: +# +# * push: diff on "## Announcements" section exercises the +# real before..after detection and post to discord channel. +# +# * workflow_dispatch: a manual live run. Optionally provide an arbitrary webhook URL and +# it POSTs for real. By default it diffs against the git empty tree, so every +# announcement currently in the fixture is posted — no need to craft a diff. +# +# NOTE: the webhook URL you type is a workflow_dispatch input and is therefore +# visible in the run's UI/logs. Use a throwaway test webhook (and/or rotate it +# afterwards), not the production go-openapi webhook. + +permissions: + contents: read + +on: + push: + branches: + - master + paths: + - 'README.md' + + workflow_dispatch: + inputs: + webhook-url: + description: | + Webhook URL to POST to (e.g. a test Discord channel webhook). + Visible in run logs — use a throwaway webhook. + type: string + default: '' + compare-base: + description: | + Git ref to diff the fixture against. The default empty-tree SHA posts + every announcement currently in the fixture. + type: string + default: "" + dry-run: + description: | + Print payloads instead of posting. + type: choice + options: + - 'false' + - 'true' + default: 'false' + +jobs: + announce: + uses: go-openapi/ci-workflows/.github/workflows/webhook-announcements.yml@af4c93f45481ea7d24ac2a9858272cc03daf424e # v0.4.0 + with: + scanned-markdown: README.md + # On push: normal before..after diff (empty + # compare-base). On dispatch: honor the provided inputs. + dry-run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry-run || 'false' }} + compare-base: ${{ github.event_name == 'workflow_dispatch' && inputs.compare-base || '' }} + secrets: inherit diff --git a/.gitignore b/.gitignore index f47cb204..d8f4186f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ *.out +*.cov +.idea +.env +.mcp.json diff --git a/.golangci.yml b/.golangci.yml index fea0b523..9d273317 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,18 +4,24 @@ linters: disable: - depguard - funlen + - goconst - godox + - gomodguard + - gomodguard_v2 - exhaustruct - nlreturn - nonamedreturns + - noinlineerr - paralleltest + - recvcheck - testpackage + - thelper - tparallel - varnamelen - whitespace - wrapcheck - wsl - - typecheck + - wsl_v5 settings: dupl: threshold: 200 diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000..02dd1341 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +.github/copilot-instructions.md \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9322b065..bac878f2 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -23,7 +23,9 @@ include: Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or + advances + * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic @@ -55,7 +57,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at ivan+abuse@flanders.co.nz. All +reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. @@ -68,7 +70,7 @@ members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +available at [][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 47d6a56d..12fd069b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,47 +4,47 @@ | Total Contributors | Total Contributions | | --- | --- | -| 38 | 388 | +| 38 | 403 | | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @casualjim | 191 | https://github.com/go-openapi/spec/commits?author=casualjim | -| @fredbi | 86 | https://github.com/go-openapi/spec/commits?author=fredbi | -| @pytlesk4 | 26 | https://github.com/go-openapi/spec/commits?author=pytlesk4 | -| @kul-amr | 10 | https://github.com/go-openapi/spec/commits?author=kul-amr | -| @keramix | 10 | https://github.com/go-openapi/spec/commits?author=keramix | -| @youyuanwu | 8 | https://github.com/go-openapi/spec/commits?author=youyuanwu | -| @pengsrc | 7 | https://github.com/go-openapi/spec/commits?author=pengsrc | -| @alphacentory | 5 | https://github.com/go-openapi/spec/commits?author=alphacentory | -| @mtfelian | 4 | https://github.com/go-openapi/spec/commits?author=mtfelian | -| @Capstan | 4 | https://github.com/go-openapi/spec/commits?author=Capstan | -| @sdghchj | 4 | https://github.com/go-openapi/spec/commits?author=sdghchj | -| @databus23 | 2 | https://github.com/go-openapi/spec/commits?author=databus23 | -| @vburenin | 2 | https://github.com/go-openapi/spec/commits?author=vburenin | -| @petrkotas | 2 | https://github.com/go-openapi/spec/commits?author=petrkotas | -| @nikhita | 2 | https://github.com/go-openapi/spec/commits?author=nikhita | -| @hypnoglow | 2 | https://github.com/go-openapi/spec/commits?author=hypnoglow | -| @carvind | 2 | https://github.com/go-openapi/spec/commits?author=carvind | -| @ujjwalsh | 1 | https://github.com/go-openapi/spec/commits?author=ujjwalsh | -| @mbohlool | 1 | https://github.com/go-openapi/spec/commits?author=mbohlool | -| @j2gg0s | 1 | https://github.com/go-openapi/spec/commits?author=j2gg0s | -| @ishveda | 1 | https://github.com/go-openapi/spec/commits?author=ishveda | -| @micln | 1 | https://github.com/go-openapi/spec/commits?author=micln | -| @GlenDC | 1 | https://github.com/go-openapi/spec/commits?author=GlenDC | -| @agmikhailov | 1 | https://github.com/go-openapi/spec/commits?author=agmikhailov | -| @tgraf | 1 | https://github.com/go-openapi/spec/commits?author=tgraf | -| @zhsj | 1 | https://github.com/go-openapi/spec/commits?author=zhsj | -| @sebastien-rosset | 1 | https://github.com/go-openapi/spec/commits?author=sebastien-rosset | -| @alexandear | 1 | https://github.com/go-openapi/spec/commits?author=alexandear | -| @morlay | 1 | https://github.com/go-openapi/spec/commits?author=morlay | -| @mikedanese | 1 | https://github.com/go-openapi/spec/commits?author=mikedanese | -| @koron | 1 | https://github.com/go-openapi/spec/commits?author=koron | -| @honza | 1 | https://github.com/go-openapi/spec/commits?author=honza | -| @gbjk | 1 | https://github.com/go-openapi/spec/commits?author=gbjk | -| @faguirre1 | 1 | https://github.com/go-openapi/spec/commits?author=faguirre1 | -| @ethantkoenig | 1 | https://github.com/go-openapi/spec/commits?author=ethantkoenig | -| @sttts | 1 | https://github.com/go-openapi/spec/commits?author=sttts | -| @ChandanChainani | 1 | https://github.com/go-openapi/spec/commits?author=ChandanChainani | -| @bvwells | 1 | https://github.com/go-openapi/spec/commits?author=bvwells | +| @casualjim | 191 | | +| @fredbi | 101 | | +| @pytlesk4 | 26 | | +| @kul-amr | 10 | | +| @keramix | 10 | | +| @youyuanwu | 8 | | +| @pengsrc | 7 | | +| @alphacentory | 5 | | +| @mtfelian | 4 | | +| @Capstan | 4 | | +| @sdghchj | 4 | | +| @databus23 | 2 | | +| @vburenin | 2 | | +| @petrkotas | 2 | | +| @nikhita | 2 | | +| @hypnoglow | 2 | | +| @carvind | 2 | | +| @ujjwalsh | 1 | | +| @mbohlool | 1 | | +| @j2gg0s | 1 | | +| @ishveda | 1 | | +| @micln | 1 | | +| @GlenDC | 1 | | +| @agmikhailov | 1 | | +| @tgraf | 1 | | +| @zhsj | 1 | | +| @sebastien-rosset | 1 | | +| @alexandear | 1 | | +| @morlay | 1 | | +| @mikedanese | 1 | | +| @koron | 1 | | +| @honza | 1 | | +| @gbjk | 1 | | +| @faguirre1 | 1 | | +| @ethantkoenig | 1 | | +| @sttts | 1 | | +| @ChandanChainani | 1 | | +| @bvwells | 1 | | - _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_ + _this file was generated by the [Contributors GitHub Action](https://github.com/github-community-projects/contributors)_ diff --git a/README.md b/README.md index 5a877d28..7c96eb9a 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,19 @@ [![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url] -[![GoDoc][godoc-badge]][godoc-url] [![Slack Channel][slack-logo]![slack-badge]][slack-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] +[![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge] --- The object model for OpenAPI v2 specification documents. +## Announcements + +* **2025-12-19** : new community chat on discord + * a new discord community channel is available to be notified of changes and support users + +You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] + ## Status API is stable. @@ -45,7 +52,7 @@ go get github.com/go-openapi/spec > There is no plan to make it evolve toward supporting OpenAPI 3.x. > This [discussion thread](https://github.com/go-openapi/spec/issues/21) relates the full story. > -> An early attempt to support Swagger 3 may be found at: https://github.com/go-openapi/spec3 +> An early attempt to support Swagger 3 may be found at: * Does the unmarshaling support YAML? @@ -54,13 +61,13 @@ go get github.com/go-openapi/spec > In order to load a YAML document as a Swagger spec, you need to use the loaders provided by > github.com/go-openapi/loads > -> Take a look at the example there: https://pkg.go.dev/github.com/go-openapi/loads#example-Spec +> Take a look at the example there: > -> See also https://github.com/go-openapi/spec/issues/164 +> See also * How can I validate a spec? -> Validation is provided by [the validate package](http://github.com/go-openapi/validate) +Validation is provided by [the validate package](http://github.com/go-openapi/validate) * Why do we have an `ID` field for `Schema` which is not part of the swagger spec? @@ -68,7 +75,7 @@ go get github.com/go-openapi/spec > how `$ref` are resolved. > This `id` does not conflict with any property named `id`. > -> See also https://github.com/go-openapi/spec/issues/23 +> See also ## Change log @@ -85,9 +92,9 @@ This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE). ## Other documentation * [All-time contributors](./CONTRIBUTORS.md) -* [Contributing guidelines](.github/CONTRIBUTING.md) -* [Maintainers documentation](docs/MAINTAINERS.md) -* [Code style](docs/STYLE.md) +* [Contributing guidelines][contributing-doc-site] +* [Maintainers documentation][maintainers-doc-site] +* [Code style][style-doc-site] ## Cutting a new release @@ -122,9 +129,9 @@ Maintainers can cut a new release by either: [doc-url]: https://goswagger.io/go-openapi [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/spec [godoc-url]: http://pkg.go.dev/github.com/go-openapi/spec -[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png -[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM -[slack-url]: https://goswagger.slack.com/archives/C04R30YMU +[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue +[discord-url]: https://discord.gg/FfnFYaC3k5 + [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg [license-url]: https://github.com/go-openapi/spec/?tab=Apache-2.0-1-ov-file#readme @@ -133,3 +140,7 @@ Maintainers can cut a new release by either: [goversion-url]: https://github.com/go-openapi/spec/blob/master/go.mod [top-badge]: https://img.shields.io/github/languages/top/go-openapi/spec [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/spec/latest + +[contributing-doc-site]: https://go-openapi.github.io/doc-site/contributing/contributing/index.html +[maintainers-doc-site]: https://go-openapi.github.io/doc-site/maintainers/index.html +[style-doc-site]: https://go-openapi.github.io/doc-site/contributing/style/index.html diff --git a/SECURITY.md b/SECURITY.md index 2a7b6f09..1fea2c57 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,14 +6,32 @@ This policy outlines the commitment and practices of the go-openapi maintainers | Version | Supported | | ------- | ------------------ | -| 0.22.x | :white_check_mark: | +| O.x | :white_check_mark: | + +## Vulnerability checks in place + +This repository uses automated vulnerability scans, at every merged commit and at least once a week. + +We use: + +* [`GitHub CodeQL`][codeql-url] +* [`trivy`][trivy-url] +* [`govulncheck`][govulncheck-url] + +Reports are centralized in github security reports and visible only to the maintainers. ## Reporting a vulnerability If you become aware of a security vulnerability that affects the current repository, -please report it privately to the maintainers. +**please report it privately to the maintainers** +rather than opening a publicly visible GitHub issue. + +Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url]. -Please follow the instructions provided by github to -[Privately report a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability). +> [!NOTE] +> On Github, navigate to the project's "Security" tab then click on "Report a vulnerability". -TL;DR: on Github, navigate to the project's "Security" tab then click on "Report a vulnerability". +[codeql-url]: https://github.com/github/codeql +[trivy-url]: https://trivy.dev/docs/latest/getting-started +[govulncheck-url]: https://go.dev/blog/govulncheck +[github-guidance-url]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability diff --git a/auth_test.go b/auth_test.go index 4f25a344..486d74de 100644 --- a/auth_test.go +++ b/auth_test.go @@ -5,118 +5,103 @@ package spec import ( "testing" + + "github.com/go-openapi/testify/v2/assert" ) func TestSerialization_AuthSerialization(t *testing.T) { - assertSerializeJSON(t, BasicAuth(), `{"type":"basic"}`) + assert.JSONMarshalAsT(t, `{"type":"basic"}`, BasicAuth()) - assertSerializeJSON(t, APIKeyAuth("api-key", "header"), `{"type":"apiKey","name":"api-key","in":"header"}`) + assert.JSONMarshalAsT(t, `{"type":"apiKey","name":"api-key","in":"header"}`, APIKeyAuth("api-key", "header")) - assertSerializeJSON( - t, - OAuth2Implicit("http://foo.com/authorization"), - `{"type":"oauth2","flow":"implicit","authorizationUrl":"http://foo.com/authorization"}`) + assert.JSONMarshalAsT(t, + `{"type":"oauth2","flow":"implicit","authorizationUrl":"http://foo.com/authorization"}`, + OAuth2Implicit("http://foo.com/authorization")) - assertSerializeJSON( - t, - OAuth2Password("http://foo.com/token"), - `{"type":"oauth2","flow":"password","tokenUrl":"http://foo.com/token"}`) + assert.JSONMarshalAsT(t, + `{"type":"oauth2","flow":"password","tokenUrl":"http://foo.com/token"}`, + OAuth2Password("http://foo.com/token")) - assertSerializeJSON(t, - OAuth2Application("http://foo.com/token"), - `{"type":"oauth2","flow":"application","tokenUrl":"http://foo.com/token"}`) + assert.JSONMarshalAsT(t, + `{"type":"oauth2","flow":"application","tokenUrl":"http://foo.com/token"}`, + OAuth2Application("http://foo.com/token")) - assertSerializeJSON( - t, - OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token"), + assert.JSONMarshalAsT(t, `{"type":"oauth2","flow":"accessCode","authorizationUrl":"http://foo.com/authorization",`+ - `"tokenUrl":"http://foo.com/token"}`) + `"tokenUrl":"http://foo.com/token"}`, + OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token")) auth1 := OAuth2Implicit("http://foo.com/authorization") auth1.AddScope("email", "read your email") - assertSerializeJSON( - t, - auth1, + assert.JSONMarshalAsT(t, `{"type":"oauth2","flow":"implicit","authorizationUrl":"http://foo.com/authorization",`+ - `"scopes":{"email":"read your email"}}`) + `"scopes":{"email":"read your email"}}`, + auth1) auth2 := OAuth2Password("http://foo.com/authorization") auth2.AddScope("email", "read your email") - assertSerializeJSON( - t, - auth2, + assert.JSONMarshalAsT(t, `{"type":"oauth2","flow":"password","tokenUrl":"http://foo.com/authorization",`+ - `"scopes":{"email":"read your email"}}`) + `"scopes":{"email":"read your email"}}`, + auth2) auth3 := OAuth2Application("http://foo.com/token") auth3.AddScope("email", "read your email") - assertSerializeJSON( - t, - auth3, - `{"type":"oauth2","flow":"application","tokenUrl":"http://foo.com/token","scopes":{"email":"read your email"}}`) + assert.JSONMarshalAsT(t, + `{"type":"oauth2","flow":"application","tokenUrl":"http://foo.com/token","scopes":{"email":"read your email"}}`, + auth3) auth4 := OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token") auth4.AddScope("email", "read your email") - assertSerializeJSON( - t, - auth4, + assert.JSONMarshalAsT(t, `{"type":"oauth2","flow":"accessCode","authorizationUrl":"http://foo.com/authorization",`+ - `"tokenUrl":"http://foo.com/token","scopes":{"email":"read your email"}}`) + `"tokenUrl":"http://foo.com/token","scopes":{"email":"read your email"}}`, + auth4) } func TestSerialization_AuthDeserialization(t *testing.T) { - assertParsesJSON(t, `{"type":"basic"}`, BasicAuth()) + assert.JSONUnmarshalAsT(t, BasicAuth(), `{"type":"basic"}`) - assertParsesJSON( - t, - `{"in":"header","name":"api-key","type":"apiKey"}`, - APIKeyAuth("api-key", "header")) + assert.JSONUnmarshalAsT(t, + APIKeyAuth("api-key", "header"), + `{"in":"header","name":"api-key","type":"apiKey"}`) - assertParsesJSON( - t, - `{"authorizationUrl":"http://foo.com/authorization","flow":"implicit","type":"oauth2"}`, - OAuth2Implicit("http://foo.com/authorization")) + assert.JSONUnmarshalAsT(t, + OAuth2Implicit("http://foo.com/authorization"), + `{"authorizationUrl":"http://foo.com/authorization","flow":"implicit","type":"oauth2"}`) - assertParsesJSON( - t, - `{"flow":"password","tokenUrl":"http://foo.com/token","type":"oauth2"}`, - OAuth2Password("http://foo.com/token")) + assert.JSONUnmarshalAsT(t, + OAuth2Password("http://foo.com/token"), + `{"flow":"password","tokenUrl":"http://foo.com/token","type":"oauth2"}`) - assertParsesJSON( - t, - `{"flow":"application","tokenUrl":"http://foo.com/token","type":"oauth2"}`, - OAuth2Application("http://foo.com/token")) + assert.JSONUnmarshalAsT(t, + OAuth2Application("http://foo.com/token"), + `{"flow":"application","tokenUrl":"http://foo.com/token","type":"oauth2"}`) - assertParsesJSON( - t, + assert.JSONUnmarshalAsT(t, + OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token"), `{"authorizationUrl":"http://foo.com/authorization","flow":"accessCode","tokenUrl":"http://foo.com/token",`+ - `"type":"oauth2"}`, - OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token")) + `"type":"oauth2"}`) auth1 := OAuth2Implicit("http://foo.com/authorization") auth1.AddScope("email", "read your email") - assertParsesJSON(t, + assert.JSONUnmarshalAsT(t, auth1, `{"authorizationUrl":"http://foo.com/authorization","flow":"implicit","scopes":{"email":"read your email"},`+ - `"type":"oauth2"}`, - auth1) + `"type":"oauth2"}`) auth2 := OAuth2Password("http://foo.com/token") auth2.AddScope("email", "read your email") - assertParsesJSON(t, - `{"flow":"password","scopes":{"email":"read your email"},"tokenUrl":"http://foo.com/token","type":"oauth2"}`, - auth2) + assert.JSONUnmarshalAsT(t, auth2, + `{"flow":"password","scopes":{"email":"read your email"},"tokenUrl":"http://foo.com/token","type":"oauth2"}`) auth3 := OAuth2Application("http://foo.com/token") auth3.AddScope("email", "read your email") - assertParsesJSON(t, - `{"flow":"application","scopes":{"email":"read your email"},"tokenUrl":"http://foo.com/token","type":"oauth2"}`, - auth3) + assert.JSONUnmarshalAsT(t, auth3, + `{"flow":"application","scopes":{"email":"read your email"},"tokenUrl":"http://foo.com/token","type":"oauth2"}`) auth4 := OAuth2AccessToken("http://foo.com/authorization", "http://foo.com/token") auth4.AddScope("email", "read your email") - assertParsesJSON( - t, + assert.JSONUnmarshalAsT(t, auth4, `{"authorizationUrl":"http://foo.com/authorization","flow":"accessCode","scopes":{"email":"read your email"},`+ - `"tokenUrl":"http://foo.com/token","type":"oauth2"}`, - auth4) + `"tokenUrl":"http://foo.com/token","type":"oauth2"}`) } diff --git a/cache.go b/cache.go index cd38dcbc..06495d2c 100644 --- a/cache.go +++ b/cache.go @@ -8,10 +8,10 @@ import ( "sync" ) -// ResolutionCache a cache for resolving urls +// ResolutionCache a cache for resolving urls. type ResolutionCache interface { - Get(key string) (any, bool) - Set(key string, value any) + Get(uri string) (any, bool) + Set(uri string, data any) } type simpleCache struct { @@ -19,7 +19,7 @@ type simpleCache struct { store map[string]any } -func (s *simpleCache) ShallowClone() ResolutionCache { +func (s *simpleCache) ShallowClone() ResolutionCache { //nolint:ireturn // returns the public interface type by design store := make(map[string]any, len(s.store)) s.lock.RLock() maps.Copy(store, s.store) @@ -30,7 +30,7 @@ func (s *simpleCache) ShallowClone() ResolutionCache { } } -// Get retrieves a cached URI +// Get retrieves a cached URI. func (s *simpleCache) Get(uri string) (any, bool) { s.lock.RLock() v, ok := s.store[uri] @@ -39,7 +39,7 @@ func (s *simpleCache) Get(uri string) (any, bool) { return v, ok } -// Set caches a URI +// Set caches a URI. func (s *simpleCache) Set(uri string, data any) { s.lock.Lock() s.store[uri] = data @@ -56,8 +56,8 @@ var ( // // All subsequent utilizations of this cache are produced from a shallow // clone of this initial version. - resCache *simpleCache - onceCache sync.Once + resCache *simpleCache //nolint:gochecknoglobals // package-level lazy cache for $ref resolution + onceCache sync.Once //nolint:gochecknoglobals // guards lazy init of resCache _ ResolutionCache = &simpleCache{} ) @@ -74,7 +74,7 @@ func defaultResolutionCache() *simpleCache { }} } -func cacheOrDefault(cache ResolutionCache) ResolutionCache { +func cacheOrDefault(cache ResolutionCache) ResolutionCache { //nolint:ireturn // returns the public interface type by design onceCache.Do(initResolutionCache) if cache != nil { diff --git a/cache_test.go b/cache_test.go index 1ef15db1..92f8a8f6 100644 --- a/cache_test.go +++ b/cache_test.go @@ -16,19 +16,19 @@ func TestDefaultResolutionCache(t *testing.T) { cache := defaultResolutionCache() sch, ok := cache.Get("not there") - assert.False(t, ok) + assert.FalseT(t, ok) assert.Nil(t, sch) sch, ok = cache.Get("http://swagger.io/v2/schema.json") - assert.True(t, ok) + assert.TrueT(t, ok) assert.Equal(t, swaggerSchema, sch) sch, ok = cache.Get("http://json-schema.org/draft-04/schema") - assert.True(t, ok) + assert.TrueT(t, ok) assert.Equal(t, jsonSchema, sch) cache.Set("something", "here") sch, ok = cache.Get("something") - assert.True(t, ok) + assert.TrueT(t, ok) assert.Equal(t, "here", sch) } diff --git a/circular_test.go b/circular_test.go index cc607c24..5a8535c7 100644 --- a/circular_test.go +++ b/circular_test.go @@ -6,7 +6,6 @@ package spec import ( "encoding/json" "net/http" - "net/http/httptest" "os" "path/filepath" "testing" @@ -73,7 +72,7 @@ func TestExpandCircular_Spec2Expansion(t *testing.T) { assertRefResolve(t, jazon, "", root) // assert stripped $ref in result - assert.NotContainsf(t, jazon, "circular-minimal.json#/", + assert.StringNotContainsTf(t, jazon, "circular-minimal.json#/", "expected %s to be expanded with stripped circular $ref", fixturePath) fixturePath = filepath.Join("fixtures", "expansion", "circularSpec2.json") @@ -89,7 +88,7 @@ func TestExpandCircular_Spec2Expansion(t *testing.T) { // circular $ref can always be further expanded against the root assertRefExpand(t, jazon, "", root) - assert.NotContainsf(t, jazon, "circularSpec.json#/", + assert.StringNotContainsTf(t, jazon, "circularSpec.json#/", "expected %s to be expanded with stripped circular $ref", fixturePath) /* @@ -152,7 +151,7 @@ func TestExpandCircular_Issue957(t *testing.T) { jazon, root := expandThisOrDieTrying(t, fixturePath) require.NotEmpty(t, jazon) - require.NotContainsf(t, jazon, "fixture-957.json#/", + require.StringNotContainsTf(t, jazon, "fixture-957.json#/", "expected %s to be expanded with stripped circular $ref", fixturePath) assertRefInJSON(t, jazon, "#/definitions/") @@ -253,9 +252,9 @@ func TestExpandCircular_RemoteCircularID(t *testing.T) { } func TestCircular_RemoteExpandAzure(t *testing.T) { - // local copy of : https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/network/resource-manager/Microsoft.Network/stable/2020-04-01/publicIpAddress.json - server := httptest.NewServer(http.FileServer(http.Dir("fixtures/azure"))) - defer server.Close() + // local copy of Azure publicIpAddress.json from azure-rest-api-specs + // (Microsoft.Network/stable/2020-04-01) + server := fixtureServer(t, "fixtures/azure") basePath := server.URL + "/publicIpAddress.json" jazon, sch := expandThisOrDieTrying(t, basePath) diff --git a/contact_info.go b/contact_info.go index fafe639b..46fada5d 100644 --- a/contact_info.go +++ b/contact_info.go @@ -17,14 +17,14 @@ type ContactInfo struct { VendorExtensible } -// ContactInfoProps hold the properties of a ContactInfo object +// ContactInfoProps hold the properties of a ContactInfo object. type ContactInfoProps struct { Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` Email string `json:"email,omitempty"` } -// UnmarshalJSON hydrates ContactInfo from json +// UnmarshalJSON hydrates ContactInfo from json. func (c *ContactInfo) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &c.ContactInfoProps); err != nil { return err @@ -32,7 +32,7 @@ func (c *ContactInfo) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &c.VendorExtensible) } -// MarshalJSON produces ContactInfo as json +// MarshalJSON produces ContactInfo as json. func (c ContactInfo) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(c.ContactInfoProps) if err != nil { diff --git a/contact_info_test.go b/contact_info_test.go index 6159f75d..bcf177e2 100644 --- a/contact_info_test.go +++ b/contact_info_test.go @@ -4,11 +4,9 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" ) const contactInfoJSON = `{ @@ -18,19 +16,13 @@ const contactInfoJSON = `{ "x-teams": "test team" }` -var contactInfo = ContactInfo{ContactInfoProps: ContactInfoProps{ +var contactInfo = ContactInfo{ContactInfoProps: ContactInfoProps{ //nolint:gochecknoglobals // test fixture Name: "wordnik api team", URL: "http://developer.wordnik.com", Email: "some@mailayada.dkdkd", }, VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-teams": "test team"}}} func TestIntegrationContactInfo(t *testing.T) { - b, err := json.MarshalIndent(contactInfo, "", "\t") - require.NoError(t, err) - assert.JSONEq(t, contactInfoJSON, string(b)) - - actual := ContactInfo{} - err = json.Unmarshal([]byte(contactInfoJSON), &actual) - require.NoError(t, err) - assert.Equal(t, contactInfo, actual) + assert.JSONMarshalAsT(t, contactInfoJSON, contactInfo) + assert.JSONUnmarshalAsT(t, contactInfo, contactInfoJSON) } diff --git a/debug.go b/debug.go index a08422d7..fa52b0c7 100644 --- a/debug.go +++ b/debug.go @@ -14,12 +14,12 @@ import ( // Debug is true when the SWAGGER_DEBUG env var is not empty. // // It enables a more verbose logging of this package. -var Debug = os.Getenv("SWAGGER_DEBUG") != "" +var Debug = os.Getenv("SWAGGER_DEBUG") != "" //nolint:gochecknoglobals // public toggle for debug logging // specLogger is a debug logger for this package. -var specLogger *log.Logger //nolint:gochecknoglobals +var specLogger *log.Logger //nolint:gochecknoglobals // package-level debug logger -func init() { +func init() { //nolint:gochecknoinits // initializes debug logger at package load debugOptions() } diff --git a/debug_test.go b/debug_test.go index bc836914..af99d1ed 100644 --- a/debug_test.go +++ b/debug_test.go @@ -11,7 +11,7 @@ import ( "github.com/go-openapi/testify/v2/assert" ) -var logMutex = &sync.Mutex{} //nolint:gochecknoglobals +var logMutex = &sync.Mutex{} //nolint:gochecknoglobals // test fixture func TestDebug(t *testing.T) { // usetesting linter disabled until https://github.com/golang/go/issues/71544 is fixed for windows @@ -42,5 +42,5 @@ func TestDebug(t *testing.T) { buf := make([]byte, 500) _, _ = flushed.Read(buf) specLogger.SetOutput(os.Stdout) - assert.Contains(t, string(buf), "A debug") + assert.StringContainsT(t, string(buf), "A debug") } diff --git a/doc.go b/doc.go index 04eea357..8b589781 100644 --- a/doc.go +++ b/doc.go @@ -4,4 +4,33 @@ // Package spec exposes an object model for OpenAPIv2 specifications (swagger). // // The exposed data structures know how to serialize to and deserialize from JSON. +// +// # Security +// +// Resolving and expanding "$ref" pointers loads documents through a pluggable loader (see +// [ExpandOptions.PathLoader] and [ExpandOptions.PathLoaderWithOptions]). By default, that +// loader is NOT sandboxed, so a specification obtained from an untrusted source can abuse it: +// +// - A local "$ref" such as "file:///etc/passwd" or a relative "../../secret.json" is read +// straight off disk. A malicious specification can therefore read any file the process can +// access (arbitrary file read / path traversal, CWE-22). +// - A remote "$ref" such as "http://169.254.169.254/..." is fetched with no restriction. A +// malicious specification can therefore probe or reach internal addresses (SSRF, CWE-918). +// +// Do NOT expand or resolve an untrusted specification with the default options. To process +// untrusted specifications safely, inject a confined loader: +// +// - Recommended: use the restricted loaders from github.com/go-openapi/loads, for example +// loads.SpecRestricted(path, root) or loads.SetRestrictedLoaders(root). They confine local +// reads to root and route remote fetches through a client that rejects loopback, private and +// link-local addresses, and the confinement applies to every "$ref" resolved during +// expansion. +// - Or directly: set [ExpandOptions.PathLoaderWithOptions] to a loader built with +// github.com/go-openapi/swag/loading options such as loading.WithRoot (to confine local +// reads to a directory) and loading.WithHTTPClient (to restrict remote fetches). A "$ref" +// that resolves outside root is then rejected, including one reached through a "file://" +// URI or a "../" traversal. +// +// Expanding an untrusted specification also has a resource-exhaustion vector ("$ref" +// amplification); see [ExpandOptions.MaxExpansionNodes], which is bounded by default. package spec diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md deleted file mode 100644 index 6c15d12c..00000000 --- a/docs/MAINTAINERS.md +++ /dev/null @@ -1,159 +0,0 @@ -# Maintainer's guide - -## Repo structure - -Single go module. - -> **NOTE** -> -> Some `go-openapi` repos are mono-repos with multiple modules, -> with adapted CI workflows. - -## Repo configuration - -* default branch: master -* protected branches: master -* branch protection rules: - * require pull requests and approval - * required status checks: - - DCO (simple email sign-off) - - Lint - - tests completed -* auto-merge enabled (used for dependabot updates) - -## Continuous Integration - -### Code Quality checks - -* meta-linter: golangci-lint -* linter config: [`.golangci.yml`](../.golangci.yml) (see our [posture](./STYLE.md) on linters) - -* Code quality assessment: [CodeFactor](https://www.codefactor.io/dashboard) -* Code quality badges - * go report card: - * CodeFactor: - -> **NOTES** -> -> codefactor inherits roles from github. There is no need to create a dedicated account. -> -> The codefactor app is installed at the organization level (`github.com/go-openapi`). -> -> There is no special token to setup in github for CI usage. - -### Testing - -* Test reports - * Uploaded to codecov: -* Test coverage reports - * Uploaded to codecov: - -* Fuzz testing - * Fuzz tests are handled separately by CI and may reuse a cached version of the fuzzing corpus. - At this moment, cache may not be shared between feature branches or feature branch and master. - The minimized corpus produced on failure is uploaded as an artifact and should be added manually - to `testdata/fuzz/...`. - -Coverage threshold status is informative and not blocking. -This is because the thresholds are difficult to tune and codecov oftentimes reports false negatives -or may fail to upload coverage. - -All tests use our fork of `stretchr/testify`: `github.com/go-openapi/testify`. -This allows for minimal test dependencies. - -> **NOTES** -> -> codecov inherits roles from github. There is no need to create a dedicated account. -> However, there is only 1 maintainer allowed to be the admin of the organization on codecov -> with their free plan. -> -> The codecov app is installed at the organization level (`github.com/go-openapi`). -> -> There is no special token to setup in github for CI usage. -> A organization-level token used to upload coverage and test reports is managed at codecov: -> no setup is required on github. - -### Automated updates - -* dependabot - * configuration: [`dependabot.yaml`](../.github/dependabot.yaml) - - Principle: - - * codecov applies updates and security patches to the github-actions and golang ecosystems. - * all updates from "trusted" dependencies (github actions, golang.org packages, go-openapi packages - are auto-merged if they successfully pass CI. - -* go version udpates - - Principle: - - * we support the 2 latest minor versions of the go compiler (`stable`, `oldstable`) - * `go.mod` should be updated (manually) whenever there is a new go minor release - (e.g. every 6 months). - -* contributors - * a [`CONTRIBUTORS.md`](../CONTRIBUTORS.md) file is updated weekly, with all-time contributors to the repository - * the `github-actions[bot]` posts a pull request to do that automatically - * at this moment, this pull request is not auto-approved/auto-merged (bot cannot approve its own PRs) - -### Vulnerability scanners - -There are 3 complementary scanners - obviously, there is some overlap, but each has a different focus. - -* github `CodeQL` -* `trivy` -* `govulnscan` - -None of these tools require an additional account or token. - -Github CodeQL configuration is set to "Advanced", so we may collect a CI status for this check (e.g. for badges). - -Scanners run on every commit to master and at least once a week. - -Reports are centralized in github security reports for code scanning tools. - -## Releases - -The release process is minimalist: - -* push a semver tag (i.e v{major}.{minor}.{patch}) to the master branch. -* the CI handles this to generate a github release with release notes - -* release notes generator: git-cliff -* configuration: [`cliff.toml`](../.cliff.toml) - -Tags are preferably PGP-signed. - -The tag message introduces the release notes (e.g. a summary of this release). - -The release notes generator does not assume that commits are necessarily "conventional commits". - -## Other files - -Standard documentation: - -* [`CONTRIBUTING.md`](../.github/CONTRIBUTING.md) guidelines -* [`DCO.md`](../.github/DCO.md) terms for first-time contributors to read -* [`CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md) -* [`SECURIY.md`](../SECURITY.md) policy: how to report vulnerabilities privately -* [`LICENSE`](../LICENSE) terms - - -Reference documentation (released): - -* [godoc](https://pkg.go.dev/github.com/go-openapi/spec) - -## TODOs & other ideas - -A few things remain ahead to ease a bit a maintainer's job: - -* [x] reuse CI workflows (e.g. in `github.com/go-openapi/workflows`) -* [x] reusable actions with custom tools pinned (e.g. in `github.com/go-openapi/gh-actions`) -* open-source license checks -* [x] auto-merge for CONTRIBUTORS.md (requires a github app to produce tokens) -* [ ] more automated code renovation / relinting work (possibly built with CLAUDE) (ongoing) -* organization-level documentation web site -* ... diff --git a/docs/STYLE.md b/docs/STYLE.md deleted file mode 100644 index 056fdb51..00000000 --- a/docs/STYLE.md +++ /dev/null @@ -1,83 +0,0 @@ -# Coding style at `go-openapi` - -> **TL;DR** -> -> Let's be honest: at `go-openapi` and `go-swagger` we've never been super-strict on code style etc. -> -> But perhaps now (2025) is the time to adopt a different stance. - -Even though our repos have been early adopters of `golangci-lint` years ago -(we used some other metalinter before), our decade-old codebase is only realigned to new rules from time to time. - -Now go-openapi and go-swagger make up a really large codebase, which is taxing to maintain and keep afloat. - -Code quality and the harmonization of rules have thus become things that we need now. - -## Meta-linter - -Universally formatted go code promotes ease of writing, reading, and maintenance. - -You should run `golangci-lint run` before committing your changes. - -Many editors have plugins that do that automatically. - -> We use the `golangci-lint` meta-linter. The configuration lies in [`.golangci.yml`](../.golangci.yml). -> You may read for additional reference. - -## Linting rules posture - -Thanks to go's original design, we developers don't have to waste much time arguing about code figures of style. - -However, the number of available linters has been growing to the point that we need to pick a choice. - -We enable all linters published by `golangci-lint` by default, then disable a few ones. - -Here are the reasons why they are disabled (update: Nov. 2025, `golangci-lint v2.6.1`): - -```yaml - disable: - - depguard # we don't want to configure rules to constrain import. That's the reviewer's job - - exhaustruct # we don't want to configure regexp's to check type name. That's the reviewer's job - - funlen # we accept cognitive complexity as a meaningful metric, but function length is relevant - - godox # we don't see any value in forbidding TODO's etc in code - - nlreturn # we usually apply this "blank line" rule to make code less compact. We just don't want to enforce it - - nonamedreturns # we don't see any valid reason why we couldn't used named returns - - noinlineerr # there is no value added forbidding inlined err - - paralleltest # we like parallel tests. We just don't want them to be enforced everywhere - - recvcheck # we like the idea of having pointer and non-pointer receivers - - testpackage # we like test packages. We just don't want them to be enforced everywhere - - tparallel # see paralleltest - - varnamelen # sometimes, we like short variables. The linter doesn't catch cases when a short name is good - - whitespace # no added value - - wrapcheck # although there is some sense with this linter's general idea, it produces too much noise - - wsl # no added value. Noise - - wsl_v5 # no added value. Noise -``` - -As you may see, we agree with the objective of most linters, at least the principle they are supposed to enforce. -But all linters do not support fine-grained tuning to tolerate some cases and not some others. - -When this is possible, we enable linters with relaxed constraints: - -```yaml - settings: - dupl: - threshold: 200 # in a older code base such as ours, we have to be tolerant with a little redundancy - # Hopefully, we'll be able to gradually get rid of those. - goconst: - min-len: 2 - min-occurrences: 3 - cyclop: - max-complexity: 20 # the default is too low for most of our functions. 20 is a nicer trade-off - gocyclo: - min-complexity: 20 - exhaustive: # when using default in switch, this should be good enough - default-signifies-exhaustive: true - default-case-required: true - lll: - line-length: 180 # we just want to avoid extremely long lines. - # It is no big deal if a line or two don't fit on your terminal. -``` - -Final note: since we have switched to a forked version of `stretchr/testify`, -we no longer benefit from the great `testifylint` linter for tests. diff --git a/errors.go b/errors.go index 4623bc82..740b773c 100644 --- a/errors.go +++ b/errors.go @@ -5,7 +5,7 @@ package spec import "errors" -// Error codes +// Error codes. var ( // ErrUnknownTypeForReference indicates that a resolved reference was found in an unsupported container type. ErrUnknownTypeForReference = errors.New("unknown type for the resolved reference") @@ -20,6 +20,13 @@ var ( // ErrExpandUnsupportedType indicates that $ref expansion is attempted on some invalid type. ErrExpandUnsupportedType = errors.New("expand: unsupported type. Input should be of type *Parameter or *Response") + // ErrExpandTooManyNodes indicates that $ref expansion exceeded the maximum number of schema nodes + // allowed for a single expansion (see ExpandOptions.MaxExpansionNodes). + // + // This is a safeguard against maliciously crafted specifications that expand to an exponential + // number of nodes from a small input (a $ref amplification / "billion laughs" style attack). + ErrExpandTooManyNodes = errors.New("expand: too many schema nodes: expansion budget exceeded (see ExpandOptions.MaxExpansionNodes)") + // ErrSpec is an error raised by the spec package. ErrSpec = errors.New("spec error") ) diff --git a/expander.go b/expander.go index de1cc4c1..11b4ae6e 100644 --- a/expander.go +++ b/expander.go @@ -6,10 +6,23 @@ package spec import ( "encoding/json" "fmt" + + "github.com/go-openapi/swag/loading" ) const smallPrealloc = 10 +// DefaultMaxExpansionNodes is the default upper bound on the number of schema nodes +// expanded during a single ExpandSpec / ExpandSchema* call. +// +// It guards against maliciously crafted specifications whose $ref graph expands to an +// exponential number of nodes from a few kilobytes of input. For reference, expanding the +// full Kubernetes API specification (the largest real-world spec we test against) visits +// roughly 47,000 nodes, so this default leaves ample headroom for legitimate documents. +// +// See ExpandOptions.MaxExpansionNodes to tune or disable this budget. +const DefaultMaxExpansionNodes = 500_000 + // ExpandOptions provides options for the spec expander. // // RelativeBase is the path to the root document. This can be a remote URL or a path to a local file. @@ -17,13 +30,59 @@ const smallPrealloc = 10 // If left empty, the root document is assumed to be located in the current working directory: // all relative $ref's will be resolved from there. // -// PathLoader injects a document loading method. By default, this resolves to the function provided by the SpecLoader package variable. +// PathLoader injects a document loading method. By default, this resolves to the function provided by the PathLoader package variable. +// +// PathLoaderWithOptions is an alternative document loader that accepts [loading.Option] values, matching the +// signature used by the go-openapi/swag/loading and go-openapi/loads loaders. When set, it takes precedence over +// PathLoader. This lets a caller inject an options-aware (e.g. path-confined) loader without an adapter closure. +// +// Security: the default loader is not sandboxed. When expanding an untrusted specification, inject a confined +// loader (for example one built with loading.WithRoot) — see the package "Security" section. type ExpandOptions struct { RelativeBase string // the path to the root document to expand. This is a file, not a directory SkipSchemas bool // do not expand schemas, just paths, parameters and responses ContinueOnError bool // continue expanding even after and error is found PathLoader func(string) (json.RawMessage, error) `json:"-"` // the document loading method that takes a path as input and yields a json document AbsoluteCircularRef bool // circular $ref remaining after expansion remain absolute URLs + + // PathLoaderWithOptions injects a document loading method that accepts loading options. + // + // It has the same role as PathLoader but matches the option-aware loader signature exposed by + // github.com/go-openapi/swag/loading (and github.com/go-openapi/loads), so such a loader can be + // injected directly, without wrapping it in an adapter closure. + // + // When set, PathLoaderWithOptions takes precedence over PathLoader. The provided loader is expected + // to carry its own loading options (for example a path confinement built with loading.WithRoot); + // the expander itself invokes it without adding options. + PathLoaderWithOptions func(string, ...loading.Option) (json.RawMessage, error) `json:"-"` + + // MaxExpansionNodes caps the number of schema nodes expanded during a single expansion call, + // as a safeguard against $ref amplification attacks (see ErrExpandTooManyNodes). + // + // The value is interpreted as follows: + // + // 0 (the zero value): use DefaultMaxExpansionNodes. Every caller is protected by default. + // <0: no limit (unbounded expansion). Use only with fully trusted specifications. + // >0: cap the expansion at this number of nodes. + // + // When the budget is exceeded, expansion stops and ErrExpandTooManyNodes is returned. + // Because this is a resource-exhaustion safeguard, the error is always returned, even when + // ContinueOnError is set. + MaxExpansionNodes int +} + +// maxExpansionNodes resolves the tri-state MaxExpansionNodes option into an effective budget. +// +// A returned value of 0 means "unbounded". +func (o *ExpandOptions) maxExpansionNodes() int { + switch { + case o.MaxExpansionNodes == 0: + return DefaultMaxExpansionNodes + case o.MaxExpansionNodes < 0: + return 0 // unbounded + default: + return o.MaxExpansionNodes + } } func optionsOrDefault(opts *ExpandOptions) *ExpandOptions { @@ -38,7 +97,11 @@ func optionsOrDefault(opts *ExpandOptions) *ExpandOptions { return &ExpandOptions{} } -// ExpandSpec expands the references in a swagger spec +// ExpandSpec expands the references in a swagger spec. +// +// Security: with default options the document loader is not sandboxed, so a "$ref" in an +// untrusted spec can read local files or reach internal addresses. See the package "Security" +// section before expanding untrusted input. func ExpandSpec(spec *Swagger, options *ExpandOptions) error { options = optionsOrDefault(options) resolver := defaultSchemaLoader(spec, options, nil, nil) @@ -92,7 +155,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { const rootBase = ".root" // baseForRoot loads in the cache the root document and produces a fake ".root" base path entry -// for further $ref resolution +// for further $ref resolution. func baseForRoot(root any, cache ResolutionCache) string { // cache the root document to resolve $ref's normalizedBase := normalizeBase(rootBase) @@ -121,25 +184,50 @@ func baseForRoot(root any, cache ResolutionCache) string { // (use ExpandSchemaWithBasePath to resolve external references). // // Setting the cache is optional and this parameter may safely be left to nil. +// +// ExpandSchema uses the package default document loader, which is not sandboxed. To expand a +// schema whose $ref may derive from untrusted input, use [ExpandSchemaWithOptions] with a confined +// loader — see the package "Security" section. func ExpandSchema(schema *Schema, root any, cache ResolutionCache) error { + return ExpandSchemaWithOptions(schema, root, cache, nil) +} + +// ExpandSchemaWithOptions expands the refs in the schema object with reference to the root object, +// honoring the provided expand options. It is the option-aware form of [ExpandSchema]. +// +// In particular, set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document +// loader when expanding a schema whose $ref may derive from an untrusted source (see the package +// "Security" section). opts.ContinueOnError, opts.AbsoluteCircularRef and opts.MaxExpansionNodes +// are honored as well. +// +// The base path is always derived from root (as with [ExpandSchema]), so opts.RelativeBase and +// opts.SkipSchemas are ignored. Passing nil opts is equivalent to [ExpandSchema]. +// +// Setting the cache is optional and this parameter may safely be left to nil. +func ExpandSchemaWithOptions(schema *Schema, root any, cache ResolutionCache, opts *ExpandOptions) error { cache = cacheOrDefault(cache) if root == nil { root = schema } - opts := &ExpandOptions{ - // when a root is specified, cache the root as an in-memory document for $ref retrieval - RelativeBase: baseForRoot(root, cache), - SkipSchemas: false, - ContinueOnError: false, + effective := ExpandOptions{} + if opts != nil { + effective = *opts // preserve caller options (loader, ContinueOnError, budget, ...) } + // when a root is specified, cache the root as an in-memory document for $ref retrieval + effective.RelativeBase = baseForRoot(root, cache) + effective.SkipSchemas = false - return ExpandSchemaWithBasePath(schema, cache, opts) + return ExpandSchemaWithBasePath(schema, cache, &effective) } // ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options. // // Setting the cache is optional and this parameter may safely be left to nil. +// +// Security: with default options the document loader is not sandboxed, so a "$ref" in an +// untrusted schema can read local files or reach internal addresses. See the package "Security" +// section before expanding untrusted input. func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error { if schema == nil { return nil @@ -190,7 +278,12 @@ func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, bas return &target, nil } +//nolint:gocognit,gocyclo,cyclop // complex but well-tested $ref expansion logic; refactoring deferred to dedicated PR func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { + if err := resolver.context.countNode(); err != nil { + return &target, err + } + if target.Ref.String() == "" && target.Ref.IsRoot() { newRef := normalizeRef(&target.Ref, basePath) target.Ref = *newRef @@ -369,13 +462,15 @@ func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) - t, err = expandSchema(*t, parentRefs, transitiveResolver, basePath) - if t != nil { - for k, v := range target.VendorExtensible.Extensions { - t.VendorExtensible.AddExtension(k, v) + expanded, err := expandSchema(*t, parentRefs, transitiveResolver, basePath) + if expanded != nil { + // Percona: keep the extensions declared next to the $ref (e.g. x-order) on the expanded schema. + for k, v := range target.Extensions { + expanded.AddExtension(k, v) } } - return t, err + + return expanded, err } func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error { @@ -459,25 +554,28 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err // // Setting the cache is optional and this parameter may safely be left to nil. func ExpandResponseWithRoot(response *Response, root any, cache ResolutionCache) error { - cache = cacheOrDefault(cache) - opts := &ExpandOptions{ - RelativeBase: baseForRoot(root, cache), - } - resolver := defaultSchemaLoader(root, opts, cache, nil) - - return expandParameterOrResponse(response, resolver, opts.RelativeBase) + return ExpandResponseWithOptions(response, root, cache, nil) } // ExpandResponse expands a response based on a basepath // -// All refs inside response will be resolved relative to basePath +// All refs inside response will be resolved relative to basePath. func ExpandResponse(response *Response, basePath string) error { - opts := optionsOrDefault(&ExpandOptions{ - RelativeBase: basePath, - }) - resolver := defaultSchemaLoader(nil, opts, nil, nil) + return ExpandResponseWithOptions(response, nil, nil, &ExpandOptions{RelativeBase: basePath}) +} - return expandParameterOrResponse(response, resolver, opts.RelativeBase) +// ExpandResponseWithOptions expands a response, honoring the provided expand options. +// +// It is the option-aware form of [ExpandResponse] and [ExpandResponseWithRoot]. When root is +// non-nil, refs resolve against the in-memory root document; otherwise they resolve relative to +// opts.RelativeBase. +// +// Set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document loader when +// the response's $ref may derive from an untrusted source — see the package "Security" section. +// +// Setting the cache is optional and this parameter may safely be left to nil. +func ExpandResponseWithOptions(response *Response, root any, cache ResolutionCache, opts *ExpandOptions) error { + return expandRefableWithOptions(response, root, cache, opts) } // ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document. @@ -485,26 +583,43 @@ func ExpandResponse(response *Response, basePath string) error { // Notice that it is impossible to reference a json schema in a different document other than root // (use ExpandParameter to resolve external references). func ExpandParameterWithRoot(parameter *Parameter, root any, cache ResolutionCache) error { - cache = cacheOrDefault(cache) - - opts := &ExpandOptions{ - RelativeBase: baseForRoot(root, cache), - } - resolver := defaultSchemaLoader(root, opts, cache, nil) - - return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) + return ExpandParameterWithOptions(parameter, root, cache, nil) } // ExpandParameter expands a parameter based on a basepath. // This is the exported version of expandParameter -// all refs inside parameter will be resolved relative to basePath +// all refs inside parameter will be resolved relative to basePath. func ExpandParameter(parameter *Parameter, basePath string) error { - opts := optionsOrDefault(&ExpandOptions{ - RelativeBase: basePath, - }) - resolver := defaultSchemaLoader(nil, opts, nil, nil) + return ExpandParameterWithOptions(parameter, nil, nil, &ExpandOptions{RelativeBase: basePath}) +} + +// ExpandParameterWithOptions expands a parameter, honoring the provided expand options. +// +// It is the option-aware form of [ExpandParameter] and [ExpandParameterWithRoot]. When root is +// non-nil, refs resolve against the in-memory root document; otherwise they resolve relative to +// opts.RelativeBase. +// +// Set opts.PathLoaderWithOptions (or opts.PathLoader) to inject a confined document loader when +// the parameter's $ref may derive from an untrusted source — see the package "Security" section. +// +// Setting the cache is optional and this parameter may safely be left to nil. +func ExpandParameterWithOptions(parameter *Parameter, root any, cache ResolutionCache, opts *ExpandOptions) error { + return expandRefableWithOptions(parameter, root, cache, opts) +} + +// expandRefableWithOptions is the shared implementation for the option-aware parameter/response +// expanders. When root is non-nil, refs resolve against the in-memory root (base derived from +// root); otherwise they resolve relative to opts.RelativeBase. opts carries the loader and other +// expand options. +func expandRefableWithOptions(input any, root any, cache ResolutionCache, opts *ExpandOptions) error { + cache = cacheOrDefault(cache) + effective := optionsOrDefault(opts) // clones and normalizes RelativeBase; preserves the loader + if root != nil { + effective.RelativeBase = baseForRoot(root, cache) + } + resolver := defaultSchemaLoader(root, effective, cache, nil) - return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) + return expandParameterOrResponse(input, resolver, effective.RelativeBase) } func getRefAndSchema(input any) (*Ref, *Schema, error) { @@ -571,7 +686,7 @@ func expandParameterOrResponse(input any, resolver *schemaLoader, basePath strin return nil } - if sch.Ref.String() != "" { + if sch.Ref.String() != "" { //nolint:nestif // intertwined ref rebasing and circularity check rebasedRef, ern := NewRef(normalizeURI(sch.Ref.String(), basePath)) if ern != nil { return ern diff --git a/expander_budget_test.go b/expander_budget_test.go new file mode 100644 index 00000000..2eb86765 --- /dev/null +++ b/expander_budget_test.go @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +// errNoExternalLoads is returned by the deny-all PathLoader used to prove that refusing +// external loads is not, on its own, a mitigation for the amplification attack. +var errNoExternalLoads = errors.New("no external loads allowed") + +// buildAmplificationSpec builds a self-contained spec where each of n definitions references +// the next one twice via allOf. Without an expansion budget, expanding d0 inlines a tree with +// 2^(n-1) leaves from O(n) bytes of input (a $ref amplification / "billion laughs" attack). +func buildAmplificationSpec(t testing.TB, n int) []byte { + t.Helper() + + defs := make(map[string]any, n) + for i := range n { + var sch any + if i == n-1 { + sch = map[string]any{"type": "string"} + } else { + next := fmt.Sprintf("#/definitions/d%d", i+1) + sch = map[string]any{"allOf": []any{ + map[string]any{"$ref": next}, + map[string]any{"$ref": next}, + }} + } + defs[fmt.Sprintf("d%d", i)] = sch + } + + doc := map[string]any{ + "swagger": "2.0", + "info": map[string]any{"title": "x", "version": "1"}, + "paths": map[string]any{}, + "definitions": defs, + } + raw, err := json.Marshal(doc) + require.NoError(t, err) + + return raw +} + +func TestMaxExpansionNodesTriState(t *testing.T) { + // 0 (zero value): default budget, so every caller is protected out of the box. + assert.EqualT(t, DefaultMaxExpansionNodes, (&ExpandOptions{}).maxExpansionNodes()) + + // negative: unbounded. + assert.EqualT(t, 0, (&ExpandOptions{MaxExpansionNodes: -1}).maxExpansionNodes()) + + // positive: explicit budget. + assert.EqualT(t, 1234, (&ExpandOptions{MaxExpansionNodes: 1234}).maxExpansionNodes()) +} + +func TestExpand_AmplificationBudget(t *testing.T) { + // A deep amplification spec. Even a modest depth would explode without a budget. + const depth = 40 + raw := buildAmplificationSpec(t, depth) + + t.Run("explicit budget trips the guard", func(t *testing.T) { + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + err := ExpandSpec(&sw, &ExpandOptions{MaxExpansionNodes: 2000}) + require.ErrorIs(t, err, ErrExpandTooManyNodes) + }) + + t.Run("deny-all PathLoader is not a mitigation on its own", func(t *testing.T) { + // All refs are fragment-only and resolve against the in-memory root, so refusing + // external loads does not prevent the blow-up: the budget is what stops it. + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + loaderCalled := false + err := ExpandSpec(&sw, &ExpandOptions{ + MaxExpansionNodes: 2000, + PathLoader: func(p string) (json.RawMessage, error) { + loaderCalled = true + return nil, fmt.Errorf("%w: %s", errNoExternalLoads, p) + }, + }) + require.ErrorIs(t, err, ErrExpandTooManyNodes) + assert.FalseT(t, loaderCalled, "expected no external load attempts") + }) + + t.Run("ContinueOnError does not suppress a budget breach", func(t *testing.T) { + // The budget is a hard resource-exhaustion safeguard: unlike an unresolvable $ref, + // it must surface even when the caller tolerates errors. + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + err := ExpandSpec(&sw, &ExpandOptions{MaxExpansionNodes: 2000, ContinueOnError: true}) + require.ErrorIs(t, err, ErrExpandTooManyNodes) + }) + + t.Run("negative budget disables the guard", func(t *testing.T) { + // A shallow spec that stays well under any real budget must expand fully when unbounded. + shallow := buildAmplificationSpec(t, 8) + var sw Swagger + require.NoError(t, json.Unmarshal(shallow, &sw)) + + require.NoError(t, ExpandSpec(&sw, &ExpandOptions{MaxExpansionNodes: -1})) + }) +} + +func TestExpand_BudgetAllowsLegitSpec(t *testing.T) { + // A shallow amplification spec (small node count) must expand cleanly under the default budget. + raw := buildAmplificationSpec(t, 8) + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + require.NoError(t, ExpandSpec(&sw, nil)) // nil options => default budget + // d0 fully expanded: no $ref remains in the leaf chain. + out, err := json.Marshal(sw.Definitions["d0"]) + require.NoError(t, err) + assert.StringNotContainsT(t, string(out), `"$ref"`) +} + +func TestExpand_BudgetErrorIsSentinel(t *testing.T) { + raw := buildAmplificationSpec(t, 40) + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + err := ExpandSpec(&sw, &ExpandOptions{MaxExpansionNodes: 100}) + require.Error(t, err) + assert.TrueT(t, errors.Is(err, ErrExpandTooManyNodes)) +} diff --git a/expander_confine_test.go b/expander_confine_test.go new file mode 100644 index 00000000..dc16d313 --- /dev/null +++ b/expander_confine_test.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +// TestExpand_ConfinedLoader validates end to end that a WithRoot-confined loader, injected +// through PathLoaderWithOptions, safely expands an untrusted spec: +// - a legitimate $ref that resolves within the root is expanded (this requires the loader to +// accept the absolute paths spec normalizes references to); +// - a "file://" $ref and a "../" traversal $ref that point outside the root are blocked, and +// no byte of the out-of-root file leaks into the result. +// +// It exercises the go-openapi/swag/loading WithRoot behavior from the consumer side, with an +// absolute RelativeBase (the realistic case). +func TestExpand_ConfinedLoader(t *testing.T) { + const secretMarker = "TOP_SECRET" + + root := t.TempDir() + outside := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(root, "child.json"), + []byte(`{"definitions":{"Thing":{"type":"string","title":"IN_ROOT"}}}`), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(outside, "secret.json"), + []byte(`{"leaked":"`+secretMarker+`"}`), 0o600)) + + secretAbs := filepath.Join(outside, "secret.json") + // a relative traversal, from the spec's base dir (root), that reaches the outside secret + traversal, err := filepath.Rel(root, secretAbs) + require.NoError(t, err) + require.True(t, strings.HasPrefix(traversal, ".."), "sanity: traversal must escape the root") + + raw := `{ + "swagger":"2.0","info":{"title":"x","version":"1"},"paths":{}, + "definitions":{ + "Local": {"$ref":"child.json#/definitions/Thing"}, + "SecretFile":{"$ref":"file://` + filepath.ToSlash(secretAbs) + `"}, + "Traversal": {"$ref":"` + filepath.ToSlash(traversal) + `"} + } + }` + var sw Swagger + require.NoError(t, json.Unmarshal([]byte(raw), &sw)) + + confined := func(pth string, _ ...loading.Option) (json.RawMessage, error) { + b, err := loading.LoadFromFileOrHTTP(pth, loading.WithRoot(root)) + return json.RawMessage(b), err + } + + // absolute base, as a real consumer would pass + err = ExpandSpec(&sw, &ExpandOptions{ + RelativeBase: filepath.Join(root, "api.json"), + PathLoaderWithOptions: confined, + ContinueOnError: true, // do not abort on the blocked refs; expand what is legitimate + }) + require.NoError(t, err) + + dump := func(name string) string { + out, err := json.Marshal(sw.Definitions[name]) + require.NoError(t, err) + return string(out) + } + + // 1) the legitimate in-root ref resolved (the WithRoot fix: absolute in-root paths are accepted) + local := dump("Local") + assert.StringContainsT(t, local, "IN_ROOT") + assert.StringNotContainsT(t, local, `"$ref"`) + + // 2) the escaping refs were blocked: they remain unexpanded and leak nothing + assert.StringContainsT(t, dump("SecretFile"), `"$ref"`) + assert.StringContainsT(t, dump("Traversal"), `"$ref"`) + + // 3) the secret never appears anywhere in the expanded document + whole, err := json.Marshal(&sw) + require.NoError(t, err) + assert.StringNotContainsT(t, string(whole), secretMarker) +} diff --git a/expander_loader_test.go b/expander_loader_test.go new file mode 100644 index 00000000..aaaf9ebd --- /dev/null +++ b/expander_loader_test.go @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +// errUnexpectedLoad is returned by a test loader asked to load a path it does not expect. +var errUnexpectedLoad = errors.New("unexpected load") + +func TestExpandSchemaWithOptions(t *testing.T) { + // A schema whose $ref points into an external document is expanded through the injected + // option-aware loader, exactly as flatten/analysis and validate need for confined expansion. + const external = `{"definitions":{"Thing":{"type":"string"}}}` + + root := map[string]any{"swagger": "2.0", "definitions": map[string]any{}} + schema := RefSchema("external.json#/definitions/Thing") + + var loaderCalls int + err := ExpandSchemaWithOptions(schema, root, nil, &ExpandOptions{ + PathLoaderWithOptions: func(pth string, _ ...loading.Option) (json.RawMessage, error) { + if strings.Contains(pth, "external.json") { + loaderCalls++ + return json.RawMessage(external), nil + } + return nil, fmt.Errorf("%w: %s", errUnexpectedLoad, pth) + }, + }) + require.NoError(t, err) + assert.TrueT(t, loaderCalls > 0, "expected the injected loader to resolve the external $ref") + + out, err := json.Marshal(schema) + require.NoError(t, err) + assert.StringContainsT(t, string(out), `"type":"string"`) + assert.StringNotContainsT(t, string(out), `"$ref"`) + + t.Run("nil options behaves like ExpandSchema (in-memory root, fragment ref)", func(t *testing.T) { + inMemRoot := map[string]any{ + "definitions": map[string]any{"Local": map[string]any{"type": "integer"}}, + } + sch := RefSchema("#/definitions/Local") + require.NoError(t, ExpandSchemaWithOptions(sch, inMemRoot, nil, nil)) + + out, err := json.Marshal(sch) + require.NoError(t, err) + assert.StringContainsT(t, string(out), `"type":"integer"`) + }) +} + +func TestExpandParameterResponseWithOptions(t *testing.T) { + // Parameter and response $ref pointing into an external document are expanded through the + // injected option-aware loader — the path go-openapi/validate needs for confined validation. + const external = `{ + "parameters":{"Foo":{"name":"foo","in":"query","type":"string"}}, + "responses":{"Bar":{"description":"ok"}} + }` + + var loaderCalls int + loader := func(pth string, _ ...loading.Option) (json.RawMessage, error) { + if strings.Contains(pth, "external.json") { + loaderCalls++ + return json.RawMessage(external), nil + } + return nil, fmt.Errorf("%w: %s", errUnexpectedLoad, pth) + } + opts := &ExpandOptions{RelativeBase: "spec.json", PathLoaderWithOptions: loader} + + t.Run("parameter", func(t *testing.T) { + param := new(Parameter) + param.Ref = MustCreateRef("external.json#/parameters/Foo") + require.NoError(t, ExpandParameterWithOptions(param, nil, nil, opts)) + assert.EqualT(t, "foo", param.Name) + assert.EqualT(t, "", param.Ref.String()) + }) + + t.Run("response", func(t *testing.T) { + resp := new(Response) + resp.Ref = MustCreateRef("external.json#/responses/Bar") + require.NoError(t, ExpandResponseWithOptions(resp, nil, nil, opts)) + assert.EqualT(t, "ok", resp.Description) + assert.EqualT(t, "", resp.Ref.String()) + }) + + assert.TrueT(t, loaderCalls >= 2, "expected the injected loader to resolve both external $ref") +} + +func TestPathLoaderSelection(t *testing.T) { + t.Run("option-aware loader is used when set", func(t *testing.T) { + var called string + ctx := newResolverContext(&ExpandOptions{ + PathLoaderWithOptions: func(string, ...loading.Option) (json.RawMessage, error) { + called = "withOptions" + return json.RawMessage(`{}`), nil + }, + }) + + _, err := ctx.loadDoc("x") + require.NoError(t, err) + assert.EqualT(t, "withOptions", called) + }) + + t.Run("option-aware loader takes precedence over the plain loader", func(t *testing.T) { + var called string + ctx := newResolverContext(&ExpandOptions{ + PathLoader: func(string) (json.RawMessage, error) { + called = "plain" + return json.RawMessage(`{}`), nil + }, + PathLoaderWithOptions: func(string, ...loading.Option) (json.RawMessage, error) { + called = "withOptions" + return json.RawMessage(`{}`), nil + }, + }) + + _, err := ctx.loadDoc("x") + require.NoError(t, err) + assert.EqualT(t, "withOptions", called) + }) + + t.Run("plain loader is used when only it is set", func(t *testing.T) { + var called string + ctx := newResolverContext(&ExpandOptions{ + PathLoader: func(string) (json.RawMessage, error) { + called = "plain" + return json.RawMessage(`{}`), nil + }, + }) + + _, err := ctx.loadDoc("x") + require.NoError(t, err) + assert.EqualT(t, "plain", called) + }) +} + +func TestExpand_PathLoaderWithOptions(t *testing.T) { + // A cross-file $ref forces a document load: prove it is routed through the option-aware loader. + const other = `{"definitions":{"Thing":{"type":"string"}}}` + + raw := []byte(`{ + "swagger":"2.0","info":{"title":"x","version":"1"},"paths":{}, + "definitions":{"Ref":{"$ref":"other.json#/definitions/Thing"}} + }`) + var sw Swagger + require.NoError(t, json.Unmarshal(raw, &sw)) + + var loaderCalls int + err := ExpandSpec(&sw, &ExpandOptions{ + RelativeBase: "/base/root.json", + PathLoaderWithOptions: func(pth string, _ ...loading.Option) (json.RawMessage, error) { + if strings.Contains(pth, "other.json") { + loaderCalls++ + return json.RawMessage(other), nil + } + return nil, fmt.Errorf("%w: %s", errUnexpectedLoad, pth) + }, + }) + require.NoError(t, err) + assert.TrueT(t, loaderCalls > 0, "expected the option-aware loader to be invoked") + + // the cross-file $ref has been expanded in place + out, err := json.Marshal(sw.Definitions["Ref"]) + require.NoError(t, err) + assert.StringContainsT(t, string(out), `"type":"string"`) + assert.StringNotContainsT(t, string(out), `"$ref"`) +} diff --git a/expander_ssrf_test.go b/expander_ssrf_test.go new file mode 100644 index 00000000..8d97af3b --- /dev/null +++ b/expander_ssrf_test.go @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "net/netip" + "testing" + "time" + + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/testify/v2/require" +) + +var errBlockedAddress = errors.New("blocked non-public address") + +// restrictedDialContext refuses to dial loopback, private, link-local or unspecified addresses. +// This mirrors the SSRF guard a caller injects via loading.WithHTTPClient (and that +// go-openapi/loads ships as RestrictedHTTPClient). +func restrictedDialContext(_ context.Context, _, addr string) (net.Conn, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + host = addr + } + + ip, err := netip.ParseAddr(host) + if err != nil { + // a hostname would need resolution then a re-check; this test only uses IP literals. + return nil, fmt.Errorf("%w: %s", errBlockedAddress, addr) + } + + ip = ip.Unmap() + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() { + return nil, fmt.Errorf("%w: %s", errBlockedAddress, addr) + } + + return nil, fmt.Errorf("%w: %s (test performs no real dial)", errBlockedAddress, addr) +} + +// TestExpand_SSRFPosture validates that a caller can neutralize the SSRF vector by injecting an +// option-aware loader bound to a restricted HTTP client through PathLoaderWithOptions: a remote +// "$ref" to a cloud metadata endpoint is refused at dial time, before any connection is made. +// +// The loader selection is shared by every expansion/resolution entry point, so blocking it here +// blocks it for ExpandSpec, ExpandSchemaWithBasePath, ExpandResponse, ExpandParameter and the +// Resolve* functions alike. +func TestExpand_SSRFPosture(t *testing.T) { + client := &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{DialContext: restrictedDialContext}, + } + loader := func(pth string, _ ...loading.Option) (json.RawMessage, error) { + b, err := loading.LoadFromFileOrHTTP(pth, loading.WithHTTPClient(client)) + return json.RawMessage(b), err + } + + // AWS IMDS endpoint, exactly as in the report's PoC + raw := `{ + "swagger":"2.0","info":{"title":"x","version":"1"},"paths":{}, + "definitions":{ + "Victim":{"$ref":"http://169.254.169.254/latest/meta-data/iam/security-credentials/role"} + } + }` + var sw Swagger + require.NoError(t, json.Unmarshal([]byte(raw), &sw)) + + err := ExpandSpec(&sw, &ExpandOptions{PathLoaderWithOptions: loader}) + + // the metadata endpoint was refused at dial time: the fetch never happened + require.Error(t, err) + require.ErrorIs(t, err, errBlockedAddress) + require.ErrorContains(t, err, "169.254.169.254") +} diff --git a/expander_test.go b/expander_test.go index d0a8ef84..40ea3132 100644 --- a/expander_test.go +++ b/expander_test.go @@ -29,13 +29,13 @@ const ( //nolint:gochecknoglobals // it's okay to have embedded test fixtures as globals var ( - //go:embed fixtures/*/*.json fixtures/*/*.yaml fixtures/*/*.yml + //go:embed all:fixtures fixtureAssets embed.FS // PetStore20 json doc for swagger 2.0 pet store. PetStore20 []byte - // PetStoreJSONMessage json raw message for Petstore20 + // PetStoreJSONMessage json raw message for Petstore20. PetStoreJSONMessage json.RawMessage expectedExtraRef []byte expectedPathItem []byte @@ -73,17 +73,17 @@ func TestExpand_Issue148(t *testing.T) { return func(t *testing.T) { require.Len(t, sp.Definitions, 2) - require.Contains(t, sp.Definitions, "empty") + require.MapContainsT(t, sp.Definitions, "empty") empty := sp.Definitions["empty"] require.NotNil(t, empty.AdditionalProperties) require.NotNil(t, empty.AdditionalProperties.Schema) - require.True(t, empty.AdditionalProperties.Allows) + require.TrueT(t, empty.AdditionalProperties.Allows) - require.Contains(t, sp.Definitions, "false") + require.MapContainsT(t, sp.Definitions, "false") additionalIsFalse := sp.Definitions["false"] require.NotNil(t, additionalIsFalse.AdditionalProperties) require.Nil(t, additionalIsFalse.AdditionalProperties.Schema) - require.False(t, additionalIsFalse.AdditionalProperties.Allows) + require.FalseT(t, additionalIsFalse.AdditionalProperties.Allows) } } @@ -100,7 +100,7 @@ func TestExpand_KnownRef(t *testing.T) { schema := RefProperty("http://json-schema.org/draft-04/schema#") require.NoError(t, ExpandSchema(schema, nil, nil)) - assert.Equal(t, "Core schema meta-schema", schema.Description) + assert.EqualT(t, "Core schema meta-schema", schema.Description) // from the expanded schema, verify that all remaining $ref actually resolve jazon := asJSON(t, schema) @@ -126,7 +126,7 @@ func TestExpand_ResponseSchema(t *testing.T) { require.NotNil(t, sch) assert.Empty(t, sch.Ref.String()) - assert.Contains(t, sch.Type, "object") + assert.SliceContainsT(t, sch.Type, "object") assert.Len(t, sch.Properties, 2) } @@ -213,7 +213,7 @@ func TestExpand_InternalResponse(t *testing.T) { jazon := asJSON(t, expectedPet) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "description": "pet response", "schema": { "required": [ @@ -252,7 +252,7 @@ func TestExpand_InternalResponse(t *testing.T) { jazon = asJSON(t, successResponse) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "$ref": "#/responses/anotherPet" }`, jazon) @@ -339,7 +339,6 @@ func TestExpand_InternalParameter(t *testing.T) { param = spec.Paths.Paths["/cars/{id}"].Parameters[0] expected = spec.Parameters["id"] - expected.VendorExtensible = param.VendorExtensible require.NoError(t, expandParameterOrResponse(¶m, resolver, basePath)) @@ -363,7 +362,6 @@ func TestExpand_Parameter(t *testing.T) { param = spec.Paths.Paths["/cars/{id}"].Parameters[0] expected = spec.Parameters["id"] - expected.VendorExtensible = param.VendorExtensible require.NoError(t, ExpandParameter(¶m, basePath)) assert.Equal(t, expected, param) @@ -468,8 +466,8 @@ func TestExpand_InternalSchemas2(t *testing.T) { require.NotNil(t, s) schema = *s - assert.Empty(t, schema.Items.Schema.Ref.String()) // no more a $ref - assert.False(t, schema.Items.Schema.Ref.IsRoot()) // no more a $ref + assert.Empty(t, schema.Items.Schema.Ref.String()) // no more a $ref + assert.FalseT(t, schema.Items.Schema.Ref.IsRoot()) // no more a $ref assert.Equal(t, spec.Definitions["car"], *schema.Items.Schema) sch := new(Schema) @@ -751,8 +749,7 @@ func TestExpand_InternalSchemas1(t *testing.T) { } func TestExpand_RelativeBaseURI(t *testing.T) { - server := httptest.NewServer(http.FileServer(http.Dir("fixtures/remote"))) - defer server.Close() + server := fixtureServer(t, "fixtures/remote") spec := new(Swagger) @@ -803,7 +800,7 @@ func TestExpand_RelativeBaseURI(t *testing.T) { // backRef navigates back to the root document (relative $ref) backRef := spec.Responses["backRef"] require.NoError(t, ExpandResponse(&backRef, opts.RelativeBase)) - assert.Equal(t, "pet response", backRef.Description) + assert.EqualT(t, "pet response", backRef.Description) assert.NotEmpty(t, backRef.Schema) assert.Empty(t, backRef.Ref) @@ -919,7 +916,7 @@ func TestExpandRemoteRef_WithNestedResolutionContext(t *testing.T) { require.Empty(t, tgt.Ref) require.NotNil(t, tgt.Items) require.NotNil(t, tgt.Schema) - assert.Equal(t, "deeper/", tgt.ID) // schema id is preserved + assert.EqualT(t, "deeper/", tgt.ID) // schema id is preserved assert.Equal(t, StringOrArray([]string{"string"}), tgt.Items.Schema.Type) assert.Empty(t, tgt.Items.Schema.Ref) @@ -950,7 +947,7 @@ func TestExpand_RemoteRefWithNestedResolutionContextWithFragment(t *testing.T) { require.Empty(t, tgt.Ref) require.NotNil(t, tgt.Items) require.NotNil(t, tgt.Schema) - assert.Equal(t, "deeper/", tgt.ID) // schema id is preserved + assert.EqualT(t, "deeper/", tgt.ID) // schema id is preserved assert.Equal(t, StringOrArray([]string{"file"}), tgt.Items.Schema.Type) assert.Empty(t, tgt.Items.Schema.Ref) @@ -971,7 +968,7 @@ func TestExpand_TransitiveRefs(t *testing.T) { require.NoError(t, ExpandSpec(spec, opts)) - assert.Equal(t, "todos.stoplight.io", spec.Host) // i.e. not empty + assert.EqualT(t, "todos.stoplight.io", spec.Host) // i.e. not empty jazon := asJSON(t, spec) // verify that the spec has been fully expanded @@ -1031,12 +1028,12 @@ func expandRootWithID(t testing.TB, root *Swagger, testcase string) { func TestExpand_PathItem(t *testing.T) { jazon, _ := expandThisOrDieTrying(t, pathItemsFixture) - assert.JSONEq(t, string(expectedPathItem), jazon) + assert.JSONEqT(t, string(expectedPathItem), jazon) } func TestExpand_ExtraItems(t *testing.T) { jazon, _ := expandThisOrDieTrying(t, extraRefFixture) - assert.JSONEq(t, string(expectedExtraRef), jazon) + assert.JSONEqT(t, string(expectedExtraRef), jazon) } func TestExpand_Issue145(t *testing.T) { @@ -1047,41 +1044,64 @@ func TestExpand_Issue145(t *testing.T) { // assert the internal behavior of baseForRoot() t.Run("with nil root, empty cache", func(t *testing.T) { cache := defaultResolutionCache() - require.Equal(t, pseudoRoot, baseForRoot(nil, cache)) + require.EqualT(t, pseudoRoot, baseForRoot(nil, cache)) t.Run("empty root is cached", func(t *testing.T) { value, ok := cache.Get(pseudoRoot) - require.True(t, ok) // found in cache + require.TrueT(t, ok) // found in cache asMap, ok := value.(map[string]any) - require.True(t, ok) + require.TrueT(t, ok) require.Empty(t, asMap) }) }) t.Run("with non-nil root, empty cache", func(t *testing.T) { cache := defaultResolutionCache() - require.Equal(t, pseudoRoot, baseForRoot(map[string]any{"key": "arbitrary"}, cache)) + require.EqualT(t, pseudoRoot, baseForRoot(map[string]any{"key": "arbitrary"}, cache)) t.Run("non-empty root is cached", func(t *testing.T) { value, ok := cache.Get(pseudoRoot) - require.True(t, ok) // found in cache + require.TrueT(t, ok) // found in cache asMap, ok := value.(map[string]any) - require.True(t, ok) - require.Contains(t, asMap, "key") + require.TrueT(t, ok) + require.MapContainsT(t, asMap, "key") require.Equal(t, "arbitrary", asMap["key"]) }) t.Run("with nil root, non-empty cache", func(t *testing.T) { - require.Equal(t, pseudoRoot, baseForRoot(nil, cache)) + require.EqualT(t, pseudoRoot, baseForRoot(nil, cache)) t.Run("non-empty root is kept", func(t *testing.T) { value, ok := cache.Get(pseudoRoot) - require.True(t, ok) // found in cache + require.TrueT(t, ok) // found in cache asMap, ok := value.(map[string]any) - require.True(t, ok) - require.Contains(t, asMap, "key") + require.TrueT(t, ok) + require.MapContainsT(t, asMap, "key") require.Equal(t, "arbitrary", asMap["key"]) }) }) }) } + +// TestExpand_KeepsSiblingExtensions asserts the Percona-specific behavior of keeping the extensions +// declared next to a $ref (e.g. x-order) on the expanded schema. +func TestExpand_KeepsSiblingExtensions(t *testing.T) { + root := new(Swagger) + require.NoError(t, json.Unmarshal([]byte(`{ + "swagger": "2.0", + "definitions": { + "Bar": {"type": "object"}, + "Foo": { + "type": "object", + "properties": { + "bar": {"$ref": "#/definitions/Bar", "x-order": 1} + } + } + } + }`), root)) + + sch := RefSchema("#/definitions/Foo") + require.NoError(t, ExpandSchema(sch, root, nil)) + + assert.Equal(t, 1.0, sch.Properties["bar"].Extensions["x-order"]) +} diff --git a/external_docs_test.go b/external_docs_test.go index 8f5d7cd7..3e8302f5 100644 --- a/external_docs_test.go +++ b/external_docs_test.go @@ -5,14 +5,17 @@ package spec import ( "testing" + + _ "github.com/go-openapi/testify/enable/yaml/v2" + "github.com/go-openapi/testify/v2/assert" ) func TestIntegrationExternalDocs(t *testing.T) { extDocs := ExternalDocumentation{Description: "the name", URL: "the url"} const extDocsYAML = "description: the name\nurl: the url\n" const extDocsJSON = `{"description":"the name","url":"the url"}` - assertSerializeJSON(t, extDocs, extDocsJSON) - assertSerializeYAML(t, extDocs, extDocsYAML) - assertParsesJSON(t, extDocsJSON, extDocs) - assertParsesYAML(t, extDocsYAML, extDocs) + assert.JSONMarshalAsT(t, extDocsJSON, extDocs) + assert.YAMLMarshalAsT(t, extDocsYAML, extDocs) + assert.JSONUnmarshalAsT(t, extDocs, extDocsJSON) + assert.YAMLUnmarshalAsT(t, extDocs, extDocsYAML) } diff --git a/fixtures/expansion/params.json b/fixtures/expansion/params.json index b920b965..76e7b418 100644 --- a/fixtures/expansion/params.json +++ b/fixtures/expansion/params.json @@ -18,11 +18,8 @@ "paths": { "/cars/{id}": { "parameters": [ - { - "$ref": "#/parameters/id", - "x-order": 1 - } + { "$ref": "#/parameters/id"} ] } } -} +} \ No newline at end of file diff --git a/go.mod b/go.mod index b12107b4..87e8f292 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,21 @@ module github.com/go-openapi/spec require ( - github.com/go-openapi/jsonpointer v0.22.4 - github.com/go-openapi/jsonreference v0.21.4 - github.com/go-openapi/swag/conv v0.25.4 - github.com/go-openapi/swag/jsonname v0.25.4 - github.com/go-openapi/swag/jsonutils v0.25.4 - github.com/go-openapi/swag/loading v0.25.4 - github.com/go-openapi/swag/stringutils v0.25.4 - github.com/go-openapi/testify/v2 v2.0.2 - go.yaml.in/yaml/v3 v3.0.4 + github.com/go-openapi/jsonpointer v1.0.0 + github.com/go-openapi/jsonreference v1.0.0 + github.com/go-openapi/swag/conv v0.27.3 + github.com/go-openapi/swag/jsonutils v0.27.3 + github.com/go-openapi/swag/loading v0.27.3 + github.com/go-openapi/swag/stringutils v0.27.3 + github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 + github.com/go-openapi/testify/v2 v2.6.0 ) require ( - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/swag/pools v0.27.3 // indirect + github.com/go-openapi/swag/typeutils v0.27.3 // indirect + github.com/go-openapi/swag/yamlutils v0.27.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect ) -go 1.25.5 +go 1.25.0 diff --git a/go.sum b/go.sum index bcc47456..3c17049d 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,27 @@ -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= -github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU= +github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ= +github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= +github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= +github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= +github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= +github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= +github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4= +github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= +github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/header.go b/header.go index ab251ef7..f656e078 100644 --- a/header.go +++ b/header.go @@ -15,7 +15,7 @@ const ( jsonArray = "array" ) -// HeaderProps describes a response header +// HeaderProps describes a response header. type HeaderProps struct { Description string `json:"description,omitempty"` } @@ -30,25 +30,25 @@ type Header struct { HeaderProps } -// ResponseHeader creates a new header instance for use in a response +// ResponseHeader creates a new header instance for use in a response. func ResponseHeader() *Header { return new(Header) } -// WithDescription sets the description on this response, allows for chaining +// WithDescription sets the description on this response, allows for chaining. func (h *Header) WithDescription(description string) *Header { h.Description = description return h } -// Typed a fluent builder method for the type of parameter +// Typed a fluent builder method for the type of parameter. func (h *Header) Typed(tpe, format string) *Header { h.Type = tpe h.Format = format return h } -// CollectionOf a fluent builder method for an array item +// CollectionOf a fluent builder method for an array item. func (h *Header) CollectionOf(items *Items, format string) *Header { h.Type = jsonArray h.Items = items @@ -56,87 +56,87 @@ func (h *Header) CollectionOf(items *Items, format string) *Header { return h } -// WithDefault sets the default value on this item +// WithDefault sets the default value on this item. func (h *Header) WithDefault(defaultValue any) *Header { h.Default = defaultValue return h } -// WithMaxLength sets a max length value +// WithMaxLength sets a max length value. func (h *Header) WithMaxLength(maximum int64) *Header { h.MaxLength = &maximum return h } -// WithMinLength sets a min length value +// WithMinLength sets a min length value. func (h *Header) WithMinLength(minimum int64) *Header { h.MinLength = &minimum return h } -// WithPattern sets a pattern value +// WithPattern sets a pattern value. func (h *Header) WithPattern(pattern string) *Header { h.Pattern = pattern return h } -// WithMultipleOf sets a multiple of value +// WithMultipleOf sets a multiple of value. func (h *Header) WithMultipleOf(number float64) *Header { h.MultipleOf = &number return h } -// WithMaximum sets a maximum number value +// WithMaximum sets a maximum number value. func (h *Header) WithMaximum(maximum float64, exclusive bool) *Header { h.Maximum = &maximum h.ExclusiveMaximum = exclusive return h } -// WithMinimum sets a minimum number value +// WithMinimum sets a minimum number value. func (h *Header) WithMinimum(minimum float64, exclusive bool) *Header { h.Minimum = &minimum h.ExclusiveMinimum = exclusive return h } -// WithEnum sets a the enum values (replace) +// WithEnum sets a the enum values (replace). func (h *Header) WithEnum(values ...any) *Header { h.Enum = append([]any{}, values...) return h } -// WithMaxItems sets the max items +// WithMaxItems sets the max items. func (h *Header) WithMaxItems(size int64) *Header { h.MaxItems = &size return h } -// WithMinItems sets the min items +// WithMinItems sets the min items. func (h *Header) WithMinItems(size int64) *Header { h.MinItems = &size return h } -// UniqueValues dictates that this array can only have unique items +// UniqueValues dictates that this array can only have unique items. func (h *Header) UniqueValues() *Header { h.UniqueItems = true return h } -// AllowDuplicates this array can have duplicates +// AllowDuplicates this array can have duplicates. func (h *Header) AllowDuplicates() *Header { h.UniqueItems = false return h } -// WithValidations is a fluent method to set header validations +// WithValidations is a fluent method to set header validations. func (h *Header) WithValidations(val CommonValidations) *Header { h.SetValidations(SchemaValidations{CommonValidations: val}) return h } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (h Header) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(h.CommonValidations) if err != nil { @@ -150,10 +150,14 @@ func (h Header) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return jsonutils.ConcatJSON(b1, b2, b3), nil + b4, err := json.Marshal(h.VendorExtensible) + if err != nil { + return nil, err + } + return jsonutils.ConcatJSON(b1, b2, b3, b4), nil } -// UnmarshalJSON unmarshals this header from JSON +// UnmarshalJSON unmarshals this header from JSON. func (h *Header) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &h.CommonValidations); err != nil { return err @@ -167,7 +171,7 @@ func (h *Header) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &h.HeaderProps) } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (h Header) JSONLookup(token string) (any, error) { if ex, ok := h.Extensions[token]; ok { return &ex, nil diff --git a/header_test.go b/header_test.go index 6d642ee7..e6e0b1a9 100644 --- a/header_test.go +++ b/header_test.go @@ -4,7 +4,6 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/swag/conv" @@ -22,7 +21,7 @@ func int64Ptr(f int64) *int64 { return &f } -var header = Header{ +var header = Header{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{Extensions: map[string]any{ "x-framework": "swagger-go", }}, @@ -75,11 +74,8 @@ const headerJSON = `{ }` func TestIntegrationHeader(t *testing.T) { - var actual Header - require.NoError(t, json.Unmarshal([]byte(headerJSON), &actual)) - assert.Equal(t, actual, header) - - assertParsesJSON(t, headerJSON, header) + assert.JSONUnmarshalAsT(t, header, headerJSON) + assert.JSONMarshalAsT(t, headerJSON, header) } func TestJSONLookupHeader(t *testing.T) { @@ -91,8 +87,8 @@ func TestJSONLookupHeader(t *testing.T) { var ok bool def, ok = res.(string) - require.True(t, ok) - assert.Equal(t, "8", def) + require.TrueT(t, ok) + assert.EqualT(t, "8", def) var x *any res, err = header.JSONLookup("x-framework") @@ -101,7 +97,7 @@ func TestJSONLookupHeader(t *testing.T) { require.IsType(t, x, res) x, ok = res.(*any) - require.True(t, ok) + require.TrueT(t, ok) assert.EqualValues(t, "swagger-go", *x) res, err = header.JSONLookup("unknown") @@ -115,8 +111,8 @@ func TestJSONLookupHeader(t *testing.T) { require.IsType(t, maximum, res) maximum, ok = res.(*float64) - require.True(t, ok) - assert.InDelta(t, float64(100), *maximum, epsilon) + require.TrueT(t, ok) + assert.InDeltaT(t, float64(100), *maximum, epsilon) } func TestResponseHeaueder(t *testing.T) { @@ -127,20 +123,20 @@ func TestResponseHeaueder(t *testing.T) { func TestWithHeader(t *testing.T) { h := new(Header).WithDescription("header description").Typed("integer", "int32") - assert.Equal(t, "header description", h.Description) - assert.Equal(t, "integer", h.Type) - assert.Equal(t, "int32", h.Format) + assert.EqualT(t, "header description", h.Description) + assert.EqualT(t, "integer", h.Type) + assert.EqualT(t, "int32", h.Format) i := new(Items).Typed("string", "date") h = new(Header).CollectionOf(i, "pipe") assert.Equal(t, *i, *h.Items) - assert.Equal(t, "pipe", h.CollectionFormat) + assert.EqualT(t, "pipe", h.CollectionFormat) h = new(Header).WithDefault([]string{"a", "b", "c"}).WithMaxLength(10).WithMinLength(3) - assert.Equal(t, int64(10), *h.MaxLength) - assert.Equal(t, int64(3), *h.MinLength) + assert.EqualT(t, int64(10), *h.MaxLength) + assert.EqualT(t, int64(3), *h.MinLength) assert.EqualValues(t, []string{"a", "b", "c"}, h.Default) h = new(Header).WithPattern("^abc$") diff --git a/helpers_spec_test.go b/helpers_spec_test.go index 1bfa45eb..7d5374f6 100644 --- a/helpers_spec_test.go +++ b/helpers_spec_test.go @@ -18,10 +18,10 @@ import ( var ( rex = regexp.MustCompile(`"\$ref":\s*"(.*?)"`) - testLoader func(string) (json.RawMessage, error) + testLoader func(string) (json.RawMessage, error) //nolint:gochecknoglobals // test fixture ) -func init() { +func init() { //nolint:gochecknoinits // sets up test loader for spec loading fixtures // mimics what the go-openapi/load does testLoader = func(path string) (json.RawMessage, error) { if loading.YAMLMatcher(path) { @@ -51,7 +51,7 @@ func assertRefInJSON(t testing.TB, jazon, prefix string) { for _, matched := range m { subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, prefix), + assert.TrueT(t, strings.HasPrefix(subMatch, prefix), "expected $ref to match %q, got: %s", prefix, matched[0]) } } @@ -66,7 +66,7 @@ func assertRefInJSONRegexp(t testing.TB, jazon, match string) { for _, matched := range m { subMatch := matched[1] - assert.True(t, refMatch.MatchString(subMatch), + assert.TrueT(t, refMatch.MatchString(subMatch), "expected $ref to match %q, got: %s", match, matched[0]) } } @@ -75,7 +75,6 @@ func assertRefInJSONRegexp(t testing.TB, jazon, match string) { // // "exclude" is a regexp pattern to ignore certain $ref (e.g. some specs may embed $ref that are not processed, such as extensions). func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*spec.ExpandOptions) { - t.Helper() if len(opts) > 0 { assertRefWithFunc(t, "expand-with-base", jazon, "", func(t *testing.T, match string) { ref := spec.RefSchema(match) @@ -92,7 +91,6 @@ func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*spec.Expa } func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*spec.ExpandOptions) { - t.Helper() assertRefWithFunc(t, "resolve", jazon, exclude, func(t *testing.T, match string) { ref := spec.MustCreateRef(match) var ( @@ -115,7 +113,6 @@ func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*sp // // "exclude" is a regexp pattern to ignore certain $ref (e.g. some specs may embed $ref that are not processed, such as extensions). func assertRefWithFunc(t *testing.T, name, jazon, exclude string, asserter func(*testing.T, string)) { - t.Helper() filterRex := regexp.MustCompile(exclude) m := rex.FindAllStringSubmatch(jazon, -1) require.NotNil(t, m) @@ -142,17 +139,15 @@ func assertRefWithFunc(t *testing.T, name, jazon, exclude string, asserter func( } } -func asJSON(tb testing.TB, sp any) string { - tb.Helper() +func asJSON(t testing.TB, sp any) string { bbb, err := json.MarshalIndent(sp, "", " ") - require.NoError(tb, err) + require.NoError(t, err) return string(bbb) } -// assertNoRef ensures that no $ref is remaining in json doc +// assertNoRef ensures that no $ref is remaining in json doc. func assertNoRef(t testing.TB, jazon string) { - t.Helper() m := rex.FindAllStringSubmatch(jazon, -1) require.Nil(t, m) } diff --git a/helpers_test.go b/helpers_test.go index 4226c1ba..06ab941d 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -6,6 +6,10 @@ package spec import ( "encoding/json" "fmt" + "io/fs" + "net/http" + "net/http/httptest" + "path/filepath" "regexp" "strings" "testing" @@ -17,6 +21,22 @@ import ( var rex = regexp.MustCompile(`"\$ref":\s*"(.*?)"`) +// fixtureServer returns an httptest.Server serving the given subdirectory +// from the embedded fixtureAssets FS. This avoids OS-level file serving +// (and the Windows TransmitFile/sendfile code path that has a data race +// in Go 1.26). +func fixtureServer(t testing.TB, dir string) *httptest.Server { + t.Helper() + + sub, err := fs.Sub(fixtureAssets, filepath.ToSlash(dir)) + require.NoError(t, err) + + server := httptest.NewServer(http.FileServerFS(sub)) + t.Cleanup(server.Close) + + return server +} + func jsonDoc(path string) (json.RawMessage, error) { data, err := loading.LoadFromFileOrHTTP(path) if err != nil { @@ -76,7 +96,7 @@ func assertRefInJSON(t testing.TB, jazon, prefix string) { for _, matched := range m { subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, prefix), + assert.TrueT(t, strings.HasPrefix(subMatch, prefix), "expected $ref to match %q, got: %s", prefix, matched[0]) } } @@ -94,12 +114,12 @@ func assertRefInJSONRegexp(t testing.TB, jazon, match string) { for _, matched := range m { subMatch := matched[1] - assert.True(t, refMatch.MatchString(subMatch), + assert.TrueT(t, refMatch.MatchString(subMatch), "expected $ref to match %q, got: %s", match, matched[0]) } } -// assertNoRef ensures that no $ref is remaining in json doc +// assertNoRef ensures that no $ref is remaining in json doc. func assertNoRef(t testing.TB, jazon string) { m := rex.FindAllStringSubmatch(jazon, -1) require.Nil(t, m) @@ -109,7 +129,6 @@ func assertNoRef(t testing.TB, jazon string) { // // "exclude" is a regexp pattern to ignore certain $ref (e.g. some specs may embed $ref that are not processed, such as extensions). func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*ExpandOptions) { - t.Helper() assertRefWithFunc(t, jazon, "", func(t *testing.T, match string) { ref := RefSchema(match) if len(opts) > 0 { @@ -125,7 +144,6 @@ func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*ExpandOpt // // "exclude" is a regexp pattern to ignore certain $ref (e.g. some specs may embed $ref that are not processed, such as extensions). func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*ExpandOptions) { - t.Helper() assertRefWithFunc(t, jazon, exclude, func(t *testing.T, match string) { ref := MustCreateRef(match) var ( @@ -148,7 +166,6 @@ func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*Ex // // "exclude" is a regexp pattern to ignore certain $ref (e.g. some specs may embed $ref that are not processed, such as extensions). func assertRefWithFunc(t *testing.T, jazon, exclude string, asserter func(t *testing.T, match string)) { - t.Helper() filterRex := regexp.MustCompile(exclude) m := rex.FindAllStringSubmatch(jazon, -1) require.NotNil(t, m) @@ -171,10 +188,9 @@ func assertRefWithFunc(t *testing.T, jazon, exclude string, asserter func(t *tes } } -func asJSON(tb testing.TB, sp any) string { - tb.Helper() +func asJSON(t testing.TB, sp any) string { bbb, err := json.MarshalIndent(sp, "", " ") - require.NoError(tb, err) + require.NoError(t, err) return string(bbb) } diff --git a/info.go b/info.go index 9401065b..0ccfdccc 100644 --- a/info.go +++ b/info.go @@ -12,16 +12,16 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -// Extensions vendor specific extensions +// Extensions vendor specific extensions. type Extensions map[string]any -// Add adds a value to these extensions +// Add adds a value to these extensions. func (e Extensions) Add(key string, value any) { realKey := strings.ToLower(key) e[realKey] = value } -// GetString gets a string value from the extensions +// GetString gets a string value from the extensions. func (e Extensions) GetString(key string) (string, bool) { if v, ok := e[strings.ToLower(key)]; ok { str, ok := v.(string) @@ -30,7 +30,7 @@ func (e Extensions) GetString(key string) (string, bool) { return "", false } -// GetInt gets a int value from the extensions +// GetInt gets a int value from the extensions. func (e Extensions) GetInt(key string) (int, bool) { realKey := strings.ToLower(key) @@ -48,7 +48,7 @@ func (e Extensions) GetInt(key string) (int, bool) { return -1, false } -// GetBool gets a string value from the extensions +// GetBool gets a string value from the extensions. func (e Extensions) GetBool(key string) (bool, bool) { if v, ok := e[strings.ToLower(key)]; ok { str, ok := v.(bool) @@ -57,7 +57,7 @@ func (e Extensions) GetBool(key string) (bool, bool) { return false, false } -// GetStringSlice gets a string value from the extensions +// GetStringSlice gets a string value from the extensions. func (e Extensions) GetStringSlice(key string) ([]string, bool) { if v, ok := e[strings.ToLower(key)]; ok { arr, isSlice := v.([]any) @@ -82,7 +82,7 @@ type VendorExtensible struct { Extensions Extensions } -// AddExtension adds an extension to this extensible object +// AddExtension adds an extension to this extensible object. func (v *VendorExtensible) AddExtension(key string, value any) { if value == nil { return @@ -93,7 +93,7 @@ func (v *VendorExtensible) AddExtension(key string, value any) { v.Extensions.Add(key, value) } -// MarshalJSON marshals the extensions to json +// MarshalJSON marshals the extensions to json. func (v VendorExtensible) MarshalJSON() ([]byte, error) { toser := make(map[string]any) for k, v := range v.Extensions { @@ -105,7 +105,7 @@ func (v VendorExtensible) MarshalJSON() ([]byte, error) { return json.Marshal(toser) } -// UnmarshalJSON for this extensible object +// UnmarshalJSON for this extensible object. func (v *VendorExtensible) UnmarshalJSON(data []byte) error { var d map[string]any if err := json.Unmarshal(data, &d); err != nil { @@ -123,7 +123,7 @@ func (v *VendorExtensible) UnmarshalJSON(data []byte) error { return nil } -// InfoProps the properties for an info definition +// InfoProps the properties for an info definition. type InfoProps struct { Description string `json:"description,omitempty"` Title string `json:"title,omitempty"` @@ -142,7 +142,7 @@ type Info struct { InfoProps } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (i Info) JSONLookup(token string) (any, error) { if ex, ok := i.Extensions[token]; ok { return &ex, nil @@ -151,7 +151,7 @@ func (i Info) JSONLookup(token string) (any, error) { return r, err } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (i Info) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(i.InfoProps) if err != nil { @@ -164,7 +164,7 @@ func (i Info) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b1, b2), nil } -// UnmarshalJSON marshal this from JSON +// UnmarshalJSON marshal this from JSON. func (i *Info) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &i.InfoProps); err != nil { return err diff --git a/info_test.go b/info_test.go index b4018612..8ebace52 100644 --- a/info_test.go +++ b/info_test.go @@ -28,7 +28,7 @@ const infoJSON = `{ "x-framework": "go-swagger" }` -var testInfo = Info{ //nolint:gochecknoglobals +var testInfo = Info{ //nolint:gochecknoglobals // test fixture InfoProps: InfoProps{ Version: "1.0.9-abcd", Title: "Swagger Sample API", @@ -48,15 +48,11 @@ var testInfo = Info{ //nolint:gochecknoglobals func TestInfo(t *testing.T) { t.Run("should marshal Info", func(t *testing.T) { - b, err := json.MarshalIndent(testInfo, "", "\t") - require.NoError(t, err) - assert.JSONEq(t, infoJSON, string(b)) + assert.JSONMarshalAsT(t, infoJSON, testInfo) }) t.Run("should unmarshal Info", func(t *testing.T) { - actual := Info{} - require.NoError(t, json.Unmarshal([]byte(infoJSON), &actual)) - assert.Equal(t, testInfo, actual) + assert.JSONUnmarshalAsT(t, testInfo, infoJSON) }) t.Run("should GobEncode Info", func(t *testing.T) { diff --git a/items.go b/items.go index d30ca356..daf5a4fd 100644 --- a/items.go +++ b/items.go @@ -15,7 +15,7 @@ const ( jsonRef = "$ref" ) -// SimpleSchema describe swagger simple schemas for parameters and headers +// SimpleSchema describe swagger simple schemas for parameters and headers. type SimpleSchema struct { Type string `json:"type,omitempty"` Nullable bool `json:"nullable,omitempty"` @@ -26,7 +26,7 @@ type SimpleSchema struct { Example any `json:"example,omitempty"` } -// TypeName return the type (or format) of a simple schema +// TypeName return the type (or format) of a simple schema. func (s *SimpleSchema) TypeName() string { if s.Format != "" { return s.Format @@ -34,7 +34,7 @@ func (s *SimpleSchema) TypeName() string { return s.Type } -// ItemsTypeName yields the type of items in a simple schema array +// ItemsTypeName yields the type of items in a simple schema array. func (s *SimpleSchema) ItemsTypeName() string { if s.Items == nil { return "" @@ -53,12 +53,12 @@ type Items struct { VendorExtensible } -// NewItems creates a new instance of items +// NewItems creates a new instance of items. func NewItems() *Items { return &Items{} } -// Typed a fluent builder method for the type of item +// Typed a fluent builder method for the type of item. func (i *Items) Typed(tpe, format string) *Items { i.Type = tpe i.Format = format @@ -71,7 +71,7 @@ func (i *Items) AsNullable() *Items { return i } -// CollectionOf a fluent builder method for an array item +// CollectionOf a fluent builder method for an array item. func (i *Items) CollectionOf(items *Items, format string) *Items { i.Type = jsonArray i.Items = items @@ -79,87 +79,87 @@ func (i *Items) CollectionOf(items *Items, format string) *Items { return i } -// WithDefault sets the default value on this item +// WithDefault sets the default value on this item. func (i *Items) WithDefault(defaultValue any) *Items { i.Default = defaultValue return i } -// WithMaxLength sets a max length value +// WithMaxLength sets a max length value. func (i *Items) WithMaxLength(maximum int64) *Items { i.MaxLength = &maximum return i } -// WithMinLength sets a min length value +// WithMinLength sets a min length value. func (i *Items) WithMinLength(minimum int64) *Items { i.MinLength = &minimum return i } -// WithPattern sets a pattern value +// WithPattern sets a pattern value. func (i *Items) WithPattern(pattern string) *Items { i.Pattern = pattern return i } -// WithMultipleOf sets a multiple of value +// WithMultipleOf sets a multiple of value. func (i *Items) WithMultipleOf(number float64) *Items { i.MultipleOf = &number return i } -// WithMaximum sets a maximum number value +// WithMaximum sets a maximum number value. func (i *Items) WithMaximum(maximum float64, exclusive bool) *Items { i.Maximum = &maximum i.ExclusiveMaximum = exclusive return i } -// WithMinimum sets a minimum number value +// WithMinimum sets a minimum number value. func (i *Items) WithMinimum(minimum float64, exclusive bool) *Items { i.Minimum = &minimum i.ExclusiveMinimum = exclusive return i } -// WithEnum sets a the enum values (replace) +// WithEnum sets a the enum values (replace). func (i *Items) WithEnum(values ...any) *Items { i.Enum = append([]any{}, values...) return i } -// WithMaxItems sets the max items +// WithMaxItems sets the max items. func (i *Items) WithMaxItems(size int64) *Items { i.MaxItems = &size return i } -// WithMinItems sets the min items +// WithMinItems sets the min items. func (i *Items) WithMinItems(size int64) *Items { i.MinItems = &size return i } -// UniqueValues dictates that this array can only have unique items +// UniqueValues dictates that this array can only have unique items. func (i *Items) UniqueValues() *Items { i.UniqueItems = true return i } -// AllowDuplicates this array can have duplicates +// AllowDuplicates this array can have duplicates. func (i *Items) AllowDuplicates() *Items { i.UniqueItems = false return i } -// WithValidations is a fluent method to set Items validations +// WithValidations is a fluent method to set Items validations. func (i *Items) WithValidations(val CommonValidations) *Items { i.SetValidations(SchemaValidations{CommonValidations: val}) return i } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (i *Items) UnmarshalJSON(data []byte) error { var validations CommonValidations if err := json.Unmarshal(data, &validations); err != nil { @@ -184,7 +184,7 @@ func (i *Items) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (i Items) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(i.CommonValidations) if err != nil { @@ -205,7 +205,7 @@ func (i Items) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b4, b3, b1, b2), nil } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (i Items) JSONLookup(token string) (any, error) { if token == jsonRef { return &i.Ref, nil diff --git a/items_test.go b/items_test.go index 7e3f0896..e59d033b 100644 --- a/items_test.go +++ b/items_test.go @@ -4,7 +4,6 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/swag/conv" @@ -12,7 +11,32 @@ import ( "github.com/go-openapi/testify/v2/require" ) -// testItems is now defined inside the test function to avoid global variable. +var testItems = Items{ //nolint:gochecknoglobals // test fixture + Refable: Refable{Ref: MustCreateRef("Dog")}, + CommonValidations: CommonValidations{ + Maximum: float64Ptr(100), + ExclusiveMaximum: true, + ExclusiveMinimum: true, + Minimum: float64Ptr(5), + MaxLength: int64Ptr(100), + MinLength: int64Ptr(5), + Pattern: "\\w{1,5}\\w+", + MaxItems: int64Ptr(100), + MinItems: int64Ptr(5), + UniqueItems: true, + MultipleOf: float64Ptr(5), + Enum: []any{"hello", "world"}, + }, + SimpleSchema: SimpleSchema{ + Type: "string", + Format: "date", + Items: &Items{ + Refable: Refable{Ref: MustCreateRef("Cat")}, + }, + CollectionFormat: "csv", + Default: "8", + }, +} const itemsJSON = `{ "items": { @@ -38,70 +62,14 @@ const itemsJSON = `{ }` func TestIntegrationItems(t *testing.T) { - testItems := Items{ - Refable: Refable{Ref: MustCreateRef("Dog")}, - CommonValidations: CommonValidations{ - Maximum: float64Ptr(100), - ExclusiveMaximum: true, - ExclusiveMinimum: true, - Minimum: float64Ptr(5), - MaxLength: int64Ptr(100), - MinLength: int64Ptr(5), - Pattern: "\\w{1,5}\\w+", - MaxItems: int64Ptr(100), - MinItems: int64Ptr(5), - UniqueItems: true, - MultipleOf: float64Ptr(5), - Enum: []any{"hello", "world"}, - }, - SimpleSchema: SimpleSchema{ - Type: "string", - Format: "date", - Items: &Items{ - Refable: Refable{Ref: MustCreateRef("Cat")}, - }, - CollectionFormat: "csv", - Default: "8", - }, - } - var actual Items - require.NoError(t, json.Unmarshal([]byte(itemsJSON), &actual)) - assert.Equal(t, actual, testItems) - - assertParsesJSON(t, itemsJSON, testItems) + assert.JSONUnmarshalAsT(t, testItems, itemsJSON) } func TestTypeNameItems(t *testing.T) { var nilItems Items assert.Empty(t, nilItems.TypeName()) - testItems := Items{ - Refable: Refable{Ref: MustCreateRef("Dog")}, - CommonValidations: CommonValidations{ - Maximum: float64Ptr(100), - ExclusiveMaximum: true, - ExclusiveMinimum: true, - Minimum: float64Ptr(5), - MaxLength: int64Ptr(100), - MinLength: int64Ptr(5), - Pattern: "\\w{1,5}\\w+", - MaxItems: int64Ptr(100), - MinItems: int64Ptr(5), - UniqueItems: true, - MultipleOf: float64Ptr(5), - Enum: []any{"hello", "world"}, - }, - SimpleSchema: SimpleSchema{ - Type: "string", - Format: "date", - Items: &Items{ - Refable: Refable{Ref: MustCreateRef("Cat")}, - }, - CollectionFormat: "csv", - Default: "8", - }, - } - assert.Equal(t, "date", testItems.TypeName()) + assert.EqualT(t, "date", testItems.TypeName()) assert.Empty(t, testItems.ItemsTypeName()) nested := Items{ @@ -117,23 +85,23 @@ func TestTypeNameItems(t *testing.T) { }, } - assert.Equal(t, "array", nested.TypeName()) - assert.Equal(t, "int32", nested.ItemsTypeName()) + assert.EqualT(t, "array", nested.TypeName()) + assert.EqualT(t, "int32", nested.ItemsTypeName()) simple := SimpleSchema{ Type: "string", Items: nil, } - assert.Equal(t, "string", simple.TypeName()) + assert.EqualT(t, "string", simple.TypeName()) assert.Empty(t, simple.ItemsTypeName()) simple.Items = NewItems() simple.Type = "array" simple.Items.Type = "string" - assert.Equal(t, "array", simple.TypeName()) - assert.Equal(t, "string", simple.ItemsTypeName()) + assert.EqualT(t, "array", simple.TypeName()) + assert.EqualT(t, "string", simple.ItemsTypeName()) } func TestItemsBuilder(t *testing.T) { @@ -167,32 +135,6 @@ func TestItemsBuilder(t *testing.T) { } func TestJSONLookupItems(t *testing.T) { - testItems := Items{ - Refable: Refable{Ref: MustCreateRef("Dog")}, - CommonValidations: CommonValidations{ - Maximum: float64Ptr(100), - ExclusiveMaximum: true, - ExclusiveMinimum: true, - Minimum: float64Ptr(5), - MaxLength: int64Ptr(100), - MinLength: int64Ptr(5), - Pattern: "\\w{1,5}\\w+", - MaxItems: int64Ptr(100), - MinItems: int64Ptr(5), - UniqueItems: true, - MultipleOf: float64Ptr(5), - Enum: []any{"hello", "world"}, - }, - SimpleSchema: SimpleSchema{ - Type: "string", - Format: "date", - Items: &Items{ - Refable: Refable{Ref: MustCreateRef("Cat")}, - }, - CollectionFormat: "csv", - Default: "8", - }, - } t.Run(`lookup should find "$ref"`, func(t *testing.T) { res, err := testItems.JSONLookup("$ref") require.NoError(t, err) @@ -200,7 +142,7 @@ func TestJSONLookupItems(t *testing.T) { require.IsType(t, &Ref{}, res) ref, ok := res.(*Ref) - require.True(t, ok) + require.TrueT(t, ok) assert.Equal(t, MustCreateRef("Dog"), *ref) }) @@ -213,8 +155,8 @@ func TestJSONLookupItems(t *testing.T) { var ok bool maximum, ok = res.(*float64) - require.True(t, ok) - assert.InDelta(t, float64(100), *maximum, epsilon) + require.TrueT(t, ok) + assert.InDeltaT(t, float64(100), *maximum, epsilon) }) t.Run(`lookup should find "collectionFormat"`, func(t *testing.T) { @@ -225,8 +167,8 @@ func TestJSONLookupItems(t *testing.T) { require.IsType(t, f, res) f, ok := res.(string) - require.True(t, ok) - assert.Equal(t, "csv", f) + require.TrueT(t, ok) + assert.EqualT(t, "csv", f) }) t.Run(`lookup should fail on "unknown"`, func(t *testing.T) { diff --git a/license.go b/license.go index 286b237e..8209f218 100644 --- a/license.go +++ b/license.go @@ -17,13 +17,13 @@ type License struct { VendorExtensible } -// LicenseProps holds the properties of a License object +// LicenseProps holds the properties of a License object. type LicenseProps struct { Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` } -// UnmarshalJSON hydrates License from json +// UnmarshalJSON hydrates License from json. func (l *License) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &l.LicenseProps); err != nil { return err @@ -31,7 +31,7 @@ func (l *License) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &l.VendorExtensible) } -// MarshalJSON produces License as json +// MarshalJSON produces License as json. func (l License) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(l.LicenseProps) if err != nil { diff --git a/license_test.go b/license_test.go index cdc3bcea..56f75b65 100644 --- a/license_test.go +++ b/license_test.go @@ -4,11 +4,9 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" ) func TestIntegrationLicense(t *testing.T) { @@ -26,15 +24,10 @@ func TestIntegrationLicense(t *testing.T) { // const licenseYAML = "name: the name\nurl: the url\n" t.Run("should marshal license", func(t *testing.T) { - b, err := json.MarshalIndent(testLicense, "", "\t") - require.NoError(t, err) - assert.JSONEq(t, licenseJSON, string(b)) + assert.JSONMarshalAsT(t, licenseJSON, testLicense) }) t.Run("should unmarshal empty license", func(t *testing.T) { - actual := License{} - err := json.Unmarshal([]byte(licenseJSON), &actual) - require.NoError(t, err) - assert.Equal(t, testLicense, actual) + assert.JSONUnmarshalAsT(t, testLicense, licenseJSON) }) } diff --git a/normalizer.go b/normalizer.go index e1d7c58d..68252dc3 100644 --- a/normalizer.go +++ b/normalizer.go @@ -138,7 +138,7 @@ func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { return MustCreateRef(newBase.String()), true } -// normalizeRef canonicalize a Ref, using a canonical relativeBase as its absolute anchor +// normalizeRef canonicalize a Ref, using a canonical relativeBase as its absolute anchor. func normalizeRef(ref *Ref, relativeBase string) *Ref { r := MustCreateRef(normalizeURI(ref.String(), relativeBase)) return &r diff --git a/normalizer_test.go b/normalizer_test.go index 48ab6bce..a7ab306c 100644 --- a/normalizer_test.go +++ b/normalizer_test.go @@ -17,10 +17,10 @@ import ( const windowsOS = "windows" -// only used for windows -var currentDriveLetter = getCurrentDrive() +// only used for windows. +var currentDriveLetter = getCurrentDrive() //nolint:gochecknoglobals // test fixture -// get the current drive letter in lowercase on windows that the test is running +// get the current drive letter in lowercase on windows that the test is running. func getCurrentDrive() string { if runtime.GOOS != windowsOS { return "" @@ -260,7 +260,7 @@ func TestNormalizer_NormalizeURI(t *testing.T) { t.Run(testCase.refPath, func(t *testing.T) { t.Parallel() out := normalizeURI(testCase.refPath, testCase.base) - assert.Equalf(t, testCase.expOutput, out, + assert.EqualTf(t, testCase.expOutput, out, "unexpected normalized URL with $ref %q and base %q", testCase.refPath, testCase.base) }) } @@ -295,7 +295,7 @@ func TestNormalizer_NormalizeBase(t *testing.T) { Base: ".", Expected: "file://$cwd", // edge case: this won't work because a document is a file }, - { + { //nolint:gosec // test data, not real credentials Base: "https://user:password@www.example.com:123/base/sub/file.json", Expected: "https://user:password@www.example.com:123/base/sub/file.json", }, @@ -466,10 +466,10 @@ func TestNormalizer_NormalizeBase(t *testing.T) { t.Run(testCase.Base, func(t *testing.T) { t.Parallel() expected := strings.ReplaceAll(strings.ReplaceAll(testCase.Expected, "$cwd", cwd), "$dir", path.Dir(cwd)) - require.Equalf(t, expected, normalizeBase(testCase.Base), "for base %q", testCase.Base) + require.EqualTf(t, expected, normalizeBase(testCase.Base), "for base %q", testCase.Base) // check for idempotence - require.Equalf(t, expected, normalizeBase(normalizeBase(testCase.Base)), + require.EqualTf(t, expected, normalizeBase(normalizeBase(testCase.Base)), "expected idempotent behavior on base %q", testCase.Base) }) } @@ -494,7 +494,7 @@ func TestNormalizer_Denormalize(t *testing.T) { Ref: "#/definitions/X", Expected: "#/definitions/X", }, - { + { //nolint:gosec // test data, not real credentials OriginalBase: "https://user:password@example.com/a/b/c/file.json", Ref: "https://user:password@example.com/a/b/c/other.json#/definitions/X", Expected: "other.json#/definitions/X", @@ -609,7 +609,7 @@ func TestNormalizer_Denormalize(t *testing.T) { ref := MustCreateRef(testCase.Ref) newRef := denormalizeRef(&ref, testCase.OriginalBase, testCase.ID) require.NotNil(t, newRef) - require.Equalf(t, expected, newRef.String(), + require.EqualTf(t, expected, newRef.String(), "expected %s, but got %s", testCase.Expected, newRef.String()) }) } diff --git a/operation.go b/operation.go index 974f68a6..cd70d254 100644 --- a/operation.go +++ b/operation.go @@ -13,7 +13,7 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -func init() { +func init() { //nolint:gochecknoinits // registers gob types for Operation serialization gob.Register(map[string]any{}) gob.Register([]any{}) } @@ -22,7 +22,7 @@ func init() { // // NOTES: // - schemes, when present must be from [http, https, ws, wss]: see validate -// - Security is handled as a special case: see MarshalJSON function +// - Security is handled as a special case: see MarshalJSON function. type OperationProps struct { Description string `json:"description,omitempty"` Consumes []string `json:"consumes,omitempty"` @@ -38,10 +38,10 @@ type OperationProps struct { Responses *Responses `json:"responses,omitempty"` } -// MarshalJSON takes care of serializing operation properties to JSON. +// MarshalJSON takes care of serializing operation properties to JSON // // We use a custom marhaller here to handle a special cases related to -// the Security field. We need to preserve zero length slice. +// the Security field. We need to preserve zero length slice // while omitting the field when the value is nil/unset. func (op OperationProps) MarshalJSON() ([]byte, error) { type Alias OperationProps @@ -82,7 +82,7 @@ func NewOperation(id string) *Operation { return op } -// SuccessResponse gets a success response model +// SuccessResponse gets a success response model. func (o *Operation) SuccessResponse() (*Response, int, bool) { if o.Responses == nil { return nil, 0, false @@ -103,7 +103,7 @@ func (o *Operation) SuccessResponse() (*Response, int, bool) { return o.Responses.Default, 0, false } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (o Operation) JSONLookup(token string) (any, error) { if ex, ok := o.Extensions[token]; ok { return &ex, nil @@ -112,7 +112,7 @@ func (o Operation) JSONLookup(token string) (any, error) { return r, err } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (o *Operation) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &o.OperationProps); err != nil { return err @@ -120,7 +120,7 @@ func (o *Operation) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &o.VendorExtensible) } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (o Operation) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(o.OperationProps) if err != nil { @@ -140,13 +140,13 @@ func (o *Operation) WithID(id string) *Operation { return o } -// WithDescription sets the description on this operation, allows for chaining +// WithDescription sets the description on this operation, allows for chaining. func (o *Operation) WithDescription(description string) *Operation { o.Description = description return o } -// WithSummary sets the summary on this operation, allows for chaining +// WithSummary sets the summary on this operation, allows for chaining. func (o *Operation) WithSummary(summary string) *Operation { o.Summary = summary return o @@ -170,7 +170,7 @@ func (o *Operation) WithExternalDocs(description, url string) *Operation { return o } -// Deprecate marks the operation as deprecated +// Deprecate marks the operation as deprecated. func (o *Operation) Deprecate() *Operation { o.Deprecated = true return o @@ -182,26 +182,26 @@ func (o *Operation) Undeprecate() *Operation { return o } -// WithConsumes adds media types for incoming body values +// WithConsumes adds media types for incoming body values. func (o *Operation) WithConsumes(mediaTypes ...string) *Operation { o.Consumes = append(o.Consumes, mediaTypes...) return o } -// WithProduces adds media types for outgoing body values +// WithProduces adds media types for outgoing body values. func (o *Operation) WithProduces(mediaTypes ...string) *Operation { o.Produces = append(o.Produces, mediaTypes...) return o } -// WithTags adds tags for this operation +// WithTags adds tags for this operation. func (o *Operation) WithTags(tags ...string) *Operation { o.Tags = append(o.Tags, tags...) return o } // AddParam adds a parameter to this operation, when a parameter for that location -// and with that name already exists it will be replaced +// and with that name already exists it will be replaced. func (o *Operation) AddParam(param *Parameter) *Operation { if param == nil { return o @@ -223,7 +223,7 @@ func (o *Operation) AddParam(param *Parameter) *Operation { return o } -// RemoveParam removes a parameter from the operation +// RemoveParam removes a parameter from the operation. func (o *Operation) RemoveParam(name, in string) *Operation { for i, p := range o.Parameters { if p.Name == name && p.In == in { @@ -241,14 +241,14 @@ func (o *Operation) SecuredWith(name string, scopes ...string) *Operation { } // WithDefaultResponse adds a default response to the operation. -// Passing a nil value will remove the response +// Passing a nil value will remove the response. func (o *Operation) WithDefaultResponse(response *Response) *Operation { return o.RespondsWith(0, response) } // RespondsWith adds a status code response to the operation. // When the code is 0 the value of the response will be used as default response value. -// When the value of the response is nil it will be removed from the operation +// When the value of the response is nil it will be removed from the operation. func (o *Operation) RespondsWith(code int, response *Response) *Operation { if o.Responses == nil { o.Responses = new(Responses) @@ -279,7 +279,7 @@ type gobAlias struct { SecurityIsEmpty bool } -// GobEncode provides a safe gob encoder for Operation, including empty security requirements +// GobEncode provides a safe gob encoder for Operation, including empty security requirements. func (o Operation) GobEncode() ([]byte, error) { raw := struct { Ext VendorExtensible @@ -293,7 +293,7 @@ func (o Operation) GobEncode() ([]byte, error) { return b.Bytes(), err } -// GobDecode provides a safe gob decoder for Operation, including empty security requirements +// GobDecode provides a safe gob decoder for Operation, including empty security requirements. func (o *Operation) GobDecode(b []byte) error { var raw struct { Ext VendorExtensible @@ -310,7 +310,7 @@ func (o *Operation) GobDecode(b []byte) error { return nil } -// GobEncode provides a safe gob encoder for Operation, including empty security requirements +// GobEncode provides a safe gob encoder for Operation, including empty security requirements. func (op OperationProps) GobEncode() ([]byte, error) { raw := gobAlias{ Alias: (*opsAlias)(&op), @@ -355,7 +355,7 @@ func (op OperationProps) GobEncode() ([]byte, error) { return b.Bytes(), err } -// GobDecode provides a safe gob decoder for Operation, including empty security requirements +// GobDecode provides a safe gob decoder for Operation, including empty security requirements. func (op *OperationProps) GobDecode(b []byte) error { var raw gobAlias diff --git a/operation_test.go b/operation_test.go index 8e813e8a..965b50e4 100644 --- a/operation_test.go +++ b/operation_test.go @@ -13,7 +13,7 @@ import ( "github.com/go-openapi/testify/v2/require" ) -var operation = Operation{ +var operation = Operation{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{ Extensions: map[string]any{ "x-framework": "go-swagger", @@ -71,15 +71,15 @@ func TestSuccessResponse(t *testing.T) { ope := &Operation{} resp, n, f := ope.SuccessResponse() assert.Nil(t, resp) - assert.Equal(t, 0, n) - assert.False(t, f) + assert.EqualT(t, 0, n) + assert.FalseT(t, f) resp, n, f = operation.SuccessResponse() require.NotNil(t, resp) - assert.Equal(t, "void response", resp.Description) + assert.EqualT(t, "void response", resp.Description) - assert.Equal(t, 0, n) - assert.False(t, f) + assert.EqualT(t, 0, n) + assert.FalseT(t, f) require.NoError(t, json.Unmarshal([]byte(operationJSON), ope)) @@ -90,10 +90,10 @@ func TestSuccessResponse(t *testing.T) { }) resp, n, f = ope.SuccessResponse() require.NotNil(t, resp) - assert.Equal(t, "void response", resp.Description) + assert.EqualT(t, "void response", resp.Description) - assert.Equal(t, 0, n) - assert.False(t, f) + assert.EqualT(t, 0, n) + assert.FalseT(t, f) ope = ope.RespondsWith(200, &Response{ ResponseProps: ResponseProps{ @@ -103,10 +103,10 @@ func TestSuccessResponse(t *testing.T) { resp, n, f = ope.SuccessResponse() require.NotNil(t, resp) - assert.Equal(t, "success", resp.Description) + assert.EqualT(t, "success", resp.Description) - assert.Equal(t, 200, n) - assert.True(t, f) + assert.EqualT(t, 200, n) + assert.TrueT(t, f) } func TestOperationBuilder(t *testing.T) { @@ -134,10 +134,7 @@ func TestOperationBuilder(t *testing.T) { WithSummary("my summary"). WithExternalDocs("some doc", "https://www.example.com") - jazon, err := json.MarshalIndent(ope, "", " ") - require.NoError(t, err) - - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "operationId": "operationID", "description": "test operation", "summary": "my summary", @@ -187,23 +184,20 @@ func TestOperationBuilder(t *testing.T) { "description": "default" } } - }`, string(jazon)) + }`, ope) // check token lookup token, err := ope.JSONLookup("responses") require.NoError(t, err) - jazon, err = json.MarshalIndent(token, "", " ") - require.NoError(t, err) - - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "200": { "description": "success" }, "default": { "description": "default" } - }`, string(jazon)) + }`, token) // check delete methods ope = ope.RespondsWith(200, nil). @@ -212,10 +206,8 @@ func TestOperationBuilder(t *testing.T) { RemoveParam("fakeParam", "query"). Undeprecate(). WithExternalDocs("", "") - jazon, err = json.MarshalIndent(ope, "", " ") - require.NoError(t, err) - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "security": [ { "scheme-name": [ @@ -242,15 +234,11 @@ func TestOperationBuilder(t *testing.T) { "description": "default" } } - }`, string(jazon)) + }`, ope) } func TestIntegrationOperation(t *testing.T) { - var actual Operation - require.NoError(t, json.Unmarshal([]byte(operationJSON), &actual)) - assert.Equal(t, actual, operation) - - assertParsesJSON(t, operationJSON, operation) + assert.JSONUnmarshalAsT(t, operation, operationJSON) } func TestSecurityProperty(t *testing.T) { @@ -258,7 +246,7 @@ func TestSecurityProperty(t *testing.T) { securityNotSet := OperationProps{} jsonResult, err := json.Marshal(securityNotSet) require.NoError(t, err) - assert.NotContains(t, string(jsonResult), "security", "security key should be omitted when unset") + assert.StringNotContainsT(t, string(jsonResult), "security", "security key should be omitted when unset") // Ensure we preserve the security key when it contains an empty (zero length) slice securityContainsEmptyArray := OperationProps{ @@ -344,11 +332,11 @@ func doTestOperationGobEncoding(t *testing.T, fixture string) { } func doTestAnyGobEncoding(t *testing.T, src, dst any) { - t.Helper() - expectedJSON, _ := json.MarshalIndent(src, "", " ") + expectedJSON, err := json.MarshalIndent(src, "", " ") + require.NoError(t, err) var b bytes.Buffer - err := gob.NewEncoder(&b).Encode(src) + err = gob.NewEncoder(&b).Encode(src) require.NoError(t, err) err = gob.NewDecoder(&b).Decode(dst) @@ -356,5 +344,5 @@ func doTestAnyGobEncoding(t *testing.T, src, dst any) { jazon, err := json.MarshalIndent(dst, "", " ") require.NoError(t, err) - assert.JSONEq(t, string(expectedJSON), string(jazon)) + assert.JSONEqT(t, string(expectedJSON), string(jazon)) } diff --git a/parameter.go b/parameter.go index e0f2cc93..516f5d95 100644 --- a/parameter.go +++ b/parameter.go @@ -11,32 +11,32 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -// QueryParam creates a query parameter +// QueryParam creates a query parameter. func QueryParam(name string) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name, In: "query"}} } -// HeaderParam creates a header parameter, this is always required by default +// HeaderParam creates a header parameter, this is always required by default. func HeaderParam(name string) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name, In: "header", Required: true}} } -// PathParam creates a path parameter, this is always required +// PathParam creates a path parameter, this is always required. func PathParam(name string) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name, In: "path", Required: true}} } -// BodyParam creates a body parameter +// BodyParam creates a body parameter. func BodyParam(name string, schema *Schema) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name, In: "body", Schema: schema}} } -// FormDataParam creates a body parameter +// FormDataParam creates a body parameter. func FormDataParam(name string) *Parameter { return &Parameter{ParamProps: ParamProps{Name: name, In: "formData"}} } -// FileParam creates a body parameter +// FileParam creates a body parameter. func FileParam(name string) *Parameter { return &Parameter{ ParamProps: ParamProps{Name: name, In: "formData"}, @@ -44,7 +44,7 @@ func FileParam(name string) *Parameter { } } -// SimpleArrayParam creates a param for a simple array (string, int, date etc) +// SimpleArrayParam creates a param for a simple array (string, int, date etc). func SimpleArrayParam(name, tpe, fmt string) *Parameter { return &Parameter{ ParamProps: ParamProps{Name: name}, @@ -55,7 +55,7 @@ func SimpleArrayParam(name, tpe, fmt string) *Parameter { } } -// ParamRef creates a parameter that's a json reference +// ParamRef creates a parameter that's a json reference. func ParamRef(uri string) *Parameter { p := new(Parameter) p.Ref = MustCreateRef(uri) @@ -66,7 +66,7 @@ func ParamRef(uri string) *Parameter { // // NOTE: // - Schema is defined when "in" == "body": see validate -// - AllowEmptyValue is allowed where "in" == "query" || "formData" +// - AllowEmptyValue is allowed where "in" == "query" || "formData". type ParamProps struct { Description string `json:"description,omitempty"` Name string `json:"name,omitempty"` @@ -110,7 +110,7 @@ type Parameter struct { ParamProps } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (p Parameter) JSONLookup(token string) (any, error) { if ex, ok := p.Extensions[token]; ok { return &ex, nil @@ -137,32 +137,32 @@ func (p Parameter) JSONLookup(token string) (any, error) { return r, err } -// WithDescription a fluent builder method for the description of the parameter +// WithDescription a fluent builder method for the description of the parameter. func (p *Parameter) WithDescription(description string) *Parameter { p.Description = description return p } -// Named a fluent builder method to override the name of the parameter +// Named a fluent builder method to override the name of the parameter. func (p *Parameter) Named(name string) *Parameter { p.Name = name return p } -// WithLocation a fluent builder method to override the location of the parameter +// WithLocation a fluent builder method to override the location of the parameter. func (p *Parameter) WithLocation(in string) *Parameter { p.In = in return p } -// Typed a fluent builder method for the type of the parameter value +// Typed a fluent builder method for the type of the parameter value. func (p *Parameter) Typed(tpe, format string) *Parameter { p.Type = tpe p.Format = format return p } -// CollectionOf a fluent builder method for an array parameter +// CollectionOf a fluent builder method for an array parameter. func (p *Parameter) CollectionOf(items *Items, format string) *Parameter { p.Type = jsonArray p.Items = items @@ -170,32 +170,32 @@ func (p *Parameter) CollectionOf(items *Items, format string) *Parameter { return p } -// WithDefault sets the default value on this parameter +// WithDefault sets the default value on this parameter. func (p *Parameter) WithDefault(defaultValue any) *Parameter { p.AsOptional() // with default implies optional p.Default = defaultValue return p } -// AllowsEmptyValues flags this parameter as being ok with empty values +// AllowsEmptyValues flags this parameter as being ok with empty values. func (p *Parameter) AllowsEmptyValues() *Parameter { p.AllowEmptyValue = true return p } -// NoEmptyValues flags this parameter as not liking empty values +// NoEmptyValues flags this parameter as not liking empty values. func (p *Parameter) NoEmptyValues() *Parameter { p.AllowEmptyValue = false return p } -// AsOptional flags this parameter as optional +// AsOptional flags this parameter as optional. func (p *Parameter) AsOptional() *Parameter { p.Required = false return p } -// AsRequired flags this parameter as required +// AsRequired flags this parameter as required. func (p *Parameter) AsRequired() *Parameter { if p.Default != nil { // with a default required makes no sense return p @@ -204,81 +204,81 @@ func (p *Parameter) AsRequired() *Parameter { return p } -// WithMaxLength sets a max length value +// WithMaxLength sets a max length value. func (p *Parameter) WithMaxLength(maximum int64) *Parameter { p.MaxLength = &maximum return p } -// WithMinLength sets a min length value +// WithMinLength sets a min length value. func (p *Parameter) WithMinLength(minimum int64) *Parameter { p.MinLength = &minimum return p } -// WithPattern sets a pattern value +// WithPattern sets a pattern value. func (p *Parameter) WithPattern(pattern string) *Parameter { p.Pattern = pattern return p } -// WithMultipleOf sets a multiple of value +// WithMultipleOf sets a multiple of value. func (p *Parameter) WithMultipleOf(number float64) *Parameter { p.MultipleOf = &number return p } -// WithMaximum sets a maximum number value +// WithMaximum sets a maximum number value. func (p *Parameter) WithMaximum(maximum float64, exclusive bool) *Parameter { p.Maximum = &maximum p.ExclusiveMaximum = exclusive return p } -// WithMinimum sets a minimum number value +// WithMinimum sets a minimum number value. func (p *Parameter) WithMinimum(minimum float64, exclusive bool) *Parameter { p.Minimum = &minimum p.ExclusiveMinimum = exclusive return p } -// WithEnum sets a the enum values (replace) +// WithEnum sets a the enum values (replace). func (p *Parameter) WithEnum(values ...any) *Parameter { p.Enum = append([]any{}, values...) return p } -// WithMaxItems sets the max items +// WithMaxItems sets the max items. func (p *Parameter) WithMaxItems(size int64) *Parameter { p.MaxItems = &size return p } -// WithMinItems sets the min items +// WithMinItems sets the min items. func (p *Parameter) WithMinItems(size int64) *Parameter { p.MinItems = &size return p } -// UniqueValues dictates that this array can only have unique items +// UniqueValues dictates that this array can only have unique items. func (p *Parameter) UniqueValues() *Parameter { p.UniqueItems = true return p } -// AllowDuplicates this array can have duplicates +// AllowDuplicates this array can have duplicates. func (p *Parameter) AllowDuplicates() *Parameter { p.UniqueItems = false return p } -// WithValidations is a fluent method to set parameter validations +// WithValidations is a fluent method to set parameter validations. func (p *Parameter) WithValidations(val CommonValidations) *Parameter { p.SetValidations(SchemaValidations{CommonValidations: val}) return p } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (p *Parameter) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &p.CommonValidations); err != nil { return err @@ -295,7 +295,7 @@ func (p *Parameter) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &p.ParamProps) } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (p Parameter) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(p.CommonValidations) if err != nil { diff --git a/parameters_test.go b/parameters_test.go index edb33d62..9d8a7623 100644 --- a/parameters_test.go +++ b/parameters_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-openapi/testify/v2/require" ) -var parameter = Parameter{ +var parameter = Parameter{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{Extensions: map[string]any{ "x-framework": "swagger-go", }}, @@ -49,6 +49,7 @@ var parameter = Parameter{ }, } +//nolint:gochecknoglobals // test fixture var parameterJSON = `{ "items": { "$ref": "Cat" @@ -81,11 +82,7 @@ var parameterJSON = `{ }` func TestIntegrationParameter(t *testing.T) { - var actual Parameter - require.NoError(t, json.Unmarshal([]byte(parameterJSON), &actual)) - assert.Equal(t, actual, parameter) - - assertParsesJSON(t, parameterJSON, parameter) + assert.JSONUnmarshalAsT(t, parameter, parameterJSON) } func TestParameterSerialization(t *testing.T) { @@ -97,27 +94,28 @@ func TestParameterSerialization(t *testing.T) { SimpleSchema: SimpleSchema{Type: "int", Format: "int32"}, } - assertSerializeJSON(t, QueryParam("").Typed("string", ""), `{"type":"string","in":"query"}`) + assert.JSONMarshalAsT(t, `{"type":"string","in":"query"}`, QueryParam("").Typed("string", "")) + + assert.JSONMarshalAsT(t, + `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"query"}`, + QueryParam("").CollectionOf(items, "multi")) - assertSerializeJSON(t, - QueryParam("").CollectionOf(items, "multi"), - `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"query"}`) + assert.JSONMarshalAsT(t, `{"type":"string","in":"path","required":true}`, PathParam("").Typed("string", "")) - assertSerializeJSON(t, PathParam("").Typed("string", ""), `{"type":"string","in":"path","required":true}`) + assert.JSONMarshalAsT(t, + `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"path","required":true}`, + PathParam("").CollectionOf(items, "multi")) - assertSerializeJSON(t, - PathParam("").CollectionOf(items, "multi"), - `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"path","required":true}`) + assert.JSONMarshalAsT(t, + `{"type":"array","items":{"type":"int","format":"int32"},"collectionFormat":"multi","in":"path","required":true}`, + PathParam("").CollectionOf(intItems, "multi")) - assertSerializeJSON(t, - PathParam("").CollectionOf(intItems, "multi"), - `{"type":"array","items":{"type":"int","format":"int32"},"collectionFormat":"multi","in":"path","required":true}`) + assert.JSONMarshalAsT(t, `{"type":"string","in":"header","required":true}`, HeaderParam("").Typed("string", "")) - assertSerializeJSON(t, HeaderParam("").Typed("string", ""), `{"type":"string","in":"header","required":true}`) + assert.JSONMarshalAsT(t, + `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"header","required":true}`, + HeaderParam("").CollectionOf(items, "multi")) - assertSerializeJSON(t, - HeaderParam("").CollectionOf(items, "multi"), - `{"type":"array","items":{"type":"string"},"collectionFormat":"multi","in":"header","required":true}`) schema := &Schema{SchemaProps: SchemaProps{ Properties: map[string]Schema{ "name": {SchemaProps: SchemaProps{ @@ -130,18 +128,18 @@ func TestParameterSerialization(t *testing.T) { SchemaProps: SchemaProps{Ref: MustCreateRef("Cat")}, } - assertSerializeJSON(t, - BodyParam("", schema), - `{"in":"body","schema":{"properties":{"name":{"type":"string"}}}}`) + assert.JSONMarshalAsT(t, + `{"in":"body","schema":{"properties":{"name":{"type":"string"}}}}`, + BodyParam("", schema)) - assertSerializeJSON(t, - BodyParam("", refSchema), - `{"in":"body","schema":{"$ref":"Cat"}}`) + assert.JSONMarshalAsT(t, + `{"in":"body","schema":{"$ref":"Cat"}}`, + BodyParam("", refSchema)) // array body param - assertSerializeJSON(t, - BodyParam("", ArrayProperty(RefProperty("Cat"))), - `{"in":"body","schema":{"type":"array","items":{"$ref":"Cat"}}}`) + assert.JSONMarshalAsT(t, + `{"in":"body","schema":{"type":"array","items":{"$ref":"Cat"}}}`, + BodyParam("", ArrayProperty(RefProperty("Cat")))) } func TestParameterGobEncoding(t *testing.T) { diff --git a/path_item.go b/path_item.go index c692b89e..4408ece4 100644 --- a/path_item.go +++ b/path_item.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -// PathItemProps the path item specific properties +// PathItemProps the path item specific properties. type PathItemProps struct { Get *Operation `json:"get,omitempty"` Put *Operation `json:"put,omitempty"` @@ -34,7 +34,7 @@ type PathItem struct { PathItemProps } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (p PathItem) JSONLookup(token string) (any, error) { if ex, ok := p.Extensions[token]; ok { return &ex, nil @@ -46,7 +46,7 @@ func (p PathItem) JSONLookup(token string) (any, error) { return r, err } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (p *PathItem) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &p.Refable); err != nil { return err @@ -57,7 +57,7 @@ func (p *PathItem) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &p.PathItemProps) } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (p PathItem) MarshalJSON() ([]byte, error) { b3, err := json.Marshal(p.Refable) if err != nil { diff --git a/path_item_test.go b/path_item_test.go index ddb5e10d..8a08f6a8 100644 --- a/path_item_test.go +++ b/path_item_test.go @@ -4,14 +4,12 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" ) -var pathItem = PathItem{ +var pathItem = PathItem{ //nolint:gochecknoglobals // test fixture Refable: Refable{Ref: MustCreateRef("Dog")}, VendorExtensible: VendorExtensible{ Extensions: map[string]any{ @@ -62,9 +60,5 @@ const pathItemJSON = `{ }` func TestIntegrationPathItem(t *testing.T) { - var actual PathItem - require.NoError(t, json.Unmarshal([]byte(pathItemJSON), &actual)) - assert.Equal(t, actual, pathItem) - - assertParsesJSON(t, pathItemJSON, pathItem) + assert.JSONUnmarshalAsT(t, pathItem, pathItemJSON) } diff --git a/paths.go b/paths.go index b9e42184..5daf5a67 100644 --- a/paths.go +++ b/paths.go @@ -23,7 +23,7 @@ type Paths struct { Paths map[string]PathItem `json:"-"` // custom serializer to flatten this, each entry must start with "/" } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (p Paths) JSONLookup(token string) (any, error) { if pi, ok := p.Paths[token]; ok { return &pi, nil @@ -34,7 +34,7 @@ func (p Paths) JSONLookup(token string) (any, error) { return nil, fmt.Errorf("object has no field %q: %w", token, ErrSpec) } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (p *Paths) UnmarshalJSON(data []byte) error { var res map[string]json.RawMessage if err := json.Unmarshal(data, &res); err != nil { @@ -65,7 +65,7 @@ func (p *Paths) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (p Paths) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(p.VendorExtensible) if err != nil { diff --git a/paths_test.go b/paths_test.go index 76868f9a..3057a9da 100644 --- a/paths_test.go +++ b/paths_test.go @@ -4,14 +4,12 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" - "github.com/go-openapi/testify/v2/require" ) -var paths = Paths{ +var paths = Paths{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-framework": "go-swagger"}}, Paths: map[string]PathItem{ "/": { @@ -23,9 +21,5 @@ var paths = Paths{ const pathsJSON = `{"x-framework":"go-swagger","/":{"$ref":"cats"}}` func TestIntegrationPaths(t *testing.T) { - var actual Paths - require.NoError(t, json.Unmarshal([]byte(pathsJSON), &actual)) - assert.Equal(t, actual, paths) - - assertParsesJSON(t, pathsJSON, paths) + assert.JSONUnmarshalAsT(t, paths, pathsJSON) } diff --git a/properties.go b/properties.go index 4142308d..b8e97271 100644 --- a/properties.go +++ b/properties.go @@ -10,7 +10,7 @@ import ( "sort" ) -// OrderSchemaItem holds a named schema (e.g. from a property of an object) +// OrderSchemaItem holds a named schema (e.g. from a property of an object). type OrderSchemaItem struct { Schema @@ -25,21 +25,26 @@ type OrderSchemaItems []OrderSchemaItem // of the OrderSchemaItems slice, keeping the original order of the slice. func (items OrderSchemaItems) MarshalJSON() ([]byte, error) { buf := bytes.NewBuffer(nil) - buf.WriteString("{") - for i := range items { - if i > 0 { - buf.WriteString(",") - } - buf.WriteString("\"") - buf.WriteString(items[i].Name) - buf.WriteString("\":") - bs, err := json.Marshal(&items[i].Schema) - if err != nil { + buf.WriteByte('{') + + if len(items) == 0 { + buf.WriteByte('}') + + return buf.Bytes(), nil + } + + if err := items.marshalJSONItem(items[0], buf); err != nil { + return nil, err + } + + for _, item := range items[1:] { + buf.WriteByte(',') + if err := items.marshalJSONItem(item, buf); err != nil { return nil, err } - buf.Write(bs) } - buf.WriteString("}") + buf.WriteByte('}') + return buf.Bytes(), nil } @@ -48,7 +53,7 @@ func (items OrderSchemaItems) Swap(i, j int) { items[i], items[j] = items[j], it func (items OrderSchemaItems) Less(i, j int) (ret bool) { ii, oki := items[i].Extensions.GetInt("x-order") ij, okj := items[j].Extensions.GetInt("x-order") - if oki { + if oki { //nolint:nestif // nested recover logic for safe type comparison if okj { defer func() { if err := recover(); err != nil { @@ -69,11 +74,27 @@ func (items OrderSchemaItems) Less(i, j int) (ret bool) { return items[i].Name < items[j].Name } +func (items OrderSchemaItems) marshalJSONItem(item OrderSchemaItem, output *bytes.Buffer) error { + nameJSON, err := json.Marshal(item.Name) + if err != nil { + return err + } + output.Write(nameJSON) + output.WriteByte(':') + schemaJSON, err := json.Marshal(&item.Schema) + if err != nil { + return err + } + output.Write(schemaJSON) + + return nil +} + // SchemaProperties is a map representing the properties of a Schema object. // It knows how to transform its keys into an ordered slice. type SchemaProperties map[string]Schema -// ToOrderedSchemaItems transforms the map of properties into a sortable slice +// ToOrderedSchemaItems transforms the map of properties into a sortable slice. func (properties SchemaProperties) ToOrderedSchemaItems() OrderSchemaItems { items := make(OrderSchemaItems, 0, len(properties)) for k, v := range properties { diff --git a/properties_test.go b/properties_test.go index 61ca76c6..1c59acab 100644 --- a/properties_test.go +++ b/properties_test.go @@ -5,6 +5,9 @@ package spec import ( "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestPropertySerialization(t *testing.T) { @@ -40,7 +43,29 @@ func TestPropertySerialization(t *testing.T) { for _, v := range propSerData { t.Log("roundtripping for", v.JSON) - assertSerializeJSON(t, v.Schema, v.JSON) - assertParsesJSON(t, v.JSON, v.Schema) + assert.JSONMarshalAsT(t, v.JSON, v.Schema) + assert.JSONUnmarshalAsT(t, v.Schema, v.JSON) + } +} + +func TestOrderedSchemaItem_Issue216(t *testing.T) { + stringSchema := new(Schema).Typed("string", "") + items := OrderSchemaItems{ + { + Name: "emails\n", // Key contains newline character + Schema: *stringSchema, + }, + { + Name: "regular", + Schema: *stringSchema, + }, } + + jazon, err := items.MarshalJSON() + require.NoError(t, err) + + require.JSONEqBytes(t, + []byte(`{"emails\n":{"type":"string"},"regular":{"type":"string"}}`), + jazon, + ) } diff --git a/ref.go b/ref.go index 18a29d7f..d1a7ab9b 100644 --- a/ref.go +++ b/ref.go @@ -7,19 +7,18 @@ import ( "bytes" "encoding/gob" "encoding/json" - "net/http" "os" "path/filepath" "github.com/go-openapi/jsonreference" ) -// Refable is a struct for things that accept a $ref property +// Refable is a struct for things that accept a $ref property. type Refable struct { Ref Ref } -// MarshalJSON marshals the ref to json +// MarshalJSON marshals the ref to json. func (r Refable) MarshalJSON() ([]byte, error) { return r.Ref.MarshalJSON() } @@ -34,8 +33,8 @@ type Ref struct { jsonreference.Ref } -// NewRef creates a new instance of a ref object. -// Returns an error when the reference uri is an invalid uri. +// NewRef creates a new instance of a ref object +// returns an error when the reference uri is an invalid uri. func NewRef(refURI string) (Ref, error) { ref, err := jsonreference.New(refURI) if err != nil { @@ -51,7 +50,7 @@ func MustCreateRef(refURI string) Ref { return Ref{Ref: jsonreference.MustCreateRef(refURI)} } -// RemoteURI gets the remote uri part of the ref +// RemoteURI gets the remote uri part of the ref. func (r *Ref) RemoteURI() string { if r.String() == "" { return "" @@ -62,7 +61,15 @@ func (r *Ref) RemoteURI() string { return u.String() } -// IsValidURI returns true when the url the ref points to can be found +// IsValidURI returns true when the ref points to a valid URI. +// +// For an absolute URL, it only checks that the reference is a well-formed URI. It deliberately +// does NOT perform a network request to verify that the remote target is reachable: doing so +// would make validation depend on network availability and expose callers to denial-of-service +// and SSRF when processing untrusted specifications. Resolving and fetching remote references is +// the responsibility of the expander, through its configurable (and confinable) document loader. +// +// For a local file reference, it checks that the file exists. func (r *Ref) IsValidURI(basepaths ...string) bool { if r.String() == "" { return true @@ -74,15 +81,8 @@ func (r *Ref) IsValidURI(basepaths ...string) bool { } if r.HasFullURL { - //nolint:noctx,gosec - rr, err := http.Get(v) - if err != nil { - return false - } - defer rr.Body.Close() - - // true if the response is >= 200 and < 300 - return rr.StatusCode/100 == 2 //nolint:mnd + // a well-formed absolute URL is a valid URI; remote reachability is not checked here (see above). + return true } if !r.HasFileScheme && !r.HasFullFilePath && !r.HasURLPathOnly { @@ -112,7 +112,7 @@ func (r *Ref) IsValidURI(basepaths ...string) bool { } // Inherits creates a new reference from a parent and a child -// If the child cannot inherit from the parent, an error is returned +// If the child cannot inherit from the parent, an error is returned. func (r *Ref) Inherits(child Ref) (*Ref, error) { ref, err := r.Ref.Inherits(child.Ref) if err != nil { @@ -121,7 +121,7 @@ func (r *Ref) Inherits(child Ref) (*Ref, error) { return &Ref{Ref: *ref}, nil } -// MarshalJSON marshals this ref into a JSON object +// MarshalJSON marshals this ref into a JSON object. func (r Ref) MarshalJSON() ([]byte, error) { str := r.String() if str == "" { @@ -134,7 +134,7 @@ func (r Ref) MarshalJSON() ([]byte, error) { return json.Marshal(v) } -// UnmarshalJSON unmarshals this ref from a JSON object +// UnmarshalJSON unmarshals this ref from a JSON object. func (r *Ref) UnmarshalJSON(d []byte) error { var v map[string]any if err := json.Unmarshal(d, &v); err != nil { @@ -143,7 +143,7 @@ func (r *Ref) UnmarshalJSON(d []byte) error { return r.fromMap(v) } -// GobEncode provides a safe gob encoder for Ref +// GobEncode provides a safe gob encoder for Ref. func (r Ref) GobEncode() ([]byte, error) { var b bytes.Buffer raw, err := r.MarshalJSON() @@ -154,7 +154,7 @@ func (r Ref) GobEncode() ([]byte, error) { return b.Bytes(), err } -// GobDecode provides a safe gob decoder for Ref +// GobDecode provides a safe gob decoder for Ref. func (r *Ref) GobDecode(b []byte) error { var raw []byte buf := bytes.NewBuffer(b) diff --git a/ref_test.go b/ref_test.go index d9e238c9..0ed9b0ef 100644 --- a/ref_test.go +++ b/ref_test.go @@ -7,13 +7,16 @@ import ( "bytes" "encoding/gob" "encoding/json" + "os" + "path/filepath" "testing" + "time" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) -// pin pointing go-swagger/go-swagger#1816 issue with cloning ref's +// pin pointing go-swagger/go-swagger#1816 issue with cloning ref's. func TestCloneRef(t *testing.T) { var b bytes.Buffer src := MustCreateRef("#/definitions/test") @@ -29,5 +32,59 @@ func TestCloneRef(t *testing.T) { jazon, err := json.Marshal(dst) require.NoError(t, err) - assert.JSONEq(t, `{"$ref":"#/definitions/test"}`, string(jazon)) + assert.JSONEqT(t, `{"$ref":"#/definitions/test"}`, string(jazon)) +} + +func TestRef_IsValidURI(t *testing.T) { + t.Run("empty and fragment-only refs are valid", func(t *testing.T) { + empty := MustCreateRef("") + assert.TrueT(t, empty.IsValidURI()) + + frag := MustCreateRef("#/definitions/Foo") + assert.TrueT(t, frag.IsValidURI()) + }) + + t.Run("absolute URLs are valid without any network request", func(t *testing.T) { + // A well-formed absolute URL is a valid URI. IsValidURI must NOT reach out to the network: + // no timeout to tune, no goroutine to leak, no SSRF against internal addresses. + // 192.0.2.0/24 is TEST-NET-1 (RFC 5737): guaranteed non-routable, so a real GET would + // stall on connect. We assert the call returns true promptly to guard against a + // reintroduced network probe. + for _, uri := range []string{ + "http://192.0.2.1/schema.json", // unreachable public address + "http://127.0.0.1:1/internal", // internal address (SSRF target) + "https://example.com/openapi.json", + } { + ref := MustCreateRef(uri) + require.TrueT(t, ref.HasFullURL) + + done := make(chan bool, 1) + go func() { done <- ref.IsValidURI() }() + + select { + case ok := <-done: + assert.TrueT(t, ok, "expected %q to be a valid URI", uri) + case <-time.After(5 * time.Second): + t.Fatalf("IsValidURI(%q) blocked: it must not perform a network request", uri) + } + } + }) + + t.Run("local file references are checked on disk", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "schema.json"), []byte(`{}`), 0o600)) + basePath := filepath.Join(dir, "root.json") // mirrors validate's IsValidURI(specFilePath) + + exists := MustCreateRef("schema.json") + assert.TrueT(t, exists.IsValidURI(basePath), + "an existing local file should be a valid URI") + + missing := MustCreateRef("does-not-exist.json") + assert.FalseT(t, missing.IsValidURI(basePath), + "a missing local file should be an invalid URI") + + asDir := MustCreateRef(".") + assert.FalseT(t, asDir.IsValidURI(basePath), + "a directory should not be a valid file URI") + }) } diff --git a/resolver.go b/resolver.go index 600574e1..1bf90c86 100644 --- a/resolver.go +++ b/resolver.go @@ -20,7 +20,7 @@ func resolveAnyWithBase(root any, ref *Ref, result any, options *ExpandOptions) return nil } -// ResolveRefWithBase resolves a reference against a context root with preservation of base path +// ResolveRefWithBase resolves a reference against a context root with preservation of base path. func ResolveRefWithBase(root any, ref *Ref, options *ExpandOptions) (*Schema, error) { result := new(Schema) @@ -34,7 +34,7 @@ func ResolveRefWithBase(root any, ref *Ref, options *ExpandOptions) (*Schema, er // ResolveRef resolves a reference for a schema against a context root // ref is guaranteed to be in root (no need to go to external files) // -// ResolveRef is ONLY called from the code generation module +// ResolveRef is ONLY called from the code generation module. func ResolveRef(root any, ref *Ref) (*Schema, error) { res, _, err := ref.GetPointer().Get(root) if err != nil { @@ -57,7 +57,7 @@ func ResolveRef(root any, ref *Ref) (*Schema, error) { } } -// ResolveParameterWithBase resolves a parameter reference against a context root and base path +// ResolveParameterWithBase resolves a parameter reference against a context root and base path. func ResolveParameterWithBase(root any, ref Ref, options *ExpandOptions) (*Parameter, error) { result := new(Parameter) @@ -68,12 +68,12 @@ func ResolveParameterWithBase(root any, ref Ref, options *ExpandOptions) (*Param return result, nil } -// ResolveParameter resolves a parameter reference against a context root +// ResolveParameter resolves a parameter reference against a context root. func ResolveParameter(root any, ref Ref) (*Parameter, error) { return ResolveParameterWithBase(root, ref, nil) } -// ResolveResponseWithBase resolves response a reference against a context root and base path +// ResolveResponseWithBase resolves response a reference against a context root and base path. func ResolveResponseWithBase(root any, ref Ref, options *ExpandOptions) (*Response, error) { result := new(Response) @@ -85,12 +85,12 @@ func ResolveResponseWithBase(root any, ref Ref, options *ExpandOptions) (*Respon return result, nil } -// ResolveResponse resolves response a reference against a context root +// ResolveResponse resolves response a reference against a context root. func ResolveResponse(root any, ref Ref) (*Response, error) { return ResolveResponseWithBase(root, ref, nil) } -// ResolvePathItemWithBase resolves response a path item against a context root and base path +// ResolvePathItemWithBase resolves response a path item against a context root and base path. func ResolvePathItemWithBase(root any, ref Ref, options *ExpandOptions) (*PathItem, error) { result := new(PathItem) @@ -103,7 +103,7 @@ func ResolvePathItemWithBase(root any, ref Ref, options *ExpandOptions) (*PathIt // ResolvePathItem resolves response a path item against a context root and base path // -// Deprecated: use ResolvePathItemWithBase instead +// Deprecated: use ResolvePathItemWithBase instead. func ResolvePathItem(root any, ref Ref, options *ExpandOptions) (*PathItem, error) { return ResolvePathItemWithBase(root, ref, options) } @@ -124,7 +124,7 @@ func ResolveItemsWithBase(root any, ref Ref, options *ExpandOptions) (*Items, er // ResolveItems resolves parameter items reference against a context root and base path. // -// Deprecated: use ResolveItemsWithBase instead +// Deprecated: use ResolveItemsWithBase instead. func ResolveItems(root any, ref Ref, options *ExpandOptions) (*Items, error) { return ResolveItemsWithBase(root, ref, options) } diff --git a/resolver_test.go b/resolver_test.go index e8d6311e..c949eca0 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -5,8 +5,6 @@ package spec import ( "encoding/json" - "net/http" - "net/http/httptest" "os" "path/filepath" "testing" @@ -18,7 +16,7 @@ import ( func TestResolveRef(t *testing.T) { var root any - require.NoError(t, json.Unmarshal([]byte(PetStore20), &root)) + require.NoError(t, json.Unmarshal(PetStore20, &root)) ref, err := NewRef("#/definitions/Category") require.NoError(t, err) @@ -29,7 +27,7 @@ func TestResolveRef(t *testing.T) { b, err := sch.MarshalJSON() require.NoError(t, err) - assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) + assert.JSONEqT(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) // WithBase variant sch, err = ResolveRefWithBase(root, &ref, &ExpandOptions{ @@ -40,7 +38,7 @@ func TestResolveRef(t *testing.T) { b, err = sch.MarshalJSON() require.NoError(t, err) - assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) + assert.JSONEqT(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) } func TestResolveResponse(t *testing.T) { @@ -58,7 +56,7 @@ func TestResolveResponse(t *testing.T) { // resolve resolves the ref, but dos not expand jazon := asJSON(t, resp2) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "$ref": "#/responses/petResponse" }`, jazon) } @@ -78,7 +76,7 @@ func TestResolveResponseWithBase(t *testing.T) { // resolve resolves the ref, but dos not expand jazon := asJSON(t, resp2) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "$ref": "#/responses/petResponse" }`, jazon) } @@ -96,7 +94,7 @@ func TestResolveParam(t *testing.T) { jazon := asJSON(t, par) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "name": "id", "in": "path", "description": "ID of pet to fetch", @@ -119,7 +117,7 @@ func TestResolveParamWithBase(t *testing.T) { jazon := asJSON(t, par) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "description":"ID of pet to fetch", "format":"int64", "in":"path", @@ -130,9 +128,7 @@ func TestResolveParamWithBase(t *testing.T) { } func TestResolveRemoteRef_RootSame(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -158,9 +154,7 @@ func TestResolveRemoteRef_RootSame(t *testing.T) { } func TestResolveRemoteRef_FromFragment(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -178,9 +172,7 @@ func TestResolveRemoteRef_FromFragment(t *testing.T) { } func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -215,9 +207,7 @@ func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { // } func TestResolveRemoteRef_ToParameter(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -231,18 +221,16 @@ func TestResolveRemoteRef_ToParameter(t *testing.T) { resolver := defaultSchemaLoader(rootDoc, nil, nil, nil) require.NoError(t, resolver.Resolve(&ref, &tgt, "")) - assert.Equal(t, "id", tgt.Name) - assert.Equal(t, "path", tgt.In) - assert.Equal(t, "ID of pet to fetch", tgt.Description) - assert.True(t, tgt.Required) - assert.Equal(t, "integer", tgt.Type) - assert.Equal(t, "int64", tgt.Format) + assert.EqualT(t, "id", tgt.Name) + assert.EqualT(t, "path", tgt.In) + assert.EqualT(t, "ID of pet to fetch", tgt.Description) + assert.TrueT(t, tgt.Required) + assert.EqualT(t, "integer", tgt.Type) + assert.EqualT(t, "int64", tgt.Format) } func TestResolveRemoteRef_ToPathItem(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -259,9 +247,7 @@ func TestResolveRemoteRef_ToPathItem(t *testing.T) { } func TestResolveRemoteRef_ToResponse(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() + server := fixtureServer(t, specs) rootDoc := new(Swagger) b, err := os.ReadFile(filepath.Join(specs, "refed.json")) @@ -298,7 +284,7 @@ func TestResolveLocalRef_FromFragment(t *testing.T) { resolver := defaultSchemaLoader(rootDoc, nil, nil, nil) require.NoError(t, resolver.Resolve(&ref, &tgt, "")) - assert.Equal(t, "Category", tgt.ID) + assert.EqualT(t, "Category", tgt.ID) } func TestResolveLocalRef_FromInvalidFragment(t *testing.T) { @@ -328,12 +314,12 @@ func TestResolveLocalRef_Parameter(t *testing.T) { resolver := defaultSchemaLoader(rootDoc, nil, nil, nil) require.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) - assert.Equal(t, "id", tgt.Name) - assert.Equal(t, "path", tgt.In) - assert.Equal(t, "ID of pet to fetch", tgt.Description) - assert.True(t, tgt.Required) - assert.Equal(t, "integer", tgt.Type) - assert.Equal(t, "int64", tgt.Format) + assert.EqualT(t, "id", tgt.Name) + assert.EqualT(t, "path", tgt.In) + assert.EqualT(t, "ID of pet to fetch", tgt.Description) + assert.TrueT(t, tgt.Required) + assert.EqualT(t, "integer", tgt.Type) + assert.EqualT(t, "int64", tgt.Format) } func TestResolveLocalRef_PathItem(t *testing.T) { @@ -384,7 +370,7 @@ func TestResolvePathItem(t *testing.T) { jazon := asJSON(t, pathItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "get": { "responses": { "200": { @@ -419,7 +405,7 @@ func TestResolveExtraItem(t *testing.T) { jazon := asJSON(t, parmItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "type": "integer", "format": "int32" }`, jazon) @@ -431,7 +417,7 @@ func TestResolveExtraItem(t *testing.T) { jazon = asJSON(t, hdrItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "type": "string", "format": "uuid" }`, jazon) diff --git a/response.go b/response.go index c5ccf962..4bb6a2bc 100644 --- a/response.go +++ b/response.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -// ResponseProps properties specific to a response +// ResponseProps properties specific to a response. type ResponseProps struct { Description string `json:"description"` Schema *Schema `json:"schema,omitempty"` @@ -39,7 +39,7 @@ func ResponseRef(url string) *Response { return resp } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (r Response) JSONLookup(token string) (any, error) { if ex, ok := r.Extensions[token]; ok { return &ex, nil @@ -51,7 +51,7 @@ func (r Response) JSONLookup(token string) (any, error) { return ptr, err } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (r *Response) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &r.ResponseProps); err != nil { return err @@ -62,7 +62,7 @@ func (r *Response) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &r.VendorExtensible) } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (r Response) MarshalJSON() ([]byte, error) { var ( b1 []byte @@ -100,20 +100,20 @@ func (r Response) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b1, b2, b3), nil } -// WithDescription sets the description on this response, allows for chaining +// WithDescription sets the description on this response, allows for chaining. func (r *Response) WithDescription(description string) *Response { r.Description = description return r } // WithSchema sets the schema on this response, allows for chaining. -// Passing a nil argument removes the schema from this response +// Passing a nil argument removes the schema from this response. func (r *Response) WithSchema(schema *Schema) *Response { r.Schema = schema return r } -// AddHeader adds a header to this response +// AddHeader adds a header to this response. func (r *Response) AddHeader(name string, header *Header) *Response { if header == nil { return r.RemoveHeader(name) @@ -125,13 +125,13 @@ func (r *Response) AddHeader(name string, header *Header) *Response { return r } -// RemoveHeader removes a header from this response +// RemoveHeader removes a header from this response. func (r *Response) RemoveHeader(name string) *Response { delete(r.Headers, name) return r } -// AddExample adds an example to this response +// AddExample adds an example to this response. func (r *Response) AddExample(mediaType string, example any) *Response { if r.Examples == nil { r.Examples = make(map[string]any) diff --git a/response_test.go b/response_test.go index cf62d576..dd9045e7 100644 --- a/response_test.go +++ b/response_test.go @@ -18,14 +18,13 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) -var response = Response{ +var response = Response{ //nolint:gochecknoglobals // test fixture Refable: Refable{Ref: MustCreateRef("Dog")}, VendorExtensible: VendorExtensible{ Extensions: map[string]any{ @@ -48,11 +47,7 @@ const responseJSON = `{ }` func TestIntegrationResponse(t *testing.T) { - var actual Response - require.NoError(t, json.Unmarshal([]byte(responseJSON), &actual)) - assert.Equal(t, actual, response) - - assertParsesJSON(t, responseJSON, response) + assert.JSONUnmarshalAsT(t, response, responseJSON) } func TestJSONLookupResponse(t *testing.T) { @@ -63,7 +58,7 @@ func TestJSONLookupResponse(t *testing.T) { var ok bool ref, ok := res.(*Ref) - require.True(t, ok) + require.TrueT(t, ok) assert.Equal(t, MustCreateRef("Dog"), *ref) var def string @@ -73,8 +68,8 @@ func TestJSONLookupResponse(t *testing.T) { require.IsType(t, def, res) def, ok = res.(string) - require.True(t, ok) - assert.Equal(t, "Dog exists", def) + require.TrueT(t, ok) + assert.EqualT(t, "Dog exists", def) var x *any res, err = response.JSONLookup("x-go-name") @@ -83,7 +78,7 @@ func TestJSONLookupResponse(t *testing.T) { require.IsType(t, x, res) x, ok = res.(*any) - require.True(t, ok) + require.TrueT(t, ok) assert.EqualValues(t, "PutDogExists", *x) res, err = response.JSONLookup("unknown") @@ -97,10 +92,7 @@ func TestResponseBuild(t *testing.T) { WithSchema(new(Schema).Typed("object", "")). AddHeader("x-header", ResponseHeader().Typed("string", "")). AddExample("application/json", `{"key":"value"}`) - jazon, err := json.MarshalIndent(resp, "", " ") - require.NoError(t, err) - - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "description": "some response", "schema": { "type": "object" @@ -113,5 +105,5 @@ func TestResponseBuild(t *testing.T) { "examples": { "application/json": "{\"key\":\"value\"}" } - }`, string(jazon)) + }`, resp) } diff --git a/responses.go b/responses.go index 733a1315..fb369e4a 100644 --- a/responses.go +++ b/responses.go @@ -31,7 +31,7 @@ type Responses struct { ResponsesProps } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (r Responses) JSONLookup(token string) (any, error) { if token == "default" { return r.Default, nil @@ -47,7 +47,7 @@ func (r Responses) JSONLookup(token string) (any, error) { return nil, fmt.Errorf("object has no field %q: %w", token, ErrSpec) } -// UnmarshalJSON hydrates this items instance with the data from JSON +// UnmarshalJSON hydrates this items instance with the data from JSON. func (r *Responses) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &r.ResponsesProps); err != nil { return err @@ -62,7 +62,7 @@ func (r *Responses) UnmarshalJSON(data []byte) error { return nil } -// MarshalJSON converts this items object to JSON +// MarshalJSON converts this items object to JSON. func (r Responses) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(r.ResponsesProps) if err != nil { @@ -84,7 +84,7 @@ type ResponsesProps struct { StatusCodeResponses map[int]Response } -// MarshalJSON marshals responses as JSON +// MarshalJSON marshals responses as JSON. func (r ResponsesProps) MarshalJSON() ([]byte, error) { toser := map[string]Response{} if r.Default != nil { @@ -96,7 +96,7 @@ func (r ResponsesProps) MarshalJSON() ([]byte, error) { return json.Marshal(toser) } -// UnmarshalJSON unmarshals responses from JSON +// UnmarshalJSON unmarshals responses from JSON. func (r *ResponsesProps) UnmarshalJSON(data []byte) error { var res map[string]json.RawMessage if err := json.Unmarshal(data, &res); err != nil { diff --git a/responses_test.go b/responses_test.go index f920ad21..c9810c1d 100644 --- a/responses_test.go +++ b/responses_test.go @@ -18,14 +18,13 @@ package spec import ( - "encoding/json" "testing" "github.com/go-openapi/testify/v2/assert" "github.com/go-openapi/testify/v2/require" ) -var responses = Responses{ +var responses = Responses{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{ Extensions: map[string]any{ "x-go-name": "PutDogExists", @@ -62,16 +61,12 @@ const responsesJSON = `{ }` func TestIntegrationResponses(t *testing.T) { - var actual Responses - require.NoError(t, json.Unmarshal([]byte(responsesJSON), &actual)) - assert.Equal(t, actual, responses) - - assertParsesJSON(t, responsesJSON, responses) + assert.JSONUnmarshalAsT(t, responses, responsesJSON) } func TestJSONLookupResponses(t *testing.T) { resp200, ok := responses.StatusCodeResponses[200] - require.True(t, ok) + require.TrueT(t, ok) res, err := resp200.JSONLookup("$ref") require.NoError(t, err) @@ -79,7 +74,7 @@ func TestJSONLookupResponses(t *testing.T) { require.IsType(t, &Ref{}, res) ref, ok := res.(*Ref) - require.True(t, ok) + require.TrueT(t, ok) assert.Equal(t, MustCreateRef("Dog"), *ref) var def string @@ -89,8 +84,8 @@ func TestJSONLookupResponses(t *testing.T) { require.IsType(t, def, res) def, ok = res.(string) - require.True(t, ok) - assert.Equal(t, "Dog exists", def) + require.TrueT(t, ok) + assert.EqualT(t, "Dog exists", def) var x *any res, err = responses.JSONLookup("x-go-name") @@ -99,7 +94,7 @@ func TestJSONLookupResponses(t *testing.T) { require.IsType(t, x, res) x, ok = res.(*any) - require.True(t, ok) + require.TrueT(t, ok) assert.EqualValues(t, "PutDogExists", *x) res, err = responses.JSONLookup("unknown") @@ -113,8 +108,7 @@ func TestResponsesBuild(t *testing.T) { WithSchema(new(Schema).Typed("object", "")). AddHeader("x-header", ResponseHeader().Typed("string", "")). AddExample("application/json", `{"key":"value"}`) - jazon, _ := json.MarshalIndent(resp, "", " ") - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "description": "some response", "schema": { "type": "object" @@ -127,5 +121,5 @@ func TestResponsesBuild(t *testing.T) { "examples": { "application/json": "{\"key\":\"value\"}" } - }`, string(jazon)) + }`, resp) } diff --git a/schema.go b/schema.go index e2941531..c71a2e5c 100644 --- a/schema.go +++ b/schema.go @@ -9,74 +9,74 @@ import ( "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag/jsonname" + "github.com/go-openapi/jsonpointer/jsonname" "github.com/go-openapi/swag/jsonutils" ) -// BooleanProperty creates a boolean property +// BooleanProperty creates a boolean property. func BooleanProperty() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"boolean"}}} } -// BoolProperty creates a boolean property +// BoolProperty creates a boolean property. func BoolProperty() *Schema { return BooleanProperty() } -// StringProperty creates a string property +// StringProperty creates a string property. func StringProperty() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"string"}}} } -// CharProperty creates a string property +// CharProperty creates a string property. func CharProperty() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"string"}}} } -// Float64Property creates a float64/double property +// Float64Property creates a float64/double property. func Float64Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"number"}, Format: "double"}} } -// Float32Property creates a float32/float property +// Float32Property creates a float32/float property. func Float32Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"number"}, Format: "float"}} } -// Int8Property creates an int8 property +// Int8Property creates an int8 property. func Int8Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"integer"}, Format: "int8"}} } -// Int16Property creates an int16 property +// Int16Property creates an int16 property. func Int16Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"integer"}, Format: "int16"}} } -// Int32Property creates an int32 property +// Int32Property creates an int32 property. func Int32Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"integer"}, Format: "int32"}} } -// Int64Property creates an int64 property +// Int64Property creates an int64 property. func Int64Property() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"integer"}, Format: "int64"}} } -// StrFmtProperty creates a property for the named string format +// StrFmtProperty creates a property for the named string format. func StrFmtProperty(format string) *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"string"}, Format: format}} } -// DateProperty creates a date property +// DateProperty creates a date property. func DateProperty() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"string"}, Format: "date"}} } -// DateTimeProperty creates a date time property +// DateTimeProperty creates a date time property. func DateTimeProperty() *Schema { return &Schema{SchemaProps: SchemaProps{Type: []string{"string"}, Format: "date-time"}} } -// MapProperty creates a map property +// MapProperty creates a map property. func MapProperty(property *Schema) *Schema { return &Schema{SchemaProps: SchemaProps{ Type: []string{"object"}, @@ -84,17 +84,17 @@ func MapProperty(property *Schema) *Schema { }} } -// RefProperty creates a ref property +// RefProperty creates a ref property. func RefProperty(name string) *Schema { return &Schema{SchemaProps: SchemaProps{Ref: MustCreateRef(name)}} } -// RefSchema creates a ref property +// RefSchema creates a ref property. func RefSchema(name string) *Schema { return &Schema{SchemaProps: SchemaProps{Ref: MustCreateRef(name)}} } -// ArrayProperty creates an array property +// ArrayProperty creates an array property. func ArrayProperty(items *Schema) *Schema { if items == nil { return &Schema{SchemaProps: SchemaProps{Type: []string{"array"}}} @@ -102,17 +102,17 @@ func ArrayProperty(items *Schema) *Schema { return &Schema{SchemaProps: SchemaProps{Items: &SchemaOrArray{Schema: items}, Type: []string{"array"}}} } -// ComposedSchema creates a schema with allOf +// ComposedSchema creates a schema with allOf. func ComposedSchema(schemas ...Schema) *Schema { s := new(Schema) s.AllOf = schemas return s } -// SchemaURL represents a schema url +// SchemaURL represents a schema url. type SchemaURL string -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (r SchemaURL) MarshalJSON() ([]byte, error) { if r == "" { return []byte("{}"), nil @@ -121,7 +121,7 @@ func (r SchemaURL) MarshalJSON() ([]byte, error) { return json.Marshal(v) } -// UnmarshalJSON unmarshal this from JSON +// UnmarshalJSON unmarshal this from JSON. func (r *SchemaURL) UnmarshalJSON(data []byte) error { var v map[string]any if err := json.Unmarshal(data, &v); err != nil { @@ -147,7 +147,7 @@ func (r *SchemaURL) fromMap(v map[string]any) error { return nil } -// SchemaProps describes a JSON schema (draft 4) +// SchemaProps describes a JSON schema (draft 4). type SchemaProps struct { ID string `json:"id,omitempty"` Ref Ref `json:"-"` @@ -186,7 +186,7 @@ type SchemaProps struct { Definitions Definitions `json:"definitions,omitempty"` } -// SwaggerSchemaProps are additional properties supported by swagger schemas, but not JSON-schema (draft 4) +// SwaggerSchemaProps are additional properties supported by swagger schemas, but not JSON-schema (draft 4). type SwaggerSchemaProps struct { Discriminator string `json:"discriminator,omitempty"` ReadOnly bool `json:"readOnly,omitempty"` @@ -210,7 +210,7 @@ type Schema struct { ExtraProps map[string]any `json:"-"` } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (s Schema) JSONLookup(token string) (any, error) { if ex, ok := s.Extensions[token]; ok { return &ex, nil @@ -228,31 +228,31 @@ func (s Schema) JSONLookup(token string) (any, error) { return r, err } -// WithID sets the id for this schema, allows for chaining +// WithID sets the id for this schema, allows for chaining. func (s *Schema) WithID(id string) *Schema { s.ID = id return s } -// WithTitle sets the title for this schema, allows for chaining +// WithTitle sets the title for this schema, allows for chaining. func (s *Schema) WithTitle(title string) *Schema { s.Title = title return s } -// WithDescription sets the description for this schema, allows for chaining +// WithDescription sets the description for this schema, allows for chaining. func (s *Schema) WithDescription(description string) *Schema { s.Description = description return s } -// WithProperties sets the properties for this schema +// WithProperties sets the properties for this schema. func (s *Schema) WithProperties(schemas map[string]Schema) *Schema { s.Properties = schemas return s } -// SetProperty sets a property on this schema +// SetProperty sets a property on this schema. func (s *Schema) SetProperty(name string, schema Schema) *Schema { if s.Properties == nil { s.Properties = make(map[string]Schema) @@ -261,32 +261,32 @@ func (s *Schema) SetProperty(name string, schema Schema) *Schema { return s } -// WithAllOf sets the all of property +// WithAllOf sets the all of property. func (s *Schema) WithAllOf(schemas ...Schema) *Schema { s.AllOf = schemas return s } -// WithMaxProperties sets the max number of properties an object can have +// WithMaxProperties sets the max number of properties an object can have. func (s *Schema) WithMaxProperties(maximum int64) *Schema { s.MaxProperties = &maximum return s } -// WithMinProperties sets the min number of properties an object must have +// WithMinProperties sets the min number of properties an object must have. func (s *Schema) WithMinProperties(minimum int64) *Schema { s.MinProperties = &minimum return s } -// Typed sets the type of this schema for a single value item +// Typed sets the type of this schema for a single value item. func (s *Schema) Typed(tpe, format string) *Schema { s.Type = []string{tpe} s.Format = format return s } -// AddType adds a type with potential format to the types for this schema +// AddType adds a type with potential format to the types for this schema. func (s *Schema) AddType(tpe, format string) *Schema { s.Type = append(s.Type, tpe) if format != "" { @@ -301,124 +301,124 @@ func (s *Schema) AsNullable() *Schema { return s } -// CollectionOf a fluent builder method for an array parameter +// CollectionOf a fluent builder method for an array parameter. func (s *Schema) CollectionOf(items Schema) *Schema { s.Type = []string{jsonArray} s.Items = &SchemaOrArray{Schema: &items} return s } -// WithDefault sets the default value on this parameter +// WithDefault sets the default value on this parameter. func (s *Schema) WithDefault(defaultValue any) *Schema { s.Default = defaultValue return s } -// WithRequired flags this parameter as required +// WithRequired flags this parameter as required. func (s *Schema) WithRequired(items ...string) *Schema { s.Required = items return s } -// AddRequired adds field names to the required properties array +// AddRequired adds field names to the required properties array. func (s *Schema) AddRequired(items ...string) *Schema { s.Required = append(s.Required, items...) return s } -// WithMaxLength sets a max length value +// WithMaxLength sets a max length value. func (s *Schema) WithMaxLength(maximum int64) *Schema { s.MaxLength = &maximum return s } -// WithMinLength sets a min length value +// WithMinLength sets a min length value. func (s *Schema) WithMinLength(minimum int64) *Schema { s.MinLength = &minimum return s } -// WithPattern sets a pattern value +// WithPattern sets a pattern value. func (s *Schema) WithPattern(pattern string) *Schema { s.Pattern = pattern return s } -// WithMultipleOf sets a multiple of value +// WithMultipleOf sets a multiple of value. func (s *Schema) WithMultipleOf(number float64) *Schema { s.MultipleOf = &number return s } -// WithMaximum sets a maximum number value +// WithMaximum sets a maximum number value. func (s *Schema) WithMaximum(maximum float64, exclusive bool) *Schema { s.Maximum = &maximum s.ExclusiveMaximum = exclusive return s } -// WithMinimum sets a minimum number value +// WithMinimum sets a minimum number value. func (s *Schema) WithMinimum(minimum float64, exclusive bool) *Schema { s.Minimum = &minimum s.ExclusiveMinimum = exclusive return s } -// WithEnum sets a the enum values (replace) +// WithEnum sets a the enum values (replace). func (s *Schema) WithEnum(values ...any) *Schema { s.Enum = append([]any{}, values...) return s } -// WithMaxItems sets the max items +// WithMaxItems sets the max items. func (s *Schema) WithMaxItems(size int64) *Schema { s.MaxItems = &size return s } -// WithMinItems sets the min items +// WithMinItems sets the min items. func (s *Schema) WithMinItems(size int64) *Schema { s.MinItems = &size return s } -// UniqueValues dictates that this array can only have unique items +// UniqueValues dictates that this array can only have unique items. func (s *Schema) UniqueValues() *Schema { s.UniqueItems = true return s } -// AllowDuplicates this array can have duplicates +// AllowDuplicates this array can have duplicates. func (s *Schema) AllowDuplicates() *Schema { s.UniqueItems = false return s } -// AddToAllOf adds a schema to the allOf property +// AddToAllOf adds a schema to the allOf property. func (s *Schema) AddToAllOf(schemas ...Schema) *Schema { s.AllOf = append(s.AllOf, schemas...) return s } -// WithDiscriminator sets the name of the discriminator field +// WithDiscriminator sets the name of the discriminator field. func (s *Schema) WithDiscriminator(discriminator string) *Schema { s.Discriminator = discriminator return s } -// AsReadOnly flags this schema as readonly +// AsReadOnly flags this schema as readonly. func (s *Schema) AsReadOnly() *Schema { s.ReadOnly = true return s } -// AsWritable flags this schema as writeable (not read-only) +// AsWritable flags this schema as writeable (not read-only). func (s *Schema) AsWritable() *Schema { s.ReadOnly = false return s } -// WithExample sets the example for this schema +// WithExample sets the example for this schema. func (s *Schema) WithExample(example any) *Schema { s.Example = example return s @@ -442,7 +442,7 @@ func (s *Schema) WithExternalDocs(description, url string) *Schema { return s } -// WithXMLName sets the xml name for the object +// WithXMLName sets the xml name for the object. func (s *Schema) WithXMLName(name string) *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -451,7 +451,7 @@ func (s *Schema) WithXMLName(name string) *Schema { return s } -// WithXMLNamespace sets the xml namespace for the object +// WithXMLNamespace sets the xml namespace for the object. func (s *Schema) WithXMLNamespace(namespace string) *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -460,7 +460,7 @@ func (s *Schema) WithXMLNamespace(namespace string) *Schema { return s } -// WithXMLPrefix sets the xml prefix for the object +// WithXMLPrefix sets the xml prefix for the object. func (s *Schema) WithXMLPrefix(prefix string) *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -469,7 +469,7 @@ func (s *Schema) WithXMLPrefix(prefix string) *Schema { return s } -// AsXMLAttribute flags this object as xml attribute +// AsXMLAttribute flags this object as xml attribute. func (s *Schema) AsXMLAttribute() *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -478,7 +478,7 @@ func (s *Schema) AsXMLAttribute() *Schema { return s } -// AsXMLElement flags this object as an xml node +// AsXMLElement flags this object as an xml node. func (s *Schema) AsXMLElement() *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -487,7 +487,7 @@ func (s *Schema) AsXMLElement() *Schema { return s } -// AsWrappedXML flags this object as wrapped, this is mostly useful for array types +// AsWrappedXML flags this object as wrapped, this is mostly useful for array types. func (s *Schema) AsWrappedXML() *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -496,7 +496,7 @@ func (s *Schema) AsWrappedXML() *Schema { return s } -// AsUnwrappedXML flags this object as an xml node +// AsUnwrappedXML flags this object as an xml node. func (s *Schema) AsUnwrappedXML() *Schema { if s.XML == nil { s.XML = new(XMLObject) @@ -526,13 +526,13 @@ func (s *Schema) SetValidations(val SchemaValidations) { s.PatternProperties = val.PatternProperties } -// WithValidations is a fluent method to set schema validations +// WithValidations is a fluent method to set schema validations. func (s *Schema) WithValidations(val SchemaValidations) *Schema { s.SetValidations(val) return s } -// Validations returns a clone of the validations for this schema +// Validations returns a clone of the validations for this schema. func (s Schema) Validations() SchemaValidations { return SchemaValidations{ CommonValidations: CommonValidations{ @@ -555,40 +555,40 @@ func (s Schema) Validations() SchemaValidations { } } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (s Schema) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(s.SchemaProps) if err != nil { - return nil, fmt.Errorf("schema props: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("schema props %w: %w", err, ErrSpec) } b2, err := json.Marshal(s.VendorExtensible) if err != nil { - return nil, fmt.Errorf("vendor props: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("vendor props %w: %w", err, ErrSpec) } b3, err := s.Ref.MarshalJSON() if err != nil { - return nil, fmt.Errorf("ref prop: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("ref prop %w: %w", err, ErrSpec) } b4, err := s.Schema.MarshalJSON() if err != nil { - return nil, fmt.Errorf("schema prop: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("schema prop %w: %w", err, ErrSpec) } b5, err := json.Marshal(s.SwaggerSchemaProps) if err != nil { - return nil, fmt.Errorf("common validations: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("common validations %w: %w", err, ErrSpec) } var b6 []byte if s.ExtraProps != nil { jj, err := json.Marshal(s.ExtraProps) if err != nil { - return nil, fmt.Errorf("extra props: %w: %w", err, ErrSpec) + return nil, fmt.Errorf("extra props %w: %w", err, ErrSpec) } b6 = jj } return jsonutils.ConcatJSON(b1, b2, b3, b4, b5, b6), nil } -// UnmarshalJSON marshal this from JSON +// UnmarshalJSON marshal this from JSON. func (s *Schema) UnmarshalJSON(data []byte) error { props := struct { SchemaProps diff --git a/schema_loader.go b/schema_loader.go index f7b6baf6..491ed020 100644 --- a/schema_loader.go +++ b/schema_loader.go @@ -5,6 +5,7 @@ package spec import ( "encoding/json" + "errors" "fmt" "log" "net/url" @@ -24,7 +25,7 @@ import ( // NOTE: if you are using the go-openapi/loads package, it will override // this value with its own default (a loader to retrieve YAML documents as // well as JSON ones). -var PathLoader = func(pth string) (json.RawMessage, error) { +var PathLoader = func(pth string) (json.RawMessage, error) { //nolint:gochecknoglobals // package-level default loader, overridable by go-openapi/loads data, err := loading.LoadFromFileOrHTTP(pth) if err != nil { return nil, err @@ -43,24 +44,48 @@ type resolverContext struct { basePath string loadDoc func(string) (json.RawMessage, error) rootID string + + // nodes counts the schema nodes expanded so far, capped by maxNodes to guard against + // $ref amplification. maxNodes == 0 means unbounded. Shared, single-threaded: no locking needed. + nodes int + maxNodes int } func newResolverContext(options *ExpandOptions) *resolverContext { expandOptions := optionsOrDefault(options) - // path loader may be overridden by options + // path loader may be overridden by options. An option-aware loader takes precedence over a + // plain one, which in turn takes precedence over the package-level default. var loader func(string) (json.RawMessage, error) - if expandOptions.PathLoader == nil { - loader = PathLoader - } else { + switch { + case expandOptions.PathLoaderWithOptions != nil: + withOptions := expandOptions.PathLoaderWithOptions + loader = func(pth string) (json.RawMessage, error) { + // the injected loader carries its own loading options: none are added here. + return withOptions(pth) + } + case expandOptions.PathLoader != nil: loader = expandOptions.PathLoader + default: + loader = PathLoader } return &resolverContext{ circulars: make(map[string]bool), basePath: expandOptions.RelativeBase, // keep the root base path in context loadDoc: loader, + maxNodes: expandOptions.maxExpansionNodes(), + } +} + +// countNode accounts for one expanded schema node and reports whether the expansion budget +// has been exceeded. A maxNodes of 0 disables the budget (unbounded expansion). +func (c *resolverContext) countNode() error { + c.nodes++ + if c.maxNodes > 0 && c.nodes > c.maxNodes { + return ErrExpandTooManyNodes } + return nil } type schemaLoader struct { @@ -117,7 +142,7 @@ func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { tgt := reflect.ValueOf(target) - if tgt.Kind() != reflect.Ptr { + if tgt.Kind() != reflect.Pointer { return ErrResolveRefNeedsAPointer } @@ -136,7 +161,7 @@ func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { root := r.root if (ref.IsRoot() || ref.HasFragmentOnly) && root == nil && basePath != "" { if baseRef, erb := NewRef(basePath); erb == nil { - root, _, _, _ = r.load(baseRef.GetURL()) + root, _ = r.load(baseRef.GetURL()) } } @@ -144,7 +169,7 @@ func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { data = root } else { baseRef := normalizeRef(ref, basePath) - data, _, _, err = r.load(baseRef.GetURL()) + data, err = r.load(baseRef.GetURL()) if err != nil { return err } @@ -160,33 +185,32 @@ func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { return jsonutils.FromDynamicJSON(res, target) } -func (r *schemaLoader) load(refURL *url.URL) (any, url.URL, bool, error) { +func (r *schemaLoader) load(refURL *url.URL) (any, error) { debugLog("loading schema from url: %s", refURL) toFetch := *refURL toFetch.Fragment = "" - var err error pth := toFetch.String() normalized := normalizeBase(pth) debugLog("loading doc from: %s", normalized) data, fromCache := r.cache.Get(normalized) if fromCache { - return data, toFetch, fromCache, nil + return data, nil } b, err := r.context.loadDoc(normalized) if err != nil { - return nil, url.URL{}, false, err + return nil, err } var doc any if err := json.Unmarshal(b, &doc); err != nil { - return nil, url.URL{}, false, err + return nil, err } r.cache.Set(normalized, doc) - return doc, toFetch, fromCache, nil + return doc, nil } // isCircular detects cycles in sequences of $ref. @@ -247,14 +271,22 @@ func (r *schemaLoader) deref(input any, parentRefs []string, basePath string) er } func (r *schemaLoader) shouldStopOnError(err error) bool { - if err != nil && !r.options.ContinueOnError { + if err == nil { + return false + } + + if errors.Is(err, ErrExpandTooManyNodes) { + // a blown expansion budget is a hard, document-level failure: it is a safeguard against + // resource exhaustion and is never suppressed by ContinueOnError. return true } - if err != nil { - log.Println(err) + if !r.options.ContinueOnError { + return true } + log.Println(err) + return false } @@ -293,8 +325,8 @@ func defaultSchemaLoader( root any, expandOptions *ExpandOptions, cache ResolutionCache, - context *resolverContext) *schemaLoader { - + context *resolverContext, +) *schemaLoader { if expandOptions == nil { expandOptions = &ExpandOptions{} } diff --git a/schema_test.go b/schema_test.go index 25264e92..7d296a5b 100644 --- a/schema_test.go +++ b/schema_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-openapi/testify/v2/require" ) -var schema = Schema{ +var schema = Schema{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-framework": "go-swagger"}}, SchemaProps: SchemaProps{ Ref: MustCreateRef("Cat"), @@ -68,6 +68,7 @@ var schema = Schema{ }, } +//nolint:gochecknoglobals // test fixture var schemaJSON = `{ "x-framework": "go-swagger", "$ref": "Cat", @@ -139,60 +140,53 @@ var schemaJSON = `{ ` func TestSchema(t *testing.T) { - expected := map[string]any{} - _ = json.Unmarshal([]byte(schemaJSON), &expected) - b, err := json.Marshal(schema) - require.NoError(t, err) - - var actual map[string]any - require.NoError(t, json.Unmarshal(b, &actual)) - assert.Equal(t, expected, actual) + assert.JSONMarshalAsT(t, schemaJSON, schema) actual2 := Schema{} require.NoError(t, json.Unmarshal([]byte(schemaJSON), &actual2)) assert.Equal(t, schema.Ref, actual2.Ref) - assert.Equal(t, schema.Description, actual2.Description) + assert.EqualT(t, schema.Description, actual2.Description) assert.Equal(t, schema.Maximum, actual2.Maximum) assert.Equal(t, schema.Minimum, actual2.Minimum) - assert.Equal(t, schema.ExclusiveMinimum, actual2.ExclusiveMinimum) - assert.Equal(t, schema.ExclusiveMaximum, actual2.ExclusiveMaximum) + assert.EqualT(t, schema.ExclusiveMinimum, actual2.ExclusiveMinimum) + assert.EqualT(t, schema.ExclusiveMaximum, actual2.ExclusiveMaximum) assert.Equal(t, schema.MaxLength, actual2.MaxLength) assert.Equal(t, schema.MinLength, actual2.MinLength) - assert.Equal(t, schema.Pattern, actual2.Pattern) + assert.EqualT(t, schema.Pattern, actual2.Pattern) assert.Equal(t, schema.MaxItems, actual2.MaxItems) assert.Equal(t, schema.MinItems, actual2.MinItems) - assert.True(t, actual2.UniqueItems) + assert.TrueT(t, actual2.UniqueItems) assert.Equal(t, schema.MultipleOf, actual2.MultipleOf) assert.Equal(t, schema.Enum, actual2.Enum) assert.Equal(t, schema.Type, actual2.Type) - assert.Equal(t, schema.Format, actual2.Format) - assert.Equal(t, schema.Title, actual2.Title) + assert.EqualT(t, schema.Format, actual2.Format) + assert.EqualT(t, schema.Title, actual2.Title) assert.Equal(t, schema.MaxProperties, actual2.MaxProperties) assert.Equal(t, schema.MinProperties, actual2.MinProperties) assert.Equal(t, schema.Required, actual2.Required) assert.Equal(t, schema.Items, actual2.Items) assert.Equal(t, schema.AllOf, actual2.AllOf) assert.Equal(t, schema.Properties, actual2.Properties) - assert.Equal(t, schema.Discriminator, actual2.Discriminator) - assert.Equal(t, schema.ReadOnly, actual2.ReadOnly) + assert.EqualT(t, schema.Discriminator, actual2.Discriminator) + assert.EqualT(t, schema.ReadOnly, actual2.ReadOnly) assert.Equal(t, schema.XML, actual2.XML) assert.Equal(t, schema.ExternalDocs, actual2.ExternalDocs) assert.Equal(t, schema.AdditionalProperties, actual2.AdditionalProperties) assert.Equal(t, schema.Extensions, actual2.Extensions) examples, ok := actual2.Example.([]any) - assert.True(t, ok, "actual2.Example is not of type []any") + require.TrueT(t, ok, "expected []any for actual2.Example") expEx, ok := schema.Example.([]any) - assert.True(t, ok, "schema.Example is not of type []any") + require.TrueT(t, ok, "expected []any for schema.Example") ex1, ok := examples[0].(map[string]any) - assert.True(t, ok, "examples[0] is not of type map[string]any") + require.TrueT(t, ok, "expected map[string]any for examples[0]") ex2, ok := examples[1].(map[string]any) - assert.True(t, ok, "examples[1] is not of type map[string]any") + require.TrueT(t, ok, "expected map[string]any for examples[1]") exp1, ok := expEx[0].(map[string]any) - assert.True(t, ok, "expEx[0] is not of type map[string]any") + require.TrueT(t, ok, "expected map[string]any for expEx[0]") exp2, ok := expEx[1].(map[string]any) - assert.True(t, ok, "expEx[1] is not of type map[string]any") + require.TrueT(t, ok, "expected map[string]any for expEx[1]") assert.EqualValues(t, exp1["id"], ex1["id"]) assert.Equal(t, exp1["name"], ex1["name"]) diff --git a/schemas/v2/README.md b/schemas/v2/README.md index 32c1b929..af4656e7 100644 --- a/schemas/v2/README.md +++ b/schemas/v2/README.md @@ -2,4 +2,4 @@ This folder contains the Swagger 2.0 specification schema files maintained here: -https://github.com/reverb/swagger-spec/blob/master/schemas/v2.0 \ No newline at end of file + diff --git a/security_scheme.go b/security_scheme.go index 46a4a7e2..6d9019e7 100644 --- a/security_scheme.go +++ b/security_scheme.go @@ -20,17 +20,17 @@ const ( accessCode = "accessCode" ) -// BasicAuth creates a basic auth security scheme +// BasicAuth creates a basic auth security scheme. func BasicAuth() *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{Type: basic}} } -// APIKeyAuth creates an api key auth security scheme +// APIKeyAuth creates an api key auth security scheme. func APIKeyAuth(fieldName, valueSource string) *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{Type: apiKey, Name: fieldName, In: valueSource}} } -// OAuth2Implicit creates an implicit flow oauth2 security scheme +// OAuth2Implicit creates an implicit flow oauth2 security scheme. func OAuth2Implicit(authorizationURL string) *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{ Type: oauth2, @@ -39,7 +39,7 @@ func OAuth2Implicit(authorizationURL string) *SecurityScheme { }} } -// OAuth2Password creates a password flow oauth2 security scheme +// OAuth2Password creates a password flow oauth2 security scheme. func OAuth2Password(tokenURL string) *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{ Type: oauth2, @@ -48,7 +48,7 @@ func OAuth2Password(tokenURL string) *SecurityScheme { }} } -// OAuth2Application creates an application flow oauth2 security scheme +// OAuth2Application creates an application flow oauth2 security scheme. func OAuth2Application(tokenURL string) *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{ Type: oauth2, @@ -57,7 +57,7 @@ func OAuth2Application(tokenURL string) *SecurityScheme { }} } -// OAuth2AccessToken creates an access token flow oauth2 security scheme +// OAuth2AccessToken creates an access token flow oauth2 security scheme. func OAuth2AccessToken(authorizationURL, tokenURL string) *SecurityScheme { return &SecurityScheme{SecuritySchemeProps: SecuritySchemeProps{ Type: oauth2, @@ -67,7 +67,7 @@ func OAuth2AccessToken(authorizationURL, tokenURL string) *SecurityScheme { }} } -// SecuritySchemeProps describes a swagger security scheme in the securityDefinitions section +// SecuritySchemeProps describes a swagger security scheme in the securityDefinitions section. type SecuritySchemeProps struct { Description string `json:"description,omitempty"` Type string `json:"type"` @@ -79,7 +79,7 @@ type SecuritySchemeProps struct { Scopes map[string]string `json:"scopes,omitempty"` // oauth2 } -// AddScope adds a scope to this security scheme +// AddScope adds a scope to this security scheme. func (s *SecuritySchemeProps) AddScope(scope, description string) { if s.Scopes == nil { s.Scopes = make(map[string]string) @@ -97,7 +97,7 @@ type SecurityScheme struct { SecuritySchemeProps } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (s SecurityScheme) JSONLookup(token string) (any, error) { if ex, ok := s.Extensions[token]; ok { return &ex, nil @@ -107,7 +107,7 @@ func (s SecurityScheme) JSONLookup(token string) (any, error) { return r, err } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (s SecurityScheme) MarshalJSON() ([]byte, error) { var ( b1 []byte @@ -150,7 +150,7 @@ func (s SecurityScheme) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b1, b2), nil } -// UnmarshalJSON marshal this from JSON +// UnmarshalJSON marshal this from JSON. func (s *SecurityScheme) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &s.SecuritySchemeProps); err != nil { return err diff --git a/spec.go b/spec.go index b58ce977..4eba04b2 100644 --- a/spec.go +++ b/spec.go @@ -19,7 +19,7 @@ const ( JSONSchemaURL = "http://json-schema.org/draft-04/schema#" ) -// MustLoadJSONSchemaDraft04 panics when Swagger20Schema returns an error +// MustLoadJSONSchemaDraft04 panics when Swagger20Schema returns an error. func MustLoadJSONSchemaDraft04() *Schema { d, e := JSONSchemaDraft04() if e != nil { @@ -42,7 +42,7 @@ func JSONSchemaDraft04() (*Schema, error) { return schema, nil } -// MustLoadSwagger20Schema panics when Swagger20Schema returns an error +// MustLoadSwagger20Schema panics when Swagger20Schema returns an error. func MustLoadSwagger20Schema() *Schema { d, e := Swagger20Schema() if e != nil { @@ -51,7 +51,7 @@ func MustLoadSwagger20Schema() *Schema { return d } -// Swagger20Schema loads the swagger 2.0 schema from the embedded assets +// Swagger20Schema loads the swagger 2.0 schema from the embedded assets. func Swagger20Schema() (*Schema, error) { b, err := v2SchemaJSONBytes() if err != nil { diff --git a/spec_test.go b/spec_test.go index 998c7c76..05976478 100644 --- a/spec_test.go +++ b/spec_test.go @@ -41,7 +41,7 @@ func TestSpec_Issue2743(t *testing.T) { require.NoError(t, spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader}), ) - require.NotContainsf(t, asJSON(t, sp), "$ref", "all $ref's should have been expanded properly") + require.StringNotContainsTf(t, asJSON(t, sp), "$ref", "all $ref's should have been expanded properly") }) }) } @@ -55,7 +55,7 @@ func TestSpec_Issue1429(t *testing.T) { require.NoError(t, err) // assert well expanded - require.Truef(t, (sp.Paths != nil && sp.Paths.Paths != nil), "expected paths to be available in fixture") + require.TrueTf(t, (sp.Paths != nil && sp.Paths.Paths != nil), "expected paths to be available in fixture") assertPaths1429(t, sp) @@ -69,12 +69,12 @@ func TestSpec_Issue1429(t *testing.T) { require.NoError(t, err) // assert well resolved - require.Truef(t, (sp.Paths != nil && sp.Paths.Paths != nil), "expected paths to be available in fixture") + require.TrueTf(t, (sp.Paths != nil && sp.Paths.Paths != nil), "expected paths to be available in fixture") assertPaths1429SkipSchema(t, sp) for _, def := range sp.Definitions { - assert.Contains(t, def.Ref.String(), "responses.yaml#/definitions/") + assert.StringContainsT(t, def.Ref.String(), "responses.yaml#/definitions/") } } @@ -112,14 +112,14 @@ func assertPaths1429SkipSchema(t testing.TB, sp *spec.Swagger) { continue case "nestedBody": // this one is local - assert.Truef(t, strings.HasPrefix(param.Schema.Ref.String(), "#/definitions/"), + assert.TrueTf(t, strings.HasPrefix(param.Schema.Ref.String(), "#/definitions/"), "expected rooted definitions $ref, got: %s", param.Schema.Ref.String()) continue case "remoteRequest": - assert.Contains(t, param.Schema.Ref.String(), "remote/remote.yaml#/") + assert.StringContainsT(t, param.Schema.Ref.String(), "remote/remote.yaml#/") continue } - assert.Contains(t, param.Schema.Ref.String(), "responses.yaml#/") + assert.StringContainsT(t, param.Schema.Ref.String(), "responses.yaml#/") } @@ -130,13 +130,13 @@ func assertPaths1429SkipSchema(t testing.TB, sp *spec.Swagger) { assert.Nilf(t, response.Schema, "expected response schema to be nil") continue case 204: - assert.Contains(t, response.Schema.Ref.String(), "remote/remote.yaml#/") + assert.StringContainsT(t, response.Schema.Ref.String(), "remote/remote.yaml#/") continue case 404: assert.Empty(t, response.Schema.Ref.String()) continue } - assert.Containsf(t, response.Schema.Ref.String(), "responses.yaml#/", "expected remote ref at resp. %d", code) + assert.StringContainsTf(t, response.Schema.Ref.String(), "responses.yaml#/", "expected remote ref at resp. %d", code) } } } @@ -149,7 +149,7 @@ func TestSpec_MoreLocalExpansion(t *testing.T) { require.NoError(t, spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader})) // asserts all $ref are expanded - assert.NotContains(t, asJSON(t, sp), `"$ref"`) + assert.StringNotContainsT(t, asJSON(t, sp), `"$ref"`) } func TestSpec_Issue69(t *testing.T) { diff --git a/structs_test.go b/structs_test.go index f5229521..f917ff16 100644 --- a/structs_test.go +++ b/structs_test.go @@ -4,114 +4,51 @@ package spec import ( - "encoding/json" - "reflect" "testing" "github.com/go-openapi/testify/v2/assert" - yaml "go.yaml.in/yaml/v3" ) -func assertSerializeJSON(tb testing.TB, actual any, expected string) bool { //nolint:unparam - tb.Helper() - ser, err := json.Marshal(actual) - if err != nil { - return assert.Failf(tb, "unable to marshal to json", "got: %v: %#v", err, actual) - } - - return assert.Equal(tb, expected, string(ser)) -} - -func assertSerializeYAML(tb testing.TB, actual any, expected string) bool { - tb.Helper() - ser, err := yaml.Marshal(actual) - if err != nil { - return assert.Failf(tb, "unable to marshal to yaml", "got: %v: %#v", err, actual) - } - return assert.Equal(tb, expected, string(ser)) -} - -func derefTypeOf(expected any) (tpe reflect.Type) { - tpe = reflect.TypeOf(expected) - if tpe.Kind() == reflect.Ptr { - tpe = tpe.Elem() - } - return -} - -func isPointed(expected any) (pointed bool) { - tpe := reflect.TypeOf(expected) - if tpe.Kind() == reflect.Ptr { - pointed = true - } - return -} - -func assertParsesJSON(tb testing.TB, actual string, expected any) bool { //nolint:unparam - tb.Helper() - parsed := reflect.New(derefTypeOf(expected)) - err := json.Unmarshal([]byte(actual), parsed.Interface()) - if err != nil { - return assert.Failf(tb, "unable to unmarshal from json", "got: %v: %s", err, actual) - } - act := parsed.Interface() - if !isPointed(expected) { - act = reflect.Indirect(parsed).Interface() - } - return assert.Equal(tb, expected, act) -} - -func assertParsesYAML(tb testing.TB, actual string, expected any) bool { - tb.Helper() - parsed := reflect.New(derefTypeOf(expected)) - err := yaml.Unmarshal([]byte(actual), parsed.Interface()) - if err != nil { - return assert.Failf(tb, "unable to unmarshal from yaml", "got: %v: %s", err, actual) - } - act := parsed.Interface() - if !isPointed(expected) { - act = reflect.Indirect(parsed).Interface() - } - return assert.Equal(tb, expected, act) -} - func TestSerialization_SerializeJSON(t *testing.T) { - assertSerializeJSON(t, []string{"hello"}, "[\"hello\"]") - assertSerializeJSON(t, []string{"hello", "world", "and", "stuff"}, "[\"hello\",\"world\",\"and\",\"stuff\"]") - assertSerializeJSON(t, StringOrArray(nil), "null") - assertSerializeJSON(t, SchemaOrArray{ + assert.JSONMarshalAsT(t, `["hello"]`, []string{"hello"}) + assert.JSONMarshalAsT(t, `["hello","world","and","stuff"]`, []string{"hello", "world", "and", "stuff"}) + assert.JSONMarshalAsT(t, `null`, StringOrArray(nil)) + assert.JSONMarshalAsT(t, `[{"type":"string"}]`, SchemaOrArray{ Schemas: []Schema{ {SchemaProps: SchemaProps{Type: []string{"string"}}}, }, - }, "[{\"type\":\"string\"}]") - assertSerializeJSON(t, SchemaOrArray{ + }) + assert.JSONMarshalAsT(t, `[{"type":"string"},{"type":"string"}]`, SchemaOrArray{ Schemas: []Schema{ {SchemaProps: SchemaProps{Type: []string{"string"}}}, {SchemaProps: SchemaProps{Type: []string{"string"}}}, }, - }, "[{\"type\":\"string\"},{\"type\":\"string\"}]") - assertSerializeJSON(t, SchemaOrArray{}, "null") + }) + assert.JSONMarshalAsT(t, `null`, SchemaOrArray{}) } func TestSerialization_DeserializeJSON(t *testing.T) { // String - assertParsesJSON(t, "\"hello\"", StringOrArray([]string{"hello"})) - assertParsesJSON(t, "[\"hello\",\"world\",\"and\",\"stuff\"]", - StringOrArray([]string{"hello", "world", "and", "stuff"})) - assertParsesJSON(t, "[\"hello\",\"world\",null,\"stuff\"]", StringOrArray([]string{"hello", "world", "", "stuff"})) - assertParsesJSON(t, "null", StringOrArray(nil)) + assert.JSONUnmarshalAsT(t, StringOrArray([]string{"hello"}), `"hello"`) + assert.JSONUnmarshalAsT(t, + StringOrArray([]string{"hello", "world", "and", "stuff"}), + `["hello","world","and","stuff"]`) + assert.JSONUnmarshalAsT(t, + StringOrArray([]string{"hello", "world", "", "stuff"}), + `["hello","world",null,"stuff"]`) + assert.JSONUnmarshalAsT(t, StringOrArray(nil), `null`) // Schema - assertParsesJSON(t, "{\"type\":\"string\"}", SchemaOrArray{ + assert.JSONUnmarshalAsT(t, SchemaOrArray{ Schema: &Schema{ SchemaProps: SchemaProps{Type: []string{"string"}}, }, - }) - assertParsesJSON(t, "[{\"type\":\"string\"},{\"type\":\"string\"}]", &SchemaOrArray{ + }, `{"type":"string"}`) + assert.JSONUnmarshalAsT(t, &SchemaOrArray{ Schemas: []Schema{ {SchemaProps: SchemaProps{Type: []string{"string"}}}, {SchemaProps: SchemaProps{Type: []string{"string"}}}, }, - }) - assertParsesJSON(t, "null", SchemaOrArray{}) + }, `[{"type":"string"},{"type":"string"}]`) + assert.JSONUnmarshalAsT(t, SchemaOrArray{}, `null`) } diff --git a/swagger.go b/swagger.go index a0119dc5..dbe32db8 100644 --- a/swagger.go +++ b/swagger.go @@ -25,7 +25,7 @@ type Swagger struct { SwaggerProps } -// JSONLookup look up a value by the json property name +// JSONLookup look up a value by the json property name. func (s Swagger) JSONLookup(token string) (any, error) { if ex, ok := s.Extensions[token]; ok { return &ex, nil @@ -34,7 +34,7 @@ func (s Swagger) JSONLookup(token string) (any, error) { return r, err } -// MarshalJSON marshals this swagger structure to json +// MarshalJSON marshals this swagger structure to json. func (s Swagger) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(s.SwaggerProps) if err != nil { @@ -47,7 +47,7 @@ func (s Swagger) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b1, b2), nil } -// UnmarshalJSON unmarshals a swagger spec from json +// UnmarshalJSON unmarshals a swagger spec from json. func (s *Swagger) UnmarshalJSON(data []byte) error { var sw Swagger if err := json.Unmarshal(data, &sw.SwaggerProps); err != nil { @@ -60,7 +60,7 @@ func (s *Swagger) UnmarshalJSON(data []byte) error { return nil } -// GobEncode provides a safe gob encoder for Swagger, including extensions +// GobEncode provides a safe gob encoder for Swagger, including extensions. func (s Swagger) GobEncode() ([]byte, error) { var b bytes.Buffer raw := struct { @@ -74,7 +74,7 @@ func (s Swagger) GobEncode() ([]byte, error) { return b.Bytes(), err } -// GobDecode provides a safe gob decoder for Swagger, including extensions +// GobDecode provides a safe gob decoder for Swagger, including extensions. func (s *Swagger) GobDecode(b []byte) error { var raw struct { Props SwaggerProps @@ -95,7 +95,7 @@ func (s *Swagger) GobDecode(b []byte) error { // NOTE: validation rules // - the scheme, when present must be from [http, https, ws, wss] // - BasePath must start with a leading "/" -// - Paths is required +// - Paths is required. type SwaggerProps struct { ID string `json:"id,omitempty"` Consumes []string `json:"consumes,omitempty"` @@ -126,7 +126,7 @@ type gobSwaggerPropsAlias struct { SecurityIsEmpty bool } -// GobEncode provides a safe gob encoder for SwaggerProps, including empty security requirements +// GobEncode provides a safe gob encoder for SwaggerProps, including empty security requirements. func (o SwaggerProps) GobEncode() ([]byte, error) { raw := gobSwaggerPropsAlias{ Alias: (*swaggerPropsAlias)(&o), @@ -171,7 +171,7 @@ func (o SwaggerProps) GobEncode() ([]byte, error) { return b.Bytes(), err } -// GobDecode provides a safe gob decoder for SwaggerProps, including empty security requirements +// GobDecode provides a safe gob decoder for SwaggerProps, including empty security requirements. func (o *SwaggerProps) GobDecode(b []byte) error { var raw gobSwaggerPropsAlias @@ -207,16 +207,16 @@ func (o *SwaggerProps) GobDecode(b []byte) error { return nil } -// Dependencies represent a dependencies property +// Dependencies represent a dependencies property. type Dependencies map[string]SchemaOrStringArray -// SchemaOrBool represents a schema or boolean value, is biased towards true for the boolean property +// SchemaOrBool represents a schema or boolean value, is biased towards true for the boolean property. type SchemaOrBool struct { Allows bool Schema *Schema } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (s SchemaOrBool) JSONLookup(token string) (any, error) { if token == "allows" { return s.Allows, nil @@ -226,11 +226,11 @@ func (s SchemaOrBool) JSONLookup(token string) (any, error) { } var ( - jsTrue = []byte("true") //nolint:gochecknoglobals - jsFalse = []byte("false") //nolint:gochecknoglobals + jsTrue = []byte("true") //nolint:gochecknoglobals // constant-like byte slices for JSON marshaling + jsFalse = []byte("false") //nolint:gochecknoglobals // constant-like byte slices for JSON marshaling ) -// MarshalJSON convert this object to JSON +// MarshalJSON convert this object to JSON. func (s SchemaOrBool) MarshalJSON() ([]byte, error) { if s.Schema != nil { return json.Marshal(s.Schema) @@ -242,7 +242,7 @@ func (s SchemaOrBool) MarshalJSON() ([]byte, error) { return jsTrue, nil } -// UnmarshalJSON converts this bool or schema object from a JSON structure +// UnmarshalJSON converts this bool or schema object from a JSON structure. func (s *SchemaOrBool) UnmarshalJSON(data []byte) error { var nw SchemaOrBool if len(data) > 0 { @@ -259,19 +259,19 @@ func (s *SchemaOrBool) UnmarshalJSON(data []byte) error { return nil } -// SchemaOrStringArray represents a schema or a string array +// SchemaOrStringArray represents a schema or a string array. type SchemaOrStringArray struct { Schema *Schema Property []string } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (s SchemaOrStringArray) JSONLookup(token string) (any, error) { r, _, err := jsonpointer.GetForToken(s.Schema, token) return r, err } -// MarshalJSON converts this schema object or array into JSON structure +// MarshalJSON converts this schema object or array into JSON structure. func (s SchemaOrStringArray) MarshalJSON() ([]byte, error) { if len(s.Property) > 0 { return json.Marshal(s.Property) @@ -282,7 +282,7 @@ func (s SchemaOrStringArray) MarshalJSON() ([]byte, error) { return []byte("null"), nil } -// UnmarshalJSON converts this schema object or array from a JSON structure +// UnmarshalJSON converts this schema object or array from a JSON structure. func (s *SchemaOrStringArray) UnmarshalJSON(data []byte) error { var first byte if len(data) > 1 { @@ -320,15 +320,15 @@ type Definitions map[string]Schema type SecurityDefinitions map[string]*SecurityScheme // StringOrArray represents a value that can either be a string -// or an array of strings. Mainly here for serialization purposes +// or an array of strings. Mainly here for serialization purposes. type StringOrArray []string -// Contains returns true when the value is contained in the slice +// Contains returns true when the value is contained in the slice. func (s StringOrArray) Contains(value string) bool { return slices.Contains(s, value) } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (s SchemaOrArray) JSONLookup(token string) (any, error) { if _, err := strconv.Atoi(token); err == nil { r, _, err := jsonpointer.GetForToken(s.Schemas, token) @@ -338,7 +338,7 @@ func (s SchemaOrArray) JSONLookup(token string) (any, error) { return r, err } -// UnmarshalJSON unmarshals this string or array object from a JSON array or JSON string +// UnmarshalJSON unmarshals this string or array object from a JSON array or JSON string. func (s *StringOrArray) UnmarshalJSON(data []byte) error { var first byte if len(data) > 1 { @@ -370,7 +370,7 @@ func (s *StringOrArray) UnmarshalJSON(data []byte) error { } } -// MarshalJSON converts this string or array to a JSON array or JSON string +// MarshalJSON converts this string or array to a JSON array or JSON string. func (s StringOrArray) MarshalJSON() ([]byte, error) { if len(s) == 1 { return json.Marshal([]string(s)[0]) @@ -379,13 +379,13 @@ func (s StringOrArray) MarshalJSON() ([]byte, error) { } // SchemaOrArray represents a value that can either be a Schema -// or an array of Schema. Mainly here for serialization purposes +// or an array of Schema. Mainly here for serialization purposes. type SchemaOrArray struct { Schema *Schema Schemas []Schema } -// Len returns the number of schemas in this property +// Len returns the number of schemas in this property. func (s SchemaOrArray) Len() int { if s.Schema != nil { return 1 @@ -393,7 +393,7 @@ func (s SchemaOrArray) Len() int { return len(s.Schemas) } -// ContainsType returns true when one of the schemas is of the specified type +// ContainsType returns true when one of the schemas is of the specified type. func (s *SchemaOrArray) ContainsType(name string) bool { if s.Schema != nil { return s.Schema.Type != nil && s.Schema.Type.Contains(name) @@ -401,7 +401,7 @@ func (s *SchemaOrArray) ContainsType(name string) bool { return false } -// MarshalJSON converts this schema object or array into JSON structure +// MarshalJSON converts this schema object or array into JSON structure. func (s SchemaOrArray) MarshalJSON() ([]byte, error) { if len(s.Schemas) > 0 { return json.Marshal(s.Schemas) @@ -409,7 +409,7 @@ func (s SchemaOrArray) MarshalJSON() ([]byte, error) { return json.Marshal(s.Schema) } -// UnmarshalJSON converts this schema object or array from a JSON structure +// UnmarshalJSON converts this schema object or array from a JSON structure. func (s *SchemaOrArray) UnmarshalJSON(data []byte) error { var nw SchemaOrArray var first byte diff --git a/swagger_test.go b/swagger_test.go index 5c167404..9ec0cad9 100644 --- a/swagger_test.go +++ b/swagger_test.go @@ -39,7 +39,7 @@ func init() { //nolint:gochecknoinits // it's okay to load embedded fixtures in } } -var spec = Swagger{ +var spec = Swagger{ //nolint:gochecknoglobals // test fixture SwaggerProps: SwaggerProps{ ID: "http://localhost:3849/api-docs", Swagger: "2.0", @@ -175,9 +175,10 @@ var spec = Swagger{ } */ -func assertSpecs(t testing.TB, actual, expected Swagger) bool { +func assertSpecs(t testing.TB, actual, expected Swagger) { + t.Helper() expected.Swagger = "2.0" - return assert.Equal(t, expected, actual) + assert.Equal(t, expected, actual) } /* @@ -225,25 +226,25 @@ func TestVendorExtensionStringSlice(t *testing.T) { var actual Swagger require.NoError(t, json.Unmarshal(specJSON, &actual)) schemes, ok := actual.Extensions.GetStringSlice("x-schemes") - require.True(t, ok) + require.TrueT(t, ok) assert.Equal(t, []string{"unix", "amqp"}, schemes) notSlice, ok := actual.Extensions.GetStringSlice("x-some-extension") assert.Nil(t, notSlice) - assert.False(t, ok) + assert.FalseT(t, ok) actual.AddExtension("x-another-ext", 100) notString, ok := actual.Extensions.GetStringSlice("x-another-ext") assert.Nil(t, notString) - assert.False(t, ok) + assert.FalseT(t, ok) actual.AddExtension("x-another-slice-ext", []any{100, 100}) notStringSlice, ok := actual.Extensions.GetStringSlice("x-another-slice-ext") assert.Nil(t, notStringSlice) - assert.False(t, ok) + assert.FalseT(t, ok) _, ok = actual.Extensions.GetStringSlice("x-notfound-ext") - assert.False(t, ok) + assert.FalseT(t, ok) } func TestOptionalSwaggerProps_Serialize(t *testing.T) { @@ -256,18 +257,18 @@ func TestOptionalSwaggerProps_Serialize(t *testing.T) { var ms map[string]any require.NoError(t, json.Unmarshal(bytes, &ms)) - assert.NotContains(t, ms, "consumes") - assert.NotContains(t, ms, "produces") - assert.NotContains(t, ms, "schemes") - assert.NotContains(t, ms, "host") - assert.NotContains(t, ms, "basePath") - assert.NotContains(t, ms, "definitions") - assert.NotContains(t, ms, "parameters") - assert.NotContains(t, ms, "responses") - assert.NotContains(t, ms, "securityDefinitions") - assert.NotContains(t, ms, "security") - assert.NotContains(t, ms, "tags") - assert.NotContains(t, ms, "externalDocs") + assert.MapNotContainsT(t, ms, "consumes") + assert.MapNotContainsT(t, ms, "produces") + assert.MapNotContainsT(t, ms, "schemes") + assert.MapNotContainsT(t, ms, "host") + assert.MapNotContainsT(t, ms, "basePath") + assert.MapNotContainsT(t, ms, "definitions") + assert.MapNotContainsT(t, ms, "parameters") + assert.MapNotContainsT(t, ms, "responses") + assert.MapNotContainsT(t, ms, "securityDefinitions") + assert.MapNotContainsT(t, ms, "security") + assert.MapNotContainsT(t, ms, "tags") + assert.MapNotContainsT(t, ms, "externalDocs") } func TestSecurityRequirements(t *testing.T) { @@ -276,11 +277,11 @@ func TestSecurityRequirements(t *testing.T) { sec := minimalSpec.Paths.Paths["/"].Get.Security require.Len(t, sec, 3) - assert.Contains(t, sec[0], "basic") - assert.Contains(t, sec[0], "apiKey") + assert.MapContainsT(t, sec[0], "basic") + assert.MapContainsT(t, sec[0], "apiKey") assert.NotNil(t, sec[1]) assert.Empty(t, sec[1]) - assert.Contains(t, sec[2], "queryKey") + assert.MapContainsT(t, sec[2], "queryKey") } func TestSwaggerGobEncoding(t *testing.T) { diff --git a/tag.go b/tag.go index c4578c1d..af3fb0a4 100644 --- a/tag.go +++ b/tag.go @@ -10,7 +10,7 @@ import ( "github.com/go-openapi/swag/jsonutils" ) -// TagProps describe a tag entry in the top level tags section of a swagger spec +// TagProps describe a tag entry in the top level tags section of a swagger spec. type TagProps struct { Description string `json:"description,omitempty"` Name string `json:"name,omitempty"` @@ -32,7 +32,7 @@ func NewTag(name, description string, externalDocs *ExternalDocumentation) Tag { return Tag{TagProps: TagProps{Description: description, Name: name, ExternalDocs: externalDocs}} } -// JSONLookup implements an interface to customize json pointer lookup +// JSONLookup implements an interface to customize json pointer lookup. func (t Tag) JSONLookup(token string) (any, error) { if ex, ok := t.Extensions[token]; ok { return &ex, nil @@ -42,7 +42,7 @@ func (t Tag) JSONLookup(token string) (any, error) { return r, err } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (t Tag) MarshalJSON() ([]byte, error) { b1, err := json.Marshal(t.TagProps) if err != nil { @@ -55,7 +55,7 @@ func (t Tag) MarshalJSON() ([]byte, error) { return jsonutils.ConcatJSON(b1, b2), nil } -// UnmarshalJSON marshal this from JSON +// UnmarshalJSON marshal this from JSON. func (t *Tag) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &t.TagProps); err != nil { return err diff --git a/validations.go b/validations.go index 2c0dc424..a82c2ffe 100644 --- a/validations.go +++ b/validations.go @@ -3,7 +3,7 @@ package spec -// CommonValidations describe common JSON-schema validations +// CommonValidations describe common JSON-schema validations. type CommonValidations struct { Maximum *float64 `json:"maximum,omitempty"` ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` @@ -143,22 +143,22 @@ func (v CommonValidations) Validations() SchemaValidations { } } -// HasNumberValidations indicates if the validations are for numbers or integers +// HasNumberValidations indicates if the validations are for numbers or integers. func (v CommonValidations) HasNumberValidations() bool { return v.Maximum != nil || v.Minimum != nil || v.MultipleOf != nil } -// HasStringValidations indicates if the validations are for strings +// HasStringValidations indicates if the validations are for strings. func (v CommonValidations) HasStringValidations() bool { return v.MaxLength != nil || v.MinLength != nil || v.Pattern != "" } -// HasArrayValidations indicates if the validations are for arrays +// HasArrayValidations indicates if the validations are for arrays. func (v CommonValidations) HasArrayValidations() bool { return v.MaxItems != nil || v.MinItems != nil || v.UniqueItems } -// HasEnum indicates if the validation includes some enum constraint +// HasEnum indicates if the validation includes some enum constraint. func (v CommonValidations) HasEnum() bool { return len(v.Enum) > 0 } @@ -175,12 +175,12 @@ type SchemaValidations struct { MinProperties *int64 `json:"minProperties,omitempty"` } -// HasObjectValidations indicates if the validations are for objects +// HasObjectValidations indicates if the validations are for objects. func (v SchemaValidations) HasObjectValidations() bool { return v.MaxProperties != nil || v.MinProperties != nil || v.PatternProperties != nil } -// SetValidations for schema validations +// SetValidations for schema validations. func (v *SchemaValidations) SetValidations(val SchemaValidations) { v.CommonValidations.SetValidations(val) v.PatternProperties = val.PatternProperties @@ -188,7 +188,7 @@ func (v *SchemaValidations) SetValidations(val SchemaValidations) { v.MinProperties = val.MinProperties } -// Validations for a schema +// Validations for a schema. func (v SchemaValidations) Validations() SchemaValidations { val := v.CommonValidations.Validations() val.PatternProperties = v.PatternProperties diff --git a/validations_test.go b/validations_test.go index 15b971fd..21404cbb 100644 --- a/validations_test.go +++ b/validations_test.go @@ -43,34 +43,34 @@ func TestValidations(t *testing.T) { expectedCV := val.CommonValidations require.Equal(t, expectedCV, cv) - require.True(t, cv.HasArrayValidations()) - require.True(t, cv.HasNumberValidations()) - require.True(t, cv.HasStringValidations()) - require.True(t, cv.HasEnum()) + require.TrueT(t, cv.HasArrayValidations()) + require.TrueT(t, cv.HasNumberValidations()) + require.TrueT(t, cv.HasStringValidations()) + require.TrueT(t, cv.HasEnum()) cv.Enum = nil - require.False(t, cv.HasEnum()) + require.FalseT(t, cv.HasEnum()) cv.MaxLength = nil - require.True(t, cv.HasStringValidations()) + require.TrueT(t, cv.HasStringValidations()) cv.MinLength = nil - require.True(t, cv.HasStringValidations()) + require.TrueT(t, cv.HasStringValidations()) cv.Pattern = "" - require.False(t, cv.HasStringValidations()) + require.FalseT(t, cv.HasStringValidations()) cv.Minimum = nil - require.True(t, cv.HasNumberValidations()) + require.TrueT(t, cv.HasNumberValidations()) cv.Maximum = nil - require.True(t, cv.HasNumberValidations()) + require.TrueT(t, cv.HasNumberValidations()) cv.MultipleOf = nil - require.False(t, cv.HasNumberValidations()) + require.FalseT(t, cv.HasNumberValidations()) cv.MaxItems = nil - require.True(t, cv.HasArrayValidations()) + require.TrueT(t, cv.HasArrayValidations()) cv.MinItems = nil - require.True(t, cv.HasArrayValidations()) + require.TrueT(t, cv.HasArrayValidations()) cv.UniqueItems = false - require.False(t, cv.HasArrayValidations()) + require.FalseT(t, cv.HasArrayValidations()) val = mkVal() expectedSV := val @@ -91,24 +91,24 @@ func TestValidations(t *testing.T) { require.Equal(t, val, sv.Validations()) - require.True(t, sv.HasObjectValidations()) + require.TrueT(t, sv.HasObjectValidations()) sv.MinProperties = nil - require.True(t, sv.HasObjectValidations()) + require.TrueT(t, sv.HasObjectValidations()) sv.MaxProperties = nil - require.True(t, sv.HasObjectValidations()) + require.TrueT(t, sv.HasObjectValidations()) sv.PatternProperties = nil - require.False(t, sv.HasObjectValidations()) + require.FalseT(t, sv.HasObjectValidations()) val = mkVal() cv.SetValidations(val) cv.ClearStringValidations() - require.False(t, cv.HasStringValidations()) + require.FalseT(t, cv.HasStringValidations()) cv.ClearNumberValidations() - require.False(t, cv.HasNumberValidations()) + require.FalseT(t, cv.HasNumberValidations()) cv.ClearArrayValidations() - require.False(t, cv.HasArrayValidations()) + require.FalseT(t, cv.HasArrayValidations()) sv.SetValidations(val) sv.ClearObjectValidations(func(validation string, _ any) { @@ -120,5 +120,5 @@ func TestValidations(t *testing.T) { t.Fail() } }) - require.Falsef(t, sv.HasObjectValidations(), "%#v", sv) + require.FalseTf(t, sv.HasObjectValidations(), "%#v", sv) } diff --git a/xml_object.go b/xml_object.go index bf2f8f18..07f7ef8c 100644 --- a/xml_object.go +++ b/xml_object.go @@ -14,43 +14,43 @@ type XMLObject struct { Wrapped bool `json:"wrapped,omitempty"` } -// WithName sets the xml name for the object +// WithName sets the xml name for the object. func (x *XMLObject) WithName(name string) *XMLObject { x.Name = name return x } -// WithNamespace sets the xml namespace for the object +// WithNamespace sets the xml namespace for the object. func (x *XMLObject) WithNamespace(namespace string) *XMLObject { x.Namespace = namespace return x } -// WithPrefix sets the xml prefix for the object +// WithPrefix sets the xml prefix for the object. func (x *XMLObject) WithPrefix(prefix string) *XMLObject { x.Prefix = prefix return x } -// AsAttribute flags this object as xml attribute +// AsAttribute flags this object as xml attribute. func (x *XMLObject) AsAttribute() *XMLObject { x.Attribute = true return x } -// AsElement flags this object as an xml node +// AsElement flags this object as an xml node. func (x *XMLObject) AsElement() *XMLObject { x.Attribute = false return x } -// AsWrapped flags this object as wrapped, this is mostly useful for array types +// AsWrapped flags this object as wrapped, this is mostly useful for array types. func (x *XMLObject) AsWrapped() *XMLObject { x.Wrapped = true return x } -// AsUnwrapped flags this object as an xml node +// AsUnwrapped flags this object as an xml node. func (x *XMLObject) AsUnwrapped() *XMLObject { x.Wrapped = false return x diff --git a/xml_object_test.go b/xml_object_test.go index f0c46ba9..e1063eb0 100644 --- a/xml_object_test.go +++ b/xml_object_test.go @@ -15,7 +15,7 @@ func TestXmlObject_Serialize(t *testing.T) { obj1 := XMLObject{} actual, err := json.Marshal(obj1) require.NoError(t, err) - assert.Equal(t, "{}", string(actual)) + assert.EqualT(t, "{}", string(actual)) obj2 := XMLObject{ Name: "the name", @@ -33,15 +33,19 @@ func TestXmlObject_Serialize(t *testing.T) { assert.Equal(t, obj2.Name, ad["name"]) assert.Equal(t, obj2.Namespace, ad["namespace"]) assert.Equal(t, obj2.Prefix, ad["prefix"]) - assert.True(t, ad["attribute"].(bool)) - assert.True(t, ad["wrapped"].(bool)) + attrVal, ok := ad["attribute"].(bool) + require.TrueT(t, ok, "expected bool for attribute") + assert.TrueT(t, attrVal) + wrappedVal, ok := ad["wrapped"].(bool) + require.TrueT(t, ok, "expected bool for wrapped") + assert.TrueT(t, wrappedVal) } func TestXmlObject_Deserialize(t *testing.T) { expected := XMLObject{} actual := XMLObject{} require.NoError(t, json.Unmarshal([]byte("{}"), &actual)) - assert.Equal(t, expected, actual) + assert.EqualT(t, expected, actual) completed := `{"name":"the name","namespace":"the namespace","prefix":"the prefix","attribute":true,"wrapped":true}` expected = XMLObject{ @@ -54,5 +58,5 @@ func TestXmlObject_Deserialize(t *testing.T) { actual = XMLObject{} require.NoError(t, json.Unmarshal([]byte(completed), &actual)) - assert.Equal(t, expected, actual) + assert.EqualT(t, expected, actual) }