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/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7dea4240..8983754c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,117 +1,251 @@ -## Contribution Guidelines +You'll find here general guidelines to contribute to this project. +They mostly correspond to standard practices for open source repositories. + +We have tried to keep things as simple as possible. + +> [!NOTE] +> If you're 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 apply the usual guidelines for a go library project on github. + +These guidelines are common to all libraries published on github by the `go-openapi` organization, +so you'll feel at home with any of our projects. + +You'll find more detailed (or repo-specific) instructions in the [maintainer's docs][maintainers-doc]. + +[maintainers-doc]: ../docs/MAINTAINERS.md + +## How can I contribute + +There are many ways in which you can contribute, not just code. Here are a few ideas: + +- Reporting issues or bugs +- Suggesting improvements +- Documentation +- Art work that makes the project look great +- Code + - proposing bug fixes and new features that are within the main project scope + - improving test coverage + - addressing code quality issues + +## Questions & issues + +### Asking a question + +You may inquire anything about this library by reporting a "Question" issue on github. + +You may also join our discord server where you may discuss issues or requests. + +[![Discord Server][discord-badge]][discord-url] + +[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue +[discord-url]: https://discord.gg/FfnFYaC3k5 + +### 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 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 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. +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've 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 lose track of things on big PRs. + +We're trying very hard to keep the go-openapi packages lean and focused. + +Together, these packages constitute a toolkit for go developers: +it won't do everything for everybody out of the box, +but everybody can use it to do just about everything related to OpenAPI. -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. +This means that we might decide against incorporating a new feature. -We're trying very hard to keep go-swagger lean and focused. We don't want it -to do everything for everybody. This means that we might decide against -incorporating a new feature. However, there might be a way to implement -that feature *on top of* go-swagger. +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 minimal go compiler version required is always the old stable (latest minor go version - 1). + +Our libraries are designed and tested to work on `Linux`, `MacOS` and `Windows`. + +If you're 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 -Fork the repo and make changes on your fork in a feature branch: +#### Git flow + +Fork the repo and make changes to your fork in a feature branch. -- If it's a bugfix branch, name it XXX-something where XXX is the number of the - issue -- If it's a feature branch, create an enhancement issue to announce your - intentions, and name it XXX-something where XXX is the number of the issue. +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. -Submit unit tests for your changes. Go has a great test framework built in; use -it! Take a look at existing tests for inspiration. Run the full test suite on -your branch before submitting a pull request. +Typically you'd follow some common naming conventions: -Update the documentation when creating or modifying features. Test -your documentation changes for clarity, concision, and correctness, as -well as a clean documentation build. See ``docs/README.md`` for more -information on building the docs and how docs get released. +- if it's a bug fixing 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. -Write clean code. Universally formatted code promotes ease of writing, reading, -and maintenance. Always run `gofmt -s -w file.go` on each changed file before -committing your changes. Most editors have plugins that do this automatically. +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 at least 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 a feature. + +Most documentation for this library is directly found in code as comments for godoc. + +The documentation for this go-openapi package is published on [the public go docs site][go-doc]. + +--- + +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 will run a godoc server locally where you may see the documentation generated from your local repository. + +[go-doc]: https://pkg.go.dev/github.com/go-openapi/spec + +#### 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 must start with a capitalized and short summary (max. 50 -chars) written in the imperative, followed by an optional, more detailed -explanatory text which is separated from the summary by an empty line. +Commit messages are not required to follow the "conventional commit" rule, but it's certainly a good +thing to follow that convention (e.g. "fix: fixed panic in XYZ", "ci: did this", "feat: did that" ...). -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. +The title in your commit message is used directly to produce our release notes: try to keep them neat. -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. +The commit message body should detail your changes. -Commits that fix or close an issue should include a reference like `Closes #XXX` -or `Fixes #XXX`, which will automatically close the issue when merged. +If an issue should be closed by a commit, please add this reference in the commit body: -### Sign your work +``` +* fixes #{issue number} +``` -The sign-off is a simple line at the end of the explanation for the -patch, which certifies that you wrote it or otherwise have the right to -pass it on as an open-source patch. The rules are pretty simple: if you -can certify the below (from -[developercertificate.org](http://developercertificate.org/)): +#### Code review -``` -Developer Certificate of Origin -Version 1.1 +Code review comments may be added to your pull request. -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -660 York Street, Suite 102, -San Francisco, CA 94110 USA +Discuss, then make the suggested modifications and push additional commits to your feature branch. -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. +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've squashed your commits into logical units of work** +using `git rebase -i` and `git push -f`. -Developer's Certificate of Origin 1.1 +After every commit the test suite should be passing. -By making a contribution to this project, I certify that: +Include documentation changes in the same commit so that a revert would remove all traces of the feature or fix. -(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 +#### Sign your work -(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 +Software is developed by real people. -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. +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. -(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. -``` +We require the simple DCO below with an email signing your commit. +PGP-signed commit are greatly appreciated but not required. -then you just add a line to every git commit message: +The rules are pretty simple: - Signed-off-by: Joe Smith +- read our [DCO][dco-doc] (from [developercertificate.org][dco-source]) +- 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`. +You can add the sign-off when creating the git commit via `git commit -s`. + +[dco-doc]: ./DCO.md +[dco-source]: https://developercertificate.org + +## Code contributions by AI agents + +Our agentic friends are welcome to contribute! + +We only have a few demands to keep-up with human maintainers. + +1. Issues and PRs written or posted by agents should always mention the original (human) poster for reference +2. We don't accept PRs attributed to agents. We don't want commits signed like "author: @claude.code". + Agents or bots may coauthor commits, though. +3. Security vulnerability reports by agents should always be reported privately and mention the original (human) poster + (see also [Security Policy][security-doc]). + +[security-doc]: ../SECURITY.md diff --git a/.github/DCO.md b/.github/DCO.md new file mode 100644 index 00000000..78a2d64f --- /dev/null +++ b/.github/DCO.md @@ -0,0 +1,40 @@ +# 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/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..9ce426e4 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,52 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + open-pull-requests-limit: 2 # <- default is 5 + groups: # <- group all github actions updates in a single PR + # 1. development-dependencies are auto-merged + development-dependencies: + patterns: + - '*' + + - package-ecosystem: "gomod" + # We define 4 groups of dependencies to regroup update pull requests: + # - development (e.g. test dependencies) + # - go-openapi updates + # - golang.org (e.g. golang.org/x/... packages) + # - other dependencies (direct or indirect) + # + # * All groups are checked once a week and each produce at most 1 PR. + # * All dependabot PRs are auto-approved + # + # Auto-merging policy, when requirements are met: + # 1. development-dependencies are auto-merged + # 2. golang.org-dependencies are auto-merged + # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge. + # 4. other dependencies require a manual merge + directory: "/" + schedule: + interval: "weekly" + day: "friday" + open-pull-requests-limit: 4 + groups: + development-dependencies: + patterns: + - "github.com/stretchr/testify" + + golang-org-dependencies: + patterns: + - "golang.org/*" + + go-openapi-dependencies: + patterns: + - "github.com/go-openapi/*" + + other-dependencies: + exclude-patterns: + - "github.com/go-openapi/*" + - "github.com/stretchr/testify" + - "golang.org/*" 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 new file mode 100644 index 00000000..faeaecf9 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,15 @@ +name: Dependabot auto-merge + +permissions: + contents: read + +on: + pull_request: + +jobs: + dependabot: + permissions: + contents: write + pull-requests: write + uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + secrets: inherit diff --git a/.github/workflows/bump-release.yml b/.github/workflows/bump-release.yml new file mode 100644 index 00000000..3e1688e5 --- /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@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + 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..4cbe535d --- /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@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + secrets: inherit diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..89944c35 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,18 @@ +name: Contributors + +on: + schedule: + - cron: '18 4 * * 6' + + workflow_dispatch: + +permissions: + contents: read + +jobs: + contributors: + permissions: + pull-requests: write + contents: write + uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + secrets: inherit diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 79a485c9..16cb4664 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -1,23 +1,17 @@ -name: Go Test +name: go test -on: [push, pull_request] +permissions: + pull-requests: read + contents: read -jobs: - - test: - - name: Test - runs-on: ${{ matrix.os }} +on: + push: + branches: + - master - strategy: - matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - steps: + pull_request: - - uses: actions/setup-go@v2 - with: - go-version: 1.x - - - uses: actions/checkout@v2 - - - run: go test +jobs: + test: + uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + secrets: inherit diff --git a/.github/workflows/scanner.yml b/.github/workflows/scanner.yml new file mode 100644 index 00000000..ed85aeab --- /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@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # V0.2.15 + secrets: inherit diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml new file mode 100644 index 00000000..097ab07c --- /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@e8e6599fe480362cb0d5cbdac5b245cc833742f5 # v0.2.15 + with: + tag: ${{ github.ref_name }} + secrets: inherit diff --git a/.gitignore b/.gitignore index dd91ed6a..d8f4186f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -secrets.yml -coverage.out +*.out +*.cov +.idea +.env +.mcp.json diff --git a/.golangci.yml b/.golangci.yml index 835d55e7..dc7c9605 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,42 +1,67 @@ -linters-settings: - govet: - check-shadowing: true - golint: - min-confidence: 0 - gocyclo: - min-complexity: 45 - maligned: - suggest-new: true - dupl: - threshold: 200 - goconst: - min-len: 2 - min-occurrences: 2 - +version: "2" linters: - enable-all: true + default: all disable: - - maligned - - unparam - - lll - - gochecknoinits - - gochecknoglobals + - depguard - funlen - godox - - gocognit - - whitespace - - wsl - - wrapcheck - - testpackage + - exhaustruct - nlreturn - - gomnd - - exhaustivestruct - - goerr113 - - errorlint - - nestif - - godot - - gofumpt + - nonamedreturns + - noinlineerr - paralleltest - - tparallel + - recvcheck + - testpackage - thelper - - ifshort + - tparallel + - varnamelen + - whitespace + - wrapcheck + - wsl + - wsl_v5 + settings: + dupl: + threshold: 200 + goconst: + min-len: 2 + min-occurrences: 3 + cyclop: + max-complexity: 20 + gocyclo: + min-complexity: 20 + exhaustive: + default-signifies-exhaustive: true + default-case-required: true + lll: + line-length: 180 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + - gofumpt + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +issues: + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 0 + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 0 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 new file mode 100644 index 00000000..0f533c01 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,50 @@ +# Contributors + +- Repository: ['go-openapi/spec'] + +| Total Contributors | Total Contributions | +| --- | --- | +| 38 | 396 | + +| Username | All Time Contribution Count | All Commits | +| --- | --- | --- | +| @casualjim | 191 | | +| @fredbi | 94 | | +| @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-community-projects/contributors)_ diff --git a/README.md b/README.md index 18782c6d..405002b8 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,38 @@ -# OAI object model +# spec -[![Build Status](https://travis-ci.org/go-openapi/spec.svg?branch=master)](https://travis-ci.org/go-openapi/spec) - -[![codecov](https://codecov.io/gh/go-openapi/spec/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/spec) -[![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) -[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/spec/master/LICENSE) -[![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/spec.svg)](https://pkg.go.dev/github.com/go-openapi/spec) -[![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/spec)](https://goreportcard.com/report/github.com/go-openapi/spec) + +[![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url] + + + +[![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] [![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 specification documents. +--- + +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 + * our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31** + +You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url] + +Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] + +## Status + +API is stable. + +## Import this library in your project + +```cmd +go get github.com/go-openapi/spec +``` ### FAQ @@ -31,4 +55,94 @@ The object model for OpenAPI specification documents. > 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? + +> Not directly. The exposed types know only how to unmarshal from JSON. +> +> 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: +> +> See also + +* How can I validate a spec? + +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? + +> We found jsonschema compatibility more important: since `id` in jsonschema influences +> how `$ref` are resolved. +> This `id` does not conflict with any property named `id`. +> +> See also + +## Change log + +See + +## References + + + +## Licensing + +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) + +## Cutting a new release + +Maintainers can cut a new release by either: + +* running [this workflow](https://github.com/go-openapi/spec/actions/workflows/bump-release.yml) +* or pushing a semver tag + * signed tags are preferred + * The tag message is prepended to release notes + + +[test-badge]: https://github.com/go-openapi/spec/actions/workflows/go-test.yml/badge.svg +[test-url]: https://github.com/go-openapi/spec/actions/workflows/go-test.yml +[cov-badge]: https://codecov.io/gh/go-openapi/spec/branch/master/graph/badge.svg +[cov-url]: https://codecov.io/gh/go-openapi/spec +[vuln-scan-badge]: https://github.com/go-openapi/spec/actions/workflows/scanner.yml/badge.svg +[vuln-scan-url]: https://github.com/go-openapi/spec/actions/workflows/scanner.yml +[codeql-badge]: https://github.com/go-openapi/spec/actions/workflows/codeql.yml/badge.svg +[codeql-url]: https://github.com/go-openapi/spec/actions/workflows/codeql.yml + +[release-badge]: https://badge.fury.io/gh/go-openapi%2Fspec.svg +[release-url]: https://badge.fury.io/gh/go-openapi%2Fspec +[gomod-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fspec.svg +[gomod-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Fspec + +[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/spec +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/spec +[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/spec +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/spec + +[doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgoswagger.io%2Fgo-openapi%2F +[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 + +[goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/spec +[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 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1fea2c57 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,37 @@ +# Security Policy + +This policy outlines the commitment and practices of the go-openapi maintainers regarding security. + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 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** +rather than opening a publicly visible GitHub issue. + +Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url]. + +> [!NOTE] +> 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/appveyor.yml b/appveyor.yml deleted file mode 100644 index 09035939..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: "0.1.{build}" - -clone_folder: C:\go-openapi\spec -shallow_clone: true # for startup speed -pull_requests: - do_not_increment_build_number: true - -#skip_tags: true -#skip_branch_with_pr: true - -# appveyor.yml -build: off - -environment: - GOPATH: c:\gopath - -stack: go 1.15 - -test_script: - - go test -v -timeout 20m ./... - -deploy: off - -notifications: - - provider: Slack - incoming_webhook: https://hooks.slack.com/services/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 - auth_token: - secure: Sf7kZf7ZGbnwWUMpffHwMu5A0cHkLK2MYY32LNTPj4+/3qC3Ghl7+9v4TSLOqOlCwdRNjOGblAq7s+GDJed6/xgRQl1JtCi1klzZNrYX4q01pgTPvvGcwbBkIYgeMaPeIRcK9OZnud7sRXdttozgTOpytps2U6Js32ip7uj5mHSg2ub0FwoSJwlS6dbezZ8+eDhoha0F/guY99BEwx8Bd+zROrT2TFGsSGOFGN6wFc7moCqTHO/YkWib13a2QNXqOxCCVBy/lt76Wp+JkeFppjHlzs/2lP3EAk13RIUAaesdEUHvIHrzCyNJEd3/+KO2DzsWOYfpktd+KBCvgaYOsoo7ubdT3IROeAegZdCgo/6xgCEsmFc9ZcqCfN5yNx2A+BZ2Vwmpws+bQ1E1+B5HDzzaiLcYfG4X2O210QVGVDLWsv1jqD+uPYeHY2WRfh5ZsIUFvaqgUEnwHwrK44/8REAhQavt1QAj5uJpsRd7CkRVPWRNK+yIky+wgbVUFEchRNmS55E7QWf+W4+4QZkQi7vUTMc9nbTUu2Es9NfvfudOpM2wZbn98fjpb/qq/nRv6Bk+ca+7XD5/IgNLMbWp2ouDdzbiHLCOfDUiHiDJhLfFZx9Bwo7ZwfzeOlbrQX66bx7xRKYmOe4DLrXhNcpbsMa8qbfxlZRCmYbubB/Y8h4= - channel: bots - on_build_success: false - on_build_failure: true - on_build_status_changed: true diff --git a/auth_test.go b/auth_test.go index a49340c5..486d74de 100644 --- a/auth_test.go +++ b/auth_test.go @@ -1,135 +1,107 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 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) { + assert.JSONUnmarshalAsT(t, BasicAuth(), `{"type":"basic"}`) - assertParsesJSON(t, `{"type":"basic"}`, BasicAuth()) - - 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/bindata.go b/bindata.go deleted file mode 100644 index afc83850..00000000 --- a/bindata.go +++ /dev/null @@ -1,297 +0,0 @@ -// Code generated by go-bindata. DO NOT EDIT. -// sources: -// schemas/jsonschema-draft-04.json (4.357kB) -// schemas/v2/schema.json (40.248kB) - -package spec - -import ( - "bytes" - "compress/gzip" - "crypto/sha256" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _jsonschemaDraft04Json = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x57\x3d\x6f\xdb\x3c\x10\xde\xf3\x2b\x08\x26\x63\xf2\x2a\x2f\xd0\xc9\x5b\xd1\x2e\x01\x5a\x34\x43\x37\x23\x03\x6d\x9d\x6c\x06\x14\xa9\x50\x54\x60\xc3\xd0\x7f\x2f\x28\x4a\x14\x29\x91\x92\x2d\xa7\x8d\x97\x28\xbc\xaf\xe7\x8e\xf7\xc5\xd3\x0d\x42\x08\x61\x9a\xe2\x15\xc2\x7b\xa5\x8a\x55\x92\xbc\x96\x82\x3f\x94\xdb\x3d\xe4\xe4\x3f\x21\x77\x49\x2a\x49\xa6\x1e\x1e\xbf\x24\xe6\xec\x16\xdf\x1b\xa1\x3b\xf3\xff\x02\xc9\x14\xca\xad\xa4\x85\xa2\x82\x6b\xe9\x6f\x42\x02\x32\x2c\x28\x07\x45\x5a\x15\x3d\x77\x46\x39\xd5\xcc\x25\x5e\x21\x83\xb8\x21\x18\xb6\xaf\x52\x92\xa3\x47\x68\x88\xea\x58\x80\x56\x4e\x1a\xf2\xbd\x4f\xcc\x29\x7f\x52\x90\x6b\x7d\xff\x0f\x48\xb4\x3d\x3f\x21\x7c\x27\x21\xd3\x2a\x6e\x31\xaa\x2d\x53\xdd\xf3\xe3\x42\x94\x54\xd1\x77\x78\xe2\x0a\x76\x20\xe3\x20\x68\xcb\x30\x86\x41\xf3\x2a\xc7\x2b\xf4\x78\x8e\xfe\xef\x90\x91\x8a\xa9\xc7\xb1\x1d\xc2\xd8\x2f\x0d\x75\xed\xc1\x4e\x9c\xc8\x25\x43\xac\xa8\xbe\xd7\xcc\xa9\xd1\xa9\x21\xa0\x1a\xbd\x04\x61\x94\x34\x2f\x18\xfc\x3e\x16\x50\x8e\x4d\x03\x6f\x1c\x58\xdb\x48\x23\xbc\x11\x82\x01\xe1\xfa\xd3\x3a\x8e\x30\xaf\x18\x33\x7f\xf3\x8d\x39\x11\x9b\x57\xd8\x2a\xfd\x55\x2a\x49\xf9\x0e\xc7\xec\x37\xd4\x25\xf7\xec\x5c\x66\xc7\xd7\x99\xaa\xcf\x4f\x89\x8a\xd3\xb7\x0a\x3a\xaa\x92\x15\xf4\x30\x6f\x1c\xb0\xd6\x46\xe7\x98\x39\x2d\xa4\x28\x40\x2a\x3a\x88\x9e\x29\xba\x88\x37\x2d\xca\x60\x38\xfa\xba\x5b\x20\xac\xa8\x62\xb0\x4c\xd4\xaf\xda\x45\x0a\xba\x5c\x3b\xb9\xc7\x79\xc5\x14\x2d\x18\x34\x19\x1c\x51\xdb\x25\x4d\xb4\x7e\x06\x14\x38\x6c\x59\x55\xd2\x77\xf8\x69\x59\xfc\x7b\x73\xed\x93\x43\xcb\x32\x6d\x3c\x28\xdc\x1b\x9a\xd3\x62\xab\xc2\x27\xf7\x41\xc9\x08\x2b\x23\x08\xad\x13\x57\x21\x9c\xd3\x72\x0d\x42\x72\xf8\x01\x7c\xa7\xf6\x83\xce\x39\xd7\x82\x3c\x1f\x2f\xd6\x60\x1b\xa2\xdf\x35\x89\x52\x20\xe7\x73\x74\xe0\x66\x26\x64\x4e\xb4\x97\x58\xc2\x0e\x0e\xe1\x60\x92\x34\x6d\xa0\x10\xd6\xb5\x83\x61\x27\xe6\x47\xd3\x89\xbd\x63\xfd\x3b\x8d\x03\x3d\x6c\x42\x2d\x5b\x70\xee\xe8\xdf\x4b\xf4\x66\x4e\xe1\x01\x45\x17\x80\x74\xad\x4f\xc3\xf3\xae\xc6\x1d\xc6\xd7\xc2\xce\xc9\xe1\x29\x30\x86\x2f\x4a\xa6\x4b\x15\x84\x73\xc9\x6f\xfd\x7f\xa5\x6e\x9e\xbd\xf1\xb0\xd4\xdd\x45\x5a\xc2\x3e\x4b\x78\xab\xa8\x84\x74\x4a\x91\x3b\x92\x23\x05\xf2\x1c\x1e\x7b\xf3\x09\xf8\xcf\xab\x24\xb6\x60\xa2\xe8\x4c\x9f\x75\x77\xaa\x8c\xe6\x01\x45\x36\x86\xcf\xc3\x63\x3a\xea\xd4\x8d\x7e\x06\xac\x14\x0a\xe0\x29\xf0\xed\x07\x22\x1a\x65\xda\x44\xae\xa2\x73\x1a\xe6\x90\x69\xa2\x8c\x46\xb2\x2f\xde\x49\x38\x08\xed\xfe\xfd\x41\xaf\x9f\xa9\x55\xd7\xdd\x22\x8d\xfa\x45\x63\xc5\x0f\x80\xf3\xb4\x08\xd6\x79\x30\x9e\x93\xee\x59\xa6\xd0\x4b\xee\x22\xe3\x33\xc1\x3a\x27\x68\x36\x78\x7e\x87\x0a\x06\xd5\x2e\x20\xd3\xaf\x15\xfb\xd8\x3b\x73\x14\xbb\x92\xed\x05\x5d\x2e\x29\x38\x2c\x94\xe4\x42\x45\x5e\xd3\xb5\x7d\xdf\x47\xca\x38\xb4\x5c\xaf\xfb\x7d\xdd\x6d\xf4\xa1\x2d\x77\xdd\x2f\xce\x6d\xc4\x7b\x8b\x4e\x67\xa9\x6f\xfe\x04\x00\x00\xff\xff\xb1\xd1\x27\x78\x05\x11\x00\x00") - -func jsonschemaDraft04JsonBytes() ([]byte, error) { - return bindataRead( - _jsonschemaDraft04Json, - "jsonschema-draft-04.json", - ) -} - -func jsonschemaDraft04Json() (*asset, error) { - bytes, err := jsonschemaDraft04JsonBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "jsonschema-draft-04.json", size: 4357, mode: os.FileMode(0640), modTime: time.Unix(1568963823, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe1, 0x48, 0x9d, 0xb, 0x47, 0x55, 0xf0, 0x27, 0x93, 0x30, 0x25, 0x91, 0xd3, 0xfc, 0xb8, 0xf0, 0x7b, 0x68, 0x93, 0xa8, 0x2a, 0x94, 0xf2, 0x48, 0x95, 0xf8, 0xe4, 0xed, 0xf1, 0x1b, 0x82, 0xe2}} - return a, nil -} - -var _v2SchemaJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x5d\x4f\x93\xdb\x36\xb2\xbf\xfb\x53\xa0\x14\x57\xd9\xae\xd8\x92\xe3\xf7\x2e\xcf\x97\xd4\xbc\xd8\x49\x66\x37\x5e\x4f\x79\x26\xbb\x87\x78\x5c\x05\x91\x2d\x09\x09\x09\x30\x00\x38\x33\x5a\xef\x7c\xf7\x2d\xf0\x9f\x08\x02\x20\x41\x8a\xd2\xc8\x0e\x0f\xa9\x78\x28\xa0\xd1\xdd\x68\x34\x7e\xdd\xf8\xf7\xf9\x11\x42\x33\x49\x64\x04\xb3\xd7\x68\x76\x86\xfe\x76\xf9\xfe\x1f\xe8\x32\xd8\x40\x8c\xd1\x8a\x71\x74\x79\x8b\xd7\x6b\xe0\xe8\xd5\xfc\x25\x3a\xbb\x38\x9f\xcf\x9e\xab\x0a\x24\x54\xa5\x37\x52\x26\xaf\x17\x0b\x91\x17\x99\x13\xb6\xb8\x79\xb5\x10\x59\xdd\xf9\xef\x82\xd1\x6f\xf2\xc2\x8f\xf3\x4f\xb5\x1a\xea\xc7\x17\x45\x41\xc6\xd7\x8b\x90\xe3\x95\x7c\xf1\xf2\x7f\x8b\xca\x45\x3d\xb9\x4d\x32\xa6\xd8\xf2\x77\x08\x64\xfe\x8d\xc3\x9f\x29\xe1\xa0\x9a\xff\xed\x11\x42\x08\xcd\x8a\xd6\xb3\x9f\x15\x67\x74\xc5\xca\x7f\x27\x58\x6e\xc4\xec\x11\x42\xd7\x59\x5d\x1c\x86\x44\x12\x46\x71\x74\xc1\x59\x02\x5c\x12\x10\xb3\xd7\x68\x85\x23\x01\x59\x81\x04\x4b\x09\x9c\x6a\xbf\x7e\xce\x49\x7d\xba\x7b\x51\xfd\xa1\x44\xe2\xb0\x52\xac\x7d\xb3\x08\x61\x45\x68\x46\x56\x2c\x6e\x80\x86\x8c\xbf\xbd\x93\x40\x05\x61\x74\x96\x95\xbe\x7f\x84\xd0\x7d\x4e\xde\x42\xb7\xe4\xbe\x46\xbb\x14\x5b\x48\x4e\xe8\xba\x90\x05\xa1\x19\xd0\x34\xae\xc4\xce\xbe\xbc\x9a\xbf\x9c\x15\x7f\x5d\x57\xc5\x42\x10\x01\x27\x89\xe2\x48\x51\xb9\xda\x40\xd5\x87\x37\xc0\x15\x5f\x88\xad\x90\xdc\x10\x81\x42\x16\xa4\x31\x50\x39\x2f\x38\xad\xab\xb0\x53\xd8\xac\x94\x56\x6f\xc3\x84\xf4\x11\xa4\x50\xb3\xfa\xe9\xd3\x6f\x9f\x3e\xdf\x2f\xd0\xeb\x8f\x1f\x3f\x7e\xbc\xfe\xf6\xe9\xf7\xaf\x5f\x7f\xfc\x18\x7e\xfb\xec\xfb\xc7\xb3\x36\x79\x54\x43\xe8\x29\xc5\x31\x20\xc6\x11\x49\x9e\xe5\x12\x41\x66\xa0\xe8\xed\x1d\x8e\x93\x08\x5e\xa3\x27\x3b\xc3\x7c\xa2\x73\xba\xc4\x02\x2e\xb0\xdc\xf4\xe5\x76\xd1\xca\x96\xa2\x8a\x94\xcd\x21\xc9\x6c\xec\x2c\x70\x42\x9e\x34\x74\x9d\x19\x7c\xcd\x20\x9c\xea\x2e\x0a\xfe\x42\x84\xd4\x29\x04\x8c\x8a\xb4\x41\xa2\xc1\xdc\x19\x8a\x88\x90\x4a\x49\xef\xce\xdf\xbd\x45\x4a\x52\x81\x70\x10\x40\x22\x21\x44\xcb\x6d\xc5\xec\x4e\x3c\x1c\x45\xef\x57\x9a\xb5\x7d\xae\xfe\xe5\xe4\x31\x86\x90\xe0\xab\x6d\x02\x3b\x2e\xcb\x11\x90\xd9\xa8\xc6\x77\xc2\x59\x98\x06\xfd\xf9\x2e\x78\x45\x01\xa6\xa8\xa0\x71\x5c\xbe\x33\xa7\xd2\xd9\x5f\x95\xef\xd9\xd5\xac\xfd\xdc\x5d\xbf\x5e\xb8\xd1\x3e\xc7\x31\x48\xe0\x5e\x4c\x14\x65\xdf\xb8\xa8\x71\x10\x09\xa3\xc2\xc7\x02\xcb\xa2\x4e\x5a\x02\x82\x94\x13\xb9\xf5\x30\xe6\xb2\xa4\xb5\xfe\x9b\x3e\x7a\xb2\x55\xd2\xa8\x4a\xbc\x16\xb6\x71\x8e\x39\xc7\xdb\x9d\xe1\x10\x09\x71\xbd\x9c\xb3\x41\x89\xd7\xa5\x89\xdc\x57\xb5\x53\x4a\xfe\x4c\xe1\xbc\xa0\x21\x79\x0a\x1a\x0f\x70\xa7\x5c\x08\x8e\xde\xb0\xc0\x43\x24\xad\x74\x63\x0e\xb1\xd9\x90\xe1\xb0\x2d\x13\xa7\x6d\x78\xfd\x04\x14\x38\x8e\x90\xaa\xce\x63\xac\x3e\x23\xbc\x64\xa9\xb4\xf8\x03\x63\xde\xcd\xbe\x16\x13\x4a\x55\xac\x82\x12\xc6\xac\xd4\x35\xf7\x22\xd4\x3a\xff\x22\x73\x0e\x6e\x51\xa0\x75\x1e\xae\x8f\xe8\x5d\xc7\x59\xe6\xe4\x9a\x18\x8d\xd6\x1c\x53\x84\x4d\xb7\x67\x28\x37\x09\x84\x69\x88\x12\x0e\x01\x11\x80\x32\xa2\xf5\xb9\xaa\xc6\xd9\x73\x53\xab\xfb\xb4\x2e\x20\xc6\x54\x92\xa0\x9a\xf3\x69\x1a\x2f\x81\x77\x37\xae\x53\x1a\xce\x40\xc4\xa8\x82\x1c\xb5\xef\xda\x24\x7d\xb9\x61\x69\x14\xa2\x25\xa0\x90\xac\x56\xc0\x81\x4a\xb4\xe2\x2c\xce\x4a\x64\x7a\x9a\x23\xf4\x13\x91\x3f\xa7\x4b\xf4\x63\x84\x6f\x18\x87\x10\xbd\xc3\xfc\x8f\x90\xdd\x52\x44\x04\xc2\x51\xc4\x6e\x21\x74\x48\x21\x81\xc7\xe2\xfd\xea\x12\xf8\x0d\x09\xf6\xe9\x47\x35\xaf\x67\xc4\x14\xf7\x22\x27\x97\xe1\xe2\x76\x2d\x06\x8c\x4a\x1c\x48\x3f\x73\x2d\x0b\x5b\x29\x45\x24\x00\x2a\x0c\x11\xec\x94\xca\xc2\xa6\xc1\x37\x21\x43\x83\x3b\x5f\x97\xf1\x43\x5e\x53\x73\x19\xa5\x36\xd8\x2d\x05\x2e\x34\x0b\xeb\x39\xfc\x1d\x63\x51\x01\xbd\x3d\xbb\x90\x84\x40\x25\x59\x6d\x09\x5d\xa3\x1c\x37\xe6\x5c\x16\x9a\x40\x09\x70\xc1\xe8\x82\xf1\x35\xa6\xe4\xdf\x99\x5c\x8e\x9e\x4d\x79\xb4\x27\x2f\xbf\x7e\xf8\x05\x25\x8c\x50\xa9\x98\x29\x90\x62\x60\xea\x75\xae\x13\xca\xbf\x2b\x1a\x29\x27\x76\xd6\x20\xc6\x64\x5f\xe6\x32\x1a\x08\x87\x21\x07\x21\xbc\xb4\xe4\xe0\x32\x67\xa6\xcd\xf3\x1e\xcd\xd9\x6b\xb6\x6f\x8e\x27\xa7\xed\xdb\xe7\xbc\xcc\x1a\x07\xce\x6f\x87\x33\xf0\xba\x51\x17\x22\x66\x78\x79\x8e\xce\xe5\x13\x81\x80\x06\x2c\xe5\x78\x0d\xa1\xb2\xb8\x54\xa8\x79\x09\xbd\xbf\x3c\x47\x01\x8b\x13\x2c\xc9\x32\xaa\xaa\x1d\xd5\xee\xab\x36\xbd\x6c\xfd\x54\x6c\xc8\x08\x01\x3c\xbd\xe7\x07\x88\xb0\x24\x37\x79\x90\x28\x4a\x1d\x10\x1a\x92\x1b\x12\xa6\x38\x42\x40\xc3\x4c\x43\x62\x8e\xae\x36\xb0\x45\x71\x2a\xa4\x9a\x23\x79\x59\xb1\xa8\xf2\xa4\x0c\x60\x9f\xcc\x8d\x40\xf5\x80\xca\xa8\x99\xc3\xa7\x85\x1f\x31\x25\xa9\x82\xc5\x6d\xbd\xd8\x36\x76\x7c\x02\x28\x97\xf6\x1d\x74\x3b\x11\x7e\x91\xae\x32\xf8\x6c\xf4\xe6\x7b\x9a\xa5\x1f\x62\xc6\x21\xcf\x9a\xe5\xed\x8b\x02\xf3\x2c\x33\x33\xdf\x00\xca\xc9\x09\xb4\x04\xf5\xa5\x08\xd7\xc3\x02\x18\x66\xf1\xab\x1e\x83\x37\x4c\xcd\x12\xc1\x1d\x50\xf6\xaa\xbd\xfe\xe2\x73\x48\x38\x08\xa0\x32\x9b\x18\x44\x86\x0b\x6a\xc1\xaa\x26\x96\x2d\x96\x3c\xa0\x54\x65\x73\xe3\x08\xb5\x8b\x99\xbd\x82\xbc\x9e\xc2\xe8\x53\x46\x83\x3f\x33\x54\x2b\x5b\xad\x92\x79\xd9\x8f\x5d\x93\x98\xf2\xe6\xc6\x1c\xe6\x9a\x9e\xfc\x43\x82\x31\x66\x8e\x53\x77\xfe\x90\xe7\xf3\xf6\xe9\x62\x23\x3f\x10\x93\x18\xae\x72\x1a\x9d\xf9\x48\xcb\xcc\x5a\x65\xc7\x4a\x04\xf0\xf3\xd5\xd5\x05\x8a\x41\x08\xbc\x86\x86\x43\x51\x6c\xe0\x46\x57\xf6\x44\x40\x0d\xfb\xff\xa2\xc3\x7c\x3d\x39\x84\xdc\x09\x22\x64\x4f\x12\xd9\xba\xaa\xf6\xe3\xbd\x56\xdd\x91\x25\x6a\x14\x9c\x89\x34\x8e\x31\xdf\xee\x15\x7e\x2f\x39\x81\x15\x2a\x28\x95\x66\x51\xf5\xfd\x83\xc5\xfe\x15\x07\xcf\xf7\x08\xee\x1d\x8e\xb6\xc5\x52\xcc\x8c\x5a\x93\x66\xc5\xd8\x79\x38\x46\xd6\xa7\x88\x37\xc9\x2e\xe3\xd2\xa5\x7b\x4b\x3a\xdc\xa1\xdc\x9e\x29\xf1\x8c\x8a\x99\x16\x47\x8d\xd4\x78\x8b\xf6\x1c\xe9\x71\x54\x1b\x69\xa8\x4a\x93\x37\xe5\xb2\x2c\x4f\x0c\x92\xab\xa0\x73\x32\x72\x59\xd3\xf0\x2d\x8d\xed\xca\x37\x16\x19\x9e\xdb\x1c\xab\x17\x49\xc3\x0f\x37\xdc\x88\xb1\xb4\xd4\x42\xcb\x58\x5e\x6a\x52\x0b\x15\x10\x0a\xb0\x04\xe7\xf8\x58\x32\x16\x01\xa6\xcd\x01\xb2\xc2\x69\x24\x35\x38\x6f\x30\x6a\xae\x1b\xb4\x71\xaa\xad\x1d\xa0\xd6\x20\x2d\x8b\x3c\xc6\x82\x62\x27\x34\x6d\x15\x84\x7b\x43\xb1\x35\x78\xa6\x24\x77\x28\xc1\x6e\xfc\xe9\x48\x74\xf4\x15\xe3\xe1\x84\x42\x88\x40\x7a\x26\x49\x3b\x48\xb1\xa4\x19\x8e\x0c\xa7\xb5\x01\x6c\x0c\x97\x61\x8a\xc2\x32\xd8\x8c\x44\x69\x24\xbf\x65\x1d\x74\xd6\xe5\x44\xef\xec\x48\x5e\xb7\x8a\xa3\x29\x8e\x41\x64\xce\x1f\x88\xdc\x00\x47\x4b\x40\x98\x6e\xd1\x0d\x8e\x48\x98\x63\x5c\x21\xb1\x4c\x05\x0a\x58\x98\xc5\x6d\x4f\x0a\x77\x53\x4f\x8b\xc4\x44\x1f\xb2\xdf\x8d\x3b\xea\x9f\xfe\xf6\xf2\xc5\xff\x5d\x7f\xfe\x9f\xfb\x67\x8f\xff\xf3\xe9\x69\xd1\xfe\xb3\xc7\xfd\x3c\xf8\x3f\x71\x94\x82\x23\xd1\x72\x00\xb7\x42\x99\x6c\xc0\x60\x7b\x0f\x79\xea\xa8\x53\x4b\x56\x31\xfa\x0b\x52\x9f\x96\xdb\xcd\x2f\xd7\x67\xcd\x04\x19\x85\xfe\xdb\x02\x9a\x59\x03\xad\x63\x3c\xea\xff\x2e\x18\xfd\x00\xd9\xe2\x56\x60\x59\x93\xb9\xb6\xb2\x3e\x3c\x2c\xab\x0f\xa7\xb2\x89\x43\xc7\xf6\xd5\xce\x2e\xad\xa6\xa9\xed\xa6\xc6\x5a\xb4\xa6\x67\xdf\x8c\x26\x7b\x50\x5a\x91\x08\x2e\x6d\xd4\x3a\xc1\x9d\xf2\xdb\xde\x1e\xb2\x2c\x6c\xa5\x64\xc9\x16\xb4\x90\xaa\x4a\xb7\x0c\xde\x13\xc3\x2a\x9a\x11\x9b\x7a\x1b\x3d\x95\x97\x37\x31\x6b\x69\x7e\x34\xc0\x67\x1f\x66\x19\x49\xef\xf1\x25\xf5\xac\x0e\xea\x0a\x28\x8d\x4d\x7e\xd9\x57\x4b\x49\xe5\xc6\xb3\x25\xfd\xe6\x57\x42\x25\xac\xcd\xcf\x36\x74\x8e\xca\x24\x47\xe7\x80\xa8\x92\x72\xbd\x3d\x84\x2d\x65\xe2\x82\x1a\x9c\xc4\x44\x92\x1b\x10\x79\x8a\xc4\x4a\x2f\x60\x51\x04\x81\xaa\xf0\xa3\x95\x27\xd7\x12\x7b\xa3\x96\x03\x45\x96\xc1\x8a\x07\xc9\xb2\xb0\x95\x52\x8c\xef\x48\x9c\xc6\x7e\x94\xca\xc2\x0e\x07\x12\x44\xa9\x20\x37\xf0\xae\x0f\x49\xa3\x96\x9d\x4b\x42\x7b\x70\x59\x14\xee\xe0\xb2\x0f\x49\xa3\x96\x4b\x97\xbf\x00\x5d\x4b\x4f\xfc\xbb\x2b\xee\x92\xb9\x17\xb5\xaa\xb8\x0b\x97\x17\x9b\x43\xfd\xd6\xc2\xb2\xc2\x2e\x29\xcf\xfd\x87\x4a\x55\xda\x25\x63\x1f\x5a\x65\x69\x2b\x2d\x3d\x67\xe9\x41\xae\x5e\xc1\x6e\x2b\xd4\xdb\x3e\xa8\xd3\x26\xd2\x48\x92\x24\xca\x61\x86\x8f\x8c\xbb\xf2\x8e\x91\xdf\x1f\x06\x19\x33\xf3\x03\x4d\xba\xcd\xe2\x2d\xfb\x69\xe9\x16\x15\x13\xd5\x56\x85\x4e\x3c\x5b\x8a\xbf\x25\x72\x83\xee\x5e\x20\x22\xf2\xc8\xaa\x7b\xdb\x8e\xe4\x29\x58\xca\x38\xb7\x3f\x2e\x59\xb8\xbd\xa8\x16\x16\xf7\xdb\x79\x51\x9f\x5a\xb4\x8d\x87\x3a\x6e\xbc\x3e\xc5\xb4\xcd\x58\xf9\xf5\x3c\xb9\x6f\x49\xaf\x57\xc1\xfa\x1c\x5d\x6d\x88\x8a\x8b\xd3\x28\xcc\xb7\xef\x10\x8a\x4a\x74\xa9\x4a\xa7\x62\xbf\x0d\x76\x23\x6f\x59\xd9\x31\xee\x40\x11\xfb\x28\xec\x8d\x22\x1c\x13\x5a\x64\x94\x23\x16\x60\xbb\xd2\x7c\xa0\x98\xb2\xe5\x6e\xbc\x54\x33\xe0\x3e\xb9\x52\x17\xdb\xb7\x1b\xc8\x12\x20\x8c\x23\xca\x64\x7e\x78\xa3\x62\x5b\x75\x56\xd9\x9e\x2a\x91\x27\xb0\x70\x34\x1f\x90\x89\xb5\x86\x73\x7e\x71\xda\x1e\xfb\x3a\x72\xdc\x5e\x79\x88\xcb\x74\x79\xd9\x64\xe4\xd4\xc2\x9e\xce\xb1\xfe\x85\x5a\xc0\xe9\x0c\x34\x3d\xd0\x43\xce\xa1\x36\x39\xd5\xa1\x4e\xf5\xf8\xb1\xa9\x23\x08\x75\x84\xac\x53\x6c\x3a\xc5\xa6\x53\x6c\x3a\xc5\xa6\x7f\xc5\xd8\xf4\x51\xfd\xff\x25\x4e\xfa\x33\x05\xbe\x9d\x60\xd2\x04\x93\x6a\x5f\x33\x9b\x98\x50\xd2\xe1\x50\x52\xc6\xcc\xdb\x38\x91\xdb\xe6\xaa\xa2\x8f\xa1\x6a\xa6\xd4\xc6\x56\xd6\x8c\x40\x02\x68\x48\xe8\x1a\xe1\x9a\xd9\x2e\xb7\x05\xc3\x34\xda\x2a\xbb\xcd\x12\x36\x98\x22\x50\x4c\xa1\x1b\xc5\xd5\x84\xf0\xbe\x24\x84\xf7\x2f\x22\x37\xef\x94\xd7\x9f\xa0\xde\x04\xf5\x26\xa8\x37\x41\x3d\x64\x40\x3d\xe5\xf2\xde\x60\x89\x27\xb4\x37\xa1\xbd\xda\xd7\xd2\x2c\x26\xc0\x37\x01\x3e\x1b\xef\x5f\x06\xe0\x6b\x7c\x5c\x91\x08\x26\x10\x38\x81\xc0\x09\x04\x76\x4a\x3d\x81\xc0\xbf\x12\x08\x4c\xb0\xdc\x7c\x99\x00\xd0\x75\x70\xb4\xf8\x5a\x7c\xea\xde\x3e\x39\x08\x30\x5a\x27\x35\xed\xb4\x65\xad\x69\x74\x10\x88\x79\xe2\x30\x52\x19\xd6\x04\x21\xa7\x95\xd5\x0e\x03\xf8\xda\x20\xd7\x84\xb4\x26\xa4\x35\x21\xad\x09\x69\x21\x03\x69\x51\x46\xff\xff\x18\x9b\x54\xed\x87\x47\x06\x9d\x4e\x73\x6e\x9a\xb3\xa9\xce\x83\x5e\x4b\xc6\x71\x20\x45\xd7\x72\xf5\x40\x72\x0e\x34\x6c\xf4\x6c\xf3\xba\x5e\x4b\x97\x0e\x52\xb8\xbe\x8b\x79\xa0\x10\x86\xa1\x75\xb0\x6f\xec\xc8\xf4\x3d\x4d\x7b\x86\xc2\x02\x31\x12\x51\xbf\x07\x94\xad\x10\xd6\x2e\x79\xcf\xe9\x1c\xf5\x1e\x31\x23\x5c\x18\xfb\x9c\xfb\x70\xe0\x62\xbd\xf7\xb5\x94\xcf\xf3\xf6\xfa\xc5\x4e\x9c\x85\x76\x1d\xae\x37\xbc\xde\xa3\x41\xcb\x29\xd0\x5e\x70\x67\x50\x93\x6d\x98\xa8\xd3\x67\x0f\x68\xb1\xeb\x38\x47\x07\x10\x1b\xd2\xe2\x18\x68\x6d\x40\xbb\xa3\x40\xba\x21\xf2\x8e\x81\xfb\xf6\x92\x77\x2f\x70\xe8\xdb\xb2\x36\xbf\x30\x91\xc5\x21\xe7\x45\xcc\x34\x0c\x48\x8e\xd0\xf2\x9b\x7c\x3c\xbd\x1c\x04\x3e\x07\xe8\x7c\x2f\x84\x7a\x48\x4d\x1f\xba\xe1\x76\x45\x7b\x60\xe0\x01\xca\xee\x04\xca\x31\xbe\x73\x5f\xa3\x70\x0c\xad\x1f\xa5\xf5\x76\xd5\xbb\xd2\x7e\xfb\x30\x90\xcf\xfa\x67\x7a\xe6\xc3\x37\x42\x19\xe2\xc9\x9c\x61\x4c\xe7\xd1\x77\x55\x86\x6e\x8f\x7b\x85\x42\x33\xa3\xaa\x57\xae\xfd\xd5\xcc\x9c\x56\x68\xe2\xde\x0e\xa8\x2c\xa9\xb0\x7d\xf0\x54\x2d\x80\xf2\x48\x39\x3d\x98\x1a\x6d\x0b\x9d\xba\x53\xfb\xce\xf8\xd1\x7e\xbb\x60\x4f\x06\xf5\xce\xda\xab\xeb\xca\xcb\xd5\xac\x20\xda\x72\x3b\xa2\x4b\x38\xd7\xb5\x89\xbe\x42\xd9\xb9\x73\xc4\x0c\x6d\xb7\xd9\xf8\x8d\xbd\x3e\x9c\xf5\x53\x68\x48\x14\x36\x8f\x09\xc5\x92\xf1\x21\xd1\x09\x07\x1c\xbe\xa7\x91\xf3\x6a\xc8\xc1\x57\xb0\xdd\xc5\xc6\x1d\xad\x76\x1d\xa8\x82\x0e\x4c\x38\xfe\xa5\x8c\xc5\x0a\x40\x5d\xa1\xbb\x98\xd1\xfb\x74\x61\xed\x1a\x98\xaf\x3c\x8c\x1e\xe3\xc2\x92\x29\x74\x3e\x99\xd0\xf9\x41\x50\xd0\x38\x4b\x57\x7e\x5b\x7a\x0e\xe6\xce\x4e\xd7\x19\x35\x57\xbb\x3c\x3c\xd2\x5e\x4f\x4b\x4c\xf7\x0f\x4d\x2b\x91\x5d\x94\xa6\x95\xc8\x69\x25\x72\x5a\x89\x7c\xb8\x95\xc8\x07\x80\x8c\xda\x9c\x64\x7b\xb7\x71\xdf\x57\x12\x4b\x9a\x1f\x72\x0c\x13\x03\xad\x3c\xd5\x4e\xde\x8e\x57\x13\x6d\x34\x86\xcf\x97\xe6\xa4\x68\xc4\xb0\xf6\xc9\xc2\xeb\x8d\x0b\xd7\xcd\xfe\xba\xa6\xf5\x30\xeb\x30\x33\xbe\xc7\x56\x27\xab\x08\xd9\x6d\xbb\x09\xee\x7c\x2d\xcf\xee\x87\x38\xac\xc8\xdd\x90\x9a\x58\x4a\x4e\x96\xa9\x79\x79\xf3\xde\x20\xf0\x96\xe3\x24\x19\xeb\xba\xf2\x53\x19\xab\x12\xaf\x47\xb3\xa0\x3e\xef\x9b\x8d\x6d\x6d\x7b\xde\x3b\x3b\x1a\xc0\x3f\x95\x7e\xed\x78\xfb\x76\xb8\xaf\xb3\xdd\xc5\xeb\x95\xed\x5a\x62\x41\x82\xb3\x54\x6e\x80\x4a\x92\x6f\x36\xbd\x34\xae\xde\x6f\xa4\xc0\xbc\x08\xe3\x84\xfc\x1d\xb6\xe3\xd0\x62\x38\x95\x9b\x57\xe7\x71\x12\x91\x80\xc8\x31\x69\x5e\x60\x21\x6e\x19\x0f\xc7\xa4\x79\x96\x28\x3e\x47\x54\x65\x41\x36\x08\x40\x88\x1f\x58\x08\x56\xaa\xd5\xbf\xaf\xad\x96\xd7\xd6\xcf\x87\xf5\x34\x0f\x71\x93\x6e\x26\xed\x98\x5b\x9f\x4f\xcf\x95\x34\xc6\xd7\x11\xfa\xb0\x81\x22\x1a\xdb\xdf\x8e\xdc\xc3\xb9\xf8\xdd\x5d\x3c\x74\xe6\xea\xb7\x8b\xbf\xf5\x6e\xb3\x46\x2e\x64\xf4\xab\x3c\x4e\xcf\x36\x1d\xfe\xfa\xb8\x36\xba\x8a\xd8\xad\xf6\xc6\x41\x2a\x37\x8c\x17\x0f\xda\xfe\xda\xe7\x65\xbc\x71\x2c\x36\x57\x8a\x47\x12\x4c\xf1\xbd\x77\x6b\xa4\x50\x7e\x77\x7b\x22\x60\x89\xef\xcd\xf5\xb9\x0c\x97\x79\x0d\x2b\x35\x43\xcb\x3d\x24\xf1\x78\xfc\xf8\xcb\x1f\x15\x06\xe2\x78\xd8\x51\x21\xd9\x1f\xf0\xf5\x8f\x86\xa4\x50\xfa\xb1\x47\x43\xa5\xdd\x69\x14\xe8\xa3\xc0\x86\x91\xa7\x81\x50\xb4\x7c\xc0\x81\x80\x77\x7a\x9f\xc6\xc2\xa9\x8c\x05\x33\xb0\x3b\x31\xa4\xf4\xd7\x1b\x26\x55\x97\x7c\x65\xf8\x69\x1a\x84\x8e\x41\x78\xd9\xec\xc5\x11\x16\x1e\x74\x91\xf5\x56\xf5\x57\x49\x47\x5c\x92\xa9\x1e\x99\x36\xf4\xdb\xb1\x0e\xd3\x78\x02\xb0\x9b\x25\xcb\xe9\xe9\x1d\x0d\x44\x01\x42\x08\x91\x64\xd9\xdd\x37\x08\x17\xef\xf9\xe5\x0f\xbd\x46\x91\xf5\xf9\x89\x92\x37\xdd\x89\x59\x44\x1f\x9c\xee\x34\x1e\xbe\x47\x83\x32\x72\x8e\x37\xdf\xac\x69\x38\xef\x75\xb0\xda\xdb\xac\x83\x94\x2f\x39\xa6\x62\x05\x1c\x25\x9c\x49\x16\xb0\xa8\x3c\xc7\x7e\x76\x71\x3e\x6f\xb5\x24\xe7\xe8\xb7\xb9\xc7\x6c\x43\x92\xee\x21\xd4\x17\xa1\x7f\xba\x35\xfe\xae\x39\xbc\xde\xba\x69\xd9\x8e\xe1\x62\xde\x64\x7d\x16\x88\x1b\xed\x29\x11\xfd\x4f\xa9\xff\x99\x90\xc4\xf6\xf4\xf9\x6e\xe9\x28\x23\xd7\xca\xe5\xee\xee\x9f\x63\xb1\x5b\xfb\x10\xd7\x2f\x1d\xf2\xe3\xbf\xb9\xb5\x6f\xa4\x6d\x7d\x25\x79\xfb\x24\x31\xea\x56\xbe\x5d\x53\xcd\x2d\x36\xa3\x6d\xdf\xab\x1c\xb8\x6d\x6f\xc0\x98\xa7\xdd\xaa\x86\x8c\x1d\x39\xa3\x9d\x70\x2b\x9b\x68\xd9\xfd\x33\xfe\xa9\xb6\x4a\x2e\x63\x0f\xcf\x68\x27\xd9\x4c\xb9\x46\x6d\xcb\xbe\xa1\xa8\xd6\x5f\xc6\xd6\x9f\xf1\x4f\xf4\xd4\xb4\x78\xd0\xd6\xf4\x13\x3c\x3b\xac\xd0\xdc\x90\x34\xda\xc9\xb4\x9a\x1a\x8d\xbd\x93\x87\xd4\xe2\x21\x1b\xb3\x2b\xd1\xbe\xe7\x69\xd4\x53\x67\xd5\x40\xa0\xe3\x19\x3f\x6d\x1a\xbc\x0e\x86\x3c\x10\xb4\x3d\x2a\xcd\x78\x32\xe6\xab\xbd\x36\xc9\xf4\x3a\x58\xae\xc3\xf4\x47\xea\xbf\xfb\x47\xff\x0d\x00\x00\xff\xff\xd2\x32\x5a\x28\x38\x9d\x00\x00") - -func v2SchemaJsonBytes() ([]byte, error) { - return bindataRead( - _v2SchemaJson, - "v2/schema.json", - ) -} - -func v2SchemaJson() (*asset, error) { - bytes, err := v2SchemaJsonBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "v2/schema.json", size: 40248, mode: os.FileMode(0640), modTime: time.Unix(1568964748, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xab, 0x88, 0x5e, 0xf, 0xbf, 0x17, 0x74, 0x0, 0xb2, 0x5a, 0x7f, 0xbc, 0x58, 0xcd, 0xc, 0x25, 0x73, 0xd5, 0x29, 0x1c, 0x7a, 0xd0, 0xce, 0x79, 0xd4, 0x89, 0x31, 0x27, 0x90, 0xf2, 0xff, 0xe6}} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "jsonschema-draft-04.json": jsonschemaDraft04Json, - - "v2/schema.json": v2SchemaJson, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} - -var _bintree = &bintree{nil, map[string]*bintree{ - "jsonschema-draft-04.json": {jsonschemaDraft04Json, map[string]*bintree{}}, - "v2": {nil, map[string]*bintree{ - "schema.json": {v2SchemaJson, map[string]*bintree{}}, - }}, -}} - -// RestoreAsset restores an asset under the given directory. -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) -} - -// RestoreAssets restores an asset under the given directory recursively. -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) -} diff --git a/cache.go b/cache.go index 122993b4..06495d2c 100644 --- a/cache.go +++ b/cache.go @@ -1,40 +1,28 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( + "maps" "sync" ) -// ResolutionCache a cache for resolving urls +// ResolutionCache a cache for resolving urls. type ResolutionCache interface { - Get(string) (interface{}, bool) - Set(string, interface{}) + Get(uri string) (any, bool) + Set(uri string, data any) } type simpleCache struct { lock sync.RWMutex - store map[string]interface{} + store map[string]any } -func (s *simpleCache) ShallowClone() ResolutionCache { - store := make(map[string]interface{}, len(s.store)) +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() - for k, v := range s.store { - store[k] = v - } + maps.Copy(store, s.store) s.lock.RUnlock() return &simpleCache{ @@ -42,8 +30,8 @@ func (s *simpleCache) ShallowClone() ResolutionCache { } } -// Get retrieves a cached URI -func (s *simpleCache) Get(uri string) (interface{}, bool) { +// Get retrieves a cached URI. +func (s *simpleCache) Get(uri string) (any, bool) { s.lock.RLock() v, ok := s.store[uri] @@ -51,8 +39,8 @@ func (s *simpleCache) Get(uri string) (interface{}, bool) { return v, ok } -// Set caches a URI -func (s *simpleCache) Set(uri string, data interface{}) { +// Set caches a URI. +func (s *simpleCache) Set(uri string, data any) { s.lock.Lock() s.store[uri] = data s.lock.Unlock() @@ -68,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{} ) @@ -80,13 +68,13 @@ func initResolutionCache() { } func defaultResolutionCache() *simpleCache { - return &simpleCache{store: map[string]interface{}{ + return &simpleCache{store: map[string]any{ "http://swagger.io/v2/schema.json": MustLoadSwagger20Schema(), "http://json-schema.org/draft-04/schema": MustLoadJSONSchemaDraft04(), }} } -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 8d6d2745..92f8a8f6 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1,9 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) func TestDefaultResolutionCache(t *testing.T) { @@ -13,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 90bc6d2c..5a8535c7 100644 --- a/circular_test.go +++ b/circular_test.go @@ -1,16 +1,18 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "encoding/json" - "io/ioutil" "net/http" - "net/http/httptest" + "os" "path/filepath" "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestExpandCircular_Issue3(t *testing.T) { @@ -70,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") @@ -86,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) /* @@ -149,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/") @@ -175,7 +177,7 @@ func TestExpandCircular_Bitbucket(t *testing.T) { func TestExpandCircular_ResponseWithRoot(t *testing.T) { rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join("fixtures", "more_circulars", "resp.json")) + b, err := os.ReadFile(filepath.Join("fixtures", "more_circulars", "resp.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -219,7 +221,7 @@ func TestExpandCircular_SpecExpansion(t *testing.T) { func TestExpandCircular_RemoteCircularID(t *testing.T) { go func() { - err := http.ListenAndServe("localhost:1234", http.FileServer(http.Dir("fixtures/more_circulars/remote"))) + err := http.ListenAndServe("localhost:1234", http.FileServer(http.Dir("fixtures/more_circulars/remote"))) //#nosec if err != nil { panic(err.Error()) } @@ -232,7 +234,7 @@ func TestExpandCircular_RemoteCircularID(t *testing.T) { assertRefResolve(t, jazon, "", root, &ExpandOptions{RelativeBase: fixturePath}) assertRefExpand(t, jazon, "", root, &ExpandOptions{RelativeBase: fixturePath}) - assert.NoError(t, ExpandSchemaWithBasePath(root, nil, &ExpandOptions{})) + require.NoError(t, ExpandSchemaWithBasePath(root, nil, &ExpandOptions{})) jazon = asJSON(t, root) @@ -250,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) @@ -263,7 +265,12 @@ func TestCircular_RemoteExpandAzure(t *testing.T) { require.NotNil(t, pth1) // check expected remaining $ref - assertRefInJSONRegexp(t, jazon, `^(#/definitions/)|(networkInterface.json#/definitions/)|(networkSecurityGroup.json#/definitions/)|(network.json#/definitions)|(virtualNetworkTap.json#/definitions/)|(virtualNetwork.json#/definitions/)|(privateEndpoint.json#/definitions/)|(\./examples/)`) + assertRefInJSONRegexp(t, jazon, + `^(#/definitions/)|(networkInterface.json#/definitions/)|`+ + `(networkSecurityGroup.json#/definitions/)|(network.json#/definitions)|`+ + `(virtualNetworkTap.json#/definitions/)|(virtualNetwork.json#/definitions/)|`+ + `(privateEndpoint.json#/definitions/)|(\./examples/)`, + ) // check all $ref resolve in the expanded root // (filter out the remaining $ref in x-ms-example extensions, which are not expanded) diff --git a/contact_info.go b/contact_info.go index 2f7bb219..46fada5d 100644 --- a/contact_info.go +++ b/contact_info.go @@ -1,23 +1,12 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "encoding/json" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) // ContactInfo contact information for the exposed API. @@ -28,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 @@ -43,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 { @@ -53,5 +42,5 @@ func (c ContactInfo) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + return jsonutils.ConcatJSON(b1, b2), nil } diff --git a/contact_info_test.go b/contact_info_test.go index 3ca2315a..bcf177e2 100644 --- a/contact_info_test.go +++ b/contact_info_test.go @@ -1,24 +1,12 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) const contactInfoJSON = `{ @@ -28,21 +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]interface{}{"x-teams": "test team"}}} +}, VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-teams": "test team"}}} func TestIntegrationContactInfo(t *testing.T) { - b, err := json.MarshalIndent(contactInfo, "", "\t") - if assert.NoError(t, err) { - assert.Equal(t, contactInfoJSON, string(b)) - } - - actual := ContactInfo{} - err = json.Unmarshal([]byte(contactInfoJSON), &actual) - if assert.NoError(t, err) { - assert.EqualValues(t, contactInfo, actual) - } + assert.JSONMarshalAsT(t, contactInfoJSON, contactInfo) + assert.JSONUnmarshalAsT(t, contactInfo, contactInfoJSON) } diff --git a/debug.go b/debug.go index fc889f6d..fa52b0c7 100644 --- a/debug.go +++ b/debug.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -25,14 +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 -var ( - // specLogger is a debug logger for this package - specLogger *log.Logger -) +// specLogger is a debug logger for this package. +var specLogger *log.Logger //nolint:gochecknoglobals // package-level debug logger -func init() { +func init() { //nolint:gochecknoinits // initializes debug logger at package load debugOptions() } @@ -40,7 +27,7 @@ func debugOptions() { specLogger = log.New(os.Stdout, "spec:", log.LstdFlags) } -func debugLog(msg string, args ...interface{}) { +func debugLog(msg string, args ...any) { // A private, trivial trace logger, based on go-openapi/spec/expander.go:debugLog() if Debug { _, file1, pos1, _ := runtime.Caller(1) diff --git a/debug_test.go b/debug_test.go index 74c2a786..6f6547bf 100644 --- a/debug_test.go +++ b/debug_test.go @@ -1,34 +1,21 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "io/ioutil" "os" "sync" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) -var ( - logMutex = &sync.Mutex{} -) +var logMutex = &sync.Mutex{} //nolint:gochecknoglobals // test fixture func TestDebug(t *testing.T) { - tmpFile, _ := ioutil.TempFile("", "debug-test") + // usetesting linter disabled until https://github.com/golang/go/issues/71544 is fixed for windows + tmpFile, _ := os.CreateTemp("", "debug-test") //nolint:usetesting tmpName := tmpFile.Name() defer func() { Debug = false @@ -51,9 +38,9 @@ func TestDebug(t *testing.T) { Debug = false _ = tmpFile.Close() - flushed, _ := os.Open(tmpName) + flushed, _ := os.Open(tmpName) //nolint:gosec // test file, path is from os.CreateTemp 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 new file mode 100644 index 00000000..04eea357 --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Package spec exposes an object model for OpenAPIv2 specifications (swagger). +// +// The exposed data structures know how to serialize to and deserialize from JSON. +package spec diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md new file mode 100644 index 00000000..8fc6befb --- /dev/null +++ b/docs/MAINTAINERS.md @@ -0,0 +1,186 @@ +> [!NOTE] +> Comprehensive guide for maintainers covering repository structure, CI/CD workflows, release procedures, and development practices. +> Essential reading for anyone contributing to or maintaining this project. + +## Repo structure + +This project is organized as a repo with a single go module. + +## 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 + * All tests completed +* Auto-merge enabled (used for dependabot updates and other auto-merged PR's, e.g. contributors update) + +## Continuous Integration + +### Code Quality checks + +* meta-linter: [golangci-lint][golangci-url] +* linter config: [`.golangci.yml`][linter-config] (see our [posture][style-doc] on linters) +* Code quality assessment: [CodeFactor][codefactor-url] +* Code quality badges + * [go report card][gocard-url] + * [CodeFactor][codefactor-url] + +> **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 across `go-openapi` use our fork of `stretchr/spec` (this repo): `github.com/go-openapi/spec`. +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`][dependabot-config] + + 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 updates + + 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). + + > This means that our projects always have a 6 months lag to enforce new features from the go compiler. + > + > However, new features of go may be used with a "go:build" tag: this allows users of the newer + > version to benefit the new feature while users still running with `oldstable` use another version + > that still builds. + +* contributors + * a [`CONTRIBUTORS.md`][contributors-doc] 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 + +**For single module repos:** + +A bump release workflow can be triggered from the github actions UI to cut a release with a few clicks. + +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: the `.cliff.toml` is defined as a share configuration on + remote repo [`ci-workflows/.cliff.toml`][remote-cliff-config] + +Commits from maintainers are preferably PGP-signed. + +Tags are preferably PGP-signed. + +We want our releases to show as "verified" on github. + +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". + +**For mono-repos with multiple modules:** + +The release process is slightly different because we need to update cross-module dependencies +before pushing a tag. + +A bump release workflow (mono-repo) can be triggered from the github actions UI to cut a release with a few clicks. + +It works with the same input as the one for single module repos, and first creates a PR (auto-merged) +that updates the different go.mod files _before_ pushing the desired git tag. + +Commits and tags pushed by the workflow bot are PGP-signed ("go-openapi[bot]"). + +## Other files + +Standard documentation: + +* [CONTRIBUTING.md][contributing-doc] guidelines +* [DCO.md][dco-doc] terms for first-time contributors to read +* [CODE_OF_CONDUCT.md][coc-doc] +* [SECURITY.md][security-doc] policy: how to report vulnerabilities privately +* [LICENSE][license-doc] terms + + + +Reference documentation (released): + +* [pkg.go.dev (fka godoc)][godoc-url] + + +[linter-config]: https://github.com/go-openapi/spec/blob/master/.golangci.yml +[remote-cliff-config]: https://github.com/go-openapi/ci-workflows/blob/master/.cliff.toml +[dependabot-config]: https://github.com/go-openapi/spec/blob/master/.github/dependabot.yaml +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/spec +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/spec +[golangci-url]: https://golangci-lint.run/ +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/spec +[contributors-doc]: ../CONTRIBUTORS.md +[contributing-doc]: ../.github/CONTRIBUTING.md +[dco-doc]: ../.github/DCO.md +[style-doc]: STYLE.md +[coc-doc]: ../CODE_OF_CONDUCT.md +[security-doc]: ../SECURITY.md +[license-doc]: ../LICENSE + diff --git a/docs/STYLE.md b/docs/STYLE.md new file mode 100644 index 00000000..46f46cef --- /dev/null +++ b/docs/STYLE.md @@ -0,0 +1,117 @@ +# 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 and linting. +> +> 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 together 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 [the linter's configuration reference][golangci-doc] for additional reference. + +This configuration is essentially the same across all `go-openapi` projects. + +Some projects may require slightly different settings. + +## 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. + +### Our approach: evaluate, don't consume blindly + +As early adopters of `golangci-lint` (and its predecessors), we've watched linting orthodoxy +shift back and forth over the years. Patterns that were idiomatic one year get flagged the next; +rules that seemed reasonable in isolation produce noise at scale. Conversations with maintainers +of other large Go projects confirmed what our own experience taught us: +**the default linter set is a starting point, not a prescription**. + +Our stance is deliberate: + +- **Start from `default: all`**, then consciously disable what doesn't earn its keep. + This forces us to evaluate every linter and articulate why we reject it — the disabled list + is a design rationale, not technical debt. +- **Tune thresholds rather than disable** when a linter's principle is sound but its defaults + are too aggressive for a mature codebase. +- **Require justification for every `//nolint`** directive. Each one must carry an inline comment + explaining why it's there. +- **Prefer disabling a linter over scattering `//nolint`** across the codebase. If a linter + produces systematic false positives on patterns we use intentionally, the linter goes — + not our code. +- **Keep the configuration consistent** across all `go-openapi` repositories. Per-repo + divergence is a maintenance tax we don't want to pay. + +We enable all linters published by `golangci-lint` by default, then disable a few ones. + +Here are the reasons why they are disabled (update: Feb. 2026, `golangci-lint v2.8.0`). + +```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 + - thelper # too many false positives on test case factories returning func(*testing.T). See note below + - 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. + +**Relaxed linter settings** + +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. + +[golangci-yml]: https://github.com/go-openapi/spec/blob/master/.golangci.yml +[golangci-doc]: https://golangci-lint.run/docs/linters/configuration/ diff --git a/embed.go b/embed.go new file mode 100644 index 00000000..0d0b6999 --- /dev/null +++ b/embed.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "embed" + "path" +) + +//go:embed schemas/*.json schemas/*/*.json +var assets embed.FS + +func jsonschemaDraft04JSONBytes() ([]byte, error) { + return assets.ReadFile(path.Join("schemas", "jsonschema-draft-04.json")) +} + +func v2SchemaJSONBytes() ([]byte, error) { + return assets.ReadFile(path.Join("schemas", "v2", "schema.json")) +} diff --git a/errors.go b/errors.go index 6992c7ba..eaca01cc 100644 --- a/errors.go +++ b/errors.go @@ -1,19 +1,25 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import "errors" -// Error codes +// Error codes. var ( - // ErrUnknownTypeForReference indicates that a resolved reference was found in an unsupported container type + // ErrUnknownTypeForReference indicates that a resolved reference was found in an unsupported container type. ErrUnknownTypeForReference = errors.New("unknown type for the resolved reference") - // ErrResolveRefNeedsAPointer indicates that a $ref target must be a valid JSON pointer + // ErrResolveRefNeedsAPointer indicates that a $ref target must be a valid JSON pointer. ErrResolveRefNeedsAPointer = errors.New("resolve ref: target needs to be a pointer") // ErrDerefUnsupportedType indicates that a resolved reference was found in an unsupported container type. - // At the moment, $ref are supported only inside: schemas, parameters, responses, path items + // At the moment, $ref are supported only inside: schemas, parameters, responses, path items. ErrDerefUnsupportedType = errors.New("deref: unsupported type") - // ErrExpandUnsupportedType indicates that $ref expansion is attempted on some invalid type + // 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") + + // ErrSpec is an error raised by the spec package. + ErrSpec = errors.New("spec error") ) diff --git a/expander.go b/expander.go index d4ea889d..f9c2fa32 100644 --- a/expander.go +++ b/expander.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,6 +8,8 @@ import ( "fmt" ) +const smallPrealloc = 10 + // 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. @@ -27,7 +18,6 @@ import ( // 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. -// 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 @@ -48,7 +38,7 @@ func optionsOrDefault(opts *ExpandOptions) *ExpandOptions { return &ExpandOptions{} } -// ExpandSpec expands the references in a swagger spec +// ExpandSpec expands the references in a swagger spec. func ExpandSpec(spec *Swagger, options *ExpandOptions) error { options = optionsOrDefault(options) resolver := defaultSchemaLoader(spec, options, nil, nil) @@ -57,8 +47,8 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { if !options.SkipSchemas { for key, definition := range spec.Definitions { - parentRefs := make([]string, 0, 10) - parentRefs = append(parentRefs, fmt.Sprintf("#/definitions/%s", key)) + parentRefs := make([]string, 0, smallPrealloc) + parentRefs = append(parentRefs, "#/definitions/"+key) def, err := expandSchema(definition, parentRefs, resolver, specBasePath) if resolver.shouldStopOnError(err) { @@ -102,16 +92,22 @@ 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 -// -// Setting the cache is optional and this parameter may safely be left to nil. -func baseForRoot(root interface{}, cache ResolutionCache) string { +// for further $ref resolution. +func baseForRoot(root any, cache ResolutionCache) string { + // cache the root document to resolve $ref's + normalizedBase := normalizeBase(rootBase) + if root == nil { - return "" + // ensure that we never leave a nil root: always cache the root base pseudo-document + cachedRoot, found := cache.Get(normalizedBase) + if found && cachedRoot != nil { + // the cache is already preloaded with a root + return normalizedBase + } + + root = map[string]any{} } - // cache the root document to resolve $ref's - normalizedBase := normalizeBase(rootBase) cache.Set(normalizedBase, root) return normalizedBase @@ -125,7 +121,7 @@ func baseForRoot(root interface{}, cache ResolutionCache) string { // (use ExpandSchemaWithBasePath to resolve external references). // // Setting the cache is optional and this parameter may safely be left to nil. -func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error { +func ExpandSchema(schema *Schema, root any, cache ResolutionCache) error { cache = cacheOrDefault(cache) if root == nil { root = schema @@ -155,7 +151,7 @@ func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *Expan resolver := defaultSchemaLoader(nil, opts, cache, nil) - parentRefs := make([]string, 0, 10) + parentRefs := make([]string, 0, smallPrealloc) s, err := expandSchema(*schema, parentRefs, resolver, opts.RelativeBase) if err != nil { return err @@ -194,6 +190,7 @@ 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 target.Ref.String() == "" && target.Ref.IsRoot() { newRef := normalizeRef(&target.Ref, basePath) @@ -208,7 +205,19 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } if target.Ref.String() != "" { - return expandSchemaRef(target, parentRefs, resolver, basePath) + if !resolver.options.SkipSchemas { + return expandSchemaRef(target, parentRefs, resolver, basePath) + } + + // when "expand" with SkipSchema, we just rebase the existing $ref without replacing + // the full schema. + rebasedRef, err := NewRef(normalizeURI(target.Ref.String(), basePath)) + if err != nil { + return nil, err + } + target.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) + + return &target, nil } for k := range target.Definitions { @@ -369,7 +378,7 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) return nil } - parentRefs := make([]string, 0, 10) + parentRefs := make([]string, 0, smallPrealloc) if err := resolver.deref(pathItem, parentRefs, basePath); resolver.shouldStopOnError(err) { return err } @@ -444,7 +453,7 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err // (use ExpandResponse to resolve external references). // // Setting the cache is optional and this parameter may safely be left to nil. -func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error { +func ExpandResponseWithRoot(response *Response, root any, cache ResolutionCache) error { cache = cacheOrDefault(cache) opts := &ExpandOptions{ RelativeBase: baseForRoot(root, cache), @@ -456,7 +465,7 @@ func ExpandResponseWithRoot(response *Response, root interface{}, cache Resoluti // 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, @@ -470,7 +479,7 @@ 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 interface{}, cache ResolutionCache) error { +func ExpandParameterWithRoot(parameter *Parameter, root any, cache ResolutionCache) error { cache = cacheOrDefault(cache) opts := &ExpandOptions{ @@ -483,7 +492,7 @@ func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache Resol // 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, @@ -493,7 +502,7 @@ func ExpandParameter(parameter *Parameter, basePath string) error { return expandParameterOrResponse(parameter, resolver, opts.RelativeBase) } -func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { +func getRefAndSchema(input any) (*Ref, *Schema, error) { var ( ref *Ref sch *Schema @@ -519,22 +528,29 @@ func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { return ref, sch, nil } -func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePath string) error { - ref, _, err := getRefAndSchema(input) +func expandParameterOrResponse(input any, resolver *schemaLoader, basePath string) error { + ref, sch, err := getRefAndSchema(input) if err != nil { return err } - if ref == nil { + if ref == nil && sch == nil { // nothing to do return nil } - parentRefs := make([]string, 0, 10) - if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { - return err + parentRefs := make([]string, 0, smallPrealloc) + if ref != nil { + // dereference this $ref + if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { + return err + } + + ref, sch, _ = getRefAndSchema(input) + if ref == nil { + ref = &Ref{} // empty ref + } } - ref, sch, _ := getRefAndSchema(input) if ref.String() != "" { transitiveResolver := resolver.transitiveResolver(basePath, *ref) basePath = resolver.updateBasePath(transitiveResolver, basePath) @@ -546,47 +562,39 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa if ref != nil { *ref = Ref{} } + 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 } - switch { - case resolver.isCircular(&rebasedRef, basePath, parentRefs...): + if resolver.isCircular(&rebasedRef, basePath, parentRefs...) { // this is a circular $ref: stop expansion if !resolver.options.AbsoluteCircularRef { sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) } else { sch.Ref = rebasedRef } - case !resolver.options.SkipSchemas: - // schema expanded to a $ref in another root - sch.Ref = rebasedRef - debugLog("rebased to: %s", sch.Ref.String()) - default: - // skip schema expansion but rebase $ref to schema - sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) } } + // $ref expansion or rebasing is performed by expandSchema below if ref != nil { *ref = Ref{} } // expand schema - if !resolver.options.SkipSchemas { - s, err := expandSchema(*sch, parentRefs, resolver, basePath) - if resolver.shouldStopOnError(err) { - return err - } - if s == nil { - // guard for when continuing on error - return nil - } + // yes, we do it even if options.SkipSchema is true: we have to go down that rabbit hole and rebase nested $ref) + s, err := expandSchema(*sch, parentRefs, resolver, basePath) + if resolver.shouldStopOnError(err) { + return err + } + + if s != nil { // guard for when continuing on error *sch = *s } diff --git a/expander_test.go b/expander_test.go index 471b5679..f7be753b 100644 --- a/expander_test.go +++ b/expander_test.go @@ -1,22 +1,13 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( + "embed" "encoding/json" - "io/ioutil" + "fmt" + "io" "log" "net/http" "net/http/httptest" @@ -24,8 +15,8 @@ import ( "path/filepath" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) const ( @@ -36,18 +27,80 @@ const ( extraRefFixture = "fixtures/expansion/extraRef.json" ) +//nolint:gochecknoglobals // it's okay to have embedded test fixtures as globals var ( - // PetStoreJSONMessage json raw message for Petstore20 - PetStoreJSONMessage = json.RawMessage([]byte(PetStore20)) - specs = filepath.Join("fixtures", "specs") + //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.RawMessage + expectedExtraRef []byte + expectedPathItem []byte + + specs = filepath.Join("fixtures", "specs") ) +func init() { //nolint:gochecknoinits // it's okay to load embedded fixtures in init(). + // load embedded fixtures + + var err error + PetStore20, err = fixtureAssets.ReadFile("fixtures/expansion/petstore2.0.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } + PetStoreJSONMessage = json.RawMessage(PetStore20) + + expectedExtraRef, err = fixtureAssets.ReadFile("fixtures/expansion/expectedExtraRef.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } + + expectedPathItem, err = fixtureAssets.ReadFile("fixtures/expansion/expectedPathItem.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } +} + +func TestExpand_Issue148(t *testing.T) { + fp := filepath.Join("fixtures", "bugs", "schema-148.json") + b, err := jsonDoc(fp) + require.NoError(t, err) + + assertAdditionalProps := func(sp Swagger) func(*testing.T) { + return func(t *testing.T) { + require.Len(t, sp.Definitions, 2) + + require.MapContainsT(t, sp.Definitions, "empty") + empty := sp.Definitions["empty"] + require.NotNil(t, empty.AdditionalProperties) + require.NotNil(t, empty.AdditionalProperties.Schema) + require.TrueT(t, empty.AdditionalProperties.Allows) + + require.MapContainsT(t, sp.Definitions, "false") + additionalIsFalse := sp.Definitions["false"] + require.NotNil(t, additionalIsFalse.AdditionalProperties) + require.Nil(t, additionalIsFalse.AdditionalProperties.Schema) + require.FalseT(t, additionalIsFalse.AdditionalProperties.Allows) + } + } + + var sp Swagger + require.NoError(t, json.Unmarshal(b, &sp)) + t.Run("check additional properties", assertAdditionalProps(sp)) + + require.NoError(t, ExpandSpec(&sp, nil)) + t.Run("check additional properties after expansion", assertAdditionalProps(sp)) +} + func TestExpand_KnownRef(t *testing.T) { // json schema draft 4 meta schema is embedded by default: it resolves without setup 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) @@ -73,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) } @@ -98,7 +151,6 @@ func TestExpand_EmptySpec(t *testing.T) { } func TestExpand_Spec(t *testing.T) { - // expansion of a rich spec specPath := filepath.Join("fixtures", "expansion", "all-the-things.json") specDoc, err := jsonDoc(specPath) @@ -161,7 +213,7 @@ func TestExpand_InternalResponse(t *testing.T) { jazon := asJSON(t, expectedPet) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "description": "pet response", "schema": { "required": [ @@ -200,7 +252,7 @@ func TestExpand_InternalResponse(t *testing.T) { jazon = asJSON(t, successResponse) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "$ref": "#/responses/anotherPet" }`, jazon) @@ -349,11 +401,11 @@ func TestExpand_ContinueOnError(t *testing.T) { specPath := filepath.Join("fixtures", "expansion", "missingRef.json") defer log.SetOutput(os.Stdout) - log.SetOutput(ioutil.Discard) + log.SetOutput(io.Discard) // missing $ref in spec missingRefDoc, err := jsonDoc(specPath) - assert.NoError(t, err) + require.NoError(t, err) testCase := struct { Input *Swagger `json:"input"` @@ -367,7 +419,7 @@ func TestExpand_ContinueOnError(t *testing.T) { } require.NoError(t, ExpandSpec(testCase.Input, opts)) - assert.Equal(t, testCase.Input, testCase.Expected, "Should continue expanding spec when a definition can't be found.") + assert.Equal(t, testCase.Expected, testCase.Input, "Should continue expanding spec when a definition can't be found.") // missing $ref in items doc, err := jsonDoc("fixtures/expansion/missingItemRef.json") @@ -414,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) @@ -697,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) @@ -749,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) @@ -785,25 +836,24 @@ func resolutionContextServer() *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if req.URL.Path == "/resolution.json" { - b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution.json")) - var ctnt map[string]interface{} + b, _ := os.ReadFile(filepath.Join(specs, "resolution.json")) + var ctnt map[string]any _ = json.Unmarshal(b, &ctnt) ctnt["id"] = servedAt rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) + rw.WriteHeader(http.StatusOK) bb, _ := json.Marshal(ctnt) _, _ = rw.Write(bb) return } if req.URL.Path == "/resolution2.json" { - b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution2.json")) - var ctnt map[string]interface{} + b, _ := os.ReadFile(filepath.Join(specs, "resolution2.json")) + var ctnt map[string]any _ = json.Unmarshal(b, &ctnt) ctnt["id"] = servedAt rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) bb, _ := json.Marshal(ctnt) _, _ = rw.Write(bb) return @@ -811,8 +861,7 @@ func resolutionContextServer() *httptest.Server { if req.URL.Path == "/boolProp.json" { rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ + b, _ := json.Marshal(map[string]any{ "type": "boolean", }) _, _ = rw.Write(b) @@ -821,8 +870,7 @@ func resolutionContextServer() *httptest.Server { if req.URL.Path == "/deeper/stringProp.json" { rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ + b, _ := json.Marshal(map[string]any{ "type": "string", }) _, _ = rw.Write(b) @@ -831,10 +879,9 @@ func resolutionContextServer() *httptest.Server { if req.URL.Path == "/deeper/arrayProp.json" { rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ + b, _ := json.Marshal(map[string]any{ "type": "array", - "items": map[string]interface{}{ + "items": map[string]any{ "type": "file", }, }) @@ -869,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) @@ -900,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) @@ -909,7 +956,7 @@ func TestExpand_RemoteRefWithNestedResolutionContextWithFragment(t *testing.T) { func TestExpand_TransitiveRefs(t *testing.T) { basePath := filepath.Join(specs, "todos.json") - rawSpec, err := ioutil.ReadFile(basePath) + rawSpec, err := os.ReadFile(basePath) require.NoError(t, err) var spec *Swagger @@ -921,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 @@ -981,375 +1028,57 @@ func expandRootWithID(t testing.TB, root *Swagger, testcase string) { func TestExpand_PathItem(t *testing.T) { jazon, _ := expandThisOrDieTrying(t, pathItemsFixture) - assert.JSONEq(t, `{ - "swagger": "2.0", - "info": { - "title": "PathItems refs", - "version": "1.0" - }, - "paths": { - "/todos": { - "get": { - "responses": { - "200": { - "description": "List Todos", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "error" - } - } - } - } - } - }`, jazon) + assert.JSONEqT(t, string(expectedPathItem), jazon) } func TestExpand_ExtraItems(t *testing.T) { jazon, _ := expandThisOrDieTrying(t, extraRefFixture) - assert.JSONEq(t, `{ - "schemes": [ - "http" - ], - "swagger": "2.0", - "info": { - "title": "Supported, but non Swagger 20 compliant $ref constructs", - "version": "2.1.0" - }, - "host": "item.com", - "basePath": "/extraRefs", - "paths": { - "/employees": { - "get": { - "summary": "List Employee Types", - "operationId": "LIST-Employees", - "parameters": [ - { - "description": "unsupported $ref in simple param", - "type": "array", - "items": { - "$ref": "#/definitions/arrayType" - }, - "name": "myQueryParam", - "in": "query" - } - ], - "responses": { - "200": { - "description": "unsupported $ref in header", - "schema": { - "type": "string" - }, - "headers": { - "X-header": { - "type": "array", - "items": { - "$ref": "#/definitions/headerType" - } - } - } - } - } - } - } - }, - "definitions": { - "arrayType": { - "type": "integer", - "format": "int32" - }, - "headerType": { - "type": "string", - "format": "uuid" - } - } - }`, jazon) + assert.JSONEqT(t, string(expectedExtraRef), jazon) } -// PetStore20 json doc for swagger 2.0 pet store -const PetStore20 = `{ - "swagger": "2.0", - "info": { - "version": "1.0.0", - "title": "Swagger Petstore", - "contact": { - "name": "Wordnik API Team", - "url": "http://developer.wordnik.com" - }, - "license": { - "name": "Creative Commons 4.0 International", - "url": "http://creativecommons.org/licenses/by/4.0/" - } - }, - "host": "petstore.swagger.wordnik.com", - "basePath": "/api", - "schemes": [ - "http" - ], - "paths": { - "/pets": { - "get": { - "security": [ - { - "basic": [] - } - ], - "tags": [ "Pet Operations" ], - "operationId": "getAllPets", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "The status to filter by", - "type": "string" - }, - { - "name": "limit", - "in": "query", - "description": "The maximum number of results to return", - "type": "integer", - "format": "int64" - } - ], - "summary": "Finds all pets in the system", - "responses": { - "200": { - "description": "Pet response", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "default": { - "description": "Unexpected error", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - }, - "post": { - "security": [ - { - "basic": [] - } - ], - "tags": [ "Pet Operations" ], - "operationId": "createPet", - "summary": "Creates a new pet", - "consumes": ["application/x-yaml"], - "produces": ["application/x-yaml"], - "parameters": [ - { - "name": "pet", - "in": "body", - "description": "The Pet to create", - "required": true, - "schema": { - "$ref": "#/definitions/newPet" - } - } - ], - "responses": { - "200": { - "description": "Created Pet response", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "default": { - "description": "Unexpected error", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - } - }, - "/pets/{id}": { - "delete": { - "security": [ - { - "apiKey": [] - } - ], - "description": "Deletes the Pet by id", - "operationId": "deletePet", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of pet to delete", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "204": { - "description": "pet deleted" - }, - "default": { - "description": "unexpected error", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - }, - "get": { - "tags": [ "Pet Operations" ], - "operationId": "getPetById", - "summary": "Finds the pet by id", - "responses": { - "200": { - "description": "Pet response", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "default": { - "description": "Unexpected error", - "schema": { - "$ref": "#/definitions/Error" - } - } - } - }, - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of pet", - "required": true, - "type": "integer", - "format": "int64" - } - ] - } - }, - "definitions": { - "Category": { - "id": "Category", - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "Pet": { - "id": "Pet", - "properties": { - "category": { - "$ref": "#/definitions/Category" - }, - "id": { - "description": "unique identifier for the pet", - "format": "int64", - "maximum": 100.0, - "minimum": 0.0, - "type": "integer" - }, - "name": { - "type": "string" - }, - "photoUrls": { - "items": { - "type": "string" - }, - "type": "array" - }, - "status": { - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ], - "type": "string" - }, - "tags": { - "items": { - "$ref": "#/definitions/Tag" - }, - "type": "array" - } - }, - "required": [ - "id", - "name" - ] - }, - "newPet": { - "anyOf": [ - { - "$ref": "#/definitions/Pet" - }, - { - "required": [ - "name" - ] - } - ] - }, - "Tag": { - "id": "Tag", - "properties": { - "id": { - "format": "int64", - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "Error": { - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - }, - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml", - "text/plain", - "text/html" - ], - "securityDefinitions": { - "basic": { - "type": "basic" - }, - "apiKey": { - "type": "apiKey", - "in": "header", - "name": "X-API-KEY" - } - } +func TestExpand_Issue145(t *testing.T) { + cwd, err := os.Getwd() + require.NoError(t, err) + pseudoRoot := normalizeBase(filepath.Join(cwd, rootBase)) + + // assert the internal behavior of baseForRoot() + t.Run("with nil root, empty cache", func(t *testing.T) { + cache := defaultResolutionCache() + require.EqualT(t, pseudoRoot, baseForRoot(nil, cache)) + + t.Run("empty root is cached", func(t *testing.T) { + value, ok := cache.Get(pseudoRoot) + require.TrueT(t, ok) // found in cache + asMap, ok := value.(map[string]any) + require.TrueT(t, ok) + require.Empty(t, asMap) + }) + }) + + t.Run("with non-nil root, empty cache", func(t *testing.T) { + cache := defaultResolutionCache() + 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.TrueT(t, ok) // found in cache + asMap, ok := value.(map[string]any) + 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.EqualT(t, pseudoRoot, baseForRoot(nil, cache)) + + t.Run("non-empty root is kept", func(t *testing.T) { + value, ok := cache.Get(pseudoRoot) + require.TrueT(t, ok) // found in cache + asMap, ok := value.(map[string]any) + require.TrueT(t, ok) + require.MapContainsT(t, asMap, "key") + require.Equal(t, "arbitrary", asMap["key"]) + }) + }) + }) } -` diff --git a/external_docs.go b/external_docs.go index 88add91b..17b8efbf 100644 --- a/external_docs.go +++ b/external_docs.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec diff --git a/external_docs_test.go b/external_docs_test.go index 91845277..3e8302f5 100644 --- a/external_docs_test.go +++ b/external_docs_test.go @@ -1,29 +1,21 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "testing" + + _ "github.com/go-openapi/testify/enable/yaml/v2" + "github.com/go-openapi/testify/v2/assert" ) func TestIntegrationExternalDocs(t *testing.T) { - var extDocs = ExternalDocumentation{Description: "the name", URL: "the url"} + 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/bugs/145/Program Files (x86)/AppName/ref.json b/fixtures/bugs/145/Program Files (x86)/AppName/ref.json new file mode 100644 index 00000000..10a8cb70 --- /dev/null +++ b/fixtures/bugs/145/Program Files (x86)/AppName/ref.json @@ -0,0 +1,17 @@ +{ +"definitions": { + "todo-partial": { + "title": "Todo Partial", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "completed": { + "type": ["boolean", "null"] + } + }, + "required": ["name", "completed"] + } + } +} diff --git a/fixtures/bugs/145/Program Files (x86)/AppName/todos.common.json b/fixtures/bugs/145/Program Files (x86)/AppName/todos.common.json new file mode 100644 index 00000000..1c43908a --- /dev/null +++ b/fixtures/bugs/145/Program Files (x86)/AppName/todos.common.json @@ -0,0 +1,103 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0", + "title": "To-do Demo", + "description": + "### Notes:\n\nThis OAS2 (Swagger 2) specification defines common models and responses, that other specifications may reference.\n\nFor example, check out the user poperty in the main.oas2 todo-partial model - it references the user model in this specification!\n\nLikewise, the main.oas2 operations reference the shared error responses in this common specification.", + "contact": { + "name": "Stoplight", + "url": "https://stoplight.io" + }, + "license": { + "name": "MIT" + } + }, + "host": "example.com", + "securityDefinitions": {}, + "paths": {}, + "responses": { + "401": { + "description": "", + "schema": { + "$ref": "#/definitions/error-response" + }, + "examples": { + "application/json": { + "status": "401", + "error": "Not Authorized" + } + } + }, + "403": { + "description": "", + "schema": { + "$ref": "#/definitions/error-response" + }, + "examples": { + "application/json": { + "status": "403", + "error": "Forbbiden" + } + } + }, + "404": { + "description": "", + "schema": { + "$ref": "#/definitions/error-response" + }, + "examples": { + "application/json": { + "status": "404", + "error": "Not Found" + } + } + }, + "500": { + "description": "", + "schema": { + "$ref": "#/definitions/error-response" + }, + "examples": { + "application/json": { + "status": "500", + "error": "Server Error" + } + } + } + }, + "definitions": { + "user": { + "title": "User", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The user's full name." + }, + "age": { + "type": "number", + "minimum": 0, + "maximum": 150 + }, + "error": { + "$ref": "#/definitions/error-response" + } + }, + "required": ["name", "age"] + }, + "error-response": { + "type": "object", + "title": "Error Response", + "properties": { + "status": { + "type": "string" + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"] + } + } +} diff --git a/fixtures/bugs/145/Program Files (x86)/AppName/todos.json b/fixtures/bugs/145/Program Files (x86)/AppName/todos.json new file mode 100644 index 00000000..9c5072ee --- /dev/null +++ b/fixtures/bugs/145/Program Files (x86)/AppName/todos.json @@ -0,0 +1,336 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0", + "title": "To-do Demo", + "description": "This OAS2 (Swagger 2) file represents a real API that lives at http://todos.stoplight.io.\n\nFor authentication information, click the apikey security scheme in the editor sidebar.", + "contact": { + "name": "Stoplight", + "url": "https://stoplight.io" + }, + "license": { + "name": "MIT" + } + }, + "host": "todos.stoplight.io", + "schemes": ["http"], + "consumes": ["application/json"], + "produces": ["application/json"], + "securityDefinitions": { + "Basic": { + "type": "basic" + }, + "API Key": { + "type": "apiKey", + "name": "apikey", + "in": "query" + } + }, + "paths": { + "/todos/{todoId}": { + "parameters": [{ + "name": "todoId", + "in": "path", + "required": true, + "type": "string" + }], + "get": { + "operationId": "GET_todo", + "summary": "Get Todo", + "tags": ["Todos"], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/todo-full" + }, + "examples": { + "application/json": { + "id": 1, + "name": "get food", + "completed": false, + "completed_at": "1955-04-23T13:22:52.685Z", + "created_at": "1994-11-05T03:26:51.471Z", + "updated_at": "1989-07-29T11:30:06.701Z" + }, + "/todos/foobar": "{\n\t\"foo\": \"bar\"\n}\n", + "/todos/chores": { + "id": 9000, + "name": "Do Chores", + "completed": false, + "created_at": "2014-08-28T14:14:28.494Z", + "updated_at": "2014-08-28T14:14:28.494Z" + }, + "new": { + "name": "esse qui proident labore", + "completed": null, + "id": 920778, + "completed_at": "2014-01-07T07:49:55.123Z", + "created_at": "1948-04-21T12:04:21.282Z", + "updated_at": "1951-12-19T11:10:34.039Z", + "user": { + "name": "irure deserunt fugiat", + "age": 121.45395681110494 + }, + "float": -47990796.228164576 + } + } + }, + "404": { + "$ref": "./todos.common.json#/responses/404" + }, + "500": { + "$ref": "./todos.common.json#/responses/500" + } + }, + "parameters": [{ + "in": "query", + "name": "", + "type": "string" + }] + }, + "put": { + "operationId": "PUT_todos", + "summary": "Update Todo", + "tags": ["Todos"], + "parameters": [{ + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/todo-partial", + "example": { + "name": "my todo's new name", + "completed": false + } + } + }], + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/todo-full" + }, + "examples": { + "application/json": { + "id": 9000, + "name": "It's Over 9000!!!", + "completed": true, + "completed_at": null, + "created_at": "2014-08-28T14:14:28.494Z", + "updated_at": "2015-08-28T14:14:28.494Z" + } + } + }, + "401": { + "$ref": "./todos.common.json#/responses/401" + }, + "404": { + "$ref": "./todos.common.json#/responses/404" + }, + "500": { + "$ref": "./todos.common.json#/responses/500" + } + }, + "security": [{ + "Basic": [] + }, + { + "API Key": [] + } + ] + }, + "delete": { + "operationId": "DELETE_todo", + "summary": "Delete Todo", + "tags": ["Todos"], + "responses": { + "204": { + "description": "" + }, + "401": { + "$ref": "./todos.common.json#/responses/401" + }, + "404": { + "$ref": "./todos.common.json#/responses/404" + }, + "500": { + "$ref": "./todos.common.json#/responses/500" + } + }, + "security": [{ + "Basic": [] + }, + { + "API Key": [] + } + ] + } + }, + "/todos": { + "post": { + "operationId": "POST_todos", + "summary": "Create Todo", + "tags": ["Todos"], + "parameters": [{ + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/todo-partial", + "example": { + "name": "my todo's name", + "completed": false + } + } + }], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/todo-full" + }, + "examples": { + "application/json": { + "id": 9000, + "name": "It's Over 9000!!!", + "completed": null, + "completed_at": null, + "created_at": "2014-08-28T14:14:28.494Z", + "updated_at": "2014-08-28T14:14:28.494Z" + }, + "/todos/chores": { + "id": 9000, + "name": "Do Chores", + "completed": false, + "created_at": "2014-08-28T14:14:28.494Z", + "updated_at": "2014-08-28T14:14:28.494Z" + } + } + }, + "401": { + "$ref": "./todos.common.json#/responses/401" + }, + "500": { + "$ref": "./todos.common.json#/responses/500" + } + }, + "security": [{ + "API Key": [] + }, + { + "Basic": [] + } + ], + "description": "This creates a Todo object.\n\nTesting `inline code`." + }, + "get": { + "operationId": "GET_todos", + "summary": "List Todos", + "tags": ["Todos"], + "parameters": [{ + "$ref": "#/parameters/limit" + }, + { + "$ref": "#/parameters/skip" + } + ], + "responses": { + "200": { + "description": "", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/todo-full" + } + }, + "examples": { + "application/json": [{ + "id": 1, + "name": "design the thingz", + "completed": true + }, + { + "id": 2, + "name": "mock the thingz", + "completed": true + }, + { + "id": 3, + "name": "code the thingz", + "completed": false + } + ], + "empty": [] + }, + "headers": { + "foo": { + "type": "string", + "default": "bar" + } + } + }, + "500": { + "$ref": "./todos.common.json#/responses/500" + } + }, + "description": "​" + } + } + }, + "parameters": { + "limit": { + "name": "limit", + "in": "query", + "description": "This is how it works.", + "required": false, + "type": "integer", + "maximum": 100 + }, + "skip": { + "name": "skip", + "in": "query", + "required": false, + "type": "string" + } + }, + "definitions": { + "todo-partial": { + "$ref": "ref.json#/definitions/todo-partial" + }, + "todo-full": { + "title": "Todo Full", + "allOf": [{ + "$ref": "#/definitions/todo-partial" + }, + { + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": 0, + "maximum": 1000000 + }, + "completed_at": { + "type": ["string", "null"], + "format": "date-time" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "user": { + "$ref": "./todos.common.json#/definitions/user" + } + }, + "required": ["id", "user"] + } + ] + } + }, + "tags": [{ + "name": "Todos" + }] +} diff --git a/fixtures/bugs/2743/not-working/minimal.yaml b/fixtures/bugs/2743/not-working/minimal.yaml new file mode 100644 index 00000000..2c467739 --- /dev/null +++ b/fixtures/bugs/2743/not-working/minimal.yaml @@ -0,0 +1,7 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /bar: + $ref: 'swagger/paths/bar.yml' diff --git a/fixtures/bugs/2743/not-working/spec.yaml b/fixtures/bugs/2743/not-working/spec.yaml new file mode 100644 index 00000000..92551731 --- /dev/null +++ b/fixtures/bugs/2743/not-working/spec.yaml @@ -0,0 +1,11 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /foo: + $ref: 'swagger/paths/foo.yml' + /bar: + $ref: 'swagger/paths/bar.yml' + /nested: + $ref: 'swagger/paths/nested.yml#/response' diff --git a/fixtures/bugs/2743/not-working/swagger/definitions.yml b/fixtures/bugs/2743/not-working/swagger/definitions.yml new file mode 100644 index 00000000..438d6ee4 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/definitions.yml @@ -0,0 +1,9 @@ +ErrorPayload: + title: Error Payload + required: + - errors + properties: + errors: + type: array + items: + $ref: './items.yml#/ErrorDetailsItem' \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/items.yml b/fixtures/bugs/2743/not-working/swagger/items.yml new file mode 100644 index 00000000..e4a050c5 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/items.yml @@ -0,0 +1,15 @@ +ErrorDetailsItem: + title: Error details item + description: Represents an item of the list of details of an error. + required: + - message + - code + properties: + message: + type: string + code: + type: string + details: + type: array + items: + type: string \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/paths/bar.yml b/fixtures/bugs/2743/not-working/swagger/paths/bar.yml new file mode 100644 index 00000000..c8135792 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/bar.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + schema: + type: array + items: + $ref: '../user/index.yml#/User' ## this doesn't work \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/paths/foo.yml b/fixtures/bugs/2743/not-working/swagger/paths/foo.yml new file mode 100644 index 00000000..a5c0de12 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/foo.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + 500: + description: OK + schema: + $ref: '../definitions.yml#/ErrorPayload' diff --git a/fixtures/bugs/2743/not-working/swagger/paths/index.yml b/fixtures/bugs/2743/not-working/swagger/paths/index.yml new file mode 100644 index 00000000..7cdcdd43 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/index.yml @@ -0,0 +1,6 @@ +/foo: + $ref: ./foo.yml +/bar: + $ref: ./bar.yml +/nested: + $ref: ./nested.yml#/response diff --git a/fixtures/bugs/2743/not-working/swagger/paths/nested.yml b/fixtures/bugs/2743/not-working/swagger/paths/nested.yml new file mode 100644 index 00000000..5c7cf0d5 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/nested.yml @@ -0,0 +1,14 @@ +response: + get: + responses: + 200: + description: OK + schema: + $ref: '#/definitions/SameFileReference' + +definitions: + SameFileReference: + type: object + properties: + name: + type: string diff --git a/fixtures/bugs/2743/not-working/swagger/user/index.yml b/fixtures/bugs/2743/not-working/swagger/user/index.yml new file mode 100644 index 00000000..99c1c6f8 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/user/index.yml @@ -0,0 +1,2 @@ +User: + $ref: './model.yml' diff --git a/fixtures/bugs/2743/not-working/swagger/user/model.yml b/fixtures/bugs/2743/not-working/swagger/user/model.yml new file mode 100644 index 00000000..5cb91cda --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/user/model.yml @@ -0,0 +1,4 @@ +type: object +properties: + name: + type: string diff --git a/fixtures/bugs/2743/working/spec.yaml b/fixtures/bugs/2743/working/spec.yaml new file mode 100644 index 00000000..92551731 --- /dev/null +++ b/fixtures/bugs/2743/working/spec.yaml @@ -0,0 +1,11 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /foo: + $ref: 'swagger/paths/foo.yml' + /bar: + $ref: 'swagger/paths/bar.yml' + /nested: + $ref: 'swagger/paths/nested.yml#/response' diff --git a/fixtures/bugs/2743/working/swagger/definitions.yml b/fixtures/bugs/2743/working/swagger/definitions.yml new file mode 100644 index 00000000..438d6ee4 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/definitions.yml @@ -0,0 +1,9 @@ +ErrorPayload: + title: Error Payload + required: + - errors + properties: + errors: + type: array + items: + $ref: './items.yml#/ErrorDetailsItem' \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/items.yml b/fixtures/bugs/2743/working/swagger/items.yml new file mode 100644 index 00000000..e4a050c5 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/items.yml @@ -0,0 +1,15 @@ +ErrorDetailsItem: + title: Error details item + description: Represents an item of the list of details of an error. + required: + - message + - code + properties: + message: + type: string + code: + type: string + details: + type: array + items: + type: string \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/paths/bar.yml b/fixtures/bugs/2743/working/swagger/paths/bar.yml new file mode 100644 index 00000000..41dc76ac --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/bar.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + schema: + type: array + items: + $ref: './swagger/user/index.yml#/User' ## this works \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/paths/foo.yml b/fixtures/bugs/2743/working/swagger/paths/foo.yml new file mode 100644 index 00000000..a5c0de12 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/foo.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + 500: + description: OK + schema: + $ref: '../definitions.yml#/ErrorPayload' diff --git a/fixtures/bugs/2743/working/swagger/paths/index.yml b/fixtures/bugs/2743/working/swagger/paths/index.yml new file mode 100644 index 00000000..7cdcdd43 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/index.yml @@ -0,0 +1,6 @@ +/foo: + $ref: ./foo.yml +/bar: + $ref: ./bar.yml +/nested: + $ref: ./nested.yml#/response diff --git a/fixtures/bugs/2743/working/swagger/paths/nested.yml b/fixtures/bugs/2743/working/swagger/paths/nested.yml new file mode 100644 index 00000000..5c7cf0d5 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/nested.yml @@ -0,0 +1,14 @@ +response: + get: + responses: + 200: + description: OK + schema: + $ref: '#/definitions/SameFileReference' + +definitions: + SameFileReference: + type: object + properties: + name: + type: string diff --git a/fixtures/bugs/2743/working/swagger/user/index.yml b/fixtures/bugs/2743/working/swagger/user/index.yml new file mode 100644 index 00000000..99c1c6f8 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/user/index.yml @@ -0,0 +1,2 @@ +User: + $ref: './model.yml' diff --git a/fixtures/bugs/2743/working/swagger/user/model.yml b/fixtures/bugs/2743/working/swagger/user/model.yml new file mode 100644 index 00000000..5cb91cda --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/user/model.yml @@ -0,0 +1,4 @@ +type: object +properties: + name: + type: string diff --git a/fixtures/bugs/schema-148.json b/fixtures/bugs/schema-148.json new file mode 100644 index 00000000..a233257b --- /dev/null +++ b/fixtures/bugs/schema-148.json @@ -0,0 +1,10 @@ +{ + "definitions": { + "empty": { + "additionalProperties": {} + }, + "false": { + "additionalProperties": false + } + } +} diff --git a/fixtures/expansion/expectedExtraRef.json b/fixtures/expansion/expectedExtraRef.json new file mode 100644 index 00000000..24da091d --- /dev/null +++ b/fixtures/expansion/expectedExtraRef.json @@ -0,0 +1,57 @@ +{ + "schemes": [ + "http" + ], + "swagger": "2.0", + "info": { + "title": "Supported, but non Swagger 20 compliant $ref constructs", + "version": "2.1.0" + }, + "host": "item.com", + "basePath": "/extraRefs", + "paths": { + "/employees": { + "get": { + "summary": "List Employee Types", + "operationId": "LIST-Employees", + "parameters": [ + { + "description": "unsupported $ref in simple param", + "type": "array", + "items": { + "$ref": "#/definitions/arrayType" + }, + "name": "myQueryParam", + "in": "query" + } + ], + "responses": { + "200": { + "description": "unsupported $ref in header", + "schema": { + "type": "string" + }, + "headers": { + "X-header": { + "type": "array", + "items": { + "$ref": "#/definitions/headerType" + } + } + } + } + } + } + } + }, + "definitions": { + "arrayType": { + "type": "integer", + "format": "int32" + }, + "headerType": { + "type": "string", + "format": "uuid" + } + } +} diff --git a/fixtures/expansion/expectedPathItem.json b/fixtures/expansion/expectedPathItem.json new file mode 100644 index 00000000..6eed8b65 --- /dev/null +++ b/fixtures/expansion/expectedPathItem.json @@ -0,0 +1,27 @@ +{ + "swagger": "2.0", + "info": { + "title": "PathItems refs", + "version": "1.0" + }, + "paths": { + "/todos": { + "get": { + "responses": { + "200": { + "description": "List Todos", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "404": { + "description": "error" + } + } + } + } + } +} diff --git a/fixtures/expansion/petstore2.0.json b/fixtures/expansion/petstore2.0.json new file mode 100644 index 00000000..7fc9d889 --- /dev/null +++ b/fixtures/expansion/petstore2.0.json @@ -0,0 +1,280 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "contact": { + "name": "Wordnik API Team", + "url": "http://developer.wordnik.com" + }, + "license": { + "name": "Creative Commons 4.0 International", + "url": "http://creativecommons.org/licenses/by/4.0/" + } + }, + "host": "petstore.swagger.wordnik.com", + "basePath": "/api", + "schemes": [ + "http" + ], + "paths": { + "/pets": { + "get": { + "security": [ + { + "basic": [] + } + ], + "tags": [ "Pet Operations" ], + "operationId": "getAllPets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "The status to filter by", + "type": "string" + }, + { + "name": "limit", + "in": "query", + "description": "The maximum number of results to return", + "type": "integer", + "format": "int64" + } + ], + "summary": "Finds all pets in the system", + "responses": { + "200": { + "description": "Pet response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "security": [ + { + "basic": [] + } + ], + "tags": [ "Pet Operations" ], + "operationId": "createPet", + "summary": "Creates a new pet", + "consumes": ["application/x-yaml"], + "produces": ["application/x-yaml"], + "parameters": [ + { + "name": "pet", + "in": "body", + "description": "The Pet to create", + "required": true, + "schema": { + "$ref": "#/definitions/newPet" + } + } + ], + "responses": { + "200": { + "description": "Created Pet response", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/pets/{id}": { + "delete": { + "security": [ + { + "apiKey": [] + } + ], + "description": "Deletes the Pet by id", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "get": { + "tags": [ "Pet Operations" ], + "operationId": "getPetById", + "summary": "Finds the pet by id", + "responses": { + "200": { + "description": "Pet response", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet", + "required": true, + "type": "integer", + "format": "int64" + } + ] + } + }, + "definitions": { + "Category": { + "id": "Category", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "Pet": { + "id": "Pet", + "properties": { + "category": { + "$ref": "#/definitions/Category" + }, + "id": { + "description": "unique identifier for the pet", + "format": "int64", + "maximum": 100.0, + "minimum": 0.0, + "type": "integer" + }, + "name": { + "type": "string" + }, + "photoUrls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "status": { + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ], + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/definitions/Tag" + }, + "type": "array" + } + }, + "required": [ + "id", + "name" + ] + }, + "newPet": { + "anyOf": [ + { + "$ref": "#/definitions/Pet" + }, + { + "required": [ + "name" + ] + } + ] + }, + "Tag": { + "id": "Tag", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "securityDefinitions": { + "basic": { + "type": "basic" + }, + "apiKey": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + } + } +} diff --git a/fixtures/specs/mini_spec.json b/fixtures/specs/mini_spec.json new file mode 100644 index 00000000..e153eaff --- /dev/null +++ b/fixtures/specs/mini_spec.json @@ -0,0 +1,18 @@ +{ + "swagger": "2.0", + "info": { + "version": "0.0.0", + "title": "Simple API" + }, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} diff --git a/fixtures/specs/minimal_spec.json b/fixtures/specs/minimal_spec.json new file mode 100644 index 00000000..c7c2f7f7 --- /dev/null +++ b/fixtures/specs/minimal_spec.json @@ -0,0 +1,44 @@ +{ + "swagger": "2.0", + "info": { + "version": "0.0.0", + "title": "Simple API" + }, + "securityDefinitions": { + "basic": { + "type": "basic" + }, + "apiKey": { + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" + }, + "queryKey": { + "type": "apiKey", + "in": "query", + "name": "api_key" + } + }, + "paths": { + "/": { + "get": { + "security": [ + { + "apiKey": [], + "basic": [] + }, + {}, + { + "queryKey": [], + "basic": [] + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} diff --git a/fixtures/specs/spec.json b/fixtures/specs/spec.json new file mode 100644 index 00000000..0b65bcc8 --- /dev/null +++ b/fixtures/specs/spec.json @@ -0,0 +1,46 @@ +{ + "id": "http://localhost:3849/api-docs", + "consumes": ["application/json", "application/x-yaml"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "swagger": "2.0", + "info": { + "contact": { + "name": "wordnik api team", + "url": "http://developer.wordnik.com" + }, + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "license": { + "name": "Creative Commons 4.0 International", + "url": "http://creativecommons.org/licenses/by/4.0/" + }, + "termsOfService": "http://helloreverb.com/terms/", + "title": "Swagger Sample API", + "version": "1.0.9-abcd", + "x-framework": "go-swagger" + }, + "host": "some.api.out.there", + "basePath": "/", + "paths": {"x-framework":"go-swagger","/":{"$ref":"cats"}}, + "definitions": { "Category": { "type": "string"} }, + "parameters": { + "categoryParam": { + "name": "category", + "in": "query", + "type": "string" + } + }, + "responses": { "EmptyAnswer": { "description": "no data to return for this operation" } }, + "securityDefinitions": { + "internalApiKey": { + "type": "apiKey", + "in": "header", + "name": "api_key" + } + }, + "security": [{"internalApiKey":[]}], + "tags": [{"name":"pets"}], + "externalDocs": {"description":"the name","url":"the url"}, + "x-some-extension": "vendor", + "x-schemes": ["unix","amqp"] +} diff --git a/go.mod b/go.mod index 9e3e36fb..6911ff7f 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,21 @@ module github.com/go-openapi/spec require ( - github.com/go-openapi/jsonpointer v0.19.5 - github.com/go-openapi/jsonreference v0.20.0 - github.com/go-openapi/swag v0.19.15 - github.com/stretchr/testify v1.6.1 - gopkg.in/yaml.v2 v2.4.0 + github.com/go-openapi/jsonpointer v0.22.5 + github.com/go-openapi/jsonreference v0.21.5 + github.com/go-openapi/swag/conv v0.25.5 + github.com/go-openapi/swag/jsonname v0.25.5 + github.com/go-openapi/swag/jsonutils v0.25.5 + github.com/go-openapi/swag/loading v0.25.5 + github.com/go-openapi/swag/stringutils v0.25.5 + github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 + github.com/go-openapi/testify/v2 v2.4.2 ) -go 1.13 +require ( + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect +) + +go 1.25.0 diff --git a/go.sum b/go.sum index 7427dbdd..2611ec52 100644 --- a/go.sum +++ b/go.sum @@ -1,41 +1,28 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= +github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= +github.com/go-openapi/testify/v2 v2.4.2/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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/header.go b/header.go index 9dfd17b1..599ba2c5 100644 --- a/header.go +++ b/header.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,14 +8,14 @@ import ( "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) const ( jsonArray = "array" ) -// HeaderProps describes a response header +// HeaderProps describes a response header. type HeaderProps struct { Description string `json:"description,omitempty"` } @@ -41,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 @@ -67,87 +56,87 @@ func (h *Header) CollectionOf(items *Items, format string) *Header { return h } -// WithDefault sets the default value on this item -func (h *Header) WithDefault(defaultValue interface{}) *Header { +// 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 -func (h *Header) WithMaxLength(max int64) *Header { - h.MaxLength = &max +// WithMaxLength sets a max length value. +func (h *Header) WithMaxLength(maximum int64) *Header { + h.MaxLength = &maximum return h } -// WithMinLength sets a min length value -func (h *Header) WithMinLength(min int64) *Header { - h.MinLength = &min +// 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 -func (h *Header) WithMaximum(max float64, exclusive bool) *Header { - h.Maximum = &max +// 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 -func (h *Header) WithMinimum(min float64, exclusive bool) *Header { - h.Minimum = &min +// 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) -func (h *Header) WithEnum(values ...interface{}) *Header { - h.Enum = append([]interface{}{}, values...) +// 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 { @@ -161,10 +150,10 @@ func (h Header) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2, b3), nil + return jsonutils.ConcatJSON(b1, b2, b3), 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 @@ -178,8 +167,8 @@ func (h *Header) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &h.HeaderProps) } -// JSONLookup look up a value by the json property name -func (h Header) JSONLookup(token string) (interface{}, error) { +// 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 19d08cc4..ad34da3e 100644 --- a/header_test.go +++ b/header_test.go @@ -1,36 +1,28 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/swag/conv" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) +const epsilon = 1e-9 + func float64Ptr(f float64) *float64 { return &f } + func int64Ptr(f int64) *int64 { return &f } -var header = Header{ - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{ +var header = Header{ //nolint:gochecknoglobals // test fixture + VendorExtensible: VendorExtensible{Extensions: map[string]any{ "x-framework": "swagger-go", }}, HeaderProps: HeaderProps{Description: "the description of this header"}, @@ -54,7 +46,7 @@ var header = Header{ MinItems: int64Ptr(5), UniqueItems: true, MultipleOf: float64Ptr(5), - Enum: []interface{}{"hello", "world"}, + Enum: []any{"hello", "world"}, }, } @@ -82,48 +74,44 @@ const headerJSON = `{ }` func TestIntegrationHeader(t *testing.T) { - var actual Header - if assert.NoError(t, json.Unmarshal([]byte(headerJSON), &actual)) { - assert.EqualValues(t, actual, header) - } - - assertParsesJSON(t, headerJSON, header) + assert.JSONUnmarshalAsT(t, header, headerJSON) } func TestJSONLookupHeader(t *testing.T) { var def string res, err := header.JSONLookup("default") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, def, res) { - t.FailNow() - return - } - def = res.(string) - assert.Equal(t, "8", def) - - var x *interface{} + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, def, res) + + var ok bool + def, ok = res.(string) + require.TrueT(t, ok) + assert.EqualT(t, "8", def) + + var x *any res, err = header.JSONLookup("x-framework") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, x, res) { - t.FailNow() - return - } + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, x, res) - x = res.(*interface{}) + x, ok = res.(*any) + require.TrueT(t, ok) assert.EqualValues(t, "swagger-go", *x) res, err = header.JSONLookup("unknown") - if !assert.Error(t, err) || !assert.Nil(t, res) { - t.FailNow() - return - } + require.Error(t, err) + require.Nil(t, res) - var max *float64 + var maximum *float64 res, err = header.JSONLookup("maximum") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, max, res) { - t.FailNow() - return - } - max = res.(*float64) - assert.Equal(t, float64(100), *max) + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, maximum, res) + + maximum, ok = res.(*float64) + require.TrueT(t, ok) + assert.InDeltaT(t, float64(100), *maximum, epsilon) } func TestResponseHeaueder(t *testing.T) { @@ -134,20 +122,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.EqualValues(t, *i, *h.Items) - assert.Equal(t, "pipe", h.CollectionFormat) + assert.Equal(t, *i, *h.Items) + 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$") @@ -159,7 +147,7 @@ func TestWithHeader(t *testing.T) { h = new(Header).WithEnum("a", "b", "c") assert.Equal(t, Header{ CommonValidations: CommonValidations{ - Enum: []interface{}{ + Enum: []any{ "a", "b", "c", @@ -169,6 +157,6 @@ func TestWithHeader(t *testing.T) { } func TestHeaderWithValidation(t *testing.T) { - h := new(Header).WithValidations(CommonValidations{MaxLength: swag.Int64(15)}) - assert.EqualValues(t, swag.Int64(15), h.MaxLength) + h := new(Header).WithValidations(CommonValidations{MaxLength: conv.Pointer(int64(15))}) + assert.Equal(t, conv.Pointer(int64(15)), h.MaxLength) } diff --git a/helpers_spec_test.go b/helpers_spec_test.go index 40fa75bf..7d5374f6 100644 --- a/helpers_spec_test.go +++ b/helpers_spec_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec_test import ( @@ -8,23 +11,23 @@ import ( "testing" "github.com/go-openapi/spec" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) 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 swag.YAMLMatcher(path) { - return swag.YAMLDoc(path) + if loading.YAMLMatcher(path) { + return loading.YAMLDoc(path) } - data, err := swag.LoadFromFileOrHTTP(path) + data, err := loading.LoadFromFileOrHTTP(path) if err != nil { return nil, err } @@ -48,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]) } } @@ -63,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]) } } @@ -71,7 +74,7 @@ func assertRefInJSONRegexp(t testing.TB, jazon, match string) { // assertRefExpand ensures that all $ref in some json doc expand properly against a root document. // // "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, exclude string, root interface{}, opts ...*spec.ExpandOptions) { +func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*spec.ExpandOptions) { if len(opts) > 0 { assertRefWithFunc(t, "expand-with-base", jazon, "", func(t *testing.T, match string) { ref := spec.RefSchema(match) @@ -87,7 +90,7 @@ func assertRefExpand(t *testing.T, jazon, exclude string, root interface{}, opts }) } -func assertRefResolve(t *testing.T, jazon, exclude string, root interface{}, opts ...*spec.ExpandOptions) { +func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*spec.ExpandOptions) { assertRefWithFunc(t, "resolve", jazon, exclude, func(t *testing.T, match string) { ref := spec.MustCreateRef(match) var ( @@ -136,14 +139,14 @@ func assertRefWithFunc(t *testing.T, name, jazon, exclude string, asserter func( } } -func asJSON(t testing.TB, sp interface{}) string { +func asJSON(t testing.TB, sp any) string { bbb, err := json.MarshalIndent(sp, "", " ") 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) { m := rex.FindAllStringSubmatch(jazon, -1) require.Nil(t, m) diff --git a/helpers_test.go b/helpers_test.go index 0d541505..06ab941d 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -1,21 +1,44 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "encoding/json" "fmt" + "io/fs" + "net/http" + "net/http/httptest" + "path/filepath" "regexp" "strings" "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) 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 := swag.LoadFromFileOrHTTP(path) + data, err := loading.LoadFromFileOrHTTP(path) if err != nil { return nil, err } @@ -73,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]) } } @@ -91,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) @@ -105,7 +128,7 @@ func assertNoRef(t testing.TB, jazon string) { // assertRefExpand ensures that all $ref in some json doc expand properly against a root document. // // "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, exclude string, root interface{}, opts ...*ExpandOptions) { +func assertRefExpand(t *testing.T, jazon, _ string, root any, opts ...*ExpandOptions) { assertRefWithFunc(t, jazon, "", func(t *testing.T, match string) { ref := RefSchema(match) if len(opts) > 0 { @@ -120,7 +143,7 @@ func assertRefExpand(t *testing.T, jazon, exclude string, root interface{}, opts // assertRefResolve ensures that all $ref in some json doc resolve properly against a root document. // // "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 interface{}, opts ...*ExpandOptions) { +func assertRefResolve(t *testing.T, jazon, exclude string, root any, opts ...*ExpandOptions) { assertRefWithFunc(t, jazon, exclude, func(t *testing.T, match string) { ref := MustCreateRef(match) var ( @@ -165,7 +188,7 @@ func assertRefWithFunc(t *testing.T, jazon, exclude string, asserter func(t *tes } } -func asJSON(t testing.TB, sp interface{}) string { +func asJSON(t testing.TB, sp any) string { bbb, err := json.MarshalIndent(sp, "", " ") require.NoError(t, err) diff --git a/info.go b/info.go index c458b49b..0ccfdccc 100644 --- a/info.go +++ b/info.go @@ -1,37 +1,27 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "encoding/json" + "strconv" "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) -// Extensions vendor specific extensions -type Extensions map[string]interface{} +// Extensions vendor specific extensions. +type Extensions map[string]any -// Add adds a value to these extensions -func (e Extensions) Add(key string, value interface{}) { +// 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) @@ -40,7 +30,25 @@ func (e Extensions) GetString(key string) (string, bool) { return "", false } -// GetBool gets a string value from the extensions +// GetInt gets a int value from the extensions. +func (e Extensions) GetInt(key string) (int, bool) { + realKey := strings.ToLower(key) + + if v, ok := e.GetString(realKey); ok { + if r, err := strconv.Atoi(v); err == nil { + return r, true + } + } + + if v, ok := e[realKey]; ok { + if r, rOk := v.(float64); rOk { + return int(r), true + } + } + return -1, false +} + +// 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) @@ -49,10 +57,10 @@ 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.([]interface{}) + arr, isSlice := v.([]any) if !isSlice { return nil, false } @@ -74,20 +82,20 @@ type VendorExtensible struct { Extensions Extensions } -// AddExtension adds an extension to this extensible object -func (v *VendorExtensible) AddExtension(key string, value interface{}) { +// AddExtension adds an extension to this extensible object. +func (v *VendorExtensible) AddExtension(key string, value any) { if value == nil { return } if v.Extensions == nil { - v.Extensions = make(map[string]interface{}) + v.Extensions = make(map[string]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]interface{}) + toser := make(map[string]any) for k, v := range v.Extensions { lk := strings.ToLower(k) if strings.HasPrefix(lk, "x-") { @@ -97,9 +105,9 @@ 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]interface{} + var d map[string]any if err := json.Unmarshal(data, &d); err != nil { return err } @@ -107,7 +115,7 @@ func (v *VendorExtensible) UnmarshalJSON(data []byte) error { lk := strings.ToLower(k) if strings.HasPrefix(lk, "x-") { if v.Extensions == nil { - v.Extensions = map[string]interface{}{} + v.Extensions = map[string]any{} } v.Extensions[k] = vv } @@ -115,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"` @@ -134,8 +142,8 @@ type Info struct { InfoProps } -// JSONLookup look up a value by the json property name -func (i Info) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -143,7 +151,7 @@ func (i Info) JSONLookup(token string) (interface{}, 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 { @@ -153,10 +161,10 @@ func (i Info) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + 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 5ecf1bba..8ebace52 100644 --- a/info_test.go +++ b/info_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,7 +7,8 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) const infoJSON = `{ @@ -38,7 +28,7 @@ const infoJSON = `{ "x-framework": "go-swagger" }` -var info = Info{ +var testInfo = Info{ //nolint:gochecknoglobals // test fixture InfoProps: InfoProps{ Version: "1.0.9-abcd", Title: "Swagger Sample API", @@ -46,36 +36,29 @@ var info = Info{ "the swagger-2.0 specification", TermsOfService: "http://helloreverb.com/terms/", Contact: &ContactInfo{ContactInfoProps: ContactInfoProps{Name: "wordnik api team", URL: "http://developer.wordnik.com"}}, - License: &License{LicenseProps: LicenseProps{ - Name: "Creative Commons 4.0 International", - URL: "http://creativecommons.org/licenses/by/4.0/", - }, + License: &License{ + LicenseProps: LicenseProps{ + Name: "Creative Commons 4.0 International", + URL: "http://creativecommons.org/licenses/by/4.0/", + }, }, }, - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{"x-framework": "go-swagger"}}, + VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-framework": "go-swagger"}}, } -func TestIntegrationInfo_Serialize(t *testing.T) { - b, err := json.MarshalIndent(info, "", "\t") - if assert.NoError(t, err) { - assert.Equal(t, infoJSON, string(b)) - } -} +func TestInfo(t *testing.T) { + t.Run("should marshal Info", func(t *testing.T) { + assert.JSONMarshalAsT(t, infoJSON, testInfo) + }) -func TestIntegrationInfo_Deserialize(t *testing.T) { - actual := Info{} - err := json.Unmarshal([]byte(infoJSON), &actual) - if assert.NoError(t, err) { - assert.EqualValues(t, info, actual) - } -} + t.Run("should unmarshal Info", func(t *testing.T) { + assert.JSONUnmarshalAsT(t, testInfo, infoJSON) + }) -func TestInfoGobEncoding(t *testing.T) { - var src, dst Info - if assert.NoError(t, json.Unmarshal([]byte(infoJSON), &src)) { - assert.EqualValues(t, src, info) - } else { - t.FailNow() - } - doTestAnyGobEncoding(t, &src, &dst) + t.Run("should GobEncode Info", func(t *testing.T) { + var src, dst Info + require.NoError(t, json.Unmarshal([]byte(infoJSON), &src)) + assert.Equal(t, src, testInfo) + doTestAnyGobEncoding(t, &src, &dst) + }) } diff --git a/items.go b/items.go index e2afb213..daf5a4fd 100644 --- a/items.go +++ b/items.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,25 +8,25 @@ import ( "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) 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"` - Format string `json:"format,omitempty"` - Items *Items `json:"items,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty"` - Default interface{} `json:"default,omitempty"` - Example interface{} `json:"example,omitempty"` + Type string `json:"type,omitempty"` + Nullable bool `json:"nullable,omitempty"` + Format string `json:"format,omitempty"` + Items *Items `json:"items,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty"` + Default any `json:"default,omitempty"` + 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 @@ -45,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 "" @@ -64,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 @@ -82,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 @@ -90,87 +79,87 @@ func (i *Items) CollectionOf(items *Items, format string) *Items { return i } -// WithDefault sets the default value on this item -func (i *Items) WithDefault(defaultValue interface{}) *Items { +// 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 -func (i *Items) WithMaxLength(max int64) *Items { - i.MaxLength = &max +// WithMaxLength sets a max length value. +func (i *Items) WithMaxLength(maximum int64) *Items { + i.MaxLength = &maximum return i } -// WithMinLength sets a min length value -func (i *Items) WithMinLength(min int64) *Items { - i.MinLength = &min +// 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 -func (i *Items) WithMaximum(max float64, exclusive bool) *Items { - i.Maximum = &max +// 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 -func (i *Items) WithMinimum(min float64, exclusive bool) *Items { - i.Minimum = &min +// 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) -func (i *Items) WithEnum(values ...interface{}) *Items { - i.Enum = append([]interface{}{}, values...) +// 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 { @@ -195,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 { @@ -213,11 +202,11 @@ func (i Items) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b4, b3, b1, b2), nil + return jsonutils.ConcatJSON(b4, b3, b1, b2), nil } -// JSONLookup look up a value by the json property name -func (i Items) JSONLookup(token string) (interface{}, error) { +// 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 edd75c29..e59d033b 100644 --- a/items_test.go +++ b/items_test.go @@ -1,28 +1,17 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/swag/conv" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -var items = Items{ +var testItems = Items{ //nolint:gochecknoglobals // test fixture Refable: Refable{Ref: MustCreateRef("Dog")}, CommonValidations: CommonValidations{ Maximum: float64Ptr(100), @@ -36,7 +25,7 @@ var items = Items{ MinItems: int64Ptr(5), UniqueItems: true, MultipleOf: float64Ptr(5), - Enum: []interface{}{"hello", "world"}, + Enum: []any{"hello", "world"}, }, SimpleSchema: SimpleSchema{ Type: "string", @@ -73,20 +62,15 @@ const itemsJSON = `{ }` func TestIntegrationItems(t *testing.T) { - var actual Items - if assert.NoError(t, json.Unmarshal([]byte(itemsJSON), &actual)) { - assert.EqualValues(t, actual, items) - } - - assertParsesJSON(t, itemsJSON, items) + assert.JSONUnmarshalAsT(t, testItems, itemsJSON) } func TestTypeNameItems(t *testing.T) { var nilItems Items - assert.Equal(t, "", nilItems.TypeName()) + assert.Empty(t, nilItems.TypeName()) - assert.Equal(t, "date", items.TypeName()) - assert.Equal(t, "", items.ItemsTypeName()) + assert.EqualT(t, "date", testItems.TypeName()) + assert.Empty(t, testItems.ItemsTypeName()) nested := Items{ SimpleSchema: SimpleSchema{ @@ -101,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.Equal(t, "", simple.ItemsTypeName()) + 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) { @@ -141,9 +125,9 @@ func TestItemsBuilder(t *testing.T) { Default: []string{"default-value"}, }, CommonValidations: CommonValidations{ - Enum: []interface{}{[]string{"abc", "efg"}, []string{"hij"}}, - MinItems: swag.Int64(1), - MaxItems: swag.Int64(4), + Enum: []any{[]string{"abc", "efg"}, []string{"hij"}}, + MinItems: conv.Pointer(int64(1)), + MaxItems: conv.Pointer(int64(4)), UniqueItems: true, }, }, @@ -151,42 +135,50 @@ func TestItemsBuilder(t *testing.T) { } func TestJSONLookupItems(t *testing.T) { - res, err := items.JSONLookup("$ref") - if !assert.NoError(t, err) { - t.FailNow() - return - } - if assert.IsType(t, &Ref{}, res) { - ref := res.(*Ref) - assert.EqualValues(t, MustCreateRef("Dog"), *ref) - } - - var max *float64 - res, err = items.JSONLookup("maximum") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, max, res) { - t.FailNow() - return - } - max = res.(*float64) - assert.Equal(t, float64(100), *max) - - var f string - res, err = items.JSONLookup("collectionFormat") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, f, res) { - t.FailNow() - return - } - f = res.(string) - assert.Equal(t, "csv", f) - - res, err = items.JSONLookup("unknown") - if !assert.Error(t, err) || !assert.Nil(t, res) { - t.FailNow() - return - } + t.Run(`lookup should find "$ref"`, func(t *testing.T) { + res, err := testItems.JSONLookup("$ref") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, &Ref{}, res) + + ref, ok := res.(*Ref) + require.TrueT(t, ok) + assert.Equal(t, MustCreateRef("Dog"), *ref) + }) + + t.Run(`lookup should find "maximum"`, func(t *testing.T) { + var maximum *float64 + res, err := testItems.JSONLookup("maximum") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, maximum, res) + + var ok bool + maximum, ok = res.(*float64) + require.TrueT(t, ok) + assert.InDeltaT(t, float64(100), *maximum, epsilon) + }) + + t.Run(`lookup should find "collectionFormat"`, func(t *testing.T) { + var f string + res, err := testItems.JSONLookup("collectionFormat") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, f, res) + + f, ok := res.(string) + require.TrueT(t, ok) + assert.EqualT(t, "csv", f) + }) + + t.Run(`lookup should fail on "unknown"`, func(t *testing.T) { + res, err := testItems.JSONLookup("unknown") + require.Error(t, err) + require.Nil(t, res) + }) } func TestItemsWithValidation(t *testing.T) { - i := new(Items).WithValidations(CommonValidations{MaxLength: swag.Int64(15)}) - assert.EqualValues(t, swag.Int64(15), i.MaxLength) + i := new(Items).WithValidations(CommonValidations{MaxLength: conv.Pointer(int64(15))}) + assert.Equal(t, conv.Pointer(int64(15)), i.MaxLength) } diff --git a/license.go b/license.go index b42f8036..8209f218 100644 --- a/license.go +++ b/license.go @@ -1,23 +1,12 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "encoding/json" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) // License information for the exposed API. @@ -28,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 @@ -42,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 { @@ -52,5 +41,5 @@ func (l License) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + return jsonutils.ConcatJSON(b1, b2), nil } diff --git a/license_test.go b/license_test.go index 407df737..56f75b65 100644 --- a/license_test.go +++ b/license_test.go @@ -1,48 +1,33 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) -var license = License{ - LicenseProps: LicenseProps{Name: "the name", URL: "the url"}, - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{"x-license": "custom term"}}} - -const licenseJSON = `{ +func TestIntegrationLicense(t *testing.T) { + const licenseJSON = `{ "name": "the name", "url": "the url", "x-license": "custom term" }` -func TestIntegrationLicense(t *testing.T) { + testLicense := License{ + LicenseProps: LicenseProps{Name: "the name", URL: "the url"}, + VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-license": "custom term"}}, + } // const licenseYAML = "name: the name\nurl: the url\n" - b, err := json.MarshalIndent(license, "", "\t") - if assert.NoError(t, err) { - assert.Equal(t, licenseJSON, string(b)) - } + t.Run("should marshal license", func(t *testing.T) { + assert.JSONMarshalAsT(t, licenseJSON, testLicense) + }) - actual := License{} - err = json.Unmarshal([]byte(licenseJSON), &actual) - if assert.NoError(t, err) { - assert.EqualValues(t, license, actual) - } + t.Run("should unmarshal empty license", func(t *testing.T) { + assert.JSONUnmarshalAsT(t, testLicense, licenseJSON) + }) } diff --git a/normalizer.go b/normalizer.go index e8b60099..68252dc3 100644 --- a/normalizer.go +++ b/normalizer.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -95,7 +84,7 @@ func denormalizeRef(ref *Ref, originalRelativeBase, id string) Ref { if id != "" { idBaseURL, err := parseURL(id) if err == nil { // if the schema id is not usable as a URI, ignore it - if ref, ok := rebase(ref, idBaseURL, true); ok { // rebase, but keep references to root unchaged (do not want $ref: "") + if ref, ok := rebase(ref, idBaseURL, true); ok { // rebase, but keep references to root unchanged (do not want $ref: "") // $ref relative to the ID of the schema in the root document return ref } @@ -129,8 +118,8 @@ func rebase(ref *Ref, v *url.URL, notEqual bool) (Ref, bool) { newBase.Fragment = u.Fragment - if strings.HasPrefix(u.Path, docPath) { - newBase.Path = strings.TrimPrefix(u.Path, docPath) + if after, ok := strings.CutPrefix(u.Path, docPath); ok { + newBase.Path = after } else { newBase.Path = strings.TrimPrefix(u.Path, v.Path) } @@ -149,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_nonwindows.go b/normalizer_nonwindows.go index 2df07231..0d556323 100644 --- a/normalizer_nonwindows.go +++ b/normalizer_nonwindows.go @@ -1,19 +1,7 @@ //go:build !windows -// +build !windows -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -40,5 +28,5 @@ func repairURI(in string) (*url.URL, string) { return u, "" } -func fixWindowsURI(u *url.URL, in string) { +func fixWindowsURI(_ *url.URL, _ string) { } diff --git a/normalizer_test.go b/normalizer_test.go index b1522970..a7ab306c 100644 --- a/normalizer_test.go +++ b/normalizer_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( @@ -8,16 +11,16 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) 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 "" @@ -219,6 +222,30 @@ func TestNormalizer_NormalizeURI(t *testing.T) { base: `https://example.com//base/Spec.json`, expOutput: `https://example.com/base/Resources.yaml#/definitions/Pets`, }, + { + // escaped characters in base (1) + refPath: `Resources.yaml#/definitions/Pets`, + base: `https://example.com/base (x86)/Spec.json`, + expOutput: `https://example.com/base%20%28x86%29/Resources.yaml#/definitions/Pets`, + }, + { + // escaped characters in base (2) + refPath: `Resources.yaml#/definitions/Pets`, + base: `https://example.com/base [x86]/Spec.json`, + expOutput: `https://example.com/base%20%5Bx86%5D/Resources.yaml#/definitions/Pets`, + }, + { + // escaped characters in joined fragment + refPath: `Resources.yaml#/definitions (x86)/Pets`, + base: `https://example.com/base/Spec.json`, + expOutput: `https://example.com/base/Resources.yaml#/definitions%20(x86)/Pets`, + }, + { + // escaped characters in joined path + refPath: `Resources [x86].yaml#/definitions/Pets`, + base: `https://example.com/base/Spec.json`, + expOutput: `https://example.com/base/Resources%20%5Bx86%5D.yaml#/definitions/Pets`, + }, } }() @@ -233,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) }) } @@ -245,6 +272,7 @@ func TestNormalizer_NormalizeBase(t *testing.T) { if runtime.GOOS == windowsOS { cwd = "/" + strings.ToLower(filepath.ToSlash(cwd)) } + const fileScheme = "file:///" for _, toPin := range []struct { Base, Expected string @@ -267,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", }, @@ -317,7 +345,7 @@ func TestNormalizer_NormalizeBase(t *testing.T) { { // path clean Base: "///folder//subfolder///file.json/", - Expected: "file:///" + currentDriveLetter + ":/folder/subfolder/file.json", + Expected: fileScheme + currentDriveLetter + ":/folder/subfolder/file.json", Windows: true, }, { @@ -344,18 +372,18 @@ func TestNormalizer_NormalizeBase(t *testing.T) { { // handling dots (3/6): valid, cleaned to /c:/ on windows Base: "/..", - Expected: "file:///" + currentDriveLetter + ":", + Expected: fileScheme + currentDriveLetter + ":", Windows: true, }, { // handling dots (4/6): dodgy specification - resolved to / Base: `file:/.`, - Expected: "file:///", + Expected: fileScheme, }, { // handling dots (5/6): dodgy specification - resolved to / Base: `file:/..`, - Expected: "file:///", + Expected: fileScheme, }, { // handling dots (6/6) @@ -377,7 +405,7 @@ func TestNormalizer_NormalizeBase(t *testing.T) { // windows-only cases { Base: "/base/sub/file.json", - Expected: "file:///" + currentDriveLetter + ":/base/sub/file.json", // on windows, filepath.Abs("/a/b") prepends the "c:" drive + Expected: fileScheme + currentDriveLetter + ":/base/sub/file.json", // on windows, filepath.Abs("/a/b") prepends the "c:" drive Windows: true, }, { @@ -422,6 +450,11 @@ func TestNormalizer_NormalizeBase(t *testing.T) { Expected: "file:///e:/base/sub/file.json", Windows: true, }, + { + // escaped characters in base (1) + Base: `file:///c:/base (x86)/spec.json`, + Expected: `file:///c:/base%20%28x86%29/spec.json`, + }, } { testCase := toPin if testCase.Windows && runtime.GOOS != windowsOS { @@ -433,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) }) } @@ -461,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", @@ -576,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/normalizer_windows.go b/normalizer_windows.go index a66c532d..61515c9a 100644 --- a/normalizer_windows.go +++ b/normalizer_windows.go @@ -1,18 +1,7 @@ // -build windows -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec diff --git a/operation.go b/operation.go index 995ce6ac..cd70d254 100644 --- a/operation.go +++ b/operation.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -21,19 +10,19 @@ import ( "sort" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) -func init() { - gob.Register(map[string]interface{}{}) - gob.Register([]interface{}{}) +func init() { //nolint:gochecknoinits // registers gob types for Operation serialization + gob.Register(map[string]any{}) + gob.Register([]any{}) } // OperationProps describes an operation // // 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"` @@ -58,19 +47,22 @@ func (op OperationProps) MarshalJSON() ([]byte, error) { type Alias OperationProps if op.Security == nil { return json.Marshal(&struct { - Security []map[string][]string `json:"security,omitempty"` *Alias + + Security []map[string][]string `json:"security,omitempty"` }{ - Security: op.Security, Alias: (*Alias)(&op), + Security: op.Security, }) } + return json.Marshal(&struct { - Security []map[string][]string `json:"security"` *Alias + + Security []map[string][]string `json:"security"` }{ - Security: op.Security, Alias: (*Alias)(&op), + Security: op.Security, }) } @@ -82,7 +74,15 @@ type Operation struct { OperationProps } -// SuccessResponse gets a success response model +// NewOperation creates a new operation instance. +// It expects an ID as parameter but not passing an ID is also valid. +func NewOperation(id string) *Operation { + op := new(Operation) + op.ID = id + return op +} + +// SuccessResponse gets a success response model. func (o *Operation) SuccessResponse() (*Response, int, bool) { if o.Responses == nil { return nil, 0, false @@ -103,8 +103,8 @@ func (o *Operation) SuccessResponse() (*Response, int, bool) { return o.Responses.Default, 0, false } -// JSONLookup look up a value by the json property name -func (o Operation) JSONLookup(token string) (interface{}, error) { +// 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) (interface{}, 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 { @@ -130,31 +130,23 @@ func (o Operation) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - concated := swag.ConcatJSON(b1, b2) + concated := jsonutils.ConcatJSON(b1, b2) return concated, nil } -// NewOperation creates a new operation instance. -// It expects an ID as parameter but not passing an ID is also valid. -func NewOperation(id string) *Operation { - op := new(Operation) - op.ID = id - return op -} - // WithID sets the ID property on this operation, allows for chaining. func (o *Operation) WithID(id string) *Operation { o.ID = id 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 @@ -178,38 +170,38 @@ 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 } -// Undeprecate marks the operation as not deprected +// Undeprecate marks the operation as not deprecated. func (o *Operation) Undeprecate() *Operation { o.Deprecated = false 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 @@ -217,9 +209,12 @@ func (o *Operation) AddParam(param *Parameter) *Operation { for i, p := range o.Parameters { if p.Name == param.Name && p.In == param.In { - params := append(o.Parameters[:i], *param) + params := make([]Parameter, 0, len(o.Parameters)+1) + params = append(params, o.Parameters[:i]...) + params = append(params, *param) params = append(params, o.Parameters[i+1:]...) o.Parameters = params + return o } } @@ -228,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 { @@ -246,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) @@ -284,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 @@ -298,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 @@ -315,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), @@ -360,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 812cdda4..965b50e4 100644 --- a/operation_test.go +++ b/operation_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -20,12 +9,13 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -var operation = Operation{ +var operation = Operation{ //nolint:gochecknoglobals // test fixture VendorExtensible: VendorExtensible{ - Extensions: map[string]interface{}{ + Extensions: map[string]any{ "x-framework": "go-swagger", }, }, @@ -81,31 +71,29 @@ func TestSuccessResponse(t *testing.T) { ope := &Operation{} resp, n, f := ope.SuccessResponse() assert.Nil(t, resp) - assert.Equal(t, 0, n) - assert.Equal(t, false, f) + assert.EqualT(t, 0, n) + assert.FalseT(t, f) resp, n, f = operation.SuccessResponse() - if assert.NotNil(t, resp) { - assert.Equal(t, "void response", resp.Description) - } - assert.Equal(t, 0, n) - assert.Equal(t, false, f) + require.NotNil(t, resp) + assert.EqualT(t, "void response", resp.Description) + + assert.EqualT(t, 0, n) + assert.FalseT(t, f) + + require.NoError(t, json.Unmarshal([]byte(operationJSON), ope)) - err := json.Unmarshal([]byte(operationJSON), ope) - if !assert.Nil(t, err) { - t.FailNow() - } ope = ope.RespondsWith(301, &Response{ ResponseProps: ResponseProps{ Description: "failure", }, }) resp, n, f = ope.SuccessResponse() - if assert.NotNil(t, resp) { - assert.Equal(t, "void response", resp.Description) - } - assert.Equal(t, 0, n) - assert.Equal(t, false, f) + require.NotNil(t, resp) + assert.EqualT(t, "void response", resp.Description) + + assert.EqualT(t, 0, n) + assert.FalseT(t, f) ope = ope.RespondsWith(200, &Response{ ResponseProps: ResponseProps{ @@ -114,11 +102,11 @@ func TestSuccessResponse(t *testing.T) { }) resp, n, f = ope.SuccessResponse() - if assert.NotNil(t, resp) { - assert.Equal(t, "success", resp.Description) - } - assert.Equal(t, 200, n) - assert.Equal(t, true, f) + require.NotNil(t, resp) + assert.EqualT(t, "success", resp.Description) + + assert.EqualT(t, 200, n) + assert.TrueT(t, f) } func TestOperationBuilder(t *testing.T) { @@ -146,8 +134,7 @@ func TestOperationBuilder(t *testing.T) { WithSummary("my summary"). WithExternalDocs("some doc", "https://www.example.com") - jazon, _ := json.MarshalIndent(ope, "", " ") - assert.JSONEq(t, `{ + assert.JSONMarshalAsT(t, `{ "operationId": "operationID", "description": "test operation", "summary": "my summary", @@ -197,20 +184,20 @@ func TestOperationBuilder(t *testing.T) { "description": "default" } } - }`, string(jazon)) + }`, ope) // check token lookup token, err := ope.JSONLookup("responses") - assert.NoError(t, err) - jazon, _ = json.MarshalIndent(token, "", " ") - assert.JSONEq(t, `{ + require.NoError(t, err) + + assert.JSONMarshalAsT(t, `{ "200": { "description": "success" }, "default": { "description": "default" } - }`, string(jazon)) + }`, token) // check delete methods ope = ope.RespondsWith(200, nil). @@ -219,8 +206,8 @@ func TestOperationBuilder(t *testing.T) { RemoveParam("fakeParam", "query"). Undeprecate(). WithExternalDocs("", "") - jazon, _ = json.MarshalIndent(ope, "", " ") - assert.JSONEq(t, `{ + + assert.JSONMarshalAsT(t, `{ "security": [ { "scheme-name": [ @@ -247,37 +234,30 @@ func TestOperationBuilder(t *testing.T) { "description": "default" } } - }`, string(jazon)) + }`, ope) } func TestIntegrationOperation(t *testing.T) { - var actual Operation - if assert.NoError(t, json.Unmarshal([]byte(operationJSON), &actual)) { - assert.EqualValues(t, actual, operation) - } - - assertParsesJSON(t, operationJSON, operation) + assert.JSONUnmarshalAsT(t, operation, operationJSON) } func TestSecurityProperty(t *testing.T) { // Ensure we omit security key when unset securityNotSet := OperationProps{} jsonResult, err := json.Marshal(securityNotSet) - if assert.NoError(t, err) { - assert.NotContains(t, string(jsonResult), "security", "security key should be omitted when unset") - } + require.NoError(t, err) + 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{ Security: []map[string][]string{}, } jsonResult, err = json.Marshal(securityContainsEmptyArray) - if assert.NoError(t, err) { - var props OperationProps - if assert.NoError(t, json.Unmarshal(jsonResult, &props)) { - assert.Equal(t, securityContainsEmptyArray, props) - } - } + require.NoError(t, err) + + var props OperationProps + require.NoError(t, json.Unmarshal(jsonResult, &props)) + assert.Equal(t, securityContainsEmptyArray, props) } func TestOperationGobEncoding(t *testing.T) { @@ -346,31 +326,23 @@ func TestOperationGobEncoding(t *testing.T) { func doTestOperationGobEncoding(t *testing.T, fixture string) { var src, dst Operation - - if !assert.NoError(t, json.Unmarshal([]byte(fixture), &src)) { - t.FailNow() - } + require.NoError(t, json.Unmarshal([]byte(fixture), &src)) doTestAnyGobEncoding(t, &src, &dst) } -func doTestAnyGobEncoding(t *testing.T, src, dst interface{}) { - expectedJSON, _ := json.MarshalIndent(src, "", " ") +func doTestAnyGobEncoding(t *testing.T, src, dst any) { + expectedJSON, err := json.MarshalIndent(src, "", " ") + require.NoError(t, err) var b bytes.Buffer - err := gob.NewEncoder(&b).Encode(src) - if !assert.NoError(t, err) { - t.FailNow() - } + err = gob.NewEncoder(&b).Encode(src) + require.NoError(t, err) err = gob.NewDecoder(&b).Decode(dst) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, err) jazon, err := json.MarshalIndent(dst, "", " ") - if !assert.NoError(t, err) { - t.FailNow() - } - assert.JSONEq(t, string(expectedJSON), string(jazon)) + require.NoError(t, err) + assert.JSONEqT(t, string(expectedJSON), string(jazon)) } diff --git a/parameter.go b/parameter.go index 2b2b89b6..516f5d95 100644 --- a/parameter.go +++ b/parameter.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,48 +8,54 @@ import ( "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "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"}, - SimpleSchema: SimpleSchema{Type: "file"}} + return &Parameter{ + ParamProps: ParamProps{Name: name, In: "formData"}, + SimpleSchema: SimpleSchema{Type: "file"}, + } } -// 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}, - SimpleSchema: SimpleSchema{Type: jsonArray, CollectionFormat: "csv", - Items: &Items{SimpleSchema: SimpleSchema{Type: tpe, Format: fmt}}}} + return &Parameter{ + ParamProps: ParamProps{Name: name}, + SimpleSchema: SimpleSchema{ + Type: jsonArray, CollectionFormat: "csv", + Items: &Items{SimpleSchema: SimpleSchema{Type: tpe, Format: fmt}}, + }, + } } -// 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) @@ -71,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"` @@ -84,27 +79,27 @@ type ParamProps struct { // Parameter a unique parameter is defined by a combination of a [name](#parameterName) and [location](#parameterIn). // // There are five possible parameter types. -// * Path - Used together with [Path Templating](#pathTemplating), where the parameter value is actually part -// of the operation's URL. This does not include the host or base path of the API. For example, in `/items/{itemId}`, -// the path parameter is `itemId`. -// * Query - Parameters that are appended to the URL. For example, in `/items?id=###`, the query parameter is `id`. -// * Header - Custom headers that are expected as part of the request. -// * Body - The payload that's appended to the HTTP request. Since there can only be one payload, there can only be -// _one_ body parameter. The name of the body parameter has no effect on the parameter itself and is used for -// documentation purposes only. Since Form parameters are also in the payload, body and form parameters cannot exist -// together for the same operation. -// * Form - Used to describe the payload of an HTTP request when either `application/x-www-form-urlencoded` or -// `multipart/form-data` are used as the content type of the request (in Swagger's definition, -// the [`consumes`](#operationConsumes) property of an operation). This is the only parameter type that can be used -// to send files, thus supporting the `file` type. Since form parameters are sent in the payload, they cannot be -// declared together with a body parameter for the same operation. Form parameters have a different format based on -// the content-type used (for further details, consult http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4). -// * `application/x-www-form-urlencoded` - Similar to the format of Query parameters but as a payload. -// For example, `foo=1&bar=swagger` - both `foo` and `bar` are form parameters. This is normally used for simple -// parameters that are being transferred. -// * `multipart/form-data` - each parameter takes a section in the payload with an internal header. -// For example, for the header `Content-Disposition: form-data; name="submit-name"` the name of the parameter is -// `submit-name`. This type of form parameters is more commonly used for file transfers. +// - Path - Used together with [Path Templating](#pathTemplating), where the parameter value is actually part +// of the operation's URL. This does not include the host or base path of the API. For example, in `/items/{itemId}`, +// the path parameter is `itemId`. +// - Query - Parameters that are appended to the URL. For example, in `/items?id=###`, the query parameter is `id`. +// - Header - Custom headers that are expected as part of the request. +// - Body - The payload that's appended to the HTTP request. Since there can only be one payload, there can only be +// _one_ body parameter. The name of the body parameter has no effect on the parameter itself and is used for +// documentation purposes only. Since Form parameters are also in the payload, body and form parameters cannot exist +// together for the same operation. +// - Form - Used to describe the payload of an HTTP request when either `application/x-www-form-urlencoded` or +// `multipart/form-data` are used as the content type of the request (in Swagger's definition, +// the [`consumes`](#operationConsumes) property of an operation). This is the only parameter type that can be used +// to send files, thus supporting the `file` type. Since form parameters are sent in the payload, they cannot be +// declared together with a body parameter for the same operation. Form parameters have a different format based on +// the content-type used (for further details, consult http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4). +// - `application/x-www-form-urlencoded` - Similar to the format of Query parameters but as a payload. +// For example, `foo=1&bar=swagger` - both `foo` and `bar` are form parameters. This is normally used for simple +// parameters that are being transferred. +// - `multipart/form-data` - each parameter takes a section in the payload with an internal header. +// For example, for the header `Content-Disposition: form-data; name="submit-name"` the name of the parameter is +// `submit-name`. This type of form parameters is more commonly used for file transfers. // // For more information: http://goo.gl/8us55a#parameterObject type Parameter struct { @@ -115,8 +110,8 @@ type Parameter struct { ParamProps } -// JSONLookup look up a value by the json property name -func (p Parameter) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -142,32 +137,32 @@ func (p Parameter) JSONLookup(token string) (interface{}, 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 @@ -175,32 +170,32 @@ func (p *Parameter) CollectionOf(items *Items, format string) *Parameter { return p } -// WithDefault sets the default value on this parameter -func (p *Parameter) WithDefault(defaultValue interface{}) *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 @@ -209,81 +204,81 @@ func (p *Parameter) AsRequired() *Parameter { return p } -// WithMaxLength sets a max length value -func (p *Parameter) WithMaxLength(max int64) *Parameter { - p.MaxLength = &max +// WithMaxLength sets a max length value. +func (p *Parameter) WithMaxLength(maximum int64) *Parameter { + p.MaxLength = &maximum return p } -// WithMinLength sets a min length value -func (p *Parameter) WithMinLength(min int64) *Parameter { - p.MinLength = &min +// 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 -func (p *Parameter) WithMaximum(max float64, exclusive bool) *Parameter { - p.Maximum = &max +// 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 -func (p *Parameter) WithMinimum(min float64, exclusive bool) *Parameter { - p.Minimum = &min +// 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) -func (p *Parameter) WithEnum(values ...interface{}) *Parameter { - p.Enum = append([]interface{}{}, values...) +// 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 @@ -300,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 { @@ -322,5 +317,5 @@ func (p Parameter) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b3, b1, b2, b4, b5), nil + return jsonutils.ConcatJSON(b3, b1, b2, b4, b5), nil } diff --git a/parameters_test.go b/parameters_test.go index 0f5ad3cb..9d8a7623 100644 --- a/parameters_test.go +++ b/parameters_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,12 +7,13 @@ import ( "encoding/json" "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/swag/conv" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -var parameter = Parameter{ - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{ +var parameter = Parameter{ //nolint:gochecknoglobals // test fixture + VendorExtensible: VendorExtensible{Extensions: map[string]any{ "x-framework": "swagger-go", }}, Refable: Refable{Ref: MustCreateRef("Dog")}, @@ -39,7 +29,7 @@ var parameter = Parameter{ MinItems: int64Ptr(5), UniqueItems: true, MultipleOf: float64Ptr(5), - Enum: []interface{}{"hello", "world"}, + Enum: []any{"hello", "world"}, }, SimpleSchema: SimpleSchema{ Type: "string", @@ -59,6 +49,7 @@ var parameter = Parameter{ }, } +//nolint:gochecknoglobals // test fixture var parameterJSON = `{ "items": { "$ref": "Cat" @@ -91,12 +82,7 @@ var parameterJSON = `{ }` func TestIntegrationParameter(t *testing.T) { - var actual Parameter - if assert.NoError(t, json.Unmarshal([]byte(parameterJSON), &actual)) { - assert.EqualValues(t, actual, parameter) - } - - assertParsesJSON(t, parameterJSON, parameter) + assert.JSONUnmarshalAsT(t, parameter, parameterJSON) } func TestParameterSerialization(t *testing.T) { @@ -108,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{ @@ -141,30 +128,27 @@ 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) { var src, dst Parameter - if !assert.NoError(t, json.Unmarshal([]byte(parameterJSON), &src)) { - t.FailNow() - } + require.NoError(t, json.Unmarshal([]byte(parameterJSON), &src)) doTestAnyGobEncoding(t, &src, &dst) } func TestParametersWithValidation(t *testing.T) { - p := new(Parameter).WithValidations(CommonValidations{MaxLength: swag.Int64(15)}) - assert.EqualValues(t, swag.Int64(15), p.MaxLength) + p := new(Parameter).WithValidations(CommonValidations{MaxLength: conv.Pointer(int64(15))}) + assert.Equal(t, conv.Pointer(int64(15)), p.MaxLength) } diff --git a/path_item.go b/path_item.go index 68fc8e90..4408ece4 100644 --- a/path_item.go +++ b/path_item.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,10 +7,10 @@ import ( "encoding/json" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "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"` @@ -45,8 +34,8 @@ type PathItem struct { PathItemProps } -// JSONLookup look up a value by the json property name -func (p PathItem) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -57,7 +46,7 @@ func (p PathItem) JSONLookup(token string) (interface{}, 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 @@ -68,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 { @@ -82,6 +71,6 @@ func (p PathItem) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - concated := swag.ConcatJSON(b3, b4, b5) + concated := jsonutils.ConcatJSON(b3, b4, b5) return concated, nil } diff --git a/path_item_test.go b/path_item_test.go index 3c78e903..8a08f6a8 100644 --- a/path_item_test.go +++ b/path_item_test.go @@ -1,30 +1,18 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) -var pathItem = PathItem{ +var pathItem = PathItem{ //nolint:gochecknoglobals // test fixture Refable: Refable{Ref: MustCreateRef("Dog")}, VendorExtensible: VendorExtensible{ - Extensions: map[string]interface{}{ + Extensions: map[string]any{ "x-framework": "go-swagger", }, }, @@ -72,10 +60,5 @@ const pathItemJSON = `{ }` func TestIntegrationPathItem(t *testing.T) { - var actual PathItem - if assert.NoError(t, json.Unmarshal([]byte(pathItemJSON), &actual)) { - assert.EqualValues(t, actual, pathItem) - } - - assertParsesJSON(t, pathItemJSON, pathItem) + assert.JSONUnmarshalAsT(t, pathItem, pathItemJSON) } diff --git a/paths.go b/paths.go index 9dc82a29..5daf5a67 100644 --- a/paths.go +++ b/paths.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,7 +8,7 @@ import ( "fmt" "strings" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) // Paths holds the relative paths to the individual endpoints. @@ -30,21 +19,22 @@ import ( // For more information: http://goo.gl/8us55a#pathsObject type Paths struct { VendorExtensible + 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 -func (p Paths) JSONLookup(token string) (interface{}, error) { +// 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 } if ex, ok := p.Extensions[token]; ok { return &ex, nil } - return nil, fmt.Errorf("object has no field %q", token) + 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 { @@ -53,9 +43,9 @@ func (p *Paths) UnmarshalJSON(data []byte) error { for k, v := range res { if strings.HasPrefix(strings.ToLower(k), "x-") { if p.Extensions == nil { - p.Extensions = make(map[string]interface{}) + p.Extensions = make(map[string]any) } - var d interface{} + var d any if err := json.Unmarshal(v, &d); err != nil { return err } @@ -75,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 { @@ -92,6 +82,6 @@ func (p Paths) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - concated := swag.ConcatJSON(b1, b2) + concated := jsonutils.ConcatJSON(b1, b2) return concated, nil } diff --git a/paths_test.go b/paths_test.go index ff5626a6..3057a9da 100644 --- a/paths_test.go +++ b/paths_test.go @@ -1,28 +1,16 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" ) -var paths = Paths{ - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{"x-framework": "go-swagger"}}, +var paths = Paths{ //nolint:gochecknoglobals // test fixture + VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-framework": "go-swagger"}}, Paths: map[string]PathItem{ "/": { Refable: Refable{Ref: MustCreateRef("cats")}, @@ -33,11 +21,5 @@ var paths = Paths{ const pathsJSON = `{"x-framework":"go-swagger","/":{"$ref":"cats"}}` func TestIntegrationPaths(t *testing.T) { - var actual Paths - if assert.NoError(t, json.Unmarshal([]byte(pathsJSON), &actual)) { - assert.EqualValues(t, actual, paths) - } - - assertParsesJSON(t, pathsJSON, paths) - + assert.JSONUnmarshalAsT(t, paths, pathsJSON) } diff --git a/properties.go b/properties.go index 2af13787..b8e97271 100644 --- a/properties.go +++ b/properties.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( @@ -7,10 +10,11 @@ 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 { - Name string Schema + + Name string } // OrderSchemaItems is a sortable slice of named schemas. @@ -21,30 +25,35 @@ 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 } func (items OrderSchemaItems) Len() int { return len(items) } func (items OrderSchemaItems) Swap(i, j int) { items[i], items[j] = items[j], items[i] } func (items OrderSchemaItems) Less(i, j int) (ret bool) { - ii, oki := items[i].Extensions.GetString("x-order") - ij, okj := items[j].Extensions.GetString("x-order") - if oki { + ii, oki := items[i].Extensions.GetInt("x-order") + ij, okj := items[j].Extensions.GetInt("x-order") + if oki { //nolint:nestif // nested recover logic for safe type comparison if okj { defer func() { if err := recover(); err != nil { @@ -56,7 +65,7 @@ func (items OrderSchemaItems) Less(i, j int) (ret bool) { ret = reflect.ValueOf(ii).String() < reflect.ValueOf(ij).String() } }() - return reflect.ValueOf(ii).Int() < reflect.ValueOf(ij).Int() + return ii < ij } return true } else if okj { @@ -65,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 d860843d..1c59acab 100644 --- a/properties_test.go +++ b/properties_test.go @@ -1,21 +1,13 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestPropertySerialization(t *testing.T) { @@ -29,7 +21,7 @@ func TestPropertySerialization(t *testing.T) { }}, }} - var propSerData = []struct { + propSerData := []struct { Schema *Schema JSON string }{ @@ -51,8 +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 b0ef9bd9..40b7d486 100644 --- a/ref.go +++ b/ref.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -25,27 +14,44 @@ import ( "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() } -// UnmarshalJSON unmarshalss the ref from json +// UnmarshalJSON unmarshals the ref from json. func (r *Refable) UnmarshalJSON(d []byte) error { return json.Unmarshal(d, &r.Ref) } -// Ref represents a json reference that is potentially resolved +// Ref represents a json reference that is potentially resolved. type Ref struct { jsonreference.Ref } -// RemoteURI gets the remote uri part of the ref +// 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 { + return Ref{}, err + } + + return Ref{Ref: ref}, nil +} + +// MustCreateRef creates a ref object but panics when refURI is invalid. +// Use the NewRef method for a version that returns an error. +func MustCreateRef(refURI string) Ref { + return Ref{Ref: jsonreference.MustCreateRef(refURI)} +} + +// RemoteURI gets the remote uri part of the ref. func (r *Ref) RemoteURI() string { if r.String() == "" { return "" @@ -56,7 +62,7 @@ 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 url the ref points to can be found. func (r *Ref) IsValidURI(basepaths ...string) bool { if r.String() == "" { return true @@ -75,10 +81,11 @@ func (r *Ref) IsValidURI(basepaths ...string) bool { } defer rr.Body.Close() - return rr.StatusCode/100 == 2 + // true if the response is >= 200 and < 300 + return rr.StatusCode/100 == 2 //nolint:mnd } - if !(r.HasFileScheme || r.HasFullFilePath || r.HasURLPathOnly) { + if !r.HasFileScheme && !r.HasFullFilePath && !r.HasURLPathOnly { return false } @@ -105,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 { @@ -114,23 +121,7 @@ func (r *Ref) Inherits(child Ref) (*Ref, error) { return &Ref{Ref: *ref}, nil } -// 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 { - return Ref{}, err - } - return Ref{Ref: ref}, nil -} - -// MustCreateRef creates a ref object but panics when refURI is invalid. -// Use the NewRef method for a version that returns an error. -func MustCreateRef(refURI string) Ref { - return Ref{Ref: jsonreference.MustCreateRef(refURI)} -} - -// 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 == "" { @@ -139,20 +130,20 @@ func (r Ref) MarshalJSON() ([]byte, error) { } return []byte("{}"), nil } - v := map[string]interface{}{"$ref": str} + v := map[string]any{"$ref": str} 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]interface{} + var v map[string]any if err := json.Unmarshal(d, &v); err != nil { return err } 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() @@ -163,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) @@ -174,7 +165,7 @@ func (r *Ref) GobDecode(b []byte) error { return json.Unmarshal(raw, r) } -func (r *Ref) fromMap(v map[string]interface{}) error { +func (r *Ref) fromMap(v map[string]any) error { if v == nil { return nil } diff --git a/ref_test.go b/ref_test.go index 7dc2f59e..8973af20 100644 --- a/ref_test.go +++ b/ref_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -20,28 +9,25 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "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") - err := gob.NewEncoder(&b).Encode(&src) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, + gob.NewEncoder(&b).Encode(&src), + ) var dst Ref - err = gob.NewDecoder(&b).Decode(&dst) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, + gob.NewDecoder(&b).Decode(&dst), + ) jazon, err := json.Marshal(dst) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, err) - assert.Equal(t, `{"$ref":"#/definitions/test"}`, string(jazon)) + assert.JSONEqT(t, `{"$ref":"#/definitions/test"}`, string(jazon)) } diff --git a/resolver.go b/resolver.go index 47d1ee13..1bf90c86 100644 --- a/resolver.go +++ b/resolver.go @@ -1,12 +1,15 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "fmt" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) -func resolveAnyWithBase(root interface{}, ref *Ref, result interface{}, options *ExpandOptions) error { +func resolveAnyWithBase(root any, ref *Ref, result any, options *ExpandOptions) error { options = optionsOrDefault(options) resolver := defaultSchemaLoader(root, options, nil, nil) @@ -17,8 +20,8 @@ func resolveAnyWithBase(root interface{}, ref *Ref, result interface{}, options return nil } -// ResolveRefWithBase resolves a reference against a context root with preservation of base path -func ResolveRefWithBase(root interface{}, ref *Ref, options *ExpandOptions) (*Schema, error) { +// 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) if err := resolveAnyWithBase(root, ref, result, options); err != nil { @@ -31,8 +34,8 @@ func ResolveRefWithBase(root interface{}, ref *Ref, options *ExpandOptions) (*Sc // 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 -func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { +// 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 { return nil, err @@ -43,9 +46,9 @@ func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { return &sch, nil case *Schema: return sch, nil - case map[string]interface{}: + case map[string]any: newSch := new(Schema) - if err = swag.DynamicJSONToStruct(sch, newSch); err != nil { + if err = jsonutils.FromDynamicJSON(sch, newSch); err != nil { return nil, err } return newSch, nil @@ -54,8 +57,8 @@ func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { } } -// ResolveParameterWithBase resolves a parameter reference against a context root and base path -func ResolveParameterWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Parameter, error) { +// 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) if err := resolveAnyWithBase(root, &ref, result, options); err != nil { @@ -65,13 +68,13 @@ func ResolveParameterWithBase(root interface{}, ref Ref, options *ExpandOptions) return result, nil } -// ResolveParameter resolves a parameter reference against a context root -func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { +// 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 -func ResolveResponseWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Response, error) { +// 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) err := resolveAnyWithBase(root, &ref, result, options) @@ -82,13 +85,13 @@ func ResolveResponseWithBase(root interface{}, ref Ref, options *ExpandOptions) return result, nil } -// ResolveResponse resolves response a reference against a context root -func ResolveResponse(root interface{}, ref Ref) (*Response, error) { +// 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 -func ResolvePathItemWithBase(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { +// 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) if err := resolveAnyWithBase(root, &ref, result, options); err != nil { @@ -100,16 +103,16 @@ func ResolvePathItemWithBase(root interface{}, ref Ref, options *ExpandOptions) // ResolvePathItem resolves response a path item against a context root and base path // -// Deprecated: use ResolvePathItemWithBase instead -func ResolvePathItem(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { +// Deprecated: use ResolvePathItemWithBase instead. +func ResolvePathItem(root any, ref Ref, options *ExpandOptions) (*PathItem, error) { return ResolvePathItemWithBase(root, ref, options) } // ResolveItemsWithBase resolves parameter items reference against a context root and base path. // -// NOTE: stricly speaking, this construct is not supported by Swagger 2.0. +// NOTE: strictly speaking, this construct is not supported by Swagger 2.0. // Similarly, $ref are forbidden in response headers. -func ResolveItemsWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { +func ResolveItemsWithBase(root any, ref Ref, options *ExpandOptions) (*Items, error) { result := new(Items) if err := resolveAnyWithBase(root, &ref, result, options); err != nil { @@ -121,7 +124,7 @@ func ResolveItemsWithBase(root interface{}, ref Ref, options *ExpandOptions) (*I // ResolveItems resolves parameter items reference against a context root and base path. // -// Deprecated: use ResolveItemsWithBase instead -func ResolveItems(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { +// 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 21df8978..c949eca0 100644 --- a/resolver_test.go +++ b/resolver_test.go @@ -1,21 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" + "os" "path/filepath" "testing" "github.com/go-openapi/jsonpointer" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestResolveRef(t *testing.T) { - var root interface{} - require.NoError(t, json.Unmarshal([]byte(PetStore20), &root)) + var root any + require.NoError(t, json.Unmarshal(PetStore20, &root)) ref, err := NewRef("#/definitions/Category") require.NoError(t, err) @@ -26,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{ @@ -37,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) { @@ -55,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) } @@ -75,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) } @@ -93,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", @@ -116,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", @@ -127,12 +128,10 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -155,12 +154,10 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -175,12 +172,10 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -189,7 +184,7 @@ func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { require.NoError(t, err) resolver := defaultSchemaLoader(rootDoc, nil, nil, nil) - assert.Error(t, resolver.Resolve(&ref, &tgt, "")) + require.Error(t, resolver.Resolve(&ref, &tgt, "")) } /* This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced */ @@ -199,7 +194,7 @@ func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { // defer server.Close() // // rootDoc := new(Swagger) -// b, err := ioutil.ReadFile("fixtures/specs/refed.json") +// b, err := os.ReadFile("fixtures/specs/refed.json") // require.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) // // var tgt Schema @@ -212,12 +207,10 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -228,21 +221,19 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -256,12 +247,10 @@ 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 := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) require.NoError(t, json.Unmarshal(b, rootDoc)) @@ -295,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) { @@ -312,7 +301,7 @@ func TestResolveLocalRef_FromInvalidFragment(t *testing.T) { func TestResolveLocalRef_Parameter(t *testing.T) { rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) basePath := filepath.Join(specs, "refed.json") @@ -325,17 +314,17 @@ 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) { rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) basePath := filepath.Join(specs, "refed.json") @@ -352,7 +341,7 @@ func TestResolveLocalRef_PathItem(t *testing.T) { func TestResolveLocalRef_Response(t *testing.T) { rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + b, err := os.ReadFile(filepath.Join(specs, "refed.json")) require.NoError(t, err) basePath := filepath.Join(specs, "refed.json") @@ -381,7 +370,7 @@ func TestResolvePathItem(t *testing.T) { jazon := asJSON(t, pathItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "get": { "responses": { "200": { @@ -409,26 +398,26 @@ func TestResolveExtraItem(t *testing.T) { require.NoError(t, json.Unmarshal(specDoc, spec)) - // Resolve param Items use case: here we explicitly resolve the unsuppord case + // Resolve param Items use case: here we explicitly resolve the unsupported case parm := spec.Paths.Paths["/employees"].Get.Parameters[0] parmItem, err := ResolveItems(spec, parm.Items.Ref, &ExpandOptions{RelativeBase: extraRefFixture}) require.NoError(t, err) jazon := asJSON(t, parmItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "type": "integer", "format": "int32" }`, jazon) - // Resolve header Items use case: here we explicitly resolve the unsuppord case + // Resolve header Items use case: here we explicitly resolve the unsupported case hdr := spec.Paths.Paths["/employees"].Get.Responses.StatusCodeResponses[200].Headers["X-header"] hdrItem, err := ResolveItems(spec, hdr.Items.Ref, &ExpandOptions{RelativeBase: extraRefFixture}) require.NoError(t, err) jazon = asJSON(t, hdrItem) - assert.JSONEq(t, `{ + assert.JSONEqT(t, `{ "type": "string", "format": "uuid" }`, jazon) diff --git a/response.go b/response.go index 0340b60d..4bb6a2bc 100644 --- a/response.go +++ b/response.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,15 +7,15 @@ import ( "encoding/json" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "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"` - Headers map[string]Header `json:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty"` + Description string `json:"description"` + Schema *Schema `json:"schema,omitempty"` + Headers map[string]Header `json:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty"` } // Response describes a single response from an API Operation. @@ -38,8 +27,20 @@ type Response struct { VendorExtensible } -// JSONLookup look up a value by the json property name -func (r Response) JSONLookup(token string) (interface{}, error) { +// NewResponse creates a new response instance. +func NewResponse() *Response { + return new(Response) +} + +// ResponseRef creates a response as a json reference. +func ResponseRef(url string) *Response { + resp := NewResponse() + resp.Ref = MustCreateRef(url) + return resp +} + +// 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 } @@ -50,7 +51,7 @@ func (r Response) JSONLookup(token string) (interface{}, 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 @@ -61,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 @@ -74,14 +75,14 @@ func (r Response) MarshalJSON() ([]byte, error) { } else { // when there is $ref inside the schema, description should be omitempty-ied b1, err = json.Marshal(struct { - Description string `json:"description,omitempty"` - Schema *Schema `json:"schema,omitempty"` - Headers map[string]Header `json:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty"` + Description string `json:"description,omitempty"` + Schema *Schema `json:"schema,omitempty"` + Headers map[string]Header `json:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty"` }{ - Description: r.ResponseProps.Description, - Schema: r.ResponseProps.Schema, - Examples: r.ResponseProps.Examples, + Description: r.Description, + Schema: r.Schema, + Examples: r.Examples, }) } if err != nil { @@ -96,35 +97,23 @@ func (r Response) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2, b3), nil -} - -// NewResponse creates a new response instance -func NewResponse() *Response { - return new(Response) -} - -// ResponseRef creates a response as a json reference -func ResponseRef(url string) *Response { - resp := NewResponse() - resp.Ref = MustCreateRef(url) - return resp + 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) @@ -136,16 +125,16 @@ 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 -func (r *Response) AddExample(mediaType string, example interface{}) *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]interface{}) + r.Examples = make(map[string]any) } r.Examples[mediaType] = example return r diff --git a/response_test.go b/response_test.go index 71578aba..dd9045e7 100644 --- a/response_test.go +++ b/response_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + // Copyright 2017 go-swagger maintainers // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,16 +18,16 @@ package spec import ( - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "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]interface{}{ + Extensions: map[string]any{ "x-go-name": "PutDogExists", }, }, @@ -44,49 +47,43 @@ const responseJSON = `{ }` func TestIntegrationResponse(t *testing.T) { - var actual Response - if assert.NoError(t, json.Unmarshal([]byte(responseJSON), &actual)) { - assert.EqualValues(t, actual, response) - } - - assertParsesJSON(t, responseJSON, response) + assert.JSONUnmarshalAsT(t, response, responseJSON) } func TestJSONLookupResponse(t *testing.T) { res, err := response.JSONLookup("$ref") - if !assert.NoError(t, err) { - t.FailNow() - return - } - if assert.IsType(t, &Ref{}, res) { - ref := res.(*Ref) - assert.EqualValues(t, MustCreateRef("Dog"), *ref) - } + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, &Ref{}, res) + + var ok bool + ref, ok := res.(*Ref) + require.TrueT(t, ok) + assert.Equal(t, MustCreateRef("Dog"), *ref) var def string res, err = response.JSONLookup("description") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, def, res) { - t.FailNow() - return - } - def = res.(string) - assert.Equal(t, "Dog exists", def) + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, def, res) + + def, ok = res.(string) + require.TrueT(t, ok) + assert.EqualT(t, "Dog exists", def) - var x *interface{} + var x *any res, err = response.JSONLookup("x-go-name") - if !assert.NoError(t, err) || !assert.NotNil(t, res) || !assert.IsType(t, x, res) { - t.FailNow() - return - } + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, x, res) - x = res.(*interface{}) + x, ok = res.(*any) + require.TrueT(t, ok) assert.EqualValues(t, "PutDogExists", *x) res, err = response.JSONLookup("unknown") - if !assert.Error(t, err) || !assert.Nil(t, res) { - t.FailNow() - return - } + require.Error(t, err) + require.Nil(t, res) } func TestResponseBuild(t *testing.T) { @@ -95,8 +92,7 @@ func TestResponseBuild(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" @@ -109,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 4efb6f86..fb369e4a 100644 --- a/responses.go +++ b/responses.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,8 +8,9 @@ import ( "fmt" "reflect" "strconv" + "strings" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) // Responses is a container for the expected responses of an operation. @@ -41,8 +31,8 @@ type Responses struct { ResponsesProps } -// JSONLookup implements an interface to customize json pointer lookup -func (r Responses) JSONLookup(token string) (interface{}, error) { +// JSONLookup implements an interface to customize json pointer lookup. +func (r Responses) JSONLookup(token string) (any, error) { if token == "default" { return r.Default, nil } @@ -54,14 +44,15 @@ func (r Responses) JSONLookup(token string) (interface{}, error) { return scr, nil } } - return nil, fmt.Errorf("object has no field %q", token) + 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 } + if err := json.Unmarshal(data, &r.VendorExtensible); err != nil { return err } @@ -71,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 { @@ -81,7 +72,7 @@ func (r Responses) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - concated := swag.ConcatJSON(b1, b2) + concated := jsonutils.ConcatJSON(b1, b2) return concated, nil } @@ -93,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 { @@ -105,22 +96,33 @@ 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]Response + var res map[string]json.RawMessage if err := json.Unmarshal(data, &res); err != nil { - return nil + return err } + if v, ok := res["default"]; ok { - r.Default = &v + var defaultRes Response + if err := json.Unmarshal(v, &defaultRes); err != nil { + return err + } + r.Default = &defaultRes delete(res, "default") } for k, v := range res { - if nk, err := strconv.Atoi(k); err == nil { - if r.StatusCodeResponses == nil { - r.StatusCodeResponses = map[int]Response{} + if !strings.HasPrefix(k, "x-") { + var statusCodeResp Response + if err := json.Unmarshal(v, &statusCodeResp); err != nil { + return err + } + if nk, err := strconv.Atoi(k); err == nil { + if r.StatusCodeResponses == nil { + r.StatusCodeResponses = map[int]Response{} + } + r.StatusCodeResponses[nk] = statusCodeResp } - r.StatusCodeResponses[nk] = v } } return nil diff --git a/responses_test.go b/responses_test.go new file mode 100644 index 00000000..c9810c1d --- /dev/null +++ b/responses_test.go @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +// Copyright 2017 go-swagger maintainers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "testing" + + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +var responses = Responses{ //nolint:gochecknoglobals // test fixture + VendorExtensible: VendorExtensible{ + Extensions: map[string]any{ + "x-go-name": "PutDogExists", + }, + }, + ResponsesProps: ResponsesProps{ + StatusCodeResponses: map[int]Response{ + 200: { + Refable: Refable{Ref: MustCreateRef("Dog")}, + VendorExtensible: VendorExtensible{ + Extensions: map[string]any{ + "x-go-name": "PutDogExists", + }, + }, + ResponseProps: ResponseProps{ + Description: "Dog exists", + Schema: &Schema{SchemaProps: SchemaProps{Type: []string{"string"}}}, + }, + }, + }, + }, +} + +const responsesJSON = `{ + "x-go-name": "PutDogExists", + "200": { + "$ref": "Dog", + "x-go-name": "PutDogExists", + "description": "Dog exists", + "schema": { + "type": "string" + } + } +}` + +func TestIntegrationResponses(t *testing.T) { + assert.JSONUnmarshalAsT(t, responses, responsesJSON) +} + +func TestJSONLookupResponses(t *testing.T) { + resp200, ok := responses.StatusCodeResponses[200] + require.TrueT(t, ok) + + res, err := resp200.JSONLookup("$ref") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, &Ref{}, res) + + ref, ok := res.(*Ref) + require.TrueT(t, ok) + assert.Equal(t, MustCreateRef("Dog"), *ref) + + var def string + res, err = resp200.JSONLookup("description") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, def, res) + + def, ok = res.(string) + require.TrueT(t, ok) + assert.EqualT(t, "Dog exists", def) + + var x *any + res, err = responses.JSONLookup("x-go-name") + require.NoError(t, err) + require.NotNil(t, res) + require.IsType(t, x, res) + + x, ok = res.(*any) + require.TrueT(t, ok) + assert.EqualValues(t, "PutDogExists", *x) + + res, err = responses.JSONLookup("unknown") + require.Error(t, err) + require.Nil(t, res) +} + +func TestResponsesBuild(t *testing.T) { + resp := NewResponse(). + WithDescription("some response"). + WithSchema(new(Schema).Typed("object", "")). + AddHeader("x-header", ResponseHeader().Typed("string", "")). + AddExample("application/json", `{"key":"value"}`) + assert.JSONMarshalAsT(t, `{ + "description": "some response", + "schema": { + "type": "object" + }, + "headers": { + "x-header": { + "type": "string" + } + }, + "examples": { + "application/json": "{\"key\":\"value\"}" + } + }`, resp) +} diff --git a/schema.go b/schema.go index 4e9be857..d7a481bf 100644 --- a/schema.go +++ b/schema.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -20,89 +9,92 @@ import ( "strings" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/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"}, - AdditionalProperties: &SchemaOrBool{Allows: true, Schema: property}}} + return &Schema{SchemaProps: SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &SchemaOrBool{Allows: true, Schema: property}, + }} } -// 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"}}} @@ -110,35 +102,35 @@ 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 } - v := map[string]interface{}{"$schema": string(r)} + v := map[string]any{"$schema": string(r)} 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]interface{} + var v map[string]any if err := json.Unmarshal(data, &v); err != nil { return err } return r.fromMap(v) } -func (r *SchemaURL) fromMap(v map[string]interface{}) error { +func (r *SchemaURL) fromMap(v map[string]any) error { if v == nil { return nil } @@ -155,7 +147,7 @@ func (r *SchemaURL) fromMap(v map[string]interface{}) 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:"-"` @@ -165,7 +157,7 @@ type SchemaProps struct { Nullable bool `json:"nullable,omitempty"` Format string `json:"format,omitempty"` Title string `json:"title,omitempty"` - Default interface{} `json:"default,omitempty"` + Default any `json:"default,omitempty"` Maximum *float64 `json:"maximum,omitempty"` ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` Minimum *float64 `json:"minimum,omitempty"` @@ -177,7 +169,7 @@ type SchemaProps struct { MinItems *int64 `json:"minItems,omitempty"` UniqueItems bool `json:"uniqueItems,omitempty"` MultipleOf *float64 `json:"multipleOf,omitempty"` - Enum []interface{} `json:"enum,omitempty"` + Enum []any `json:"enum,omitempty"` MaxProperties *int64 `json:"maxProperties,omitempty"` MinProperties *int64 `json:"minProperties,omitempty"` Required []string `json:"required,omitempty"` @@ -194,13 +186,13 @@ 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"` XML *XMLObject `json:"xml,omitempty"` ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` - Example interface{} `json:"example,omitempty"` + Example any `json:"example,omitempty"` } // Schema the schema object allows the definition of input and output data types. @@ -214,11 +206,12 @@ type Schema struct { VendorExtensible SchemaProps SwaggerSchemaProps - ExtraProps map[string]interface{} `json:"-"` + + ExtraProps map[string]any `json:"-"` } -// JSONLookup implements an interface to customize json pointer lookup -func (s Schema) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -235,31 +228,31 @@ func (s Schema) JSONLookup(token string) (interface{}, 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) @@ -268,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 -func (s *Schema) WithMaxProperties(max int64) *Schema { - s.MaxProperties = &max +// 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 -func (s *Schema) WithMinProperties(min int64) *Schema { - s.MinProperties = &min +// 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 != "" { @@ -308,125 +301,125 @@ 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 -func (s *Schema) WithDefault(defaultValue interface{}) *Schema { +// 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 -func (s *Schema) WithMaxLength(max int64) *Schema { - s.MaxLength = &max +// WithMaxLength sets a max length value. +func (s *Schema) WithMaxLength(maximum int64) *Schema { + s.MaxLength = &maximum return s } -// WithMinLength sets a min length value -func (s *Schema) WithMinLength(min int64) *Schema { - s.MinLength = &min +// 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 -func (s *Schema) WithMaximum(max float64, exclusive bool) *Schema { - s.Maximum = &max +// 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 -func (s *Schema) WithMinimum(min float64, exclusive bool) *Schema { - s.Minimum = &min +// 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) -func (s *Schema) WithEnum(values ...interface{}) *Schema { - s.Enum = append([]interface{}{}, values...) +// 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 -func (s *Schema) WithExample(example interface{}) *Schema { +// WithExample sets the example for this schema. +func (s *Schema) WithExample(example any) *Schema { s.Example = example return s } @@ -449,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) @@ -458,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) @@ -467,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) @@ -476,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) @@ -485,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) @@ -494,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) @@ -503,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) @@ -533,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{ @@ -562,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 %v", err) + 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 %v", err) + return nil, fmt.Errorf("vendor props %w: %w", err, ErrSpec) } b3, err := s.Ref.MarshalJSON() if err != nil { - return nil, fmt.Errorf("ref prop %v", err) + return nil, fmt.Errorf("ref prop %w: %w", err, ErrSpec) } b4, err := s.Schema.MarshalJSON() if err != nil { - return nil, fmt.Errorf("schema prop %v", err) + 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 %v", err) + 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 %v", err) + return nil, fmt.Errorf("extra props %w: %w", err, ErrSpec) } b6 = jj } - return swag.ConcatJSON(b1, b2, b3, b4, b5, b6), nil + 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 @@ -610,7 +603,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { SwaggerSchemaProps: props.SwaggerSchemaProps, } - var d map[string]interface{} + var d map[string]any if err := json.Unmarshal(data, &d); err != nil { return err } @@ -620,7 +613,7 @@ func (s *Schema) UnmarshalJSON(data []byte) error { delete(d, "$ref") delete(d, "$schema") - for _, pn := range swag.DefaultJSONNameProvider.GetJSONNames(s) { + for _, pn := range jsonname.DefaultJSONNameProvider.GetJSONNames(s) { delete(d, pn) } @@ -628,13 +621,13 @@ func (s *Schema) UnmarshalJSON(data []byte) error { lk := strings.ToLower(k) if strings.HasPrefix(lk, "x-") { if sch.Extensions == nil { - sch.Extensions = map[string]interface{}{} + sch.Extensions = map[string]any{} } sch.Extensions[k] = vv continue } if sch.ExtraProps == nil { - sch.ExtraProps = map[string]interface{}{} + sch.ExtraProps = map[string]any{} } sch.ExtraProps[k] = vv } diff --git a/schema_loader.go b/schema_loader.go index b81175af..0894c932 100644 --- a/schema_loader.go +++ b/schema_loader.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -22,7 +11,9 @@ import ( "reflect" "strings" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" + "github.com/go-openapi/swag/loading" + "github.com/go-openapi/swag/stringutils" ) // PathLoader is a function to use when loading remote refs. @@ -33,8 +24,8 @@ 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) { - data, err := swag.LoadFromFileOrHTTP(pth) +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 } @@ -73,12 +64,23 @@ func newResolverContext(options *ExpandOptions) *resolverContext { } type schemaLoader struct { - root interface{} + root any options *ExpandOptions cache ResolutionCache context *resolverContext } +// Resolve resolves a reference against basePath and stores the result in target. +// +// Resolve is not in charge of following references: it only resolves ref by following its URL. +// +// If the schema the ref is referring to holds nested refs, Resolve doesn't resolve them. +// +// If basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct. +func (r *schemaLoader) Resolve(ref *Ref, target any, basePath string) error { + return r.resolveRef(ref, target, basePath) +} + func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) *schemaLoader { if ref.IsRoot() || ref.HasFragmentOnly { return r @@ -113,7 +115,7 @@ func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) return basePath } -func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error { +func (r *schemaLoader) resolveRef(ref *Ref, target any, basePath string) error { tgt := reflect.ValueOf(target) if tgt.Kind() != reflect.Ptr { return ErrResolveRefNeedsAPointer @@ -124,8 +126,8 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) } var ( - res interface{} - data interface{} + res any + data any err error ) @@ -134,7 +136,7 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) 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()) } } @@ -142,7 +144,7 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) data = root } else { baseRef := normalizeRef(ref, basePath) - data, _, _, err = r.load(baseRef.GetURL()) + data, err = r.load(baseRef.GetURL()) if err != nil { return err } @@ -155,43 +157,35 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) return err } } - return swag.DynamicJSONToStruct(res, target) + return jsonutils.FromDynamicJSON(res, target) } -func (r *schemaLoader) load(refURL *url.URL) (interface{}, 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) - unescaped, err := url.PathUnescape(normalized) - if err != nil { - return nil, url.URL{}, false, err - } - - u := url.URL{Path: unescaped} - - data, fromCache := r.cache.Get(u.RequestURI()) + 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 interface{} + 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. @@ -204,25 +198,14 @@ func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...strin foundCycle = true return } - foundCycle = swag.ContainsStrings(parentRefs, normalizedRef) // normalized windows url's are lower cased + foundCycle = stringutils.ContainsStrings(parentRefs, normalizedRef) // normalized windows url's are lower cased if foundCycle { r.context.circulars[normalizedRef] = true } return } -// Resolve resolves a reference against basePath and stores the result in target. -// -// Resolve is not in charge of following references: it only resolves ref by following its URL. -// -// If the schema the ref is referring to holds nested refs, Resolve doesn't resolve them. -// -// If basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct -func (r *schemaLoader) Resolve(ref *Ref, target interface{}, basePath string) error { - return r.resolveRef(ref, target, basePath) -} - -func (r *schemaLoader) deref(input interface{}, parentRefs []string, basePath string) error { +func (r *schemaLoader) deref(input any, parentRefs []string, basePath string) error { var ref *Ref switch refable := input.(type) { case *Schema: @@ -274,7 +257,7 @@ func (r *schemaLoader) shouldStopOnError(err error) bool { return false } -func (r *schemaLoader) setSchemaID(target interface{}, id, basePath string) (string, string) { +func (r *schemaLoader) setSchemaID(target any, id, basePath string) (string, string) { debugLog("schema has ID: %s", id) // handling the case when id is a folder @@ -306,11 +289,11 @@ func (r *schemaLoader) setSchemaID(target interface{}, id, basePath string) (str } func defaultSchemaLoader( - root interface{}, + root any, expandOptions *ExpandOptions, cache ResolutionCache, - context *resolverContext) *schemaLoader { - + context *resolverContext, +) *schemaLoader { if expandOptions == nil { expandOptions = &ExpandOptions{} } diff --git a/schema_loader_test.go b/schema_loader_test.go new file mode 100644 index 00000000..cc682d7d --- /dev/null +++ b/schema_loader_test.go @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import ( + "encoding/json" + "path/filepath" + "testing" + + "github.com/go-openapi/testify/v2/require" +) + +func TestLoader_Issue145(t *testing.T) { + t.Run("with ExpandSpec", func(t *testing.T) { + basePath := filepath.Join("fixtures", "bugs", "145", "Program Files (x86)", "AppName", "todos.json") + todosDoc, err := jsonDoc(basePath) + require.NoError(t, err) + + spec := new(Swagger) + require.NoError(t, json.Unmarshal(todosDoc, spec)) + + require.NoError(t, ExpandSpec(spec, &ExpandOptions{RelativeBase: basePath})) + }) + + t.Run("with ExpandSchema", func(t *testing.T) { + basePath := filepath.Join("fixtures", "bugs", "145", "Program Files (x86)", "AppName", "ref.json") + schemaDoc, err := jsonDoc(basePath) + require.NoError(t, err) + + sch := new(Schema) + require.NoError(t, json.Unmarshal(schemaDoc, sch)) + + require.NoError(t, ExpandSchema(sch, nil, nil)) + }) +} diff --git a/schema_test.go b/schema_test.go index 110764e2..7d296a5b 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,12 +7,13 @@ import ( "encoding/json" "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/swag/conv" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -var schema = Schema{ - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{"x-framework": "go-swagger"}}, +var schema = Schema{ //nolint:gochecknoglobals // test fixture + VendorExtensible: VendorExtensible{Extensions: map[string]any{"x-framework": "go-swagger"}}, SchemaProps: SchemaProps{ Ref: MustCreateRef("Cat"), Type: []string{"string"}, @@ -42,7 +32,7 @@ var schema = Schema{ MinItems: int64Ptr(5), UniqueItems: true, MultipleOf: float64Ptr(5), - Enum: []interface{}{"hello", "world"}, + Enum: []any{"hello", "world"}, MaxProperties: int64Ptr(5), MinProperties: int64Ptr(1), Required: []string{"id", "name"}, @@ -65,12 +55,12 @@ var schema = Schema{ Description: "the documentation etc", URL: "http://readthedocs.org/swagger", }, - Example: []interface{}{ - map[string]interface{}{ + Example: []any{ + map[string]any{ "id": 1, "name": "a book", }, - map[string]interface{}{ + map[string]any{ "id": 2, "name": "the thing", }, @@ -78,6 +68,7 @@ var schema = Schema{ }, } +//nolint:gochecknoglobals // test fixture var schemaJSON = `{ "x-framework": "go-swagger", "$ref": "Cat", @@ -149,75 +140,73 @@ var schemaJSON = `{ ` func TestSchema(t *testing.T) { - - expected := map[string]interface{}{} - _ = json.Unmarshal([]byte(schemaJSON), &expected) - b, err := json.Marshal(schema) - if assert.NoError(t, err) { - var actual map[string]interface{} - _ = json.Unmarshal(b, &actual) - assert.Equal(t, expected, actual) - } + assert.JSONMarshalAsT(t, schemaJSON, schema) actual2 := Schema{} - if assert.NoError(t, json.Unmarshal([]byte(schemaJSON), &actual2)) { - assert.Equal(t, schema.Ref, actual2.Ref) - assert.Equal(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.Equal(t, schema.MaxLength, actual2.MaxLength) - assert.Equal(t, schema.MinLength, actual2.MinLength) - assert.Equal(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.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.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.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 := actual2.Example.([]interface{}) - expEx := schema.Example.([]interface{}) - ex1 := examples[0].(map[string]interface{}) - ex2 := examples[1].(map[string]interface{}) - exp1 := expEx[0].(map[string]interface{}) - exp2 := expEx[1].(map[string]interface{}) + require.NoError(t, json.Unmarshal([]byte(schemaJSON), &actual2)) - assert.EqualValues(t, exp1["id"], ex1["id"]) - assert.Equal(t, exp1["name"], ex1["name"]) - assert.EqualValues(t, exp2["id"], ex2["id"]) - assert.Equal(t, exp2["name"], ex2["name"]) - } + assert.Equal(t, schema.Ref, actual2.Ref) + assert.EqualT(t, schema.Description, actual2.Description) + assert.Equal(t, schema.Maximum, actual2.Maximum) + assert.Equal(t, schema.Minimum, actual2.Minimum) + 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.EqualT(t, schema.Pattern, actual2.Pattern) + assert.Equal(t, schema.MaxItems, actual2.MaxItems) + assert.Equal(t, schema.MinItems, actual2.MinItems) + 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.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.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) + require.TrueT(t, ok, "expected []any for actual2.Example") + expEx, ok := schema.Example.([]any) + require.TrueT(t, ok, "expected []any for schema.Example") + ex1, ok := examples[0].(map[string]any) + require.TrueT(t, ok, "expected map[string]any for examples[0]") + ex2, ok := examples[1].(map[string]any) + require.TrueT(t, ok, "expected map[string]any for examples[1]") + exp1, ok := expEx[0].(map[string]any) + require.TrueT(t, ok, "expected map[string]any for expEx[0]") + exp2, ok := expEx[1].(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"]) + assert.EqualValues(t, exp2["id"], ex2["id"]) + assert.Equal(t, exp2["name"], ex2["name"]) } func BenchmarkSchemaUnmarshal(b *testing.B) { - for i := 0; i < b.N; i++ { + for b.Loop() { sch := &Schema{} _ = sch.UnmarshalJSON([]byte(schemaJSON)) } } func TestSchemaWithValidation(t *testing.T) { - s := new(Schema).WithValidations(SchemaValidations{CommonValidations: CommonValidations{MaxLength: swag.Int64(15)}}) - assert.EqualValues(t, swag.Int64(15), s.MaxLength) + s := new(Schema).WithValidations(SchemaValidations{CommonValidations: CommonValidations{MaxLength: conv.Pointer(int64(15))}}) + assert.Equal(t, conv.Pointer(int64(15)), s.MaxLength) val := mkVal() s = new(Schema).WithValidations(val) - assert.EqualValues(t, val, s.Validations()) + assert.Equal(t, val, s.Validations()) } 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 9d0bdae9..6d9019e7 100644 --- a/security_scheme.go +++ b/security_scheme.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,7 +7,7 @@ import ( "encoding/json" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) const ( @@ -31,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, @@ -50,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, @@ -59,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, @@ -68,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, @@ -78,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"` @@ -90,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) @@ -108,8 +97,8 @@ type SecurityScheme struct { SecuritySchemeProps } -// JSONLookup implements an interface to customize json pointer lookup -func (s SecurityScheme) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -118,7 +107,7 @@ func (s SecurityScheme) JSONLookup(token string) (interface{}, error) { return r, err } -// MarshalJSON marshal this to JSON +// MarshalJSON marshal this to JSON. func (s SecurityScheme) MarshalJSON() ([]byte, error) { var ( b1 []byte @@ -158,10 +147,10 @@ func (s SecurityScheme) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + 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 7d38b6e6..4eba04b2 100644 --- a/spec.go +++ b/spec.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -24,13 +13,13 @@ import ( //go:generate perl -pi -e s,Json,JSON,g bindata.go const ( - // SwaggerSchemaURL the url for the swagger 2.0 schema to validate specs + // SwaggerSchemaURL the url for the swagger 2.0 schema to validate specs. SwaggerSchemaURL = "http://swagger.io/v2/schema.json#" - // JSONSchemaURL the url for the json schema schema + // JSONSchemaURL the url for the json schema. 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 { @@ -39,9 +28,9 @@ func MustLoadJSONSchemaDraft04() *Schema { return d } -// JSONSchemaDraft04 loads the json schema document for json shema draft04 +// JSONSchemaDraft04 loads the json schema document for json schema draft04. func JSONSchemaDraft04() (*Schema, error) { - b, err := Asset("jsonschema-draft-04.json") + b, err := jsonschemaDraft04JSONBytes() if err != nil { return nil, err } @@ -53,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 { @@ -62,10 +51,9 @@ 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 := Asset("v2/schema.json") + b, err := v2SchemaJSONBytes() if err != nil { return nil, err } diff --git a/spec_test.go b/spec_test.go index 8274c955..05976478 100644 --- a/spec_test.go +++ b/spec_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec_test @@ -20,11 +9,43 @@ import ( "testing" "github.com/go-openapi/spec" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) // Test unitary fixture for dev and bug fixing + +func TestSpec_Issue2743(t *testing.T) { + t.Run("should expand but produce unresolvable $ref", func(t *testing.T) { + path := filepath.Join("fixtures", "bugs", "2743", "working", "spec.yaml") + sp := loadOrFail(t, path) + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: true, PathLoader: testLoader}), + ) + + t.Run("all $ref do not resolve when expanding again", func(t *testing.T) { + err := spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader}) + require.Error(t, err) + require.ErrorContains(t, err, filepath.FromSlash("swagger/paths/swagger/user/index.yml")) + }) + }) + + t.Run("should expand and produce resolvable $ref", func(t *testing.T) { + path := filepath.Join("fixtures", "bugs", "2743", "not-working", "spec.yaml") + sp := loadOrFail(t, path) + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: true, PathLoader: testLoader}), + ) + + t.Run("all $ref properly resolve when expanding again", func(t *testing.T) { + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader}), + ) + require.StringNotContainsTf(t, asJSON(t, sp), "$ref", "all $ref's should have been expanded properly") + }) + }) +} + func TestSpec_Issue1429(t *testing.T) { path := filepath.Join("fixtures", "bugs", "1429", "swagger.yaml") @@ -34,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) @@ -48,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/") } } @@ -63,7 +84,7 @@ func assertPaths1429(t testing.TB, sp *spec.Swagger) { require.NotNilf(t, param.Schema, "expected param schema not to be nil") // all param fixtures are body param with schema // all $ref expanded - assert.Equal(t, "", param.Schema.Ref.String()) + assert.Empty(t, param.Schema.Ref.String()) } for code, response := range pi.Get.Responses.StatusCodeResponses { @@ -73,7 +94,7 @@ func assertPaths1429(t testing.TB, sp *spec.Swagger) { continue } require.NotNilf(t, response.Schema, "expected response schema not to be nil") - assert.Equal(t, "", response.Schema.Ref.String()) + assert.Empty(t, response.Schema.Ref.String()) } } } @@ -87,18 +108,18 @@ func assertPaths1429SkipSchema(t testing.TB, sp *spec.Swagger) { switch param.Name { case "plainRequest": // this one is expanded - assert.Equal(t, "", param.Schema.Ref.String()) + assert.Empty(t, param.Schema.Ref.String()) 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#/") } @@ -109,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.Equal(t, "", response.Schema.Ref.String()) + 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) } } } @@ -128,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 d4b87f4f..f917ff16 100644 --- a/structs_test.go +++ b/structs_test.go @@ -1,119 +1,54 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( - "encoding/json" - "reflect" "testing" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v2" + "github.com/go-openapi/testify/v2/assert" ) -func assertSerializeJSON(t testing.TB, actual interface{}, expected string) bool { - ser, err := json.Marshal(actual) - if err != nil { - return assert.Fail(t, "unable to marshal to json (%s): %#v", err, actual) - } - return assert.Equal(t, string(ser), expected) -} - -func assertSerializeYAML(t testing.TB, actual interface{}, expected string) bool { - ser, err := yaml.Marshal(actual) - if err != nil { - return assert.Fail(t, "unable to marshal to yaml (%s): %#v", err, actual) - } - return assert.Equal(t, string(ser), expected) -} - -func derefTypeOf(expected interface{}) (tpe reflect.Type) { - tpe = reflect.TypeOf(expected) - if tpe.Kind() == reflect.Ptr { - tpe = tpe.Elem() - } - return -} - -func isPointed(expected interface{}) (pointed bool) { - tpe := reflect.TypeOf(expected) - if tpe.Kind() == reflect.Ptr { - pointed = true - } - return -} - -func assertParsesJSON(t testing.TB, actual string, expected interface{}) bool { - parsed := reflect.New(derefTypeOf(expected)) - err := json.Unmarshal([]byte(actual), parsed.Interface()) - if err != nil { - return assert.Fail(t, "unable to unmarshal from json (%s): %s", err, actual) - } - act := parsed.Interface() - if !isPointed(expected) { - act = reflect.Indirect(parsed).Interface() - } - return assert.Equal(t, act, expected) -} - -func assertParsesYAML(t testing.TB, actual string, expected interface{}) bool { - parsed := reflect.New(derefTypeOf(expected)) - err := yaml.Unmarshal([]byte(actual), parsed.Interface()) - if err != nil { - return assert.Fail(t, "unable to unmarshal from yaml (%s): %s", err, actual) - } - act := parsed.Interface() - if !isPointed(expected) { - act = reflect.Indirect(parsed).Interface() - } - return assert.EqualValues(t, act, expected) -} - 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{ + {SchemaProps: SchemaProps{Type: []string{"string"}}}, + }, + }) + 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{Schema: &Schema{ - SchemaProps: SchemaProps{Type: []string{"string"}}}, - }) - assertParsesJSON(t, "[{\"type\":\"string\"},{\"type\":\"string\"}]", &SchemaOrArray{ + assert.JSONUnmarshalAsT(t, SchemaOrArray{ + Schema: &Schema{ + SchemaProps: SchemaProps{Type: []string{"string"}}, + }, + }, `{"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 44722ffd..dbe32db8 100644 --- a/swagger.go +++ b/swagger.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -19,10 +8,11 @@ import ( "encoding/gob" "encoding/json" "fmt" + "slices" "strconv" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "github.com/go-openapi/swag/jsonutils" ) // Swagger this is the root document object for the API specification. @@ -35,8 +25,8 @@ type Swagger struct { SwaggerProps } -// JSONLookup look up a value by the json property name -func (s Swagger) JSONLookup(token string) (interface{}, error) { +// 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 } @@ -44,7 +34,7 @@ func (s Swagger) JSONLookup(token string) (interface{}, 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 { @@ -54,10 +44,10 @@ func (s Swagger) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + 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 { @@ -70,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 { @@ -84,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 @@ -105,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"` @@ -136,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), @@ -181,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 @@ -217,17 +207,17 @@ 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 -func (s SchemaOrBool) JSONLookup(token string) (interface{}, error) { +// JSONLookup implements an interface to customize json pointer lookup. +func (s SchemaOrBool) JSONLookup(token string) (any, error) { if token == "allows" { return s.Allows, nil } @@ -235,10 +225,12 @@ func (s SchemaOrBool) JSONLookup(token string) (interface{}, error) { return r, err } -var jsTrue = []byte("true") -var jsFalse = []byte("false") +var ( + 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) @@ -250,10 +242,10 @@ 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) >= 4 { + if len(data) > 0 { if data[0] == '{' { var sch Schema if err := json.Unmarshal(data, &sch); err != nil { @@ -261,25 +253,25 @@ func (s *SchemaOrBool) UnmarshalJSON(data []byte) error { } nw.Schema = &sch } - nw.Allows = !(data[0] == 'f' && data[1] == 'a' && data[2] == 'l' && data[3] == 's' && data[4] == 'e') + nw.Allows = !bytes.Equal(data, []byte("false")) } *s = nw 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 -func (s SchemaOrStringArray) JSONLookup(token string) (interface{}, error) { +// 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) @@ -290,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 { @@ -328,21 +320,16 @@ 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 { - for _, str := range s { - if str == value { - return true - } - } - return false + return slices.Contains(s, value) } -// JSONLookup implements an interface to customize json pointer lookup -func (s SchemaOrArray) JSONLookup(token string) (interface{}, error) { +// 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) return r, err @@ -351,7 +338,7 @@ func (s SchemaOrArray) JSONLookup(token string) (interface{}, 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 { @@ -367,7 +354,7 @@ func (s *StringOrArray) UnmarshalJSON(data []byte) error { return nil } - var single interface{} + var single any if err := json.Unmarshal(data, &single); err != nil { return err } @@ -379,11 +366,11 @@ func (s *StringOrArray) UnmarshalJSON(data []byte) error { *s = StringOrArray([]string{v}) return nil default: - return fmt.Errorf("only string or array is allowed, not %T", single) + return fmt.Errorf("only string or array is allowed, not %T: %w", single, ErrSpec) } } -// 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]) @@ -392,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 @@ -406,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) @@ -414,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) @@ -422,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 23aa5094..9ec0cad9 100644 --- a/swagger_test.go +++ b/swagger_test.go @@ -1,35 +1,52 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec import ( "encoding/json" + "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) -var spec = Swagger{ +//nolint:gochecknoglobals // it's okay to have embedded test fixtures as globals +var ( + specJSON []byte + minimalJSONSpec []byte + miniJSONSpec []byte +) + +func init() { //nolint:gochecknoinits // it's okay to load embedded fixtures in init(). + // load embedded fixtures + + var err error + specJSON, err = fixtureAssets.ReadFile("fixtures/specs/spec.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } + + minimalJSONSpec, err = fixtureAssets.ReadFile("fixtures/specs/minimal_spec.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } + + miniJSONSpec, err = fixtureAssets.ReadFile("fixtures/specs/mini_spec.json") + if err != nil { + panic(fmt.Sprintf("could not find fixture: %v", err)) + } +} + +var spec = Swagger{ //nolint:gochecknoglobals // test fixture SwaggerProps: SwaggerProps{ ID: "http://localhost:3849/api-docs", Swagger: "2.0", Consumes: []string{"application/json", "application/x-yaml"}, Produces: []string{"application/json"}, Schemes: []string{"http", "https"}, - Info: &info, + Info: &testInfo, Host: "some.api.out.there", BasePath: "/", Paths: &paths, @@ -53,61 +70,12 @@ var spec = Swagger{ Tags: []Tag{NewTag("pets", "", nil)}, ExternalDocs: &ExternalDocumentation{Description: "the name", URL: "the url"}, }, - VendorExtensible: VendorExtensible{Extensions: map[string]interface{}{ + VendorExtensible: VendorExtensible{Extensions: map[string]any{ "x-some-extension": "vendor", - "x-schemes": []interface{}{"unix", "amqp"}, + "x-schemes": []any{"unix", "amqp"}, }}, } -const specJSON = `{ - "id": "http://localhost:3849/api-docs", - "consumes": ["application/json", "application/x-yaml"], - "produces": ["application/json"], - "schemes": ["http", "https"], - "swagger": "2.0", - "info": { - "contact": { - "name": "wordnik api team", - "url": "http://developer.wordnik.com" - }, - "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0` + - ` specification", - "license": { - "name": "Creative Commons 4.0 International", - "url": "http://creativecommons.org/licenses/by/4.0/" - }, - "termsOfService": "http://helloreverb.com/terms/", - "title": "Swagger Sample API", - "version": "1.0.9-abcd", - "x-framework": "go-swagger" - }, - "host": "some.api.out.there", - "basePath": "/", - "paths": {"x-framework":"go-swagger","/":{"$ref":"cats"}}, - "definitions": { "Category": { "type": "string"} }, - "parameters": { - "categoryParam": { - "name": "category", - "in": "query", - "type": "string" - } - }, - "responses": { "EmptyAnswer": { "description": "no data to return for this operation" } }, - "securityDefinitions": { - "internalApiKey": { - "type": "apiKey", - "in": "header", - "name": "api_key" - } - }, - "security": [{"internalApiKey":[]}], - "tags": [{"name":"pets"}], - "externalDocs": {"description":"the name","url":"the url"}, - "x-some-extension": "vendor", - "x-schemes": ["unix","amqp"] -}` - -// // func verifySpecSerialize(specJSON []byte, spec Swagger) { // expected := map[string]interface{}{} // json.Unmarshal(specJSON, &expected) @@ -120,96 +88,97 @@ const specJSON = `{ // } /* -// assertEquivalent is currently unused -func assertEquivalent(t testing.TB, actual, expected interface{}) bool { - if actual == nil || expected == nil || reflect.DeepEqual(actual, expected) { - return true - } - - actualType := reflect.TypeOf(actual) - expectedType := reflect.TypeOf(expected) - if reflect.TypeOf(actual).ConvertibleTo(expectedType) { - expectedValue := reflect.ValueOf(expected) - if swag.IsZero(expectedValue) && swag.IsZero(reflect.ValueOf(actual)) { + // assertEquivalent is currently unused + func assertEquivalent(t testing.TB, actual, expected interface{}) bool { + if actual == nil || expected == nil || reflect.DeepEqual(actual, expected) { return true } - // Attempt comparison after type conversion - if reflect.DeepEqual(actual, expectedValue.Convert(actualType).Interface()) { + actualType := reflect.TypeOf(actual) + expectedType := reflect.TypeOf(expected) + if reflect.TypeOf(actual).ConvertibleTo(expectedType) { + expectedValue := reflect.ValueOf(expected) + if typeutil.IsZero(expectedValue) && typeutils.IsZero(reflect.ValueOf(actual)) { + return true + } + + // Attempt comparison after type conversion + if reflect.DeepEqual(actual, expectedValue.Convert(actualType).Interface()) { + return true + } + } + + // Last ditch effort + if fmt.Sprintf("%#v", expected) == fmt.Sprintf("%#v", actual) { return true } + errFmt := "Expected: '%[1]T(%[1]#v)'\nActual: '%[2]T(%[2]#v)'\n(Should be equivalent)!" + return assert.Fail(t, errFmt, expected, actual) } - // Last ditch effort - if fmt.Sprintf("%#v", expected) == fmt.Sprintf("%#v", actual) { - return true - } - errFmt := "Expected: '%T(%#v)'\nActual: '%T(%#v)'\n(Should be equivalent)!" - return assert.Fail(t, errFmt, expected, expected, actual, actual) -} + // ShouldBeEquivalentTo is currently unused + func ShouldBeEquivalentTo(actual interface{}, expecteds ...interface{}) string { + expected := expecteds[0] + if actual == nil || expected == nil { + return "" + } -// ShouldBeEquivalentTo is currently unused -func ShouldBeEquivalentTo(actual interface{}, expecteds ...interface{}) string { - expected := expecteds[0] - if actual == nil || expected == nil { - return "" - } + if reflect.DeepEqual(expected, actual) { + return "" + } - if reflect.DeepEqual(expected, actual) { - return "" - } + actualType := reflect.TypeOf(actual) + expectedType := reflect.TypeOf(expected) + if reflect.TypeOf(actual).ConvertibleTo(expectedType) { + expectedValue := reflect.ValueOf(expected) + if typeutils.IsZero(expectedValue) && typeutils.IsZero(reflect.ValueOf(actual)) { + return "" + } - actualType := reflect.TypeOf(actual) - expectedType := reflect.TypeOf(expected) - if reflect.TypeOf(actual).ConvertibleTo(expectedType) { - expectedValue := reflect.ValueOf(expected) - if swag.IsZero(expectedValue) && swag.IsZero(reflect.ValueOf(actual)) { - return "" + // Attempt comparison after type conversion + if reflect.DeepEqual(actual, expectedValue.Convert(actualType).Interface()) { + return "" + } } - // Attempt comparison after type conversion - if reflect.DeepEqual(actual, expectedValue.Convert(actualType).Interface()) { + // Last ditch effort + if fmt.Sprintf("%#v", expected) == fmt.Sprintf("%#v", actual) { return "" } + errFmt := "Expected: '%[1]T(%[1]#v)'\nActual: '%[2]T(%[2]#v)'\n(Should be equivalent)!" + return fmt.Sprintf(errFmt, expected, actual) } - // Last ditch effort - if fmt.Sprintf("%#v", expected) == fmt.Sprintf("%#v", actual) { - return "" - } - errFmt := "Expected: '%T(%#v)'\nActual: '%T(%#v)'\n(Should be equivalent)!" - return fmt.Sprintf(errFmt, expected, expected, actual, actual) - -} - -// assertSpecMaps is currently unused -func assertSpecMaps(t testing.TB, actual, expected map[string]interface{}) bool { - res := true - if id, ok := expected["id"]; ok { - res = assert.Equal(t, id, actual["id"]) + // assertSpecMaps is currently unused + func assertSpecMaps(t testing.TB, actual, expected map[string]interface{}) bool { + res := true + if id, ok := expected["id"]; ok { + res = assert.Equal(t, id, actual["id"]) + } + res = res && assert.Equal(t, expected["consumes"], actual["consumes"]) + res = res && assert.Equal(t, expected["produces"], actual["produces"]) + res = res && assert.Equal(t, expected["schemes"], actual["schemes"]) + res = res && assert.Equal(t, expected["swagger"], actual["swagger"]) + res = res && assert.Equal(t, expected["info"], actual["info"]) + res = res && assert.Equal(t, expected["host"], actual["host"]) + res = res && assert.Equal(t, expected["basePath"], actual["basePath"]) + res = res && assert.Equal(t, expected["paths"], actual["paths"]) + res = res && assert.Equal(t, expected["definitions"], actual["definitions"]) + res = res && assert.Equal(t, expected["responses"], actual["responses"]) + res = res && assert.Equal(t, expected["securityDefinitions"], actual["securityDefinitions"]) + res = res && assert.Equal(t, expected["tags"], actual["tags"]) + res = res && assert.Equal(t, expected["externalDocs"], actual["externalDocs"]) + res = res && assert.Equal(t, expected["x-some-extension"], actual["x-some-extension"]) + res = res && assert.Equal(t, expected["x-schemes"], actual["x-schemes"]) + + return res } - res = res && assert.Equal(t, expected["consumes"], actual["consumes"]) - res = res && assert.Equal(t, expected["produces"], actual["produces"]) - res = res && assert.Equal(t, expected["schemes"], actual["schemes"]) - res = res && assert.Equal(t, expected["swagger"], actual["swagger"]) - res = res && assert.Equal(t, expected["info"], actual["info"]) - res = res && assert.Equal(t, expected["host"], actual["host"]) - res = res && assert.Equal(t, expected["basePath"], actual["basePath"]) - res = res && assert.Equal(t, expected["paths"], actual["paths"]) - res = res && assert.Equal(t, expected["definitions"], actual["definitions"]) - res = res && assert.Equal(t, expected["responses"], actual["responses"]) - res = res && assert.Equal(t, expected["securityDefinitions"], actual["securityDefinitions"]) - res = res && assert.Equal(t, expected["tags"], actual["tags"]) - res = res && assert.Equal(t, expected["externalDocs"], actual["externalDocs"]) - res = res && assert.Equal(t, expected["x-some-extension"], actual["x-some-extension"]) - res = res && assert.Equal(t, expected["x-schemes"], actual["x-schemes"]) - - return res -} */ -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, actual, expected) + assert.Equal(t, expected, actual) } /* @@ -233,173 +202,99 @@ func assertSpecJSON(t testing.TB, specJSON []byte) bool { if !assert.NoError(t, json.Unmarshal(cb, &actual)) { return false } - return assertSpecMaps(t, actual, expected) + return assertSpecMaps(t, expected, actual ) } */ func TestSwaggerSpec_Serialize(t *testing.T) { - expected := make(map[string]interface{}) - _ = json.Unmarshal([]byte(specJSON), &expected) + expected := make(map[string]any) + _ = json.Unmarshal(specJSON, &expected) b, err := json.MarshalIndent(spec, "", " ") - if assert.NoError(t, err) { - var actual map[string]interface{} - err := json.Unmarshal(b, &actual) - if assert.NoError(t, err) { - assert.EqualValues(t, actual, expected) - } - } + require.NoError(t, err) + var actual map[string]any + require.NoError(t, json.Unmarshal(b, &actual)) + assert.Equal(t, expected, actual) } func TestSwaggerSpec_Deserialize(t *testing.T) { var actual Swagger - err := json.Unmarshal([]byte(specJSON), &actual) - if assert.NoError(t, err) { - assert.EqualValues(t, actual, spec) - } + require.NoError(t, json.Unmarshal(specJSON, &actual)) + assert.Equal(t, actual, spec) } func TestVendorExtensionStringSlice(t *testing.T) { var actual Swagger - err := json.Unmarshal([]byte(specJSON), &actual) - if assert.NoError(t, err) { - schemes, ok := actual.Extensions.GetStringSlice("x-schemes") - if assert.True(t, ok) { - assert.EqualValues(t, []string{"unix", "amqp"}, schemes) - } - notSlice, ok := actual.Extensions.GetStringSlice("x-some-extension") - assert.Nil(t, notSlice) - assert.False(t, ok) - - actual.AddExtension("x-another-ext", 100) - notString, ok := actual.Extensions.GetStringSlice("x-another-ext") - assert.Nil(t, notString) - assert.False(t, ok) - - actual.AddExtension("x-another-slice-ext", []interface{}{100, 100}) - notStringSlice, ok := actual.Extensions.GetStringSlice("x-another-slice-ext") - assert.Nil(t, notStringSlice) - assert.False(t, ok) - - _, ok = actual.Extensions.GetStringSlice("x-notfound-ext") - assert.False(t, ok) - } + require.NoError(t, json.Unmarshal(specJSON, &actual)) + schemes, ok := actual.Extensions.GetStringSlice("x-schemes") + require.TrueT(t, ok) + assert.Equal(t, []string{"unix", "amqp"}, schemes) + + notSlice, ok := actual.Extensions.GetStringSlice("x-some-extension") + assert.Nil(t, notSlice) + assert.FalseT(t, ok) + + actual.AddExtension("x-another-ext", 100) + notString, ok := actual.Extensions.GetStringSlice("x-another-ext") + assert.Nil(t, notString) + 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.FalseT(t, ok) + + _, ok = actual.Extensions.GetStringSlice("x-notfound-ext") + assert.FalseT(t, ok) } func TestOptionalSwaggerProps_Serialize(t *testing.T) { - minimalJSONSpec := []byte(`{ - "swagger": "2.0", - "info": { - "version": "0.0.0", - "title": "Simple API" - }, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "OK" - } - } - } - } - } -}`) - var minimalSpec Swagger - err := json.Unmarshal(minimalJSONSpec, &minimalSpec) - if assert.NoError(t, err) { - bytes, err := json.Marshal(&minimalSpec) - if assert.NoError(t, err) { - var ms map[string]interface{} - if err := json.Unmarshal(bytes, &ms); assert.NoError(t, err) { - 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") - } - } - } + err := json.Unmarshal(miniJSONSpec, &minimalSpec) + require.NoError(t, err) + bytes, err := json.Marshal(&minimalSpec) + require.NoError(t, err) + + var ms map[string]any + require.NoError(t, json.Unmarshal(bytes, &ms)) + + 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") } -var minimalJSONSpec = []byte(`{ - "swagger": "2.0", - "info": { - "version": "0.0.0", - "title": "Simple API" - }, - "securityDefinitions": { - "basic": { - "type": "basic" - }, - "apiKey": { - "type": "apiKey", - "in": "header", - "name": "X-API-KEY" - }, - "queryKey": { - "type": "apiKey", - "in": "query", - "name": "api_key" - } - }, - "paths": { - "/": { - "get": { - "security": [ - { - "apiKey": [], - "basic": [] - }, - {}, - { - "queryKey": [], - "basic": [] - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - } - } - }`) - func TestSecurityRequirements(t *testing.T) { var minimalSpec Swagger - err := json.Unmarshal(minimalJSONSpec, &minimalSpec) - if assert.NoError(t, err) { - sec := minimalSpec.Paths.Paths["/"].Get.Security - require.Len(t, sec, 3) - assert.Contains(t, sec[0], "basic") - assert.Contains(t, sec[0], "apiKey") - assert.NotNil(t, sec[1]) - assert.Empty(t, sec[1]) - assert.Contains(t, sec[2], "queryKey") - } + require.NoError(t, json.Unmarshal(minimalJSONSpec, &minimalSpec)) + + sec := minimalSpec.Paths.Paths["/"].Get.Security + require.Len(t, sec, 3) + assert.MapContainsT(t, sec[0], "basic") + assert.MapContainsT(t, sec[0], "apiKey") + assert.NotNil(t, sec[1]) + assert.Empty(t, sec[1]) + assert.MapContainsT(t, sec[2], "queryKey") } func TestSwaggerGobEncoding(t *testing.T) { doTestSwaggerGobEncoding(t, specJSON) - doTestSwaggerGobEncoding(t, string(minimalJSONSpec)) + doTestSwaggerGobEncoding(t, minimalJSONSpec) } -func doTestSwaggerGobEncoding(t *testing.T, fixture string) { - var src, dst Swagger +func doTestSwaggerGobEncoding(t *testing.T, fixture []byte) { + t.Helper() - if !assert.NoError(t, json.Unmarshal([]byte(fixture), &src)) { - t.FailNow() - } + var src, dst Swagger + require.NoError(t, json.Unmarshal(fixture, &src)) doTestAnyGobEncoding(t, &src, &dst) } diff --git a/tag.go b/tag.go index faa3d3de..af3fb0a4 100644 --- a/tag.go +++ b/tag.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,21 +7,16 @@ import ( "encoding/json" "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" + "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"` ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` } -// NewTag creates a new tag -func NewTag(name, description string, externalDocs *ExternalDocumentation) Tag { - return Tag{TagProps: TagProps{Description: description, Name: name, ExternalDocs: externalDocs}} -} - // Tag allows adding meta data to a single tag that is used by the // [Operation Object](http://goo.gl/8us55a#operationObject). // It is not mandatory to have a Tag Object per tag used there. @@ -43,8 +27,13 @@ type Tag struct { TagProps } -// JSONLookup implements an interface to customize json pointer lookup -func (t Tag) JSONLookup(token string) (interface{}, error) { +// NewTag creates a new tag. +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. +func (t Tag) JSONLookup(token string) (any, error) { if ex, ok := t.Extensions[token]; ok { return &ex, nil } @@ -53,7 +42,7 @@ func (t Tag) JSONLookup(token string) (interface{}, 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 { @@ -63,10 +52,10 @@ func (t Tag) MarshalJSON() ([]byte, error) { if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + 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/url_go18.go b/url_go18.go deleted file mode 100644 index 60b78515..00000000 --- a/url_go18.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build !go1.19 -// +build !go1.19 - -package spec - -import "net/url" - -var parseURL = url.Parse diff --git a/url_go19.go b/url_go19.go index 392e3e63..8d0c81ac 100644 --- a/url_go19.go +++ b/url_go19.go @@ -1,5 +1,5 @@ -//go:build go1.19 -// +build go1.19 +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec diff --git a/validations.go b/validations.go index 6360a8ea..a82c2ffe 100644 --- a/validations.go +++ b/validations.go @@ -1,19 +1,22 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + 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"` - Minimum *float64 `json:"minimum,omitempty"` - ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` - MaxLength *int64 `json:"maxLength,omitempty"` - MinLength *int64 `json:"minLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - MaxItems *int64 `json:"maxItems,omitempty"` - MinItems *int64 `json:"minItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty"` - Enum []interface{} `json:"enum,omitempty"` + Maximum *float64 `json:"maximum,omitempty"` + ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` + Minimum *float64 `json:"minimum,omitempty"` + ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` + MaxLength *int64 `json:"maxLength,omitempty"` + MinLength *int64 `json:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + MaxItems *int64 `json:"maxItems,omitempty"` + MinItems *int64 `json:"minItems,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty"` + Enum []any `json:"enum,omitempty"` } // SetValidations defines all validations for a simple schema. @@ -37,12 +40,12 @@ func (v *CommonValidations) SetValidations(val SchemaValidations) { type clearedValidation struct { Validation string - Value interface{} + Value any } type clearedValidations []clearedValidation -func (c clearedValidations) apply(cbs []func(string, interface{})) { +func (c clearedValidations) apply(cbs []func(string, any)) { for _, cb := range cbs { for _, cleared := range c { cb(cleared.Validation, cleared.Value) @@ -53,8 +56,9 @@ func (c clearedValidations) apply(cbs []func(string, interface{})) { // ClearNumberValidations clears all number validations. // // Some callbacks may be set by the caller to capture changed values. -func (v *CommonValidations) ClearNumberValidations(cbs ...func(string, interface{})) { - done := make(clearedValidations, 0, 5) +func (v *CommonValidations) ClearNumberValidations(cbs ...func(string, any)) { + const maxNumberValidations = 5 + done := make(clearedValidations, 0, maxNumberValidations) defer func() { done.apply(cbs) }() @@ -84,8 +88,9 @@ func (v *CommonValidations) ClearNumberValidations(cbs ...func(string, interface // ClearStringValidations clears all string validations. // // Some callbacks may be set by the caller to capture changed values. -func (v *CommonValidations) ClearStringValidations(cbs ...func(string, interface{})) { - done := make(clearedValidations, 0, 3) +func (v *CommonValidations) ClearStringValidations(cbs ...func(string, any)) { + const maxStringValidations = 3 + done := make(clearedValidations, 0, maxStringValidations) defer func() { done.apply(cbs) }() @@ -107,8 +112,9 @@ func (v *CommonValidations) ClearStringValidations(cbs ...func(string, interface // ClearArrayValidations clears all array validations. // // Some callbacks may be set by the caller to capture changed values. -func (v *CommonValidations) ClearArrayValidations(cbs ...func(string, interface{})) { - done := make(clearedValidations, 0, 3) +func (v *CommonValidations) ClearArrayValidations(cbs ...func(string, any)) { + const maxArrayValidations = 3 + done := make(clearedValidations, 0, maxArrayValidations) defer func() { done.apply(cbs) }() @@ -137,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 } @@ -160,7 +166,7 @@ func (v CommonValidations) HasEnum() bool { // SchemaValidations describes the validation properties of a schema // // NOTE: at this moment, this is not embedded in SchemaProps because this would induce a breaking change -// in the exported members: all initializers using litterals would fail. +// in the exported members: all initializers using literals would fail. type SchemaValidations struct { CommonValidations @@ -169,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 @@ -182,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 @@ -194,8 +200,9 @@ func (v SchemaValidations) Validations() SchemaValidations { // ClearObjectValidations returns a clone of the validations with all object validations cleared. // // Some callbacks may be set by the caller to capture changed values. -func (v *SchemaValidations) ClearObjectValidations(cbs ...func(string, interface{})) { - done := make(clearedValidations, 0, 3) +func (v *SchemaValidations) ClearObjectValidations(cbs ...func(string, any)) { + const maxObjectValidations = 3 + done := make(clearedValidations, 0, maxObjectValidations) defer func() { done.apply(cbs) }() diff --git a/validations_test.go b/validations_test.go index b62f82e9..21404cbb 100644 --- a/validations_test.go +++ b/validations_test.go @@ -1,116 +1,117 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + package spec import ( "testing" - "github.com/go-openapi/swag" - "github.com/stretchr/testify/require" + "github.com/go-openapi/swag/conv" + "github.com/go-openapi/testify/v2/require" ) func mkVal() SchemaValidations { return SchemaValidations{ CommonValidations: CommonValidations{ - Maximum: swag.Float64(2.5), + Maximum: conv.Pointer(2.5), ExclusiveMaximum: true, - Minimum: swag.Float64(3.4), + Minimum: conv.Pointer(3.4), ExclusiveMinimum: true, - MaxLength: swag.Int64(15), - MinLength: swag.Int64(16), + MaxLength: conv.Pointer(int64(15)), + MinLength: conv.Pointer(int64(16)), Pattern: "abc", - MaxItems: swag.Int64(17), - MinItems: swag.Int64(18), + MaxItems: conv.Pointer(int64(17)), + MinItems: conv.Pointer(int64(18)), UniqueItems: true, - MultipleOf: swag.Float64(4.4), - Enum: []interface{}{"a", 12.5}, + MultipleOf: conv.Pointer(4.4), + Enum: []any{"a", 12.5}, }, PatternProperties: SchemaProperties{ "x": *BooleanProperty(), "y": *BooleanProperty(), }, - MinProperties: swag.Int64(19), - MaxProperties: swag.Int64(20), + MinProperties: conv.Pointer(int64(19)), + MaxProperties: conv.Pointer(int64(20)), } } func TestValidations(t *testing.T) { - var cv CommonValidations val := mkVal() cv.SetValidations(val) expectedCV := val.CommonValidations - require.EqualValues(t, expectedCV, cv) + 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 expectedSV.PatternProperties = nil expectedSV.MinProperties = nil expectedSV.MaxProperties = nil - val = mkVal() val = mkVal() cv.SetValidations(val) - require.EqualValues(t, expectedSV, cv.Validations()) + require.Equal(t, expectedSV, cv.Validations()) var sv SchemaValidations val = mkVal() sv.SetValidations(val) expectedSV = val - require.EqualValues(t, expectedSV, sv) + require.Equal(t, expectedSV, sv) - require.EqualValues(t, val, sv.Validations()) + 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, value interface{}) { + sv.ClearObjectValidations(func(validation string, _ any) { switch validation { case "minProperties", "maxProperties", "patternProperties": return @@ -119,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 945a4670..07f7ef8c 100644 --- a/xml_object.go +++ b/xml_object.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -25,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 da616a9b..e1063eb0 100644 --- a/xml_object_test.go +++ b/xml_object_test.go @@ -1,16 +1,5 @@ -// Copyright 2015 go-swagger maintainers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 package spec @@ -18,15 +7,15 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" ) func TestXmlObject_Serialize(t *testing.T) { obj1 := XMLObject{} actual, err := json.Marshal(obj1) - if assert.NoError(t, err) { - assert.Equal(t, "{}", string(actual)) - } + require.NoError(t, err) + assert.EqualT(t, "{}", string(actual)) obj2 := XMLObject{ Name: "the name", @@ -37,24 +26,26 @@ func TestXmlObject_Serialize(t *testing.T) { } actual, err = json.Marshal(obj2) - if assert.NoError(t, err) { - var ad map[string]interface{} - if assert.NoError(t, json.Unmarshal(actual, &ad)) { - 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)) - } - } + require.NoError(t, err) + + var ad map[string]any + require.NoError(t, json.Unmarshal(actual, &ad)) + assert.Equal(t, obj2.Name, ad["name"]) + assert.Equal(t, obj2.Namespace, ad["namespace"]) + assert.Equal(t, obj2.Prefix, ad["prefix"]) + 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{} - if assert.NoError(t, json.Unmarshal([]byte("{}"), &actual)) { - assert.Equal(t, expected, actual) - } + require.NoError(t, json.Unmarshal([]byte("{}"), &actual)) + assert.EqualT(t, expected, actual) completed := `{"name":"the name","namespace":"the namespace","prefix":"the prefix","attribute":true,"wrapped":true}` expected = XMLObject{ @@ -64,8 +55,8 @@ func TestXmlObject_Deserialize(t *testing.T) { Attribute: true, Wrapped: true, } + actual = XMLObject{} - if assert.NoError(t, json.Unmarshal([]byte(completed), &actual)) { - assert.Equal(t, expected, actual) - } + require.NoError(t, json.Unmarshal([]byte(completed), &actual)) + assert.EqualT(t, expected, actual) }