diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 0000000..f830ad1 --- /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 0000000..d4b5e4e --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,45 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`github.com/go-openapi/loads` loads, parses, and analyzes Swagger/OpenAPI v2.0 specifications from local files or remote URLs in JSON and YAML formats. It is part of the `go-openapi` ecosystem. + +See [docs/MAINTAINERS.md](../docs/MAINTAINERS.md) for CI/CD, release process, and repo structure details. + +### Package layout (single package) + +| File | Contents | +|------|----------| +| `doc.go` | Package documentation | +| `spec.go` | `Document` type; main entry points: `Spec`, `JSONSpec`, `Analyzed`, `Embedded` | +| `loaders.go` | Loader chain (linked list of `DocLoaderWithMatch`); `JSONDoc`, `AddLoader` | +| `options.go` | `LoaderOption` functional options (`WithDocLoader`, `WithDocLoaderMatches`, `WithLoadingOptions`) | +| `errors.go` | Sentinel errors: `ErrLoads`, `ErrNoLoader` | +| `fmts/yaml.go` | Re-exports YAML utilities from `swag` (`YAMLMatcher`, `YAMLDoc`, `YAMLToJSON`, `BytesToYAMLDoc`) | + +### Key API + +- `Spec(path, ...LoaderOption) (*Document, error)` — main entry point, auto-detects JSON/YAML +- `JSONSpec(path, ...LoaderOption) (*Document, error)` — explicit JSON loading +- `Analyzed(data, version, ...LoaderOption) (*Document, error)` — from raw JSON bytes +- `Embedded(orig, flat, ...LoaderOption) (*Document, error)` — from pre-parsed specs +- `Document.Expanded() (*Document, error)` — resolves all `$ref` references +- `Document.Pristine() *Document` — deep clone via gob round-trip +- `AddLoader(DocMatcher, DocLoader)` — register custom loader at package level (not thread-safe) + +### Dependencies + +- `github.com/go-openapi/analysis` — spec analysis +- `github.com/go-openapi/spec` — Swagger v2.0 types +- `github.com/go-openapi/swag/loading` — HTTP/file loading +- `github.com/go-openapi/swag/yamlutils` — YAML conversion +- `github.com/go-openapi/testify/v2` — test-only assertions (zero-dep testify fork) + +### Notable historical design decisions + +- **Loader chain pattern**: linked list of `DocLoaderWithMatch` nodes; YAML matcher checked first, JSON loader is the fallback (matches any path). Extensible via `AddLoader()` or per-call `LoaderOption`. +- **Global `spec.PathLoader` bridge**: the `spec` package's `PathLoader` function pointer is set to this package's loader, enabling cross-package `$ref` resolution. +- **Deep cloning via gob**: `Pristine()` uses `encoding/gob` round-trip to deep-copy the full `Document`, preserving all nested structures. +- **Separate `origSpec`**: `Document` keeps an untouched copy of the original spec alongside the working copy, so expansion/mutation is non-destructive. diff --git a/.claude/rules/go-conventions.md b/.claude/rules/go-conventions.md new file mode 100644 index 0000000..08d1ad6 --- /dev/null +++ b/.claude/rules/go-conventions.md @@ -0,0 +1,10 @@ +--- +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`). diff --git a/.claude/rules/linting.md b/.claude/rules/linting.md new file mode 100644 index 0000000..a4456d4 --- /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 0000000..6974aba --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/*_test.go" +--- + +# Testing conventions (go-openapi) + +## Running tests + +**Single module repos:** + +```sh +go test ./... +``` + +**Mono-repos (with `go.work`):** + +```sh +# All modules +go test work ./... + +# Single module +go test ./conv/... +``` + +Note: in mono-repos, plain `go test ./...` only tests the root module. +The `work` pattern expands to all modules listed in `go.work`. + +CI runs tests on `{ubuntu, macos, windows} x {stable, oldstable}` with `-race` via `gotestsum`. + +## Fuzz tests + +```sh +# List all fuzz targets +go test -list Fuzz ./... + +# Run a specific target (go test -fuzz cannot span multiple packages) +go test -fuzz=Fuzz -run='FuzzTargetName$' -fuzztime=1m30s ./package +``` + +Fuzz corpus lives in `testdata/fuzz/` within each package. CI runs each fuzz target for 1m30s +with a 5m minimize timeout. + +## Test framework + +`github.com/go-openapi/testify/v2` — a zero-dep fork of `stretchr/testify`. +Because it's a fork, `testifylint` does not work. diff --git a/.cliff.toml b/.cliff.toml deleted file mode 100644 index 702629f..0000000 --- a/.cliff.toml +++ /dev/null @@ -1,181 +0,0 @@ -# git-cliff ~ configuration file -# https://git-cliff.org/docs/configuration - -[changelog] -header = """ -""" - -footer = """ - ------ - -**[{{ remote.github.repo }}]({{ self::remote_url() }}) license terms** - -[![License][license-badge]][license-url] - -[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg -[license-url]: {{ self::remote_url() }}/?tab=Apache-2.0-1-ov-file#readme - -{%- macro remote_url() -%} - https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} -{%- endmacro -%} -""" - -body = """ -{%- if version %} -## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} -{%- else %} -## [unreleased] -{%- endif %} -{%- if message %} - {%- raw %}\n{% endraw %} -{{ message }} - {%- raw %}\n{% endraw %} -{%- endif %} -{%- if version %} - {%- if previous.version %} - -**Full Changelog**: <{{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}> - {%- endif %} -{%- else %} - {%- raw %}\n{% endraw %} -{%- endif %} - -{%- if statistics %}{% if statistics.commit_count %} - {%- raw %}\n{% endraw %} -{{ statistics.commit_count }} commits in this release. - {%- raw %}\n{% endraw %} -{%- endif %}{% endif %} ------ - -{%- for group, commits in commits | group_by(attribute="group") %} - {%- raw %}\n{% endraw %} -### {{ group | upper_first }} - {%- raw %}\n{% endraw %} - {%- for commit in commits %} - {%- if commit.remote.pr_title %} - {%- set commit_message = commit.remote.pr_title %} - {%- else %} - {%- set commit_message = commit.message %} - {%- endif %} -* {{ commit_message | split(pat="\n") | first | trim }} - {%- if commit.remote.username %} -{%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }}) - {%- endif %} - {%- if commit.remote.pr_number %} -{%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) - {%- endif %} -{%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }}) - {%- endfor %} -{%- endfor %} - -{%- if github %} -{%- raw %}\n{% endraw -%} - {%- set all_contributors = github.contributors | length %} - {%- if github.contributors | filter(attribute="username", value="dependabot[bot]") | length < all_contributors %} ------ - -### People who contributed to this release - {% endif %} - {%- for contributor in github.contributors | filter(attribute="username") | sort(attribute="username") %} - {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %} -* [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) - {%- endif %} - {%- endfor %} - - {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} ------ - {%- raw %}\n{% endraw %} - -### New Contributors - {%- endif %} - - {%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} - {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %} -* @{{ contributor.username }} made their first contribution - {%- if contributor.pr_number %} - in [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ - {%- endif %} - {%- endif %} - {%- endfor %} -{%- endif %} - -{%- raw %}\n{% endraw %} - -{%- macro remote_url() -%} - https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} -{%- endmacro -%} -""" -# Remove leading and trailing whitespaces from the changelog's body. -trim = true -# Render body even when there are no releases to process. -render_always = true -# An array of regex based postprocessors to modify the changelog. -postprocessors = [ - # Replace the placeholder with a URL. - #{ pattern = '', replace = "https://github.com/orhun/git-cliff" }, -] -# output file path -# output = "test.md" - -[git] -# Parse commits according to the conventional commits specification. -# See https://www.conventionalcommits.org -conventional_commits = false -# Exclude commits that do not match the conventional commits specification. -filter_unconventional = false -# Require all commits to be conventional. -# Takes precedence over filter_unconventional. -require_conventional = false -# Split commits on newlines, treating each line as an individual commit. -split_commits = false -# An array of regex based parsers to modify commit messages prior to further processing. -commit_preprocessors = [ - # Replace issue numbers with link templates to be updated in `changelog.postprocessors`. - #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, - # Check spelling of the commit message using https://github.com/crate-ci/typos. - # If the spelling is incorrect, it will be fixed automatically. - #{ pattern = '.*', replace_command = 'typos --write-changes -' } -] -# Prevent commits that are breaking from being excluded by commit parsers. -protect_breaking_commits = false -# An array of regex based parsers for extracting data from the commit message. -# Assigns commits to groups. -# Optionally sets the commit's scope and can decide to exclude commits from further processing. -commit_parsers = [ - { message = "^[Cc]hore\\([Rr]elease\\): prepare for", skip = true }, - { message = "(^[Mm]erge)|([Mm]erge conflict)", skip = true }, - { field = "author.name", pattern = "dependabot*", group = "Updates" }, - { message = "([Ss]ecurity)|([Vv]uln)", group = "Security" }, - { body = "(.*[Ss]ecurity)|([Vv]uln)", group = "Security" }, - { message = "([Cc]hore\\(lint\\))|(style)|(lint)|(codeql)|(golangci)", group = "Code quality" }, - { message = "(^[Dd]oc)|((?i)readme)|(badge)|(typo)|(documentation)", group = "Documentation" }, - { message = "(^[Ff]eat)|(^[Ee]nhancement)", group = "Implemented enhancements" }, - { message = "(^ci)|(\\(ci\\))|(fixup\\s+ci)|(fix\\s+ci)|(license)|(example)", group = "Miscellaneous tasks" }, - { message = "^test", group = "Testing" }, - { message = "(^fix)|(panic)", group = "Fixed bugs" }, - { message = "(^refact)|(rework)", group = "Refactor" }, - { message = "(^[Pp]erf)|(performance)", group = "Performance" }, - { message = "(^[Cc]hore)", group = "Miscellaneous tasks" }, - { message = "^[Rr]evert", group = "Reverted changes" }, - { message = "(upgrade.*?go)|(go\\s+version)", group = "Updates" }, - { message = ".*", group = "Other" }, -] -# Exclude commits that are not matched by any commit parser. -filter_commits = false -# An array of link parsers for extracting external references, and turning them into URLs, using regex. -link_parsers = [] -# Include only the tags that belong to the current branch. -use_branch_tags = false -# Order releases topologically instead of chronologically. -topo_order = false -# Order releases topologically instead of chronologically. -topo_order_commits = true -# Order of commits in each group/release within the changelog. -# Allowed values: newest, oldest -sort_commits = "newest" -# Process submodules commits -recurse_submodules = false - -#[remote.github] -#owner = "go-openapi" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 85707f7..59ba87f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,48 +1,57 @@ -## Contribution Guidelines +You'll find here general guidelines to contribute to this project. +They mostly correspond to standard practices for open source repositories. -You'll find below general guidelines, which mostly correspond to standard practices for open sourced repositories. +We have tried to keep things as simple as possible. ->**TL;DR** -> -> If you're already an experienced go developer on github, then you should just feel at home with us +> [!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 find the usual guideline for a go library project on github. +> 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. -These guidelines are general to all libraries published on github by the `go-openapi` organization. +You'll find more detailed (or repo-specific) instructions in the [maintainer's docs][maintainers-doc]. -You'll find more detailed (or repo-specific) instructions in the [maintainer's docs](../docs). +[maintainers-doc]: ../docs/MAINTAINERS.md -## How can I contribute? +## How can I contribute -There are many ways in which you can contribute. Here are a few ideas: +There are many ways in which you can contribute, not just code. Here are a few ideas: - * Reporting Issues / Bugs - * Suggesting Improvements - * Code - * bug fixes and new features that are within the main project scope - * improving test coverage - * addressing code quality issues - * Documentation - * Art work that makes the project look great +- 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 questions +### Asking a question + +You may inquire anything about this library by reporting a "Question" issue on github. -You may inquire about 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/twZ9BwT3 ### Reporting issues Reporting a problem with our libraries _is_ a valuable contribution. - You can do this on the github issues page of this repository. Please be as specific as possible when describing your issue. Whenever relevant, please provide information about your environment (go version, OS). -Adding a code snippet to reproduce the issue is great, and as a big time saver for maintainers. +Adding a code snippet to reproduce the issue is great, and a big time saver for maintainers. ### Triaging issues @@ -62,14 +71,16 @@ process them as fast as possible. Not sure if that typo is worth a pull request? Do it! We will appreciate it. If your pull request is not accepted on the first try, don't be discouraged! -If there's a problem with the implementation, hopefully you received feedback on what to improve. +If 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 lost track of things on big PRs. +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. -These packages constitute a toolkit: it won't do everything for everybody out of the box, + +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. This means that we might decide against incorporating a new feature. @@ -80,9 +91,11 @@ However, there might be a way to implement that feature *on top of* our librarie You just need a `go` compiler to be installed. No special tools are needed to work with our libraries. -The go compiler version required is always the old stable (latest minor go version - 1). +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 already used to work with `go` you should already have everything in place. +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. @@ -104,12 +117,12 @@ github will propose to open a pull request on the original repository. Typically you'd follow some common naming conventions: -- if it's a bugfix branch, name it `fix/XXX-something`where XXX is the number of the +- 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. -> NOTE: we don't enforce naming conventions on branches: it's your fork after all. +NOTE: we don't enforce naming conventions on branches: it's your fork after all. #### Tests @@ -121,10 +134,10 @@ Take a look at existing tests for inspiration, and run the full test suite on yo before submitting a pull request. Our CI measures test coverage and the test coverage of every patch. + Although not a blocking step - because there are so many special cases - this is an indicator that maintainers consider when approving a PR. - -Please try your best to cover about 80% of your patch. +Please try your best to cover at least 80% of your patch. #### Code style @@ -132,13 +145,13 @@ You may read our stance on code style [there](../docs/STYLE.md). #### Documentation -Don't forget to update the documentation when creating or modifying features. +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 the go-openapi packages is published on the public go docs site: +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. @@ -150,11 +163,14 @@ go install golang.org/x/pkgsite/cmd/pkgsite@latest ``` Then run on the repository folder: + ```sh pkgsite . ``` -This wil run a godoc server locally where you may see the documentation generated from your local repository. +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/loads #### Commit messages @@ -164,7 +180,7 @@ reference to all the issues that they address. Pull requests must not contain commits from other users or branches. Commit messages are not required to follow the "conventional commit" rule, but it's certainly a good -thing to follow this guidelinea (e.g. "fix: blah blah", "ci: did this", "feat: did that" ...). +thing to follow that convention (e.g. "fix: fixed panic in XYZ", "ci: did this", "feat: did that" ...). The title in your commit message is used directly to produce our release notes: try to keep them neat. @@ -186,7 +202,7 @@ Be sure to post a comment after pushing. The new commits will show up in the pul request automatically, but the reviewers will not be notified unless you comment. Before the pull request is merged, -**make sure that you squash your commits into logical units of work** +**make sure that you've squashed your commits into logical units of work** using `git rebase -i` and `git push -f`. After every commit the test suite should be passing. @@ -195,6 +211,8 @@ Include documentation changes in the same commit so that a revert would remove a #### Sign your work +Software is developed by real people. + 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. @@ -204,11 +222,30 @@ PGP-signed commit are greatly appreciated but not required. The rules are pretty simple: -* read our [DCO](./DCO.md) (from [developercertificate.org](http://developercertificate.org/)) -* if you agree with these terms, then you just add a line to every git commit message +- 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 +``` +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 index e168dc4..78a2d64 100644 --- a/.github/DCO.md +++ b/.github/DCO.md @@ -1,4 +1,4 @@ - # Developer's Certificate of Origin +# Developer's Certificate of Origin ``` Developer Certificate of Origin diff --git a/.github/copilot b/.github/copilot new file mode 120000 index 0000000..5269483 --- /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 0000000..6da53b2 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +# Copilot Instructions + +`github.com/go-openapi/loads` loads, parses, and analyzes Swagger/OpenAPI v2.0 specifications from local files or remote URLs in JSON and YAML formats. It is part of the `go-openapi` ecosystem. + +See [docs/MAINTAINERS.md](../docs/MAINTAINERS.md) for CI/CD, release process, and repo structure details. + +When editing go files, follow the conventions documented in `.github/copilot/` (conventions, linting, testing). diff --git a/.github/wordlist.txt b/.github/wordlist.txt new file mode 100644 index 0000000..4def2fa --- /dev/null +++ b/.github/wordlist.txt @@ -0,0 +1,47 @@ +CodeFactor +CodeQL +DCO +GoDoc +JSON +Maintainer's +OAI +PR's +PRs +Repo +SPDX +TODOs +Triaging +UI +XYZ +YAML +acronym +agentic +ci +codebase +codecov +config +dependabot +dev +developercertificate +fka +github +globals +godoc +golang +golangci +http +jsonpointer +linter's +linters +maintainer's +md +metalinter +monorepo +openapi +prepended +repos +semver +sexualized +unmarshaled +vuln +yaml diff --git a/.gitignore b/.gitignore index e4f15f1..d8f4186 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -secrets.yml -coverage.out -profile.cov -profile.out +*.out +*.cov +.idea +.env +.mcp.json diff --git a/.golangci.yml b/.golangci.yml index ff2b35b..83968f3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,7 @@ linters: - paralleltest - recvcheck - testpackage + - thelper - tparallel - varnamelen - whitespace diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cd4a7c3..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -after_success: -- bash <(curl -s https://codecov.io/bash) -go: -- 1.16.x -- 1.x -install: -- go get gotest.tools/gotestsum -language: go -arch: -- amd64 -- ppc64le -jobs: - include: - # include linting job, but only for latest go version and amd64 arch - - go: 1.x - arch: amd64 - install: - go get github.com/golangci/golangci-lint/cmd/golangci-lint - script: - - golangci-lint run --new-from-rev master -notifications: - slack: - secure: OxkPwVp35qBTUilgWC8xykSj+sGMcj0h8IIOKD+Rflx2schZVlFfdYdyVBM+s9OqeOfvtuvnR9v1Ye2rPKAvcjWdC4LpRGUsgmItZaI6Um8Aj6+K9udCw5qrtZVfOVmRu8LieH//XznWWKdOultUuniW0MLqw5+II87Gd00RWbCGi0hk0PykHe7uK+PDA2BEbqyZ2WKKYCvfB3j+0nrFOHScXqnh0V05l2E83J4+Sgy1fsPy+1WdX58ZlNBG333ibaC1FS79XvKSmTgKRkx3+YBo97u6ZtUmJa5WZjf2OdLG3KIckGWAv6R5xgxeU31N0Ng8L332w/Edpp2O/M2bZwdnKJ8hJQikXIAQbICbr+lTDzsoNzMdEIYcHpJ5hjPbiUl3Bmd+Jnsjf5McgAZDiWIfpCKZ29tPCEkVwRsOCqkyPRMNMzHHmoja495P5jR+ODS7+J8RFg5xgcnOgpP9D4Wlhztlf5WyZMpkLxTUD+bZq2SRf50HfHFXTkfq22zPl3d1eq0yrLwh/Z/fWKkfb6SyysROL8y6s8u3dpFX1YHSg0BR6i913h4aoZw9B2BG27cafLLTwKYsp2dFo1PWl4O6u9giFJIeqwloZHLKKrwh0cBFhB7RH0I58asxkZpCH6uWjJierahmHe7iS+E6i+9oCHkOZ59hmCYNimIs3hM= -script: -- gotestsum -f short-verbose -- -race -timeout=20m -coverprofile=coverage.txt -covermode=atomic ./... diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 9322b06..bac878f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -23,7 +23,9 @@ include: Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or + advances + * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic @@ -55,7 +57,7 @@ further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at ivan+abuse@flanders.co.nz. All +reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. @@ -68,7 +70,7 @@ members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +available at [][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8692d6d..36b836a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,19 +8,19 @@ | Username | All Time Contribution Count | All Commits | | --- | --- | --- | -| @casualjim | 48 | https://github.com/go-openapi/loads/commits?author=casualjim | -| @fredbi | 45 | https://github.com/go-openapi/loads/commits?author=fredbi | -| @youyuanwu | 6 | https://github.com/go-openapi/loads/commits?author=youyuanwu | -| @vburenin | 4 | https://github.com/go-openapi/loads/commits?author=vburenin | -| @keramix | 4 | https://github.com/go-openapi/loads/commits?author=keramix | -| @orisano | 3 | https://github.com/go-openapi/loads/commits?author=orisano | -| @GlenDC | 3 | https://github.com/go-openapi/loads/commits?author=GlenDC | -| @pengsrc | 2 | https://github.com/go-openapi/loads/commits?author=pengsrc | -| @a2800276 | 2 | https://github.com/go-openapi/loads/commits?author=a2800276 | -| @tklauser | 2 | https://github.com/go-openapi/loads/commits?author=tklauser | -| @hypnoglow | 1 | https://github.com/go-openapi/loads/commits?author=hypnoglow | -| @koron | 1 | https://github.com/go-openapi/loads/commits?author=koron | -| @kreativka | 1 | https://github.com/go-openapi/loads/commits?author=kreativka | -| @petrkotas | 1 | https://github.com/go-openapi/loads/commits?author=petrkotas | +| @casualjim | 48 | | +| @fredbi | 45 | | +| @youyuanwu | 6 | | +| @vburenin | 4 | | +| @keramix | 4 | | +| @orisano | 3 | | +| @GlenDC | 3 | | +| @pengsrc | 2 | | +| @a2800276 | 2 | | +| @tklauser | 2 | | +| @hypnoglow | 1 | | +| @koron | 1 | | +| @kreativka | 1 | | +| @petrkotas | 1 | | _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_ diff --git a/README.md b/README.md index b15c7ad..d92e62a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ You may join the discord community by clicking the invite link on the discord ba Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url] - ## Status API is stable. @@ -59,7 +58,6 @@ go get github.com/go-openapi/loads See also the provided [examples](https://pkg.go.dev/github.com/go-openapi/loads#pkg-examples). - ## Change log See @@ -96,23 +94,19 @@ Maintainers can cut a new release by either: [release-badge]: https://badge.fury.io/gh/go-openapi%2Floads.svg [release-url]: https://badge.fury.io/gh/go-openapi%2Floads -[gomod-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Floads.svg -[gomod-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Floads [gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/loads [gocard-url]: https://goreportcard.com/report/github.com/go-openapi/loads [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/loads [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/loads -[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/loads [godoc-url]: http://pkg.go.dev/github.com/go-openapi/loads [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/DrafRmZx +[discord-url]: https://discord.gg/twZ9BwT3 [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6ceb159 --- /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 | +| ------- | ------------------ | +| 0.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/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..76e97b4 --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,41 @@ +package loads + +import ( + _ "embed" + "encoding/json" + "strconv" + "testing" +) + +//go:embed fixtures/json/bench/header.partial +var benchHeader []byte + +//go:embed fixtures/json/bench/path-item.partial +var benchPathItem []byte + +//go:embed fixtures/json/bench/footer.partial +var benchFooter []byte + +func BenchmarkAnalyzed(b *testing.B) { + d := make([]byte, 0, len(benchHeader)+1000*(len(benchPathItem)+20)+len(benchFooter)) + d = append(d, benchHeader...) + + for i := range 1000 { + d = append(d, `, + "/pets/`...) + d = strconv.AppendInt(d, int64(i), 10) + d = append(d, `": `...) + d = append(d, benchPathItem...) + } + + d = append(d, benchFooter...) + rm := json.RawMessage(d) + b.ResetTimer() + + for b.Loop() { + _, err := Analyzed(rm, "") + if err != nil { + b.Fatal(err) + } + } +} diff --git a/doc_test.go b/doc_test.go deleted file mode 100644 index 9d28de3..0000000 --- a/doc_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers -// SPDX-License-Identifier: Apache-2.0 - -package loads_test - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/go-openapi/loads" - "github.com/go-openapi/swag/loading" -) - -// Example with default loaders defined at the package level. -func ExampleSpec_file() { - path := "fixtures/yaml/swagger/spec.yml" - doc, err := loads.Spec(path) - if err != nil { - fmt.Println("Could not load this spec") - return - } - - fmt.Printf("Spec loaded: %q\n", doc.Host()) - - // Output: Spec loaded: "api.example.com" -} - -// Example with custom loaders passed as options. -func ExampleLoaderOption() { - path := "fixtures/yaml/swagger/spec.yml" - - // a simpler version of loads.JSONDoc - jsonLoader := loads.NewDocLoaderWithMatch( - func(pth string, _ ...loading.Option) (json.RawMessage, error) { - buf, err := os.ReadFile(pth) - return json.RawMessage(buf), err - }, - func(pth string) bool { - return filepath.Ext(pth) == ".json" - }, - ) - - // equivalent to the default loader at the package level, which does: - // - // loads.AddLoader(loading.YAMLMatcher, loading.YAMLDoc) - yamlLoader := loads.NewDocLoaderWithMatch( - loading.YAMLDoc, - func(pth string) bool { - return filepath.Ext(pth) == ".yml" - }, - ) - - doc, err := loads.Spec(path, loads.WithDocLoaderMatches(jsonLoader, yamlLoader)) - if err != nil { - fmt.Println("Could not load this spec") - return - } - - fmt.Printf("Spec loaded: %q\n", doc.Host()) - - // Output: Spec loaded: "api.example.com" -} diff --git a/docs/MAINTAINERS.md b/docs/MAINTAINERS.md index d9d7637..e75f2c9 100644 --- a/docs/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -1,37 +1,33 @@ -# Maintainer's guide +> [!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 -Single go module. - -> **NOTE** -> -> Some `go-openapi` repos are mono-repos with multiple modules, -> with adapted CI workflows. +This project is organized as a repo with a single go module. ## Repo configuration -* default branch: master -* protected branches: master -* branch protection rules: +* Default branch: master +* Protected branches: master +* Branch protection rules: * require pull requests and approval - * required status checks: - - DCO (simple email sign-off) - - Lint - - tests completed -* auto-merge enabled (used for dependabot updates) + * 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 -* linter config: [`.golangci.yml`](../.golangci.yml) (see our [posture](./STYLE.md) on linters) - -* Code quality assessment: [CodeFactor](https://www.codefactor.io/dashboard) +* 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: - * CodeFactor: + * [go report card][gocard-url] + * [CodeFactor][codefactor-url] > **NOTES** > @@ -58,7 +54,7 @@ Coverage threshold status is informative and not blocking. This is because the thresholds are difficult to tune and codecov oftentimes reports false negatives or may fail to upload coverage. -All tests use our fork of `stretchr/testify`: `github.com/go-openapi/testify`. +All tests across `go-openapi` use our fork of `stretchr/loads` (this repo): `github.com/go-openapi/loads`. This allows for minimal test dependencies. > **NOTES** @@ -76,7 +72,7 @@ This allows for minimal test dependencies. ### Automated updates * dependabot - * configuration: [`dependabot.yaml`](../.github/dependabot.yaml) + * configuration: [`dependabot.yaml`][dependabot-config] Principle: @@ -84,7 +80,7 @@ This allows for minimal test dependencies. * all updates from "trusted" dependencies (github actions, golang.org packages, go-openapi packages are auto-merged if they successfully pass CI. -* go version udpates +* go version updates Principle: @@ -92,8 +88,14 @@ This allows for minimal test dependencies. * `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.md) file is updated weekly, with all-time contributors to the repository + * 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) @@ -101,7 +103,7 @@ This allows for minimal test dependencies. There are 3 complementary scanners - obviously, there is some overlap, but each has a different focus. -* github `CodeQL` +* GitHub `CodeQL` * `trivy` * `govulnscan` @@ -115,45 +117,69 @@ 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: [`cliff.toml`](../.cliff.toml) +* 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". -## Other files +**For mono-repos with multiple modules:** -Standard documentation: +The release process is slightly different because we need to update cross-module dependencies +before pushing a tag. -* [`CONTRIBUTING.md`](../.github/CONTRIBUTING.md) guidelines -* [`DCO.md`](../.github/DCO.md) terms for first-time contributors to read -* [`CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md) -* [`SECURIY.md`](../SECURITY.md) policy: how to report vulnerabilities privately -* [`LICENSE`](../LICENSE) terms - +A bump release workflow (mono-repo) can be triggered from the github actions UI to cut a release with a few clicks. -Reference documentation (released): +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: -* [godoc](https://pkg.go.dev/github.com/go-openapi/jsonpointer) +* [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 -## TODOs & other ideas + -A few things remain ahead to ease a bit a maintainer's job: +Reference documentation (released): -* reuse CI workflows (e.g. in `github.com/go-openapi/workflows`) -* reusable actions with custom tools pinned (e.g. in `github.com/go-openapi/gh-actions`) -* open-source license checks -* auto-merge for CONTRIBUTORS.md (requires a github app to produce tokens) -* more automated code renovation / relinting work (possibly built with CLAUDE) -* organization-level documentation web site -* ... +* [pkg.go.dev (fka godoc)][godoc-url] + + +[linter-config]: https://github.com/go-openapi/loads/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/loads/blob/master/.github/dependabot.yaml +[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/loads +[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/loads +[golangci-url]: https://golangci-lint.run/ +[godoc-url]: https://pkg.go.dev/github.com/go-openapi/loads +[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 index 056fdb5..48724a4 100644 --- a/docs/STYLE.md +++ b/docs/STYLE.md @@ -2,14 +2,14 @@ > **TL;DR** > -> Let's be honest: at `go-openapi` and `go-swagger` we've never been super-strict on code style etc. +> 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 make up a really large codebase, which is taxing to maintain and keep afloat. +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. @@ -21,8 +21,13 @@ You should run `golangci-lint run` before committing your changes. Many editors have plugins that do that automatically. -> We use the `golangci-lint` meta-linter. The configuration lies in [`.golangci.yml`](../.golangci.yml). -> You may read for additional reference. +> 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 @@ -30,9 +35,32 @@ Thanks to go's original design, we developers don't have to waste much time argu 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: Nov. 2025, `golangci-lint v2.6.1`): +Here are the reasons why they are disabled (update: Feb. 2026, `golangci-lint v2.8.0`). ```yaml disable: @@ -46,6 +74,7 @@ Here are the reasons why they are disabled (update: Nov. 2025, `golangci-lint v2 - 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 @@ -55,9 +84,11 @@ Here are the reasons why they are disabled (update: Nov. 2025, `golangci-lint v2 ``` 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. +But all linters do not support fine-grained tuning to tolerate some cases and not some others. -When this is possible, we enable linters with relaxed constraints: +**Relaxed linter settings** + +When this is possible, we enable linters with relaxed constraints. ```yaml settings: @@ -81,3 +112,6 @@ When this is possible, we enable linters with relaxed constraints: 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/loads/blob/master/.golangci.yml +[golangci-doc]: https://golangci-lint.run/docs/linters/configuration/ diff --git a/examples_test.go b/examples_test.go index 50929d8..f272f97 100644 --- a/examples_test.go +++ b/examples_test.go @@ -5,6 +5,7 @@ package loads_test import ( "embed" + "encoding/json" "fmt" "io" "net/http" @@ -20,6 +21,56 @@ import ( //go:embed fixtures var embeddedFixtures embed.FS +// Example with default loaders defined at the package level. +func ExampleSpec_file() { + path := "fixtures/yaml/swagger/spec.yml" + doc, err := loads.Spec(path) + if err != nil { + fmt.Println("Could not load this spec") + return + } + + fmt.Printf("Spec loaded: %q\n", doc.Host()) + + // Output: Spec loaded: "api.example.com" +} + +// Example with custom loaders passed as options. +func ExampleLoaderOption() { + path := "fixtures/yaml/swagger/spec.yml" + + // a simpler version of loads.JSONDoc + jsonLoader := loads.NewDocLoaderWithMatch( + func(pth string, _ ...loading.Option) (json.RawMessage, error) { + buf, err := os.ReadFile(pth) + return json.RawMessage(buf), err + }, + func(pth string) bool { + return filepath.Ext(pth) == ".json" + }, + ) + + // equivalent to the default loader at the package level, which does: + // + // loads.AddLoader(loading.YAMLMatcher, loading.YAMLDoc) + yamlLoader := loads.NewDocLoaderWithMatch( + loading.YAMLDoc, + func(pth string) bool { + return filepath.Ext(pth) == ".yml" + }, + ) + + doc, err := loads.Spec(path, loads.WithDocLoaderMatches(jsonLoader, yamlLoader)) + if err != nil { + fmt.Println("Could not load this spec") + return + } + + fmt.Printf("Spec loaded: %q\n", doc.Host()) + + // Output: Spec loaded: "api.example.com" +} + // Loads a JSON document from http, with a custom header. func ExampleJSONSpec_http_custom_header() { ts := serveSomeJSONDocument() diff --git a/fixtures/json/bench/footer.partial b/fixtures/json/bench/footer.partial new file mode 100644 index 0000000..968e14b --- /dev/null +++ b/fixtures/json/bench/footer.partial @@ -0,0 +1,119 @@ + + }, + "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/json/bench/header.partial b/fixtures/json/bench/header.partial new file mode 100644 index 0000000..5f41d7e --- /dev/null +++ b/fixtures/json/bench/header.partial @@ -0,0 +1,101 @@ +{ + "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" + } + } + } + } + } \ No newline at end of file diff --git a/fixtures/json/bench/path-item.partial b/fixtures/json/bench/path-item.partial new file mode 100644 index 0000000..5d6167b --- /dev/null +++ b/fixtures/json/bench/path-item.partial @@ -0,0 +1,61 @@ +{ + "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" + } + ] + } \ No newline at end of file diff --git a/fixtures/json/cascade-ref-expanded.json b/fixtures/json/cascade-ref-expanded.json new file mode 100644 index 0000000..28aae78 --- /dev/null +++ b/fixtures/json/cascade-ref-expanded.json @@ -0,0 +1,60 @@ +{ + "swagger": "2.0", + "consumes":[ + "application/json" + ], + "produces":[ + "application/json" + ], + "schemes":[ + "http" + ], + "host": "api.example.com", + "info":{ + "description":"recursively following JSON references", + "title":"test 1", + "contact":{ + "name":"Fred" + }, + "version":"0.1.1" + }, + "paths":{ + "/getAll":{ + "get":{ + "operationId":"getAll", + "parameters":[ + { + "description":"max number of results", + "name":"a", + "in":"body", + "schema":{ + "type":"string" + } + } + ], + "responses":{ + "200":{ + "description":"Success", + "schema":{ + "type":"array", + "items":{ + "type":"string" + } + } + } + } + } + } + }, + "definitions":{ + "a":{ + "type":"string" + }, + "b":{ + "type":"array", + "items":{ + "type":"string" + } + } + } +} diff --git a/fixtures/json/expected-expanded.json b/fixtures/json/expected-expanded.json new file mode 100644 index 0000000..3a80d5f --- /dev/null +++ b/fixtures/json/expected-expanded.json @@ -0,0 +1,126 @@ +{ + "produces":[ + "application/json", + "plain/text" + ], + "schemes":[ + "https", + "http" + ], + "swagger":"2.0", + "info":{ + "description":"Something", + "title":"Something", + "contact":{ + "name":"Somebody", + "url":"https://url.com", + "email":"email@url.com" + }, + "version":"v1" + }, + "host":"security.sonusnet.com", + "basePath":"/api", + "paths":{ + "/whatnot":{ + "get":{ + "description":"Get something", + "responses":{ + "200":{ + "description":"The something", + "schema":{ + "description":"A collection of service events", + "type":"object", + "properties":{ + "page":{ + "description":"A description of a paged result", + "type":"object", + "properties":{ + "page":{ + "description":"the page that was requested", + "type":"integer" + }, + "page_items":{ + "description":"the number of items per page requested", + "type":"integer" + }, + "pages":{ + "description":"the total number of pages available", + "type":"integer" + }, + "total_items":{ + "description":"the total number of items available", + "type":"integer", + "format":"int64" + } + } + }, + "something":{ + "description":"Something", + "type":"object", + "properties":{ + "p1":{ + "description":"A string", + "type":"string" + }, + "p2":{ + "description":"An integer", + "type":"integer" + } + } + } + } + } + }, + "500":{ + "description":"Oops" + } + } + } + } + }, + "definitions":{ + "Something":{ + "description":"A collection of service events", + "type":"object", + "properties":{ + "page":{ + "description":"A description of a paged result", + "type":"object", + "properties":{ + "page":{ + "description":"the page that was requested", + "type":"integer" + }, + "page_items":{ + "description":"the number of items per page requested", + "type":"integer" + }, + "pages":{ + "description":"the total number of pages available", + "type":"integer" + }, + "total_items":{ + "description":"the total number of items available", + "type":"integer", + "format":"int64" + } + } + }, + "something":{ + "description":"Something", + "type":"object", + "properties":{ + "p1":{ + "description":"A string", + "type":"string" + }, + "p2":{ + "description":"An integer", + "type":"integer" + } + } + } + } + } + } +} diff --git a/fixtures/json/petstore-basic.json b/fixtures/json/petstore-basic.json new file mode 100644 index 0000000..05fe4dd --- /dev/null +++ b/fixtures/json/petstore-basic.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/json/petstore.json b/fixtures/json/petstore.json new file mode 100644 index 0000000..8a1c552 --- /dev/null +++ b/fixtures/json/petstore.json @@ -0,0 +1,272 @@ +{ + "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": [ + { + "oauth2": ["read"] + } + ], + "tags": [ "Pet Operations" ], + "operationId": "getAllPets", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "The status to filter by", + "type": "string" + } + ], + "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": [ + { + "oauth2": ["write"] + } + ], + "tags": [ "Pet Operations" ], + "operationId": "createPet", + "summary": "Creates a new pet", + "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": [ + { + "oauth2": ["write"] + } + ], + "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": { + "security": [ + { + "oauth2": ["read"] + } + ], + "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": { + "allOf": [ + { + "$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" + } + } + } + }, + "produces": [ + "application/json", + "application/xml", + "text/plain", + "text/html" + ], + "securityDefinitions": { + "oauth2": { + "type": "oauth2", + "scopes": { + "read": "Read access.", + "write": "Write access" + }, + "flow": "accessCode", + "authorizationUrl": "http://petstore.swagger.wordnik.com/oauth/authorize", + "tokenUrl": "http://petstore.swagger.wordnik.com/oauth/token" + } + } +} diff --git a/fixtures/yaml/search.yaml b/fixtures/yaml/search.yaml new file mode 100644 index 0000000..3009e94 --- /dev/null +++ b/fixtures/yaml/search.yaml @@ -0,0 +1,174 @@ +swagger: '2.0' + +info: + version: "1.0.0" + title: Simple Search API + description: | + A very simple api description that makes a x-www-form-urlencoded only API to submit searches. + +produces: + - application/json + +consumes: + - application/json + +paths: + /search: + post: + operationId: search + summary: searches tasks + description: searches the task titles and descriptions for a match + consumes: + - application/x-www-form-urlencoded + parameters: + - name: q + in: formData + type: string + description: the search string + required: true + /tasks: + get: + operationId: getTasks + summary: Gets Task objects. + description: | + Optional query param of **size** determines + size of returned array + tags: + - tasks + parameters: + - name: size + in: query + description: Size of task list + type: integer + format: int32 + default: 20 + - name: completed + in: query + description: when true shows completed tasks + type: boolean + + responses: + default: + description: Generic Error + 200: + description: Successful response + headers: + X-Rate-Limit: + type: integer + format: int32 + X-Rate-Limit-Remaining: + type: integer + format: int32 + default: 42 + X-Rate-Limit-Reset: + type: integer + format: int32 + default: "1449875311" + X-Rate-Limit-Reset-Human: + type: string + default: 3 days + X-Rate-Limit-Reset-Human-Number: + type: string + default: 3 + Access-Control-Allow-Origin: + type: string + default: "*" + schema: + type: array + items: + $ref: "#/definitions/Task" + post: + operationId: createTask + summary: Creates a 'Task' object. + description: | + Validates the content property for length etc. + parameters: + - name: body + in: body + schema: + $ref: "#/definitions/Task" + tags: + - tasks + responses: + default: + description: Generic Error + 201: + description: Task Created + + /tasks/{id}: + parameters: + - name: id + in: path + type: integer + format: int32 + description: The id of the task + required: true + minimum: 1 + put: + operationId: updateTask + summary: updates a task. + description: | + Validates the content property for length etc. + tags: + - tasks + parameters: + - name: body + in: body + description: the updated task + schema: + $ref: "#/definitions/Task" + responses: + default: + description: Generic Error + 200: + description: Task updated + schema: + $ref: "#/definitions/Task" + delete: + operationId: deleteTask + summary: deletes a task + description: | + Deleting a task is irrevocable. + tags: + - tasks + responses: + default: + description: Generic Error + 204: + description: Task Deleted + + +definitions: + Task: + title: A Task object + description: | + This describes a task. Tasks require a content property to be set. + required: + - content + type: object + properties: + id: + title: the unique id of the task + description: | + This id property is autogenerated when a task is created. + type: integer + format: int64 + readOnly: true + content: + title: The content of the task + description: | + Task content can contain [GFM](https://help.github.com/articles/github-flavored-markdown/). + type: string + minLength: 5 + completed: + title: when true this task is completed + type: boolean + creditcard: + title: the credit card format usage + type: string + format: creditcard + createdAt: + title: task creation time + type: string + format: date-time + readOnly: true diff --git a/fmts/fixture_test.go b/fmts/fixture_test.go index ca12553..52a772f 100644 --- a/fmts/fixture_test.go +++ b/fmts/fixture_test.go @@ -81,18 +81,9 @@ func roundTripTestJSON(t *testing.T, fixtureType, fileName string, schema any) b b, err := os.ReadFile(fileName) require.NoError(t, err) - var expected map[string]any - require.NoError(t, json.Unmarshal(b, &expected)) - require.NoError(t, json.Unmarshal(b, schema)) - cb, err := json.MarshalIndent(schema, "", " ") - require.NoError(t, err) - - var actual map[string]any - require.NoError(t, json.Unmarshal(cb, &actual)) - - return assert.Equal(t, expected, actual) + return assert.JSONMarshalAsT(t, b, schema) } func roundTripTestYAML(t *testing.T, fixtureType, fileName string, schema any) bool { @@ -104,18 +95,9 @@ func roundTripTestYAML(t *testing.T, fixtureType, fileName string, schema any) b b, err := YAMLDoc(fileName) require.NoError(t, err) - var expected map[string]any - require.NoError(t, json.Unmarshal(b, &expected)) - require.NoError(t, json.Unmarshal(b, schema)) - cb, err := json.MarshalIndent(schema, "", " ") - require.NoError(t, err) - - var actual map[string]any - require.NoError(t, json.Unmarshal(cb, &actual)) - - return assert.Equal(t, expected, actual) + return assert.JSONMarshalAsT(t, b, schema) } func TestPropertyFixtures(t *testing.T) { @@ -135,16 +117,10 @@ func TestAdditionalPropertiesWithObject(t *testing.T) { schema := new(spec.Schema) b, err := YAMLDoc("../fixtures/yaml/models/modelWithObjectMap.yaml") require.NoError(t, err) - var expected map[string]any - require.NoError(t, json.Unmarshal(b, &expected)) - require.NoError(t, json.Unmarshal(b, schema)) - cb, err := json.MarshalIndent(schema, "", " ") - require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, schema)) - var actual map[string]any - require.NoError(t, json.Unmarshal(cb, &actual)) - assert.Equal(t, expected, actual) + assert.JSONMarshalAsT(t, b, schema) } func TestModelFixtures(t *testing.T) { diff --git a/fmts/testdata/petstore.yaml b/fmts/testdata/petstore.yaml new file mode 100644 index 0000000..653b962 --- /dev/null +++ b/fmts/testdata/petstore.yaml @@ -0,0 +1,154 @@ +swagger: '2.0' +info: + version: '1.0.0' + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification + termsOfService: http://helloreverb.com/terms/ + contact: + name: Swagger API team + email: foo@example.com + url: http://swagger.io + license: + name: MIT + url: http://opensource.org/licenses/MIT +host: petstore.swagger.wordnik.com +basePath: /api +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /pets: + get: + description: Returns all pets from the system that the user has access to + operationId: findPets + produces: + - application/json + - application/xml + - text/xml + - text/html + parameters: + - name: tags + in: query + description: tags to filter by + required: false + type: array + items: + type: string + collectionFormat: csv + - name: limit + in: query + description: maximum number of results to return + required: false + type: integer + format: int32 + responses: + '200': + description: pet response + schema: + type: array + items: + $ref: '#/definitions/pet' + default: + description: unexpected error + schema: + $ref: '#/definitions/errorModel' + post: + description: Creates a new pet in the store. Duplicates are allowed + operationId: addPet + produces: + - application/json + parameters: + - name: pet + in: body + description: Pet to add to the store + required: true + schema: + $ref: '#/definitions/newPet' + responses: + '200': + description: pet response + schema: + $ref: '#/definitions/pet' + default: + description: unexpected error + schema: + $ref: '#/definitions/errorModel' + /pets/{id}: + get: + description: Returns a user based on a single ID, if the user does not have access to the pet + operationId: findPetById + produces: + - application/json + - application/xml + - text/xml + - text/html + parameters: + - name: id + in: path + description: ID of pet to fetch + required: true + type: integer + format: int64 + responses: + '200': + description: pet response + schema: + $ref: '#/definitions/pet' + default: + description: unexpected error + schema: + $ref: '#/definitions/errorModel' + delete: + description: deletes a single pet based on the ID supplied + 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/errorModel' +definitions: + pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + newPet: + allOf: + - $ref: '#/definitions/pet' + - required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + errorModel: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/fmts/testdata/with-quoted-y-key.yaml b/fmts/testdata/with-quoted-y-key.yaml new file mode 100644 index 0000000..e8cb3c9 --- /dev/null +++ b/fmts/testdata/with-quoted-y-key.yaml @@ -0,0 +1,50 @@ +consumes: +- application/json +definitions: + viewBox: + type: object + properties: + x: + type: integer + format: int16 + # y -> types don't match: expect map key string or int get: bool + "y": + type: integer + format: int16 + width: + type: integer + format: int16 + height: + type: integer + format: int16 +info: + description: Test RESTful APIs + title: Test Server + version: 1.0.0 +basePath: /api +paths: + /test: + get: + operationId: findAll + parameters: + - name: since + in: query + type: integer + format: int64 + - name: limit + in: query + type: integer + format: int32 + default: 20 + responses: + 200: + description: Array[Trigger] + schema: + type: array + items: + $ref: "#/definitions/viewBox" +produces: +- application/json +schemes: +- https +swagger: "2.0" diff --git a/fmts/testdata/with-y-key.yaml b/fmts/testdata/with-y-key.yaml new file mode 100644 index 0000000..dac927f --- /dev/null +++ b/fmts/testdata/with-y-key.yaml @@ -0,0 +1,50 @@ +consumes: +- application/json +definitions: + viewBox: + type: object + properties: + x: + type: integer + format: int16 + # y -> types don't match: expect map key string or int get: bool + y: + type: integer + format: int16 + width: + type: integer + format: int16 + height: + type: integer + format: int16 +info: + description: Test RESTful APIs + title: Test Server + version: 1.0.0 +basePath: /api +paths: + /test: + get: + operationId: findAll + parameters: + - name: since + in: query + type: integer + format: int64 + - name: limit + in: query + type: integer + format: int32 + default: 20 + responses: + 200: + description: Array[Trigger] + schema: + type: array + items: + $ref: "#/definitions/viewBox" +produces: +- application/json +schemes: +- https +swagger: "2.0" diff --git a/fmts/yaml_test.go b/fmts/yaml_test.go index 0bb428c..9264562 100644 --- a/fmts/yaml_test.go +++ b/fmts/yaml_test.go @@ -4,6 +4,7 @@ package fmts import ( + _ "embed" "encoding/json" "errors" "net/http" @@ -18,6 +19,15 @@ import ( "github.com/go-openapi/testify/v2/require" ) +//go:embed testdata/petstore.yaml +var yamlPetStore []byte + +//go:embed testdata/with-y-key.yaml +var withYKey []byte + +//go:embed testdata/with-quoted-y-key.yaml +var withQuotedYKey []byte + var errTest = errors.New("expected") type failJSONMarshal struct{} @@ -61,7 +71,7 @@ name: a string value d, err := YAMLToJSON(data) require.NoError(t, err) - assert.JSONEq(t, + assert.JSONEqT(t, `{"1":"the int key value","name":"a string value","y":"some value"}`, string(d), ) @@ -84,7 +94,7 @@ name: a string value d, err := YAMLToJSON(data) require.NoError(t, err) - assert.JSONEq(t, + assert.JSONEqT(t, `{"1":"the int key value","name":"a string value","y":"some value","tag":{"name":"tag name"}}`, string(d), ) @@ -94,7 +104,7 @@ name: a string value lst := []any{"hello"} d, err := YAMLToJSON(&lst) require.NoError(t, err) - assert.JSONEq(t, `["hello"]`, string(d)) + assert.JSONEqT(t, `["hello"]`, string(d)) }) t.Run("fail to convert to JSON", func(t *testing.T) { @@ -115,13 +125,13 @@ name: a string value d, err := YAMLToJSON(dd) require.NoError(t, err) - assert.YAMLEq(t, `{"description":"object created"}`, string(d)) + assert.YAMLEqT(t, `{"description":"object created"}`, string(d)) }) } func TestLoadStrategy(t *testing.T) { loader := func(_ string) ([]byte, error) { - return []byte(yamlPetStore), nil + return yamlPetStore, nil } remLoader := func(_ string) ([]byte, error) { return []byte("not it"), nil @@ -129,9 +139,12 @@ func TestLoadStrategy(t *testing.T) { ld := loading.LoadStrategy("blah", loader, remLoader) b, _ := ld("") - assert.YAMLEq(t, yamlPetStore, string(b)) + assert.YAMLEqT(t, string(yamlPetStore), string(b)) - serv := httptest.NewServer(http.HandlerFunc(yamlPestoreServer)) + serv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write(yamlPetStore) + })) defer serv.Close() s, err := YAMLDoc(serv.URL) @@ -147,14 +160,9 @@ func TestLoadStrategy(t *testing.T) { require.Error(t, err) } -var yamlPestoreServer = func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - _, _ = rw.Write([]byte(yamlPetStore)) -} - func TestWithYKey(t *testing.T) { t.Run("with YAMLv3, unquoted y as key is parsed correctly", func(t *testing.T) { - doc, err := BytesToYAMLDoc([]byte(withYKey)) + doc, err := BytesToYAMLDoc(withYKey) require.NoError(t, err) _, err = YAMLToJSON(doc) @@ -162,7 +170,7 @@ func TestWithYKey(t *testing.T) { }) t.Run("quoted y as key is parsed correctly", func(t *testing.T) { - doc, err := BytesToYAMLDoc([]byte(withQuotedYKey)) + doc, err := BytesToYAMLDoc(withQuotedYKey) require.NoError(t, err) jsond, err := YAMLToJSON(doc) @@ -181,266 +189,6 @@ func TestWithYKey(t *testing.T) { } require.NoError(t, json.Unmarshal(jsond, &yt)) - assert.Equal(t, "integer", yt.Definitions.Viewbox.Properties.Y.Type) + assert.EqualT(t, "integer", yt.Definitions.Viewbox.Properties.Y.Type) }) } - -const withQuotedYKey = `consumes: -- application/json -definitions: - viewBox: - type: object - properties: - x: - type: integer - format: int16 - # y -> types don't match: expect map key string or int get: bool - "y": - type: integer - format: int16 - width: - type: integer - format: int16 - height: - type: integer - format: int16 -info: - description: Test RESTful APIs - title: Test Server - version: 1.0.0 -basePath: /api -paths: - /test: - get: - operationId: findAll - parameters: - - name: since - in: query - type: integer - format: int64 - - name: limit - in: query - type: integer - format: int32 - default: 20 - responses: - 200: - description: Array[Trigger] - schema: - type: array - items: - $ref: "#/definitions/viewBox" -produces: -- application/json -schemes: -- https -swagger: "2.0" -` - -const withYKey = `consumes: -- application/json -definitions: - viewBox: - type: object - properties: - x: - type: integer - format: int16 - # y -> types don't match: expect map key string or int get: bool - y: - type: integer - format: int16 - width: - type: integer - format: int16 - height: - type: integer - format: int16 -info: - description: Test RESTful APIs - title: Test Server - version: 1.0.0 -basePath: /api -paths: - /test: - get: - operationId: findAll - parameters: - - name: since - in: query - type: integer - format: int64 - - name: limit - in: query - type: integer - format: int32 - default: 20 - responses: - 200: - description: Array[Trigger] - schema: - type: array - items: - $ref: "#/definitions/viewBox" -produces: -- application/json -schemes: -- https -swagger: "2.0" -` - -const yamlPetStore = `swagger: '2.0' -info: - version: '1.0.0' - title: Swagger Petstore - description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification - termsOfService: http://helloreverb.com/terms/ - contact: - name: Swagger API team - email: foo@example.com - url: http://swagger.io - license: - name: MIT - url: http://opensource.org/licenses/MIT -host: petstore.swagger.wordnik.com -basePath: /api -schemes: - - http -consumes: - - application/json -produces: - - application/json -paths: - /pets: - get: - description: Returns all pets from the system that the user has access to - operationId: findPets - produces: - - application/json - - application/xml - - text/xml - - text/html - parameters: - - name: tags - in: query - description: tags to filter by - required: false - type: array - items: - type: string - collectionFormat: csv - - name: limit - in: query - description: maximum number of results to return - required: false - type: integer - format: int32 - responses: - '200': - description: pet response - schema: - type: array - items: - $ref: '#/definitions/pet' - default: - description: unexpected error - schema: - $ref: '#/definitions/errorModel' - post: - description: Creates a new pet in the store. Duplicates are allowed - operationId: addPet - produces: - - application/json - parameters: - - name: pet - in: body - description: Pet to add to the store - required: true - schema: - $ref: '#/definitions/newPet' - responses: - '200': - description: pet response - schema: - $ref: '#/definitions/pet' - default: - description: unexpected error - schema: - $ref: '#/definitions/errorModel' - /pets/{id}: - get: - description: Returns a user based on a single ID, if the user does not have access to the pet - operationId: findPetById - produces: - - application/json - - application/xml - - text/xml - - text/html - parameters: - - name: id - in: path - description: ID of pet to fetch - required: true - type: integer - format: int64 - responses: - '200': - description: pet response - schema: - $ref: '#/definitions/pet' - default: - description: unexpected error - schema: - $ref: '#/definitions/errorModel' - delete: - description: deletes a single pet based on the ID supplied - 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/errorModel' -definitions: - pet: - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - newPet: - allOf: - - $ref: '#/definitions/pet' - - required: - - name - properties: - id: - type: integer - format: int64 - name: - type: string - errorModel: - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string -` diff --git a/go.mod b/go.mod index 4f9d1ed..c907f20 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,31 @@ module github.com/go-openapi/loads require ( - github.com/go-openapi/analysis v0.24.2 + github.com/go-openapi/analysis v0.24.3 github.com/go-openapi/spec v0.22.4 github.com/go-openapi/swag/loading v0.25.5 github.com/go-openapi/swag/yamlutils v0.25.5 - github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 - github.com/go-openapi/testify/v2 v2.4.0 + github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 + github.com/go-openapi/testify/v2 v2.4.1 go.yaml.in/yaml/v3 v3.0.4 ) require ( - github.com/go-openapi/errors v0.22.5 // indirect + github.com/go-openapi/errors v0.22.7 // indirect github.com/go-openapi/jsonpointer v0.22.5 // indirect github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/strfmt v0.26.0 // indirect github.com/go-openapi/swag/conv v0.25.5 // indirect github.com/go-openapi/swag/jsonname v0.25.5 // indirect github.com/go-openapi/swag/jsonutils v0.25.5 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect github.com/go-openapi/swag/stringutils v0.25.5 // indirect github.com/go-openapi/swag/typeutils v0.25.5 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/oklog/ulid v1.3.1 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/text v0.32.0 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/text v0.34.0 // indirect ) go 1.24.0 diff --git a/go.sum b/go.sum index 10084c3..9e6d913 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,15 @@ -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/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= -github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= -github.com/go-openapi/errors v0.22.5 h1:Yfv4O/PRYpNF3BNmVkEizcHb3uLVVsrDt3LNdgAKRY4= -github.com/go-openapi/errors v0.22.5/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk= +github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= 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/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU= +github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= 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= @@ -22,33 +20,30 @@ github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbF 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/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= -github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= 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.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= -github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= -github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/json_test.go b/json_test.go index a432484..d7e307d 100644 --- a/json_test.go +++ b/json_test.go @@ -4,6 +4,7 @@ package loads import ( + _ "embed" "net/http" "net/http/httptest" "testing" @@ -11,8 +12,14 @@ import ( "github.com/go-openapi/testify/v2/require" ) +//go:embed fixtures/json/petstore.json +var petstoreJSON []byte + func TestLoadJSON(t *testing.T) { - serv := httptest.NewServer(http.HandlerFunc(jsonPestoreServer)) + serv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write(petstoreJSON) + })) defer serv.Close() s, err := JSONSpec(serv.URL) @@ -27,281 +34,3 @@ func TestLoadJSON(t *testing.T) { _, err = JSONSpec(ts2.URL) require.Error(t, err) } - -var jsonPestoreServer = func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusOK) - _, _ = rw.Write([]byte(petstoreJSON)) -} - -const petstoreJSON = `{ - "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": [ - { - "oauth2": ["read"] - } - ], - "tags": [ "Pet Operations" ], - "operationId": "getAllPets", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "The status to filter by", - "type": "string" - } - ], - "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": [ - { - "oauth2": ["write"] - } - ], - "tags": [ "Pet Operations" ], - "operationId": "createPet", - "summary": "Creates a new pet", - "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": [ - { - "oauth2": ["write"] - } - ], - "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": { - "security": [ - { - "oauth2": ["read"] - } - ], - "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": { - "allOf": [ - { - "$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" - } - } - } - }, - "produces": [ - "application/json", - "application/xml", - "text/plain", - "text/html" - ], - "securityDefinitions": { - "oauth2": { - "type": "oauth2", - "scopes": { - "read": "Read access.", - "write": "Write access" - }, - "flow": "accessCode", - "authorizationUrl": "http://petstore.swagger.wordnik.com/oauth/authorize", - "tokenUrl": "http://petstore.swagger.wordnik.com/oauth/token" - } - } -}` diff --git a/loaders.go b/loaders.go index c3a6381..ac8adfe 100644 --- a/loaders.go +++ b/loaders.go @@ -53,7 +53,7 @@ type DocLoaderWithMatch struct { Match DocMatcher } -// NewDocLoaderWithMatch builds a DocLoaderWithMatch to be used in load options. +// NewDocLoaderWithMatch builds a [DocLoaderWithMatch] to be used in load options. func NewDocLoaderWithMatch(fn DocLoader, matcher DocMatcher) DocLoaderWithMatch { return DocLoaderWithMatch{ Fn: fn, @@ -121,9 +121,9 @@ func (l *loader) clone() *loader { } } -// JSONDoc loads a json document from either a file or a remote url. +// JSONDoc loads a json document from either a file or a remote URL. // -// See [loading.Option] for available options (e.g. configuring authentifaction, +// See [loading.Option] for available options (e.g. configuring authentication, // headers or using embedded file system resources). func JSONDoc(path string, opts ...loading.Option) (json.RawMessage, error) { data, err := loading.LoadFromFileOrHTTP(path, opts...) @@ -137,9 +137,10 @@ func JSONDoc(path string, opts ...loading.Option) (json.RawMessage, error) { // // This sets the configuration at the package level. // -// NOTE: -// - this updates the default loader used by github.com/go-openapi/spec -// - since this sets package level globals, you shouln't call this concurrently +// # Concurrency +// +// This function updates the default loader used by [github.com/go-openapi/spec]. +// Since this sets package level globals, you shouldn't call this concurrently. func AddLoader(predicate DocMatcher, load DocLoader) { loaders = loaders.WithHead(&loader{ DocLoaderWithMatch: DocLoaderWithMatch{ diff --git a/options.go b/options.go index 9ba1fe3..045ece5 100644 --- a/options.go +++ b/options.go @@ -48,7 +48,7 @@ func WithDocLoader(l DocLoader) LoaderOption { // WithDocLoaderMatches sets a chain of custom loaders for loading specs // for different extension matches. // -// Loaders are executed in the order of provided DocLoaderWithMatch'es. +// Loaders are executed in the order of provided [DocLoaderWithMatch] 'es. func WithDocLoaderMatches(l ...DocLoaderWithMatch) LoaderOption { return func(opt *options) { var final, prev *loader diff --git a/options_test.go b/options_test.go index 80a03d9..859536a 100644 --- a/options_test.go +++ b/options_test.go @@ -63,7 +63,7 @@ func TestOptionsLoaderFromOptions(t *testing.T) { require.NoError(t, err) require.NotNil(t, b) - require.Equal(t, 2, called) + require.EqualT(t, 2, called) } func TestOptionsWithDocLoaderMatches(t *testing.T) { diff --git a/spec.go b/spec.go index 51aab9d..606a01d 100644 --- a/spec.go +++ b/spec.go @@ -33,7 +33,7 @@ type Document struct { raw json.RawMessage } -// JSONSpec loads a spec from a json document, using the [JSONDoc] loader. +// JSONSpec loads a spec from a JSON document, using the [JSONDoc] loader. // // A set of [loading.Option] may be passed to this loader using [WithLoadingOptions]. func JSONSpec(path string, opts ...LoaderOption) (*Document, error) { @@ -57,7 +57,7 @@ func JSONSpec(path string, opts ...LoaderOption) (*Document, error) { return doc, nil } -// Embedded returns a Document based on embedded specs (i.e. as a raw [json.RawMessage]). No analysis is required. +// Embedded returns a Document based on embedded specs (i.e. as a [json.RawMessage]). No analysis is required. func Embedded(orig, flat json.RawMessage, opts ...LoaderOption) (*Document, error) { var origSpec, flatSpec spec.Swagger if err := json.Unmarshal(orig, &origSpec); err != nil { @@ -96,7 +96,7 @@ func Spec(path string, opts ...LoaderOption) (*Document, error) { return document, nil } -// Analyzed creates a new analyzed spec document for a root json.RawMessage. +// Analyzed creates a new analyzed spec document for a root [json.RawMessage]. func Analyzed(data json.RawMessage, version string, options ...LoaderOption) (*Document, error) { if version == "" { version = "2.0" diff --git a/spec_test.go b/spec_test.go index dbc69a5..7ea365e 100644 --- a/spec_test.go +++ b/spec_test.go @@ -4,10 +4,10 @@ package loads import ( + _ "embed" "encoding/json" "path/filepath" "regexp" - "strconv" "strings" "testing" @@ -15,31 +15,42 @@ import ( "github.com/go-openapi/testify/v2/require" ) +//go:embed fixtures/json/petstore-basic.json +var petStoreJSON []byte + +//go:embed fixtures/yaml/search.yaml +var yamlSpec []byte + +//go:embed fixtures/json/expected-expanded.json +var expectedExpanded []byte + +//go:embed fixtures/json/cascade-ref-expanded.json +var cascadeRefExpanded []byte + func TestUnknownSpecVersion(t *testing.T) { _, err := Analyzed([]byte{}, "0.9") require.Error(t, err) } func TestDefaultsTo20(t *testing.T) { - d, err := Analyzed(PetStoreJSONMessage, "") + d, err := Analyzed(petStoreJSON, "") require.NoError(t, err) require.NotNil(t, d) - assert.Equal(t, "2.0", d.Version()) - // assert.Equal(t, "2.0", d.data["swagger"].(string)) - assert.Equal(t, "/api", d.BasePath()) + assert.EqualT(t, "2.0", d.Version()) + assert.EqualT(t, "/api", d.BasePath()) } func TestLoadsYAMLContent(t *testing.T) { - d, err := Analyzed(json.RawMessage([]byte(YAMLSpec)), "") + d, err := Analyzed(yamlSpec, "") require.NoError(t, err) require.NotNil(t, d) sw := d.Spec() - assert.Equal(t, "1.0.0", sw.Info.Version) + assert.EqualT(t, "1.0.0", sw.Info.Version) } -// for issue 11. +// for issue #11. func TestRegressionExpand(t *testing.T) { swaggerFile := "fixtures/yaml/swagger/1/2/3/4/swagger.yaml" document, err := Spec(swaggerFile) @@ -50,9 +61,7 @@ func TestRegressionExpand(t *testing.T) { require.NoError(t, err) require.NotNil(t, d) - b, err := d.Spec().MarshalJSON() - require.NoError(t, err) - assert.JSONEq(t, expectedExpanded, string(b)) + assert.JSONMarshalAsT(t, expectedExpanded, d.Spec()) } func TestCascadingRefExpand(t *testing.T) { @@ -65,8 +74,7 @@ func TestCascadingRefExpand(t *testing.T) { require.NoError(t, err) require.NotNil(t, d) - b, _ := d.Spec().MarshalJSON() - assert.JSONEq(t, cascadeRefExpanded, string(b)) + assert.JSONMarshalAsT(t, cascadeRefExpanded, d.Spec()) } func TestFailsInvalidJSON(t *testing.T) { @@ -93,7 +101,7 @@ func TestIssue1846(t *testing.T) { for _, matched := range m { subMatch := matched[1] - require.Truef(t, + require.TrueTf(t, strings.HasPrefix(subMatch, "#/definitions") || strings.HasPrefix(subMatch, "#/responses"), "expected $ref to point either to definitions or responses section, got: %s", matched[0]) } @@ -115,31 +123,18 @@ func TestEmbedded(t *testing.T) { require.NoError(t, err) require.NotNil(t, d) - rawEmbedded, err := json.Marshal(d.Raw()) - require.NoError(t, err) - - spcEmbedded, err := json.Marshal(d.Spec()) - require.NoError(t, err) - - assert.JSONEq(t, string(raw), string(rawEmbedded)) - assert.JSONEq(t, string(spc), string(spcEmbedded)) + assert.JSONMarshalAsT(t, raw, d.Raw()) + assert.JSONMarshalAsT(t, spc, d.Spec()) } func TestDocument(t *testing.T) { - document, err := Embedded(PetStoreJSONMessage, PetStoreJSONMessage) - require.NoError(t, err) - - require.Equal(t, "petstore.swagger.wordnik.com", document.Host()) - - orig, err := json.Marshal(document.OrigSpec()) + document, err := Embedded(petStoreJSON, petStoreJSON) require.NoError(t, err) - require.JSONEq(t, string(PetStoreJSONMessage), string(orig)) + require.EqualT(t, "petstore.swagger.wordnik.com", document.Host()) - cloned, err := json.Marshal(document.Pristine().Spec()) - require.NoError(t, err) - - require.JSONEq(t, string(PetStoreJSONMessage), string(cloned)) + require.JSONMarshalAsT(t, petStoreJSON, document.OrigSpec()) + require.JSONMarshalAsT(t, petStoreJSON, document.Pristine().Spec()) spc := document.Spec() spc.Definitions = nil @@ -149,965 +144,8 @@ func TestDocument(t *testing.T) { reset := document.ResetDefinitions() - afterReset, err := json.Marshal(reset.Spec()) - require.NoError(t, err) - - require.JSONEq(t, string(PetStoreJSONMessage), string(afterReset)) -} - -func BenchmarkAnalyzed(b *testing.B) { - d := []byte(`{ - "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" - } - } - } - } - }`) - - for i := range 1000 { - d = append(d, []byte(`, - "/pets/`)...) - d = strconv.AppendInt(d, int64(i), 10) - d = append(d, []byte(`": { - "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" - } - ] - }`)...) - } - - d = append(d, []byte(` - }, - "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" - } - } -} -`)...) - rm := json.RawMessage(d) - b.ResetTimer() - for b.Loop() { - _, err := Analyzed(rm, "") - if err != nil { - b.Fatal(err) - } - } -} - -const YAMLSpec = `swagger: '2.0' - -info: - version: "1.0.0" - title: Simple Search API - description: | - A very simple api description that makes a x-www-form-urlencoded only API to submit searches. - -produces: - - application/json - -consumes: - - application/json - -paths: - /search: - post: - operationId: search - summary: searches tasks - description: searches the task titles and descriptions for a match - consumes: - - application/x-www-form-urlencoded - parameters: - - name: q - in: formData - type: string - description: the search string - required: true - /tasks: - get: - operationId: getTasks - summary: Gets Task objects. - description: | - Optional query param of **size** determines - size of returned array - tags: - - tasks - parameters: - - name: size - in: query - description: Size of task list - type: integer - format: int32 - default: 20 - - name: completed - in: query - description: when true shows completed tasks - type: boolean - - responses: - default: - description: Generic Error - 200: - description: Successful response - headers: - X-Rate-Limit: - type: integer - format: int32 - X-Rate-Limit-Remaining: - type: integer - format: int32 - default: 42 - X-Rate-Limit-Reset: - type: integer - format: int32 - default: "1449875311" - X-Rate-Limit-Reset-Human: - type: string - default: 3 days - X-Rate-Limit-Reset-Human-Number: - type: string - default: 3 - Access-Control-Allow-Origin: - type: string - default: "*" - schema: - type: array - items: - $ref: "#/definitions/Task" - post: - operationId: createTask - summary: Creates a 'Task' object. - description: | - Validates the content property for length etc. - parameters: - - name: body - in: body - schema: - $ref: "#/definitions/Task" - tags: - - tasks - responses: - default: - description: Generic Error - 201: - description: Task Created - - /tasks/{id}: - parameters: - - name: id - in: path - type: integer - format: int32 - description: The id of the task - required: true - minimum: 1 - put: - operationId: updateTask - summary: updates a task. - description: | - Validates the content property for length etc. - tags: - - tasks - parameters: - - name: body - in: body - description: the updated task - schema: - $ref: "#/definitions/Task" - responses: - default: - description: Generic Error - 200: - description: Task updated - schema: - $ref: "#/definitions/Task" - delete: - operationId: deleteTask - summary: deletes a task - description: | - Deleting a task is irrevocable. - tags: - - tasks - responses: - default: - description: Generic Error - 204: - description: Task Deleted - - -definitions: - Task: - title: A Task object - description: | - This describes a task. Tasks require a content property to be set. - required: - - content - type: object - properties: - id: - title: the unique id of the task - description: | - This id property is autogenerated when a task is created. - type: integer - format: int64 - readOnly: true - content: - title: The content of the task - description: | - Task content can contain [GFM](https://help.github.com/articles/github-flavored-markdown/). - type: string - minLength: 5 - completed: - title: when true this task is completed - type: boolean - creditcard: - title: the credit card format usage - type: string - format: creditcard - createdAt: - title: task creation time - type: string - format: date-time - readOnly: true -` - -// PetStoreJSONMessage json raw message for Petstore20. -var PetStoreJSONMessage = json.RawMessage([]byte(PetStore20)) - -// 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" - } - } -} -` - -const expectedExpanded = ` -{ - "produces":[ - "application/json", - "plain/text" - ], - "schemes":[ - "https", - "http" - ], - "swagger":"2.0", - "info":{ - "description":"Something", - "title":"Something", - "contact":{ - "name":"Somebody", - "url":"https://url.com", - "email":"email@url.com" - }, - "version":"v1" - }, - "host":"security.sonusnet.com", - "basePath":"/api", - "paths":{ - "/whatnot":{ - "get":{ - "description":"Get something", - "responses":{ - "200":{ - "description":"The something", - "schema":{ - "description":"A collection of service events", - "type":"object", - "properties":{ - "page":{ - "description":"A description of a paged result", - "type":"object", - "properties":{ - "page":{ - "description":"the page that was requested", - "type":"integer" - }, - "page_items":{ - "description":"the number of items per page requested", - "type":"integer" - }, - "pages":{ - "description":"the total number of pages available", - "type":"integer" - }, - "total_items":{ - "description":"the total number of items available", - "type":"integer", - "format":"int64" - } - } - }, - "something":{ - "description":"Something", - "type":"object", - "properties":{ - "p1":{ - "description":"A string", - "type":"string" - }, - "p2":{ - "description":"An integer", - "type":"integer" - } - } - } - } - } - }, - "500":{ - "description":"Oops" - } - } - } - } - }, - "definitions":{ - "Something":{ - "description":"A collection of service events", - "type":"object", - "properties":{ - "page":{ - "description":"A description of a paged result", - "type":"object", - "properties":{ - "page":{ - "description":"the page that was requested", - "type":"integer" - }, - "page_items":{ - "description":"the number of items per page requested", - "type":"integer" - }, - "pages":{ - "description":"the total number of pages available", - "type":"integer" - }, - "total_items":{ - "description":"the total number of items available", - "type":"integer", - "format":"int64" - } - } - }, - "something":{ - "description":"Something", - "type":"object", - "properties":{ - "p1":{ - "description":"A string", - "type":"string" - }, - "p2":{ - "description":"An integer", - "type":"integer" - } - } - } - } - } - } -} -` - -const cascadeRefExpanded = ` -{ - "swagger": "2.0", - "consumes":[ - "application/json" - ], - "produces":[ - "application/json" - ], - "schemes":[ - "http" - ], - "host": "api.example.com", - "info":{ - "description":"recursively following JSON references", - "title":"test 1", - "contact":{ - "name":"Fred" - }, - "version":"0.1.1" - }, - "paths":{ - "/getAll":{ - "get":{ - "operationId":"getAll", - "parameters":[ - { - "description":"max number of results", - "name":"a", - "in":"body", - "schema":{ - "type":"string" - } - } - ], - "responses":{ - "200":{ - "description":"Success", - "schema":{ - "type":"array", - "items":{ - "type":"string" - } - } - } - } - } - } - }, - "definitions":{ - "a":{ - "type":"string" - }, - "b":{ - "type":"array", - "items":{ - "type":"string" - } - } - } + require.JSONMarshalAsT(t, petStoreJSON, reset.Spec()) } -` func TestSpecCircular(t *testing.T) { swaggerFile := "fixtures/json/resources/pathLoaderIssue.json" @@ -1147,12 +185,12 @@ func TestIssueSpec145(t *testing.T) { require.NoError(t, err) require.NotNil(t, document) - require.Equal(t, docPath, document.SpecFilePath()) + require.EqualT(t, docPath, document.SpecFilePath()) expanded, err := document.Expanded() require.NoError(t, err) - require.Equal(t, docPath, expanded.SpecFilePath()) + require.EqualT(t, docPath, expanded.SpecFilePath()) }) t.Run("with JSONSpec loader", func(t *testing.T) { @@ -1166,7 +204,7 @@ func TestIssueSpec145(t *testing.T) { t.Run("with Pristine", func(t *testing.T) { pristine := document.Pristine() - require.Equal(t, document.SpecFilePath(), pristine.SpecFilePath()) + require.EqualT(t, document.SpecFilePath(), pristine.SpecFilePath()) }) }) })