├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── enhancement_request.md ├── pull_request_template.md ├── scripts │ ├── create-checksums.sh │ └── version-sync ├── test-specs │ └── petstore.json └── workflows │ ├── release-binaries.yml │ ├── release.yml │ └── test.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── .vscode ├── extensions.json └── settings.json ├── .yarn └── releases │ └── yarn-4.1.1.cjs ├── .yarnrc.yml ├── Dockerfile ├── LICENSE ├── README.md ├── Taskfile.yml ├── compass.yml ├── docs ├── docker.md ├── dockerhub-readme.md ├── generate-openapi.md └── release.md ├── etc └── openapi-cli │ └── .env.build.production ├── package.json ├── projects ├── json-pointer-helpers │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── json-pointers │ │ │ ├── __snapshots__ │ │ │ └── json-pointer-helpers.test.ts.snap │ │ │ ├── json-pointer-helpers.test.ts │ │ │ └── json-pointer-helpers.ts │ └── tsconfig.json ├── openapi-io │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── inputs │ │ ├── date-example.yml │ │ ├── openapi3-with-references │ │ │ ├── circular-references-multiple-chain.yaml │ │ │ ├── circular-references-multiple-refs.yaml │ │ │ ├── circular-references-with-expanded-refs.yaml │ │ │ ├── circular-references.yaml │ │ │ ├── definitions.yaml │ │ │ ├── external-multiple-branches.yaml │ │ │ ├── external-multiple.yaml │ │ │ ├── internal-multiple.yaml │ │ │ └── openapi.yaml │ │ ├── openapi3 │ │ │ ├── 000-baseline.yaml │ │ │ ├── 001-ok-add-property-field.yaml │ │ │ ├── broken-open-api.json │ │ │ ├── components │ │ │ │ ├── common.yaml │ │ │ │ ├── errors.yaml │ │ │ │ ├── headers │ │ │ │ │ └── headers.yaml │ │ │ │ ├── parameters │ │ │ │ │ ├── pagination.yaml │ │ │ │ │ └── version.yaml │ │ │ │ ├── responses │ │ │ │ │ ├── 204.yaml │ │ │ │ │ ├── 400.yaml │ │ │ │ │ ├── 401.yaml │ │ │ │ │ ├── 403.yaml │ │ │ │ │ ├── 404.yaml │ │ │ │ │ ├── 409.yaml │ │ │ │ │ └── 500.yaml │ │ │ │ ├── tag.yaml │ │ │ │ ├── types.yaml │ │ │ │ └── version.yaml │ │ │ ├── empty-with-url-ref.json │ │ │ ├── empty.json │ │ │ ├── openapi-webhook.json │ │ │ ├── petstore0.json │ │ │ ├── petstore0.json.flattened-without-sourcemap.json │ │ │ ├── petstore0.json.flattened.json │ │ │ ├── petstore1.json │ │ │ ├── smallpetstore0.json │ │ │ ├── smallpetstore1.json │ │ │ └── todo-api-3_1.json │ │ └── swagger2 │ │ │ └── spec.yml │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── denormalizers │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── denormalize.test.ts.snap │ │ │ │ ├── denormalize.test.ts │ │ │ │ └── specs │ │ │ │ │ ├── v2 │ │ │ │ │ ├── openapi.yaml │ │ │ │ │ └── parameters.yml │ │ │ │ │ └── v3 │ │ │ │ │ ├── allOf │ │ │ │ │ ├── in-type-array.yaml │ │ │ │ │ ├── nested.yaml │ │ │ │ │ ├── no-merge.yaml │ │ │ │ │ ├── single-allof.yaml │ │ │ │ │ └── single-child.yaml │ │ │ │ │ ├── openapi.yaml │ │ │ │ │ └── parameters.yml │ │ │ ├── denormalize.ts │ │ │ ├── denormalizeProperty.ts │ │ │ └── pointer.ts │ │ ├── index.ts │ │ ├── parser │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── parse-with-sourcemap.test.ts.snap │ │ │ │ ├── parse-with-sourcemap.test.ts │ │ │ │ └── windows-git-pathing.test.ts │ │ │ ├── insourced-dereference.ts │ │ │ ├── insourced-yaml.ts │ │ │ ├── openapi-sourcemap-parser.ts │ │ │ ├── resolvers │ │ │ │ ├── custom-http-ref-handler.ts │ │ │ │ └── git-branch-file-resolver.ts │ │ │ ├── sourcemap.ts │ │ │ └── types.ts │ │ ├── react-app-env.d.ts │ │ ├── validation │ │ │ ├── __snapshots__ │ │ │ │ └── validator.test.ts.snap │ │ │ ├── advanced-validation.test.ts │ │ │ ├── advanced-validation.ts │ │ │ ├── errors.ts │ │ │ ├── log-json-pointer.ts │ │ │ ├── openapi-versions.test.ts │ │ │ ├── openapi-versions.ts │ │ │ ├── validation-schemas.ts │ │ │ ├── validator.test.ts │ │ │ └── validator.ts │ │ └── write │ │ │ ├── __snapshots__ │ │ │ └── yaml-roundtrip.test.ts.snap │ │ │ ├── index.ts │ │ │ ├── yaml-roundtrip.test.ts │ │ │ └── yaml-roundtrip.ts │ └── tsconfig.json ├── openapi-utilities │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── Taskfile.yml │ ├── babel.config.js │ ├── inputs │ │ ├── openapi3-with-references │ │ │ ├── definitions.yaml │ │ │ ├── external-multiple-branches.yaml │ │ │ ├── external-multiple.yaml │ │ │ ├── internal-multiple.yaml │ │ │ └── openapi.yaml │ │ └── openapi3 │ │ │ ├── broken-open-api.json │ │ │ ├── component-schema-examples.json │ │ │ ├── empty.json │ │ │ ├── operation-examples-with-partial-schemas.json │ │ │ ├── operation-examples-without-schemas.json │ │ │ ├── petstore0.json │ │ │ ├── petstore0.json.flattened-without-sourcemap.json │ │ │ ├── petstore0.json.flattened.json │ │ │ ├── petstore1.json │ │ │ ├── polymorphic-schemas-3_1.json │ │ │ ├── polymorphic-schemas.json │ │ │ ├── private │ │ │ └── snyk │ │ │ │ ├── beta.json │ │ │ │ ├── experimental.json │ │ │ │ ├── org-id-versions │ │ │ │ └── all-dates │ │ │ │ │ ├── 0001.1-Oct-2021.19ca0c13463b42ce1bd34d636ae5b3a5350a8784.spec.yaml │ │ │ │ │ ├── 0001.1-Oct-2021.19ca0c13463b42ce1bd34d636ae5b3a5350a8784.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0001.1-Oct-2021.19ca0c13463b42ce1bd34d636ae5b3a5350a8784.spec.yaml.flattened.json │ │ │ │ │ ├── 0002.20-Sep-2021.b9febabad5b30b9f40e661ab288538976181c191.spec.yaml │ │ │ │ │ ├── 0002.20-Sep-2021.b9febabad5b30b9f40e661ab288538976181c191.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0002.20-Sep-2021.b9febabad5b30b9f40e661ab288538976181c191.spec.yaml.flattened.json │ │ │ │ │ ├── 0003.16-Sep-2021.56fe23a8ee00f877771d1c021da0b21c9a9eaea0.spec.yaml │ │ │ │ │ ├── 0003.16-Sep-2021.56fe23a8ee00f877771d1c021da0b21c9a9eaea0.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0003.16-Sep-2021.56fe23a8ee00f877771d1c021da0b21c9a9eaea0.spec.yaml.flattened.json │ │ │ │ │ ├── 0004.14-Sep-2021.1f10caff83376ab5f38fd24d2d45521c82aa00bb.spec.yaml │ │ │ │ │ ├── 0004.14-Sep-2021.1f10caff83376ab5f38fd24d2d45521c82aa00bb.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0004.14-Sep-2021.1f10caff83376ab5f38fd24d2d45521c82aa00bb.spec.yaml.flattened.json │ │ │ │ │ ├── 0005.7-Sep-2021.9d9ffc5d0e817da468820b7936105af564dde349.spec.yaml │ │ │ │ │ ├── 0005.7-Sep-2021.9d9ffc5d0e817da468820b7936105af564dde349.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0005.7-Sep-2021.9d9ffc5d0e817da468820b7936105af564dde349.spec.yaml.flattened.json │ │ │ │ │ ├── 0006.7-Sep-2021.e0fdd67bf73bc26b378d93ecf4d502bef02823ca.spec.yaml │ │ │ │ │ ├── 0006.7-Sep-2021.e0fdd67bf73bc26b378d93ecf4d502bef02823ca.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0006.7-Sep-2021.e0fdd67bf73bc26b378d93ecf4d502bef02823ca.spec.yaml.flattened.json │ │ │ │ │ ├── 0007.27-Aug-2021.9cf5f64b3c3ac1907884b35f7f509aff59266abb.spec.yaml │ │ │ │ │ ├── 0007.27-Aug-2021.9cf5f64b3c3ac1907884b35f7f509aff59266abb.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0007.27-Aug-2021.9cf5f64b3c3ac1907884b35f7f509aff59266abb.spec.yaml.flattened.json │ │ │ │ │ ├── 0008.18-Aug-2021.cf1155a0a237552603d90cda560e6184970c4879.spec.yaml │ │ │ │ │ ├── 0008.18-Aug-2021.cf1155a0a237552603d90cda560e6184970c4879.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0008.18-Aug-2021.cf1155a0a237552603d90cda560e6184970c4879.spec.yaml.flattened.json │ │ │ │ │ ├── 0009.16-Jul-2021.2c662df3651293281df8e55a5b2d0d77fe89a25d.spec.yaml │ │ │ │ │ ├── 0009.16-Jul-2021.2c662df3651293281df8e55a5b2d0d77fe89a25d.spec.yaml.flattened-without-sourcemap.json │ │ │ │ │ ├── 0009.16-Jul-2021.2c662df3651293281df8e55a5b2d0d77fe89a25d.spec.yaml.flattened.json │ │ │ │ │ └── 0010.5-Apr-2016.87e9639ddaa52f57f3fba1c704f757213c2681d5.spec.yaml │ │ │ │ ├── schemas │ │ │ │ ├── 400.yaml │ │ │ │ ├── 401.yaml │ │ │ │ ├── 403.yaml │ │ │ │ ├── 404.yaml │ │ │ │ ├── 500.yaml │ │ │ │ ├── app.yaml │ │ │ │ ├── appOrg.yaml │ │ │ │ ├── appWithSecret.yaml │ │ │ │ ├── code-issue.yaml │ │ │ │ ├── common.yaml │ │ │ │ ├── errors.yaml │ │ │ │ ├── headers │ │ │ │ │ └── headers.yaml │ │ │ │ ├── issue-severity.yaml │ │ │ │ ├── issue-summary.yaml │ │ │ │ ├── issue-type.yaml │ │ │ │ ├── models │ │ │ │ │ ├── app.yaml │ │ │ │ │ ├── appOrg.yaml │ │ │ │ │ ├── appWithSecret.yaml │ │ │ │ │ ├── code-issue.yaml │ │ │ │ │ ├── issue-summary.yaml │ │ │ │ │ ├── org-invitation.yaml │ │ │ │ │ ├── project.yaml │ │ │ │ │ ├── target.yaml │ │ │ │ │ └── user.yaml │ │ │ │ ├── org-invitation.yaml │ │ │ │ ├── pagination.yaml │ │ │ │ ├── parameters │ │ │ │ │ ├── issue-severity.yaml │ │ │ │ │ ├── issue-type.yaml │ │ │ │ │ ├── pagination.yaml │ │ │ │ │ ├── project-id.yaml │ │ │ │ │ ├── snapshot-id.yaml │ │ │ │ │ └── version.yaml │ │ │ │ ├── project-id.yaml │ │ │ │ ├── project.yaml │ │ │ │ ├── responses │ │ │ │ │ ├── 400.yaml │ │ │ │ │ ├── 401.yaml │ │ │ │ │ ├── 403.yaml │ │ │ │ │ ├── 404.yaml │ │ │ │ │ └── 500.yaml │ │ │ │ ├── shared │ │ │ │ │ ├── issue-severity.yaml │ │ │ │ │ ├── issue-type.yaml │ │ │ │ │ └── tag.yaml │ │ │ │ ├── snapshot-id.yaml │ │ │ │ ├── tag.yaml │ │ │ │ ├── target.yaml │ │ │ │ ├── user.yaml │ │ │ │ └── version.yaml │ │ │ │ └── wip.json │ │ │ ├── smallpetstore0.json │ │ │ ├── smallpetstore1.json │ │ │ └── todo-api-3_1.json │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── compare-specs │ │ │ └── compare-specs.ts │ │ ├── coverage │ │ │ └── coverage.ts │ │ ├── diff │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── diff.test.ts.snap │ │ │ │ ├── diff.test.ts │ │ │ │ ├── mock-data.ts │ │ │ │ └── openapi-matchers.test.ts │ │ │ ├── array-identifiers.ts │ │ │ ├── diff.ts │ │ │ └── openapi-matchers.ts │ │ ├── errors.ts │ │ ├── examples │ │ │ ├── petstore-base.ts │ │ │ └── petstore-updated.ts │ │ ├── flat-openapi-types.ts │ │ ├── index.ts │ │ ├── openapi3 │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── group-diffs.test.ts.snap │ │ │ │ │ ├── json-path-utilities.test.ts.snap │ │ │ │ │ └── traverser.test.ts.snap │ │ │ │ ├── fact-mock.ts │ │ │ │ ├── group-diffs.test.ts │ │ │ │ ├── json-path-utilities.test.ts │ │ │ │ └── traverser.test.ts │ │ │ ├── constants.ts │ │ │ ├── group-diff.ts │ │ │ ├── implementations │ │ │ │ └── openapi3 │ │ │ │ │ ├── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── openapi-traverser.test.ts.snap │ │ │ │ │ │ └── sourcemap-reader.test.ts.snap │ │ │ │ │ ├── openapi-traverser.test.ts │ │ │ │ │ └── sourcemap-reader.test.ts │ │ │ │ │ ├── openapi-traverser.ts │ │ │ │ │ ├── sourcemap-reader.ts │ │ │ │ │ └── types.ts │ │ │ ├── json-path-interpreters.ts │ │ │ ├── sdk │ │ │ │ ├── facts-to-changelog.ts │ │ │ │ ├── isType.ts │ │ │ │ └── types │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── location.ts │ │ │ │ │ └── openApiKinds.ts │ │ │ ├── traverser.ts │ │ │ └── types.ts │ │ ├── results.ts │ │ ├── specs │ │ │ ├── __tests__ │ │ │ │ └── tags.test.ts │ │ │ └── tags.ts │ │ ├── swagger2 │ │ │ └── index.ts │ │ ├── types.ts │ │ └── utilities │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── group-changes.test.ts.snap │ │ │ └── group-changes.test.ts │ │ │ ├── changelog.ts │ │ │ ├── compare-changes-by-path.ts │ │ │ ├── count-changed-operations.ts │ │ │ ├── group-changes.ts │ │ │ ├── id.ts │ │ │ ├── traverse-spec.ts │ │ │ └── truthy.ts │ └── tsconfig.json ├── optic │ ├── .env.example │ ├── .env.production │ ├── .gitignore │ ├── .npmignore │ ├── Developers.md │ ├── babel.config.js │ ├── ci │ │ └── configs │ │ │ ├── github.yml │ │ │ └── gitlab.yml │ ├── jest-preset.js │ ├── jest.config.js │ ├── package.json │ ├── specs │ │ ├── smallpetstore0.json │ │ ├── smallpetstore1.json │ │ └── test-spec.yml │ ├── src │ │ ├── __tests__ │ │ │ ├── config.test.ts │ │ │ ├── integration │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── bundle.test.ts.snap │ │ │ │ │ ├── capture-init.test.ts.snap │ │ │ │ │ ├── capture.test.ts.snap │ │ │ │ │ ├── diff-all.test.ts.snap │ │ │ │ │ ├── diff.test.ts.snap │ │ │ │ │ ├── history.test.ts.snap │ │ │ │ │ └── lint.test.ts.snap │ │ │ │ ├── bundle.test.ts │ │ │ │ ├── capture-init.test.ts │ │ │ │ ├── capture.test.ts │ │ │ │ ├── diff-all.test.ts │ │ │ │ ├── diff.test.ts │ │ │ │ ├── history.test.ts │ │ │ │ ├── integration.ts │ │ │ │ ├── lint.test.ts │ │ │ │ └── workspaces │ │ │ │ │ ├── api-add │ │ │ │ │ ├── many-files │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── nested │ │ │ │ │ │ │ └── speccopy2.yml │ │ │ │ │ │ ├── not-added.yml │ │ │ │ │ │ └── spec.yml │ │ │ │ │ └── one-file │ │ │ │ │ │ └── spec.yml │ │ │ │ │ ├── api-list │ │ │ │ │ └── many-files │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── nested │ │ │ │ │ │ └── speccopy2.yml │ │ │ │ │ │ ├── not-added.yml │ │ │ │ │ │ └── spec.yml │ │ │ │ │ ├── bundle │ │ │ │ │ └── specs │ │ │ │ │ │ ├── anotherschema.yml │ │ │ │ │ │ ├── openapi.yml │ │ │ │ │ │ └── schemas.yml │ │ │ │ │ ├── capture-init │ │ │ │ │ ├── no-yml │ │ │ │ │ │ └── empty.txt │ │ │ │ │ └── yml │ │ │ │ │ │ └── optic.yml │ │ │ │ │ ├── capture │ │ │ │ │ ├── har │ │ │ │ │ │ ├── har.har │ │ │ │ │ │ └── openapi.yml │ │ │ │ │ ├── postman │ │ │ │ │ │ ├── openapi.yml │ │ │ │ │ │ └── postman_collection.json │ │ │ │ │ └── with-server │ │ │ │ │ │ ├── components │ │ │ │ │ │ └── books.yml │ │ │ │ │ │ ├── file.txt │ │ │ │ │ │ ├── openapi-prefix-and-server-urls.yml │ │ │ │ │ │ ├── openapi-prefixed-url.yml │ │ │ │ │ │ ├── openapi-with-external-ref-spaces.yml │ │ │ │ │ │ ├── openapi-with-external-ref.yml │ │ │ │ │ │ ├── openapi-with-ignore.yml │ │ │ │ │ │ ├── openapi-with-overlapping-paths.yml │ │ │ │ │ │ ├── openapi-with-server-prefix.yml │ │ │ │ │ │ ├── openapi.yml │ │ │ │ │ │ ├── optic.yml │ │ │ │ │ │ ├── server.js │ │ │ │ │ │ └── with space │ │ │ │ │ │ └── books.yml │ │ │ │ │ ├── diff-all │ │ │ │ │ ├── cloud-diff │ │ │ │ │ │ ├── spec-no-url.json │ │ │ │ │ │ └── spec.json │ │ │ │ │ ├── empty │ │ │ │ │ │ └── optic.yml │ │ │ │ │ ├── globs │ │ │ │ │ │ ├── folder-to-run │ │ │ │ │ │ │ ├── ignore │ │ │ │ │ │ │ │ └── should-ignore.yml │ │ │ │ │ │ │ └── should-run.yml │ │ │ │ │ │ └── specwithkey.yml │ │ │ │ │ ├── repo │ │ │ │ │ │ ├── folder │ │ │ │ │ │ │ └── in-folder.yml │ │ │ │ │ │ ├── mvspec.yml │ │ │ │ │ │ ├── optic.dev.yml │ │ │ │ │ │ ├── random-json-with-openapi.json │ │ │ │ │ │ ├── spec-with-invalid-url.yml │ │ │ │ │ │ ├── specwithkey.json │ │ │ │ │ │ ├── specwithkey.yml │ │ │ │ │ │ ├── specwithoutkey.json │ │ │ │ │ │ └── specwithoutkey.yml │ │ │ │ │ └── without-optic-url │ │ │ │ │ │ └── mvspec.yml │ │ │ │ │ ├── diff │ │ │ │ │ ├── basic-rules-dev-yml │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── example-api-v1.json │ │ │ │ │ │ └── optic.dev.yml │ │ │ │ │ ├── custom-rules │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── example-api-v1.json │ │ │ │ │ │ ├── optic.dev.yml │ │ │ │ │ │ └── rules │ │ │ │ │ │ │ ├── cloud-mock.js │ │ │ │ │ │ │ └── local.js │ │ │ │ │ ├── extends │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── example-api-v1.json │ │ │ │ │ │ └── optic.dev.yml │ │ │ │ │ ├── files-no-repo │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ └── example-api-v1.json │ │ │ │ │ ├── petstore │ │ │ │ │ │ ├── petstore-base.json │ │ │ │ │ │ ├── petstore-updated.json │ │ │ │ │ │ └── ruleset.yml │ │ │ │ │ ├── ref-resolve-headers │ │ │ │ │ │ └── optic.yml │ │ │ │ │ ├── repo │ │ │ │ │ │ ├── example-api-1-updated.json │ │ │ │ │ │ ├── example-api-1.json │ │ │ │ │ │ ├── example-api-2-updated.json │ │ │ │ │ │ ├── example-api-2.json │ │ │ │ │ │ └── optic.yml │ │ │ │ │ ├── upload │ │ │ │ │ │ └── spec.json │ │ │ │ │ ├── with-last-change-arg │ │ │ │ │ │ └── example-api.json │ │ │ │ │ ├── with-standard-arg │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ ├── example-api-v1.json │ │ │ │ │ │ └── ruleset.yml │ │ │ │ │ └── with-x-optic-standard │ │ │ │ │ │ ├── example-api-v0.json │ │ │ │ │ │ └── example-api-v1.json │ │ │ │ │ ├── history │ │ │ │ │ └── petstore │ │ │ │ │ │ ├── petstore-base.json │ │ │ │ │ │ └── petstore-updated.json │ │ │ │ │ ├── lint │ │ │ │ │ └── specs │ │ │ │ │ │ ├── optic.dev.yml │ │ │ │ │ │ ├── spec-fails-requirement.yml │ │ │ │ │ │ ├── spec-fails-validation.yml │ │ │ │ │ │ ├── spec-good-spec.yml │ │ │ │ │ │ └── spec-with-bad-formatting.yml │ │ │ │ │ ├── ruleset-publish │ │ │ │ │ ├── invalid-js-file │ │ │ │ │ │ └── rules.js │ │ │ │ │ ├── no-rulesConstructor │ │ │ │ │ │ └── rules.js │ │ │ │ │ └── valid-js-file │ │ │ │ │ │ └── rules.js │ │ │ │ │ ├── run │ │ │ │ │ ├── gitignore │ │ │ │ │ │ ├── openapi.yml │ │ │ │ │ │ └── optic.yml │ │ │ │ │ └── multi-spec │ │ │ │ │ │ ├── openapi.yml │ │ │ │ │ │ ├── optic.yml │ │ │ │ │ │ └── server.js │ │ │ │ │ ├── spec-push │ │ │ │ │ ├── no-x-optic-url │ │ │ │ │ │ └── spec.yml │ │ │ │ │ └── simple │ │ │ │ │ │ └── spec.yml │ │ │ │ │ └── update │ │ │ │ │ ├── empty-spec │ │ │ │ │ ├── har.har │ │ │ │ │ └── openapi.yml │ │ │ │ │ ├── existing-spec │ │ │ │ │ ├── har.har │ │ │ │ │ └── openapi.yml │ │ │ │ │ └── prefix-server-spec │ │ │ │ │ ├── har.har │ │ │ │ │ └── openapi.yml │ │ │ └── optic.yml │ │ ├── client │ │ │ ├── JsonHttpClient.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── optic-backend-types.ts │ │ │ └── optic-backend.ts │ │ ├── commands │ │ │ ├── api │ │ │ │ ├── add.ts │ │ │ │ ├── create.ts │ │ │ │ ├── default-ruleset-config.ts │ │ │ │ ├── get-file-candidates.ts │ │ │ │ ├── git-get-file-candidates.ts │ │ │ │ └── list.ts │ │ │ ├── bundle │ │ │ │ ├── bundle.ts │ │ │ │ ├── json-iterator.test.ts │ │ │ │ └── json-iterator.ts │ │ │ ├── capture │ │ │ │ ├── actions │ │ │ │ │ ├── add-ignore-paths.ts │ │ │ │ │ ├── captureRequests.ts │ │ │ │ │ ├── documented.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── undocumented.ts │ │ │ │ ├── capture-init.ts │ │ │ │ ├── capture.ts │ │ │ │ ├── coverage │ │ │ │ │ └── api-coverage.ts │ │ │ │ ├── init.ts │ │ │ │ ├── interactions │ │ │ │ │ └── grouped-interactions.ts │ │ │ │ ├── operations │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── queries.test.ts.snap │ │ │ │ │ │ ├── path-inference.test.ts │ │ │ │ │ │ └── queries.test.ts │ │ │ │ │ ├── path-inference.ts │ │ │ │ │ └── queries.ts │ │ │ │ ├── patches │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── patches.test.ts.snap │ │ │ │ │ │ └── patches.test.ts │ │ │ │ │ ├── patch-operations.ts │ │ │ │ │ ├── patchers │ │ │ │ │ │ ├── closeness │ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ │ └── closeness.test.ts.snap │ │ │ │ │ │ │ ├── closeness.test.ts │ │ │ │ │ │ │ ├── closeness.ts │ │ │ │ │ │ │ └── schema-inventory.ts │ │ │ │ │ │ ├── shapes │ │ │ │ │ │ │ ├── diff.ts │ │ │ │ │ │ │ ├── documented-bodies.ts │ │ │ │ │ │ │ ├── handlers │ │ │ │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ │ │ │ ├── enum.test.ts.snap │ │ │ │ │ │ │ │ │ │ ├── oneOf.test.ts.snap │ │ │ │ │ │ │ │ │ │ ├── required.test.ts.snap │ │ │ │ │ │ │ │ │ │ └── type.test.ts.snap │ │ │ │ │ │ │ │ │ ├── enum.test.ts │ │ │ │ │ │ │ │ │ ├── oneOf.test.ts │ │ │ │ │ │ │ │ │ ├── required.test.ts │ │ │ │ │ │ │ │ │ └── type.test.ts │ │ │ │ │ │ │ │ ├── additionalProperties.ts │ │ │ │ │ │ │ │ ├── enum.ts │ │ │ │ │ │ │ │ ├── newSchema.ts │ │ │ │ │ │ │ │ ├── oneOf.ts │ │ │ │ │ │ │ │ ├── required.ts │ │ │ │ │ │ │ │ ├── type.ts │ │ │ │ │ │ │ │ └── unevaluatedProperties.ts │ │ │ │ │ │ │ ├── patches.ts │ │ │ │ │ │ │ └── schema.ts │ │ │ │ │ │ └── spec │ │ │ │ │ │ │ ├── operations.ts │ │ │ │ │ │ │ ├── patches.ts │ │ │ │ │ │ │ ├── request-params.ts │ │ │ │ │ │ │ ├── response-headers.ts │ │ │ │ │ │ │ ├── spec.ts │ │ │ │ │ │ │ └── types.ts │ │ │ │ │ ├── patches.ts │ │ │ │ │ └── summaries.ts │ │ │ │ ├── sources │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ ├── har.test.ts.snap │ │ │ │ │ │ │ └── interaction.test.ts.snap │ │ │ │ │ │ ├── body.test.ts │ │ │ │ │ │ ├── fixtures │ │ │ │ │ │ │ ├── echo.postman_collection.json │ │ │ │ │ │ │ ├── githubpaths.json │ │ │ │ │ │ │ └── petstore.swagger.io.har │ │ │ │ │ │ ├── har.test.ts │ │ │ │ │ │ ├── interaction.test.ts │ │ │ │ │ │ └── postman.test.ts │ │ │ │ │ ├── body.ts │ │ │ │ │ ├── captured-interactions.ts │ │ │ │ │ ├── har.ts │ │ │ │ │ ├── postman.ts │ │ │ │ │ └── proxy.ts │ │ │ │ ├── storage.ts │ │ │ │ └── write │ │ │ │ │ └── file.ts │ │ │ ├── ci │ │ │ │ ├── comment │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── common.test.ts.snap │ │ │ │ │ │ └── common.test.ts │ │ │ │ │ ├── comment-api.ts │ │ │ │ │ ├── comment.ts │ │ │ │ │ └── common.ts │ │ │ │ └── setup.ts │ │ │ ├── config.ts │ │ │ ├── dereference │ │ │ │ └── dereference.ts │ │ │ ├── diff │ │ │ │ ├── changelog-renderers │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── json-changelog.test.ts.snap │ │ │ │ │ │ ├── fixtures │ │ │ │ │ │ │ ├── params-new.yaml │ │ │ │ │ │ │ ├── params-old.yaml │ │ │ │ │ │ │ ├── response-headers-new.yaml │ │ │ │ │ │ │ └── response-headers-old.yaml │ │ │ │ │ │ └── json-changelog.test.ts │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── json-changelog.ts │ │ │ │ │ └── terminal-changelog.ts │ │ │ │ ├── compressResults.ts │ │ │ │ ├── compute.ts │ │ │ │ ├── diff-all.ts │ │ │ │ ├── diff.ts │ │ │ │ └── generate-rule-runner.ts │ │ │ ├── history.ts │ │ │ ├── lint │ │ │ │ └── lint.ts │ │ │ ├── login │ │ │ │ └── login.ts │ │ │ ├── oas │ │ │ │ ├── capture-clear.ts │ │ │ │ ├── capture.ts │ │ │ │ ├── captures │ │ │ │ │ ├── capture-storage.ts │ │ │ │ │ ├── getInteractions.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── mac-system-proxy.ts │ │ │ │ │ ├── proxy.test.ts │ │ │ │ │ ├── proxy.ts │ │ │ │ │ ├── run-command.ts │ │ │ │ │ ├── system-proxy.test.ts │ │ │ │ │ └── system-proxy.ts │ │ │ │ ├── diffing │ │ │ │ │ ├── document.test.ts │ │ │ │ │ ├── document.ts │ │ │ │ │ ├── infer-path-structure-legacy.ts │ │ │ │ │ └── patch.ts │ │ │ │ ├── lib │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── forkable.iter.test.ts.snap │ │ │ │ │ │ └── forkable.iter.test.ts │ │ │ │ │ ├── async-tools.ts │ │ │ │ │ └── shell-utils.ts │ │ │ │ ├── new.ts │ │ │ │ ├── operations │ │ │ │ │ ├── diffs │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ └── index.test.ts.snap │ │ │ │ │ │ ├── index.test.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── traversers.ts │ │ │ │ │ │ └── visitors │ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ ├── method.test.ts.snap │ │ │ │ │ │ │ └── path.test.ts.snap │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── method.test.ts │ │ │ │ │ │ │ ├── method.ts │ │ │ │ │ │ │ ├── path.test.ts │ │ │ │ │ │ │ └── path.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── streams │ │ │ │ │ │ ├── documented-interactions.ts │ │ │ │ │ │ └── undocumented.ts │ │ │ │ ├── reporters │ │ │ │ │ ├── feedback.ts │ │ │ │ │ ├── next-command.ts │ │ │ │ │ └── update.ts │ │ │ │ ├── setup-tls.ts │ │ │ │ ├── specs │ │ │ │ │ ├── files │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── reconcilers │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── stringify.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── io.ts │ │ │ │ │ ├── patches │ │ │ │ │ │ └── generators │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── missing-method.ts │ │ │ │ │ │ │ ├── missing-path.ts │ │ │ │ │ │ │ ├── new-spec.ts │ │ │ │ │ │ │ └── template.ts │ │ │ │ │ ├── streams │ │ │ │ │ │ ├── files.ts │ │ │ │ │ │ └── patches.ts │ │ │ │ │ └── templates │ │ │ │ │ │ └── index.ts │ │ │ │ ├── tests │ │ │ │ │ ├── fixtures │ │ │ │ │ │ ├── documented-body.ts │ │ │ │ │ │ ├── facts.ts │ │ │ │ │ │ └── oneof-schemas.ts │ │ │ │ │ ├── inputs │ │ │ │ │ │ ├── invalid-refs.yml │ │ │ │ │ │ └── petstore.yml │ │ │ │ │ └── shapes │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── extend.test.ts.snap │ │ │ │ │ │ └── generate.test.ts.snap │ │ │ │ │ │ ├── extend.test.ts │ │ │ │ │ │ └── generate.test.ts │ │ │ │ ├── update.ts │ │ │ │ └── verify.ts │ │ │ ├── ruleset │ │ │ │ ├── init.ts │ │ │ │ └── upload.ts │ │ │ ├── run.ts │ │ │ └── spec │ │ │ │ ├── add-api-url.ts │ │ │ │ └── push.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── error-handler.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── lib.ts │ │ ├── logger.ts │ │ ├── segment.ts │ │ ├── sentry.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── __snapshots__ │ │ │ └── write-to-file.test.ts.snap │ │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── diff-renderer.test.ts.snap │ │ │ ├── cloud-urls.test.ts │ │ │ ├── diff-renderer.test.ts │ │ │ ├── pathPattern.test.ts │ │ │ └── write-to-file.test.ts.snap │ │ │ ├── capture.ts │ │ │ ├── checksum.ts │ │ │ ├── ci-data.ts │ │ │ ├── cloud-urls.ts │ │ │ ├── diff-renderer.ts │ │ │ ├── git-utils.ts │ │ │ ├── open-url.ts │ │ │ ├── pathPatterns.ts │ │ │ ├── render-cloud.ts │ │ │ ├── s3.ts │ │ │ ├── spec-loaders.ts │ │ │ ├── specs.ts │ │ │ ├── spinner.ts │ │ │ ├── tags.ts │ │ │ ├── write-optic-config.ts │ │ │ ├── write-to-file.test.ts │ │ │ └── write-to-file.ts │ ├── tmp │ │ └── .gitkeep │ ├── tsconfig.json │ └── web │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── craco.config.js │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ │ ├── src │ │ ├── app │ │ │ ├── ChangelogLayout.tsx │ │ │ ├── ChangelogOperation.tsx │ │ │ ├── ChangelogPage.tsx │ │ │ ├── OperationDoc.tsx │ │ │ ├── OperationYml.tsx │ │ │ ├── attributes │ │ │ │ ├── SummarizeSchema.tsx │ │ │ │ ├── Yaml.tsx │ │ │ │ ├── any-attribute.tsx │ │ │ │ ├── base-node.tsx │ │ │ │ ├── change-indicator.tsx │ │ │ │ ├── description.tsx │ │ │ │ ├── heading.tsx │ │ │ │ ├── parameter.tsx │ │ │ │ ├── required.tsx │ │ │ │ ├── text-diff.tsx │ │ │ │ └── url.tsx │ │ │ ├── constants.ts │ │ │ ├── issues │ │ │ │ ├── issue.tsx │ │ │ │ └── issues.tsx │ │ │ ├── schemas │ │ │ │ ├── SchemaContext.tsx │ │ │ │ ├── SchemaDoc.tsx │ │ │ │ └── SchemaExample.tsx │ │ │ └── utils │ │ │ │ ├── all-items.ts │ │ │ │ ├── changelog-tree.ts │ │ │ │ ├── index.ts │ │ │ │ ├── oas3.ts │ │ │ │ ├── oas3_1.ts │ │ │ │ ├── operationId.ts │ │ │ │ ├── rules.ts │ │ │ │ ├── swagger2.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ └── index.tsx │ │ └── tsconfig.json ├── rulesets-base │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── docs │ │ ├── DataShapes.md │ │ ├── OperationRule.md │ │ ├── PropertyRule.md │ │ ├── Reference.md │ │ ├── RequestRule.md │ │ ├── ResponseBodyRule.md │ │ ├── ResponseRule.md │ │ ├── Ruleset.md │ │ └── SpecificationRule.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── operation-rules.test.ts.snap │ │ │ │ ├── property-rules.test.ts.snap │ │ │ │ ├── request-rules.test.ts.snap │ │ │ │ ├── response-body-rules.test.ts.snap │ │ │ │ ├── response-rules.test.ts.snap │ │ │ │ ├── specification-rules.test.ts.snap │ │ │ │ └── spectral-rule.test.ts.snap │ │ │ ├── operation-rules.test.ts │ │ │ ├── property-rules.test.ts │ │ │ ├── request-rules.test.ts │ │ │ ├── response-body-rules.test.ts │ │ │ ├── response-rules.test.ts │ │ │ ├── rule.test.ts │ │ │ ├── ruleset.test.ts │ │ │ ├── specification-rules.test.ts │ │ │ └── spectral-rule.test.ts │ │ ├── custom-rulesets │ │ │ ├── __tests__ │ │ │ │ ├── download-ruleset.test.ts │ │ │ │ ├── mocks │ │ │ │ │ ├── fake-custom-ruleset.ts │ │ │ │ │ └── local-fake-custom-ruleset.ts │ │ │ │ └── prepare-ruleset.test.ts │ │ │ ├── download-ruleset.ts │ │ │ ├── load-ruleset.ts │ │ │ ├── prepare-rulesets.ts │ │ │ └── resolve-ruleset.ts │ │ ├── errors.ts │ │ ├── extended-rules │ │ │ └── spectral-rule.ts │ │ ├── index.ts │ │ ├── rule-runner │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── data-constructors.test.ts.snap │ │ │ │ │ ├── group-facts.test.ts.snap │ │ │ │ │ └── rule-runner.test.ts.snap │ │ │ │ ├── data-constructors.test.ts │ │ │ │ ├── examples │ │ │ │ │ ├── petstore-small.ts │ │ │ │ │ └── petstore.ts │ │ │ │ ├── group-facts.test.ts │ │ │ │ └── rule-runner.test.ts │ │ │ ├── assertions.ts │ │ │ ├── data-constructors.ts │ │ │ ├── group-facts.ts │ │ │ ├── index.ts │ │ │ ├── matchers │ │ │ │ ├── __tests__ │ │ │ │ │ ├── petstore.base.ts │ │ │ │ │ └── utils.test.ts │ │ │ │ ├── operation-matchers.ts │ │ │ │ ├── request-body-matchers.ts │ │ │ │ ├── response-body-matchers.ts │ │ │ │ ├── response-matchers.ts │ │ │ │ ├── specification-matchers.ts │ │ │ │ └── utils.ts │ │ │ ├── operation.ts │ │ │ ├── request.ts │ │ │ ├── response-body.ts │ │ │ ├── response.ts │ │ │ ├── rule-filters.ts │ │ │ ├── rule-runner-types.ts │ │ │ ├── rule-runner.ts │ │ │ ├── specification.ts │ │ │ └── utils.ts │ │ ├── rules │ │ │ ├── external-rule-base.ts │ │ │ ├── index.ts │ │ │ ├── operation-rule.ts │ │ │ ├── property-rule.ts │ │ │ ├── request-rule.ts │ │ │ ├── response-body-rule.ts │ │ │ ├── response-rule.ts │ │ │ ├── ruleset.ts │ │ │ └── specification-rule.ts │ │ ├── test-helpers.ts │ │ └── types.ts │ └── tsconfig.json └── standard-rulesets │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── breaking-changes │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── breaking-changes.test.ts.snap │ │ │ │ ├── enum-breaking-changes.test.ts.snap │ │ │ │ ├── parameter-requirement-breaking-changes.test.ts.snap │ │ │ │ ├── parameter-type-breaking-changes.test.ts.snap │ │ │ │ └── response-status-code-removal.test.ts.snap │ │ │ ├── breaking-changes.test.ts │ │ │ ├── enum-breaking-changes.test.ts │ │ │ ├── parameter-requirement-breaking-changes.test.ts │ │ │ ├── parameter-type-breaking-changes.test.ts │ │ │ └── response-status-code-removal.test.ts │ │ ├── helpers │ │ │ ├── __tests__ │ │ │ │ ├── type-change.test.ts │ │ │ │ └── unions.test.ts │ │ │ ├── getOperationAssertionsParameter.ts │ │ │ ├── type-change.ts │ │ │ ├── types.ts │ │ │ └── unions.ts │ │ ├── index.ts │ │ ├── preventEnumBreak.ts │ │ ├── preventNewRequiredParameter.ts │ │ ├── preventOperationRemoval.ts │ │ ├── preventParameterTypeChange.ts │ │ ├── preventRequestExpandingWithUnionTypes.ts │ │ ├── preventRequestPropertyRequired.ts │ │ ├── preventRequestPropertyTypeChange.ts │ │ ├── preventRequireExistingParameter.ts │ │ ├── preventResponseNarrowingWithUnionType.ts │ │ ├── preventResponsePropertyOptional.ts │ │ ├── preventResponsePropertyRemoval.ts │ │ ├── preventResponsePropertyTypeChange.ts │ │ └── preventResponseStatusCodeRemoval.ts │ ├── documentation │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── documentation.test.ts.snap │ │ │ └── documentation.test.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── requireOperationDescription.ts │ │ ├── requireOperationId.ts │ │ ├── requireOperationSummary.ts │ │ └── requirePropertyDescriptions.ts │ ├── examples │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── examples-are-required.test.ts.snap │ │ │ │ └── examples-are-valid-rules.test.ts.snap │ │ │ ├── examples-are-required.test.ts │ │ │ └── examples-are-valid-rules.test.ts │ │ ├── constants.ts │ │ ├── example-ruleset.test.ts │ │ ├── index.ts │ │ ├── qualifiedContentType.ts │ │ ├── requireExample.ts │ │ └── requireValidExamples.ts │ ├── index.ts │ ├── lintgpt │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── prepare-openapi.test.ts.snap │ │ │ └── prepare-openapi.test.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── prepare-openapi.ts │ │ └── rules-helper.ts │ ├── naming-changes │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── naming-changes.test.ts.snap │ │ │ ├── index.test.ts │ │ │ ├── is-case.test.ts │ │ │ └── naming-changes.test.ts │ │ ├── constants.ts │ │ ├── cookieParameters.ts │ │ ├── index.ts │ │ ├── isCase.ts │ │ ├── operationIds.ts │ │ ├── pathComponents.ts │ │ ├── propertyNames.ts │ │ ├── queryParameters.ts │ │ ├── requestHeaders.ts │ │ └── responseHeader.ts │ ├── spectral │ │ └── index.ts │ └── utils.ts │ └── tsconfig.json ├── renovate.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,ts,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/ @notnmeyer 2 | projects/ @niclim @acunniffe 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Details (please complete the following information):** 26 | - OS and version: [e.g. Mac OS 13.1] 27 | - Optic version: [e.g. v0.37.1] 28 | - NodeJS version: [e.g. 18.0.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Optic Documentation 4 | url: https://www.useoptic.com/docs 5 | about: View Optic's documentation. 6 | - name: Optic Discord 7 | url: https://discord.useoptic.com 8 | about: Join our community and ask questions. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Create a request to help us improve. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | **Describe the enhancement** 10 | A clear and concise description of what the features or enhancement you need. 11 | 12 | **Additional information** 13 | Add any other context about the feature here. 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 🍗 Description 2 | _What does this PR do? Anything folks should know?_ 3 | 4 | ## 📚 References 5 | _Links to relevant docs (Notion, Twist, GH issues, etc.), if applicable._ 6 | 7 | ## 👹 QA 8 | _How can other humans verify that this PR is correct?_ 9 | -------------------------------------------------------------------------------- /.github/scripts/create-checksums.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | archive_dir="dist/archives" 5 | checksums_file="${archive_dir}/checksums.txt" 6 | 7 | # create the checksums file 8 | for file in $(find dist/archives -maxdepth 1 -name '*.tar.gz'); do 9 | shasum -a 256 "$file" >> "$checksums_file" 10 | done 11 | 12 | # strip path components from a checksums file. given a file containing 13 | # checkums in the format, 14 | # > some-checksum some/file.tar.gz 15 | # each line will be rewritten as, 16 | # > some-checksum file.tar.gz 17 | while IFS= read -r line; do 18 | IFS=' ' read -r checksum filepath <<< "$line" 19 | filename=$(basename "$filepath") 20 | echo "$checksum $filename" >> "$checksums_file".tmp 21 | done < "$checksums_file" 22 | 23 | mv "$checksums_file".tmp "$checksums_file" 24 | -------------------------------------------------------------------------------- /.github/scripts/version-sync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eu 3 | 4 | # 5 | # Inspect all packages versions and verify the package.jsons are in sync. 6 | # 7 | 8 | version=$(jq -r '.version' /etc/machine-id 18 | 19 | COPY --from=dl /usr/local/bin/optic /usr/local/bin/ 20 | COPY --from=dl /usr/local/bin/spectral /usr/local/bin/ 21 | 22 | USER optic 23 | ENTRYPOINT ["/usr/local/bin/optic"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022, Optic Labs Corporation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /compass.yml: -------------------------------------------------------------------------------- 1 | name: optic 2 | id: ari:cloud:compass:4e6b61f9-966e-45df-a7e4-a34732c9ac54:component/5cf8af03-9ce9-4892-8ff2-85cd73a86fa2/20f569ca-46c8-4c8d-8e4f-b9d7d43b6d95 3 | description: OpenAPI linting, diffing and testing. Optic helps prevent breaking changes, publish accurate documentation and improve the design of your APIs. 4 | configVersion: 1 5 | typeId: APPLICATION 6 | ownerId: ari:cloud:identity::team/2a95fb33-9faf-4f41-8b90-c9c79d8fb229 7 | fields: 8 | tier: 4 9 | links: 10 | - name: null 11 | type: REPOSITORY 12 | url: https://github.com/opticdev/optic 13 | relationships: 14 | DEPENDS_ON: [] 15 | labels: 16 | - api-documentation 17 | - api-linter 18 | - apis 19 | - documentation 20 | - language:typescript 21 | - openapi 22 | - openapi3 23 | - source:github 24 | - swagger 25 | customFields: null 26 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # optic - Containerized Optic 2 | 3 | ## Build 4 | 5 | `task -l | grep docker` for build tasks and descriptions 6 | 7 | ## Run 8 | 9 | Mount the desired repo to `/repo` and ensure you set the workdir accordingly. For example to run a diff, 10 | 11 | ``` 12 | ➜ docker run --rm --volume=$HOME/code/optic-test:/repo --workdir /repo -it docker.io/useoptic/doptic:local optic diff ./petstore.yml 13 | ``` 14 | 15 | ## Publishing 16 | 17 | See `task docker:build:release --summary` for details. 18 | -------------------------------------------------------------------------------- /docs/dockerhub-readme.md: -------------------------------------------------------------------------------- 1 | ![Optic Logo](https://www.useoptic.com/logo.svg) 2 | 3 | # Quick Reference 4 | 5 | Maintained by: 6 | [The Optic team](https://github.com/opticdev/optic) 7 | 8 | Where to file issues: 9 | [github.com/useoptic/optic/issues](https://github.com/opticdev/optic/issues) 10 | 11 | # What is Optic? 12 | 13 | Optic makes it easy to Track and Review all your API changes before they get released. Start working API-first and ship better APIs, faster. 14 | 15 | # How to use this image 16 | 17 | This image is a thin wrapper around the Optic CLI. It supports the same functions and commands as the [NPM package](https://www.npmjs.com/package/@useoptic/optic). For commands that interact with your files locally, you'll need to mount your Git repo to the container. 18 | 19 | For example, to run `optic diff` your command would look something like this: 20 | 21 | ``` 22 | docker run --rm -it \ 23 | --volume=$HOME/code/optic-test:/repo \ 24 | --workdir /repo \ 25 | docker.io/useoptic/optic:latest \ 26 | diff ./petstore.yml 27 | ``` 28 | -------------------------------------------------------------------------------- /etc/openapi-cli/.env.build.production: -------------------------------------------------------------------------------- 1 | OPTIC_OPENCLI_SEGMENT_KEY=RnaIpitypsVrdB42sEIA5HSEZRYLqRAI 2 | OPTIC_OPENCLI_SENTRY_DSN=https://3c233262587c46f788583aa0964898a2@o446328.ingest.sentry.io/6541152 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi-workspaces", 3 | "license": "MIT", 4 | "private": true, 5 | "version": "1.0.8", 6 | "workspaces": [ 7 | "projects/json-pointer-helpers", 8 | "projects/openapi-io", 9 | "projects/openapi-utilities", 10 | "projects/rulesets-base", 11 | "projects/optic", 12 | "projects/standard-rulesets" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/opticdev/optic" 17 | }, 18 | "scripts": { 19 | "release": "gh release create --target=$(git branch --show-current) v$(node -e \"process.stdout.write(require('./package.json').version)\")", 20 | "version": "yarn workspaces foreach -Av version", 21 | "prepare": "husky install", 22 | "lint": "prettier --check 'projects/*/src/**/*.(js|jsx|ts|tsx|json|css)'" 23 | }, 24 | "devDependencies": { 25 | "husky": "^8.0.0", 26 | "lint-staged": "^15.0.0", 27 | "prettier": "^3.0.0" 28 | }, 29 | "lint-staged": { 30 | "**/*.+(js|jsx|ts|tsx|json|css)": [ 31 | "yarn prettier --write" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /projects/json-pointer-helpers/.gitignore: -------------------------------------------------------------------------------- 1 | src/**/*.test.cjs -------------------------------------------------------------------------------- /projects/json-pointer-helpers/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/json-pointer-helpers/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | - `yarn install` 3 | - `yarn dev:test` 4 | 5 | ## Dependencies 6 | - This codebase uses Yarn 2, Typescript 4, Tap for testing, and should use Ink for CLI behaviors -------------------------------------------------------------------------------- /projects/json-pointer-helpers/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | env: { 5 | production: { 6 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /projects/json-pointer-helpers/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'], 8 | }; 9 | -------------------------------------------------------------------------------- /projects/json-pointer-helpers/src/index.ts: -------------------------------------------------------------------------------- 1 | import jsonPointerHelpers from './json-pointers/json-pointer-helpers'; 2 | export { jsonPointerHelpers }; 3 | -------------------------------------------------------------------------------- /projects/openapi-io/.gitignore: -------------------------------------------------------------------------------- 1 | src/**/*.test.cjs -------------------------------------------------------------------------------- /projects/openapi-io/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/openapi-io/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | - `yarn install` 3 | - `yarn dev:test` 4 | 5 | ## Dependencies 6 | - This codebase uses Yarn 2, Typescript 4, Tap for testing, and should use Ink for CLI behaviors -------------------------------------------------------------------------------- /projects/openapi-io/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | env: { 5 | production: { 6 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/date-example.yml: -------------------------------------------------------------------------------- 1 | object: 2 | key1: "ABC" 3 | "2022-01-02": "DEF" 4 | 2022-01-01: "GHI" 5 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/circular-references.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Recursive Example 4 | description: >- 5 | Documentation of services 6 | version: '1.0' 7 | paths: 8 | /api/example: 9 | get: 10 | summary: Get example 11 | description: example 12 | operationId: GetExample 13 | responses: 14 | '200': 15 | description: Successful operation 16 | headers: {} 17 | content: 18 | application/json: 19 | schema: 20 | $ref: '#/components/schemas/CircularExample' 21 | 22 | deprecated: false 23 | components: 24 | schemas: 25 | CircularExample: 26 | title: CircularExample 27 | type: object 28 | properties: 29 | name: 30 | type: string 31 | description: name of the example 32 | id: 33 | type: string 34 | description: identifier of the example 35 | circular_property: 36 | type: object 37 | additionalProperties: 38 | $ref: '#/components/schemas/CircularExample' 39 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/definitions.yaml: -------------------------------------------------------------------------------- 1 | User: 2 | type: object 3 | required: [name] 4 | properties: 5 | name: 6 | type: string 7 | example: 8 | name: 'Homer' 9 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/external-multiple-branches.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [] 3 | properties: {} 4 | example: 5 | user: 6 | $ref: 'definitions.yaml#/User/example' 7 | user2: 8 | $ref: 'definitions.yaml#/User/example' 9 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/external-multiple.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [user, token] 3 | properties: 4 | token: 5 | type: string 6 | user: 7 | $ref: 'definitions.yaml#/User' 8 | example: 9 | token: '11111111' 10 | user: 11 | $ref: 'definitions.yaml#/User/example' 12 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/internal-multiple.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [user, token] 3 | properties: 4 | token: 5 | type: string 6 | user: 7 | $ref: 'definitions.yaml#/User' 8 | example: 9 | internalRef: 10 | $ref: '#/components/FieldA' 11 | user: 12 | $ref: 'definitions.yaml#/User/example' 13 | components: 14 | FieldA: 15 | type: string 16 | example: 17 | $ref: 'definitions.yaml#/User' 18 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3-with-references/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | title: "some thing" 4 | version: "v0" 5 | paths: 6 | /example: 7 | get: 8 | responses: 9 | 200: 10 | description: "some thing" 11 | content: 12 | application/json: 13 | schema: {$ref: external-multiple.yaml} -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/errors.yaml: -------------------------------------------------------------------------------- 1 | ErrorDocument: 2 | type: object 3 | properties: 4 | jsonapi: { $ref: './common.yaml#/JsonApi' } 5 | errors: 6 | type: array 7 | items: { $ref: '#/Error' } 8 | minItems: 1 9 | additionalProperties: false 10 | required: [ 'jsonapi', 'errors'] 11 | 12 | Error: 13 | type: object 14 | properties: 15 | id: 16 | type: string 17 | format: uuid 18 | status: 19 | type: string 20 | detail: 21 | type: string 22 | source: 23 | type: object 24 | properties: 25 | pointer: 26 | type: string 27 | parameter: 28 | type: string 29 | additionalProperties: false 30 | meta: 31 | type: object 32 | additionalProperties: true 33 | required: ['status', 'detail'] 34 | additionalProperties: false 35 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/parameters/pagination.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | StartingAfter: 3 | name: starting_after 4 | in: query 5 | description: Return the page of results immediately after this cursor 6 | schema: 7 | type: string 8 | EndingBefore: 9 | name: ending_before 10 | in: query 11 | description: Return the page of results immediately before this cursor 12 | schema: 13 | type: string 14 | Limit: 15 | name: limit 16 | in: query 17 | description: Number of results to return per page 18 | schema: 19 | type: integer 20 | minimum: 10 21 | maximum: 100 22 | default: 10 23 | multipleOf: 10 24 | format: int32 25 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/parameters/version.yaml: -------------------------------------------------------------------------------- 1 | Version: 2 | name: version 3 | in: query 4 | required: true 5 | description: The requested version of the endpoint to process the request 6 | schema: 7 | type: string 8 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/204.yaml: -------------------------------------------------------------------------------- 1 | '204': 2 | description: 'The operation completed successfully with no content' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/400.yaml: -------------------------------------------------------------------------------- 1 | '400': 2 | description: 'Bad Request: A parameter provided as a part of the request was invalid.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/401.yaml: -------------------------------------------------------------------------------- 1 | '401': 2 | description: 'Unauthorized: the request requires an authentication token or a token with more permissions.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/403.yaml: -------------------------------------------------------------------------------- 1 | '403': 2 | description: 'Unauthorized: the request requires an authentication token or a token with more permissions.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/404.yaml: -------------------------------------------------------------------------------- 1 | '404': 2 | description: 'Not Found: The resource being operated on could not be found.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/409.yaml: -------------------------------------------------------------------------------- 1 | '409': 2 | description: 'Conflict: The requested operation conflicts with the current state of the resource in some way.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/responses/500.yaml: -------------------------------------------------------------------------------- 1 | '500': 2 | description: 'Internal Server Error: An error was encountered while attempting to process the request.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/tag.yaml: -------------------------------------------------------------------------------- 1 | Tag: 2 | type: object 3 | properties: 4 | key: 5 | type: string 6 | value: 7 | type: string 8 | required: [key, value] 9 | additionalProperties: false 10 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/types.yaml: -------------------------------------------------------------------------------- 1 | Types: 2 | type: string 3 | example: "sometype" 4 | # TODO: enum, generated from all known resources 5 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/components/version.yaml: -------------------------------------------------------------------------------- 1 | Version: 2 | type: string 3 | pattern: '^(wip|work-in-progress|experimental|beta|(([0-9]{4})-([0-1][0-9]))-((3[01])|(0[1-9])|([12][0-9])))$' 4 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/empty-with-url-ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/abc": { 5 | "get": { 6 | "responses": { 7 | "400": { 8 | "$ref": "https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.0/components/responses/400.yaml#/400" 9 | } 10 | } 11 | } 12 | } 13 | }, 14 | "info": { 15 | "version": "0.0.0", 16 | "title": "Empty" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/openapi-io/inputs/openapi3/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": {}, 4 | "info": { 5 | "version": "0.0.0", 6 | "title": "Empty" 7 | } 8 | } -------------------------------------------------------------------------------- /projects/openapi-io/inputs/swagger2/spec.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: '1.2.3' 4 | title: my spec 5 | paths: 6 | /api/users: 7 | get: 8 | produces: ['application/json'] 9 | parameters: 10 | - name: search 11 | in: query 12 | description: search for users 13 | required: true 14 | type: string 15 | responses: 16 | '200': 17 | description: 'response' 18 | schema: 19 | type: object 20 | properties: 21 | id: 22 | type: string 23 | 24 | 25 | -------------------------------------------------------------------------------- /projects/openapi-io/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'], 8 | }; 9 | -------------------------------------------------------------------------------- /projects/openapi-io/src/denormalizers/__tests__/specs/v2/parameters.yml: -------------------------------------------------------------------------------- 1 | something: 2 | name: something 3 | in: query 4 | required: true 5 | description: something here 6 | schema: 7 | type: string 8 | format: uuid 9 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 10 | -------------------------------------------------------------------------------- /projects/openapi-io/src/denormalizers/__tests__/specs/v3/allOf/no-merge.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.1' 2 | info: 3 | title: 'some thing' 4 | version: 'v0' 5 | paths: 6 | /example: 7 | get: 8 | requestBody: 9 | content: 10 | application/json: 11 | schema: 12 | type: object 13 | properties: 14 | a: 15 | type: string 16 | b: 17 | type: number 18 | c: 19 | type: boolean 20 | d: 21 | allOf: 22 | - type: object 23 | properties: 24 | a: 25 | type: string 26 | - type: string 27 | responses: 28 | 200: 29 | description: 'some thing' 30 | content: 31 | application/json: 32 | schema: 33 | type: 'string' 34 | -------------------------------------------------------------------------------- /projects/openapi-io/src/denormalizers/__tests__/specs/v3/allOf/single-child.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.1' 2 | info: 3 | title: 'some thing' 4 | version: 'v0' 5 | paths: 6 | /example: 7 | get: 8 | requestBody: 9 | responses: 10 | 200: 11 | description: 'some thing' 12 | content: 13 | application/json: 14 | schema: 15 | allOf: 16 | - type: string 17 | format: uuid 18 | -------------------------------------------------------------------------------- /projects/openapi-io/src/denormalizers/__tests__/specs/v3/parameters.yml: -------------------------------------------------------------------------------- 1 | something: 2 | name: something 3 | in: query 4 | required: true 5 | description: something here 6 | schema: 7 | type: string 8 | format: uuid 9 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 10 | -------------------------------------------------------------------------------- /projects/openapi-io/src/denormalizers/pointer.ts: -------------------------------------------------------------------------------- 1 | import { sourcemapReader } from '@useoptic/openapi-utilities'; 2 | import { JsonSchemaSourcemap } from '../parser/sourcemap'; 3 | 4 | function getFilePathFromPointer(sourcemap: JsonSchemaSourcemap, path: string) { 5 | const maybePath = sourcemapReader(sourcemap).findFile(path); 6 | 7 | return maybePath?.filePath ?? null; 8 | } 9 | 10 | export function logPointer( 11 | sourcemap: JsonSchemaSourcemap, 12 | pointers: { old: string; new: string } 13 | ) { 14 | const maybeFilePath = getFilePathFromPointer(sourcemap, pointers.old); 15 | 16 | if (maybeFilePath) { 17 | sourcemap.logPointerInFile(maybeFilePath, pointers.old, pointers.new); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/openapi-io/src/parser/__tests__/windows-git-pathing.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from '@jest/globals'; 2 | import { filePathToGitPath } from '../resolvers/git-branch-file-resolver'; 3 | 4 | it('can relativize a windows path', () => { 5 | expect( 6 | filePathToGitPath( 7 | 'C:Users\\circleupx\\source\\openapi-demo\\', 8 | 'C:Users\\circleupx\\source\\openapi-demo\\todo-api.yaml' 9 | ) 10 | ).toMatchInlineSnapshot('"todo-api.yaml"'); 11 | }); 12 | it('can relativize a deep windows path', () => { 13 | expect( 14 | filePathToGitPath( 15 | 'C:Users\\circleupx\\source\\openapi-demo\\', 16 | 'C:Users\\circleupx\\source\\openapi-demo\\todo\\specs\\a-service.yaml' 17 | ) 18 | ).toMatchInlineSnapshot(`"todo/specs/a-service.yaml"`); 19 | }); 20 | it(' windows path', () => { 21 | expect( 22 | filePathToGitPath( 23 | 'C:Users/aidancunniffe/Desktop/openapi-demo', 24 | 'C:Users/aidancunniffe/Desktop/openapi-demo/todo-api.yaml' 25 | ) 26 | ).toMatchInlineSnapshot(`"todo-api.yaml"`); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/openapi-io/src/parser/types.ts: -------------------------------------------------------------------------------- 1 | export type ExternalRefHandler = { 2 | order: number; 3 | canRead: (file: { url: string }) => boolean; 4 | read: (file: { url: string }) => Promise; 5 | }; 6 | -------------------------------------------------------------------------------- /projects/openapi-io/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /projects/openapi-io/src/validation/advanced-validation.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from '@jest/globals'; 2 | import { validateSchema } from './advanced-validation'; 3 | 4 | it('a polymorphic schema have overlapping keywords type', () => { 5 | expect(() => 6 | validateSchema({ oneOf: [], anyOf: [], allOf: [] }) 7 | ).toThrowErrorMatchingInlineSnapshot( 8 | `"schema with oneOf cannot also include keywords: allOf, anyOf"` 9 | ); 10 | }); 11 | 12 | it('an object schema cannot have array items', () => { 13 | expect(() => 14 | // @ts-ignore 15 | validateSchema({ type: 'object', items: [] }) 16 | ).toThrowErrorMatchingInlineSnapshot( 17 | `"schema with type "object" cannot also include keywords: items"` 18 | ); 19 | }); 20 | it('an array schema cannot have properties or required', () => { 21 | expect(() => 22 | // @ts-ignore 23 | validateSchema({ type: 'array', required: [], properties: {} }) 24 | ).toThrowErrorMatchingInlineSnapshot( 25 | `"schema with type "array" cannot also include keywords: properties, required"` 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/openapi-io/src/validation/errors.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | public type: 'validation-error'; 3 | 4 | constructor(message?: string) { 5 | super(message); 6 | this.type = 'validation-error'; 7 | 8 | Object.setPrototypeOf(this, ValidationError.prototype); 9 | } 10 | 11 | static isInstance(v: any): v is OpenAPIVersionError { 12 | return v?.type === 'validation-error'; 13 | } 14 | } 15 | 16 | export class OpenAPIVersionError extends Error { 17 | public type: 'oas-version-error'; 18 | public version: string | undefined; 19 | 20 | constructor(message?: string, version?: string) { 21 | super(message); 22 | this.type = 'oas-version-error'; 23 | this.version = version; 24 | 25 | Object.setPrototypeOf(this, ValidationError.prototype); 26 | } 27 | 28 | static isInstance(v: any): v is OpenAPIVersionError { 29 | return v?.type === 'oas-version-error'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /projects/openapi-io/src/validation/openapi-versions.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@jest/globals'; 2 | import { checkOpenAPIVersion } from './openapi-versions'; 3 | 4 | test('detects a 3.1.x openapi', async () => { 5 | expect(checkOpenAPIVersion({ openapi: '3.1.0' })).toBe('3.1.x'); 6 | expect(checkOpenAPIVersion({ openapi: '3.1.1' })).toBe('3.1.x'); 7 | }); 8 | 9 | test('detects a 3.0.x openapi', async () => { 10 | expect(checkOpenAPIVersion({ openapi: '3.0.3' })).toBe('3.0.x'); 11 | expect(checkOpenAPIVersion({ openapi: '3.0.2' })).toBe('3.0.x'); 12 | expect(checkOpenAPIVersion({ openapi: '3.0.2' })).toBe('3.0.x'); 13 | }); 14 | 15 | test('detects a 2.x.x openapi', async () => { 16 | expect(checkOpenAPIVersion({ swagger: '2.0' })).toBe('2.x.x'); 17 | expect(checkOpenAPIVersion({ swagger: '2.0.3' })).toBe('2.x.x'); 18 | expect(checkOpenAPIVersion({ swagger: '2.1.2' })).toBe('2.x.x'); 19 | }); 20 | 21 | test('throws for unsupported spec version', () => { 22 | expect(() => checkOpenAPIVersion({ openapi: '4.0.2' })).toThrow(); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/openapi-io/src/validation/openapi-versions.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver'; 2 | import { OpenAPIVersionError } from './errors'; 3 | 4 | export type SupportedOpenAPIVersions = '3.1.x' | '3.0.x' | '2.x.x'; 5 | 6 | export function checkOpenAPIVersion(spec: { 7 | openapi?: string; 8 | swagger?: string; 9 | }): SupportedOpenAPIVersions { 10 | if (spec.openapi && semver.satisfies(spec.openapi, '3.1.x')) return '3.1.x'; 11 | if (spec.openapi && semver.satisfies(spec.openapi, '3.0.x')) return '3.0.x'; 12 | if ( 13 | spec.swagger && 14 | (spec.swagger === '2.0' || semver.satisfies(spec.swagger, '2.x.x')) 15 | ) 16 | return '2.x.x'; 17 | throw new OpenAPIVersionError( 18 | `Unsupported OpenAPI version ${ 19 | spec.openapi ?? spec.swagger 20 | }. Optic supports OpenAPI 3.1.x and 3.0.x`, 21 | spec.openapi ?? spec.swagger 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /projects/openapi-io/src/write/index.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'yaml'; 2 | 3 | export function writeYaml(document: any) { 4 | return yaml.stringify(document, { 5 | aliasDuplicateObjects: false, 6 | lineWidth: 0, 7 | }); 8 | } 9 | export function loadYaml(contents: string) { 10 | return yaml.parse(contents); 11 | } 12 | 13 | export function isJson(filePath: string) { 14 | return filePath.endsWith('.json'); 15 | } 16 | export function isYaml(filePath: string) { 17 | return filePath.endsWith('.yml') || filePath.endsWith('.yaml'); 18 | } 19 | -------------------------------------------------------------------------------- /projects/openapi-utilities/.gitignore: -------------------------------------------------------------------------------- 1 | src/**/*.test.cjs -------------------------------------------------------------------------------- /projects/openapi-utilities/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/openapi-utilities/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | - `yarn install` 3 | - `yarn dev:test` 4 | 5 | ## Dependencies 6 | - This codebase uses Yarn 2, Typescript 4, Tap for testing, and should use Ink for CLI behaviors -------------------------------------------------------------------------------- /projects/openapi-utilities/Taskfile.yml: -------------------------------------------------------------------------------- 1 | # https://taskfile.dev/#/installation 2 | version: "3" 3 | 4 | tasks: 5 | test: 6 | desc: Test 7 | cmds: 8 | - yarn run ws:test 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | env: { 5 | production: { 6 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3-with-references/definitions.yaml: -------------------------------------------------------------------------------- 1 | User: 2 | type: object 3 | required: [name] 4 | properties: 5 | name: 6 | type: string 7 | example: 8 | name: 'Homer' 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3-with-references/external-multiple-branches.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [] 3 | properties: {} 4 | example: 5 | user: 6 | $ref: 'definitions.yaml#/User/example' 7 | user2: 8 | $ref: 'definitions.yaml#/User/example' 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3-with-references/external-multiple.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [user, token] 3 | properties: 4 | token: 5 | type: string 6 | user: 7 | $ref: 'definitions.yaml#/User' 8 | example: 9 | token: '11111111' 10 | user: 11 | $ref: 'definitions.yaml#/User/example' 12 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3-with-references/internal-multiple.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: [user, token] 3 | properties: 4 | token: 5 | type: string 6 | user: 7 | $ref: 'definitions.yaml#/User' 8 | example: 9 | internalRef: 10 | $ref: '#/components/FieldA' 11 | user: 12 | $ref: 'definitions.yaml#/User/example' 13 | components: 14 | FieldA: 15 | type: string 16 | example: 17 | $ref: 'definitions.yaml#/User' 18 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3-with-references/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | info: 3 | title: "some thing" 4 | version: "v0" 5 | paths: 6 | /example: 7 | get: 8 | responses: 9 | 200: 10 | description: "some thing" 11 | content: 12 | application/json: 13 | schema: {$ref: external-multiple.yaml} -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": {}, 4 | "info": { 5 | "version": "0.0.0", 6 | "title": "Empty" 7 | } 8 | } -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/org-id-versions/all-dates/0010.5-Apr-2016.87e9639ddaa52f57f3fba1c704f757213c2681d5.spec.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/openapi-utilities/inputs/openapi3/private/snyk/org-id-versions/all-dates/0010.5-Apr-2016.87e9639ddaa52f57f3fba1c704f757213c2681d5.spec.yaml -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/400.yaml: -------------------------------------------------------------------------------- 1 | '400': 2 | description: 'Bad Request: A parameter provided as a part of the request was invalid.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/401.yaml: -------------------------------------------------------------------------------- 1 | '401': 2 | description: 'Unauthorized: the request requires an authentication token or a token with more permissions.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/403.yaml: -------------------------------------------------------------------------------- 1 | '403': 2 | description: 'Forbidden: The client does not have access rights to the content.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/404.yaml: -------------------------------------------------------------------------------- 1 | '404': 2 | description: 'Not Found: The resource being operated on could not be found.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/500.yaml: -------------------------------------------------------------------------------- 1 | '500': 2 | description: 'Internal Server Error: An error was encountered while attempting to process the request.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/appOrg.yaml: -------------------------------------------------------------------------------- 1 | AppOrg: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: 10 | type: object 11 | additionalProperties: false 12 | required: ['type', 'id'] 13 | additionalProperties: false 14 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/appWithSecret.yaml: -------------------------------------------------------------------------------- 1 | AppWithSecret: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: 10 | allOf: 11 | - $ref: './app.yaml#/AppAttributes' 12 | - type: object 13 | properties: 14 | clientSecret: 15 | description: The oauth2 client secret for the app. This is the only time this secret will be returned, store it securely and don't lose it. 16 | type: string 17 | required: ['clientSecret'] 18 | links: { $ref: '../common.yaml#/Links' } 19 | required: ['type', 'id', 'attributes', 'links'] 20 | additionalProperties: false 21 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/errors.yaml: -------------------------------------------------------------------------------- 1 | ErrorDocument: 2 | type: object 3 | properties: 4 | jsonapi: { $ref: './common.yaml#/JsonApi' } 5 | errors: 6 | type: array 7 | items: { $ref: '#/Error' } 8 | minItems: 1 9 | additionalProperties: false 10 | required: [ 'jsonapi', 'errors'] 11 | 12 | Error: 13 | type: object 14 | properties: 15 | id: 16 | type: string 17 | format: uuid 18 | status: 19 | type: string 20 | detail: 21 | type: string 22 | source: 23 | type: object 24 | properties: 25 | pointer: 26 | type: string 27 | parameter: 28 | type: string 29 | additionalProperties: false 30 | meta: 31 | type: object 32 | additionalProperties: true 33 | required: ['status', 'detail'] 34 | additionalProperties: false 35 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/issue-severity.yaml: -------------------------------------------------------------------------------- 1 | IssueSeverity: 2 | name: severity 3 | in: query 4 | required: false 5 | description: Severity of issues to match 6 | schema: { $ref: '../shared/issue-severity.yaml#/IssueSeverity' } 7 | 8 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/issue-type.yaml: -------------------------------------------------------------------------------- 1 | IssueType: 2 | name: type 3 | in: query 4 | required: false 5 | description: Issue type(s) to match, comma-separated 6 | style: form 7 | explode: false 8 | schema: 9 | type: array 10 | items: { $ref: '../shared/issue-type.yaml#/IssueType' } 11 | 12 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/models/appOrg.yaml: -------------------------------------------------------------------------------- 1 | AppOrg: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: 10 | type: object 11 | additionalProperties: false 12 | required: ['type', 'id'] 13 | additionalProperties: false 14 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/models/appWithSecret.yaml: -------------------------------------------------------------------------------- 1 | AppWithSecret: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: 10 | allOf: 11 | - $ref: './app.yaml#/AppAttributes' 12 | - type: object 13 | properties: 14 | clientSecret: 15 | description: The oauth2 client secret for the app. This is the only time this secret will be returned, store it securely and don't lose it. 16 | type: string 17 | required: ['clientSecret'] 18 | links: { $ref: '../common.yaml#/Links' } 19 | required: ['type', 'id', 'attributes', 'links'] 20 | additionalProperties: false 21 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/models/org-invitation.yaml: -------------------------------------------------------------------------------- 1 | OrgInvitation: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: { $ref: '#/OrgInvitationAttributes' } 10 | required: [type, id, attributes] 11 | additionalProperties: false 12 | 13 | OrgInvitationAttributes: 14 | type: object 15 | properties: 16 | email: 17 | description: The email address of the invitee. 18 | type: string 19 | example: 'example@email.com' 20 | isActive: 21 | description: The active status of the invitation. 22 | type: boolean 23 | role: 24 | description: The role assigned to the invitee on acceptance. 25 | type: string 26 | example: 'Developer' 27 | org: 28 | description: The organization the invite was created for. 29 | type: string 30 | example: 'Example org' 31 | required: [email, isActive, role, org] 32 | additionalProperties: false 33 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/models/user.yaml: -------------------------------------------------------------------------------- 1 | User: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | description: Content type. 7 | example: 'user' 8 | id: 9 | type: string 10 | description: The Snyk ID corresponding to this user 11 | format: uuid 12 | example: '55a348e2-c3ad-4bbc-b40e-9b232d1f4121' 13 | attributes: 14 | type: object 15 | properties: 16 | name: 17 | type: string 18 | nullable: false 19 | description: The name of the user. 20 | example: 'user' 21 | email: 22 | type: string 23 | nullable: false 24 | description: The email of the user. 25 | example: 'user@someorg.com' 26 | username: 27 | type: string 28 | nullable: false 29 | description: The username of the user. 30 | example: 'username' 31 | additionalProperties: false 32 | required: ['type', 'id', 'attributes'] 33 | additionalProperties: false 34 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/org-invitation.yaml: -------------------------------------------------------------------------------- 1 | OrgInvitation: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | id: 7 | type: string 8 | format: uuid 9 | attributes: { $ref: '#/OrgInvitationAttributes' } 10 | required: [type, id, attributes] 11 | additionalProperties: false 12 | 13 | OrgInvitationAttributes: 14 | type: object 15 | properties: 16 | email: 17 | description: The email address of the invitee. 18 | type: string 19 | example: 'example@email.com' 20 | isActive: 21 | description: The active status of the invitation. 22 | type: boolean 23 | role: 24 | description: The role assigned to the invitee on acceptance. 25 | type: string 26 | example: 'Developer' 27 | org: 28 | description: The organization the invite was created for. 29 | type: string 30 | example: 'Example org' 31 | required: [email, isActive, role, org] 32 | additionalProperties: false 33 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/pagination.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | StartingAfter: 3 | name: starting_after 4 | in: query 5 | description: Return the page of results immediately after this cursor 6 | schema: 7 | type: string 8 | EndingBefore: 9 | name: ending_before 10 | in: query 11 | description: Return the page of results immediately before this cursor 12 | schema: 13 | type: string 14 | Limit: 15 | name: limit 16 | in: query 17 | description: Number of results to return per page 18 | schema: 19 | type: integer 20 | minimum: 10 21 | maximum: 100 22 | default: 10 23 | multipleOf: 10 24 | format: int32 25 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/issue-severity.yaml: -------------------------------------------------------------------------------- 1 | IssueSeverity: 2 | name: severity 3 | in: query 4 | required: false 5 | description: Severity of issues to match 6 | schema: { $ref: '../shared/issue-severity.yaml#/IssueSeverity' } 7 | 8 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/issue-type.yaml: -------------------------------------------------------------------------------- 1 | IssueType: 2 | name: type 3 | in: query 4 | required: false 5 | description: Issue type(s) to match, comma-separated 6 | style: form 7 | explode: false 8 | schema: 9 | type: array 10 | items: { $ref: '../shared/issue-type.yaml#/IssueType' } 11 | 12 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/pagination.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | StartingAfter: 3 | name: starting_after 4 | in: query 5 | description: Return the page of results immediately after this cursor 6 | schema: 7 | type: string 8 | EndingBefore: 9 | name: ending_before 10 | in: query 11 | description: Return the page of results immediately before this cursor 12 | schema: 13 | type: string 14 | Limit: 15 | name: limit 16 | in: query 17 | description: Number of results to return per page 18 | schema: 19 | type: integer 20 | minimum: 10 21 | maximum: 100 22 | default: 10 23 | multipleOf: 10 24 | format: int32 25 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/project-id.yaml: -------------------------------------------------------------------------------- 1 | ProjectID: 2 | name: projectId 3 | description: Project ID 4 | in: query 5 | required: false 6 | schema: 7 | type: string 8 | format: uuid 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/snapshot-id.yaml: -------------------------------------------------------------------------------- 1 | SnapshotID: 2 | name: snapshot 3 | description: Project Snapshot ID 4 | in: query 5 | required: false 6 | schema: 7 | type: string 8 | format: uuid 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/parameters/version.yaml: -------------------------------------------------------------------------------- 1 | Version: 2 | name: version 3 | in: query 4 | required: true 5 | description: The requested version of the endpoint to process the request 6 | schema: 7 | type: string 8 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/project-id.yaml: -------------------------------------------------------------------------------- 1 | ProjectID: 2 | name: projectId 3 | description: Project ID 4 | in: query 5 | required: false 6 | schema: 7 | type: string 8 | format: uuid 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/responses/400.yaml: -------------------------------------------------------------------------------- 1 | '400': 2 | description: 'Bad Request: A parameter provided as a part of the request was invalid.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/responses/401.yaml: -------------------------------------------------------------------------------- 1 | '401': 2 | description: 'Unauthorized: the request requires an authentication token or a token with more permissions.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/responses/403.yaml: -------------------------------------------------------------------------------- 1 | '403': 2 | description: 'Forbidden: The client does not have access rights to the content.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/responses/404.yaml: -------------------------------------------------------------------------------- 1 | '404': 2 | description: 'Not Found: The resource being operated on could not be found.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/responses/500.yaml: -------------------------------------------------------------------------------- 1 | '500': 2 | description: 'Internal Server Error: An error was encountered while attempting to process the request.' 3 | headers: 4 | snyk-version-requested: { $ref: '../headers/headers.yaml#/VersionRequestedResponseHeader' } 5 | snyk-version-served: { $ref: '../headers/headers.yaml#/VersionServedResponseHeader' } 6 | snyk-request-id: { $ref: '../headers/headers.yaml#/RequestIdResponseHeader' } 7 | snyk-version-lifecycle-stage: { $ref: '../headers/headers.yaml#/VersionStageResponseHeader' } 8 | deprecation: { $ref: '../headers/headers.yaml#/DeprecationHeader' } 9 | sunset: { $ref: '../headers/headers.yaml#/SunsetHeader' } 10 | content: 11 | application/vnd.api+json: 12 | schema: { $ref: '../errors.yaml#/ErrorDocument' } 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/shared/issue-severity.yaml: -------------------------------------------------------------------------------- 1 | IssueSeverity: 2 | type: string 3 | description: Severity of an issue 4 | enum: 5 | - low 6 | - medium 7 | - high 8 | - critical 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/shared/issue-type.yaml: -------------------------------------------------------------------------------- 1 | IssueType: 2 | type: string 3 | description: > 4 | Issue type. Implies the existence of a resource 5 | /issues/detail/{issue-type}/{id}. 6 | enum: 7 | - code 8 | # more to come... 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/shared/tag.yaml: -------------------------------------------------------------------------------- 1 | Tag: 2 | type: object 3 | properties: 4 | key: 5 | type: string 6 | value: 7 | type: string 8 | required: [key, value] 9 | additionalProperties: false 10 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/snapshot-id.yaml: -------------------------------------------------------------------------------- 1 | SnapshotID: 2 | name: snapshot 3 | description: Project Snapshot ID 4 | in: query 5 | required: false 6 | schema: 7 | type: string 8 | format: uuid 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/tag.yaml: -------------------------------------------------------------------------------- 1 | Tag: 2 | type: object 3 | properties: 4 | key: 5 | type: string 6 | value: 7 | type: string 8 | required: [key, value] 9 | additionalProperties: false 10 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/user.yaml: -------------------------------------------------------------------------------- 1 | User: 2 | type: object 3 | properties: 4 | type: 5 | type: string 6 | description: Content type. 7 | example: 'user' 8 | id: 9 | type: string 10 | description: The Snyk ID corresponding to this user 11 | format: uuid 12 | example: '55a348e2-c3ad-4bbc-b40e-9b232d1f4121' 13 | attributes: 14 | type: object 15 | properties: 16 | name: 17 | type: string 18 | nullable: false 19 | description: The name of the user. 20 | example: 'user' 21 | email: 22 | type: string 23 | nullable: false 24 | description: The email of the user. 25 | example: 'user@someorg.com' 26 | username: 27 | type: string 28 | nullable: false 29 | description: The username of the user. 30 | example: 'username' 31 | additionalProperties: false 32 | required: ['type', 'id', 'attributes'] 33 | additionalProperties: false 34 | -------------------------------------------------------------------------------- /projects/openapi-utilities/inputs/openapi3/private/snyk/schemas/version.yaml: -------------------------------------------------------------------------------- 1 | Version: 2 | name: version 3 | in: query 4 | required: true 5 | description: The requested version of the endpoint to process the request 6 | schema: 7 | type: string 8 | -------------------------------------------------------------------------------- /projects/openapi-utilities/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'] 8 | }; 9 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/coverage/coverage.ts: -------------------------------------------------------------------------------- 1 | export type CoverageNode = { 2 | seen: boolean; 3 | diffs: boolean; 4 | }; 5 | 6 | export type OperationCoverage = CoverageNode & { 7 | checksum: string; 8 | interactions: number; 9 | requestBody?: CoverageNode; 10 | responses: { 11 | [statusCode: string]: CoverageNode; 12 | }; 13 | }; 14 | 15 | export type ApiCoverage = { 16 | paths: { 17 | [pathPattern: string]: { 18 | [methods: string]: OperationCoverage; 19 | }; 20 | }; 21 | }; 22 | 23 | export function countOperationCoverage( 24 | operation: OperationCoverage, 25 | fn: (x: CoverageNode) => boolean 26 | ): number { 27 | let coverage = 0; 28 | 29 | if (fn(operation)) { 30 | coverage++; 31 | } 32 | if (operation.requestBody && fn(operation.requestBody)) { 33 | coverage++; 34 | } 35 | for (const response of Object.values(operation.responses)) { 36 | if (fn(response)) { 37 | coverage++; 38 | } 39 | } 40 | 41 | return coverage; 42 | } 43 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/diff/array-identifiers.ts: -------------------------------------------------------------------------------- 1 | export const isParameterObject = ( 2 | value: any 3 | ): value is { name: string; in: string } => 4 | typeof value === 'object' && 5 | !Array.isArray(value) && 6 | value !== null && 7 | 'name' in value && 8 | 'in' in value; 9 | 10 | export const getParameterIdentity = ( 11 | obj: T 12 | ): string => `name${obj.name}:in${obj.in}`; 13 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/diff/openapi-matchers.ts: -------------------------------------------------------------------------------- 1 | import { jsonPointerHelpers } from '@useoptic/json-pointer-helpers'; 2 | import { getParameterIdentity } from './array-identifiers'; 3 | import { FlatOpenAPIV3 } from '../flat-openapi-types'; 4 | 5 | const methods = `{get,post,put,delete,patch,head,options}`; 6 | export const isPathParameterArray = (pointer: string): boolean => { 7 | return ( 8 | jsonPointerHelpers.matches(pointer, ['paths', '**', 'parameters']) || 9 | jsonPointerHelpers.matches(pointer, ['paths', '**', methods, 'parameters']) 10 | ); 11 | }; 12 | 13 | export const isPathsMap = (pointer: string): boolean => { 14 | return jsonPointerHelpers.matches(pointer, ['paths']); 15 | }; 16 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class UserError extends Error { 2 | public type: 'user-error'; 3 | public initialError?: Error; 4 | constructor(opts: { message?: string; initialError?: Error } = {}) { 5 | super(opts.message); 6 | this.name = 'UserError'; 7 | this.type = 'user-error'; 8 | this.initialError = opts.initialError; 9 | 10 | // https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work 11 | Object.setPrototypeOf(this, UserError.prototype); 12 | } 13 | 14 | static isInstance(v: any): v is UserError { 15 | return v?.type === 'user-error'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/openapi3/constants.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from 'openapi-types'; 2 | 3 | export const defaultEmptySpec: OpenAPIV3.Document = { 4 | openapi: '3.1.0', 5 | paths: {}, 6 | info: { version: '0.0.0', title: 'Empty' }, 7 | }; 8 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/openapi3/sdk/isType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FactVariant, 3 | ChangeVariant, 4 | OpenApiKind, 5 | IFact, 6 | IChange, 7 | } from './types'; 8 | 9 | // We need these type asserters since we don't have a top level property to 10 | // narrow types 11 | // https://stackoverflow.com/a/50872262 -> No narrowing by nested properties 12 | export const isFactVariant = ( 13 | fact: IFact, 14 | kind: FactKind 15 | ): fact is Extract> => fact.location.kind === kind; 16 | 17 | export const isChangeVariant = ( 18 | change: IChange, 19 | kind: ChangeKind 20 | ): change is Extract> => 21 | change.location.kind === kind; 22 | 23 | export const isFactOrChangeVariant = ( 24 | factOrChange: IFact | IChange, 25 | kind: Kind 26 | ): factOrChange is 27 | | Extract> 28 | | Extract> => factOrChange.location.kind === kind; 29 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/openapi3/sdk/types/openApiKinds.ts: -------------------------------------------------------------------------------- 1 | export enum OpenApiKind { 2 | Specification = 'specification', 3 | Operation = 'operation', 4 | Request = 'request', 5 | QueryParameter = 'query-parameter', 6 | PathParameter = 'path-parameter', 7 | HeaderParameter = 'header-parameter', 8 | CookieParameter = 'cookie-parameter', 9 | ResponseHeader = 'response-header', 10 | Response = 'response', 11 | Body = 'body', 12 | BodyExample = 'body-example', 13 | Field = 'field', 14 | Schema = 'schema', 15 | ComponentSchemaExample = 'component-schema-example', 16 | } 17 | 18 | export type OpenApiParameterKind = Extract< 19 | OpenApiKind, 20 | | OpenApiKind.HeaderParameter 21 | | OpenApiKind.PathParameter 22 | | OpenApiKind.QueryParameter 23 | | OpenApiKind.CookieParameter 24 | >; 25 | 26 | // allow for iterations and `.includes` calls 27 | export const OpenApiParameterKind: OpenApiKind[] = [ 28 | OpenApiKind.HeaderParameter, 29 | OpenApiKind.PathParameter, 30 | OpenApiKind.QueryParameter, 31 | OpenApiKind.CookieParameter, 32 | ]; 33 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/specs/tags.ts: -------------------------------------------------------------------------------- 1 | // Valid tags are alphanumeric and can include one `:` 2 | export const SPEC_TAG_REGEXP = /^[a-zA-Z0-9-_\./]+(:[a-zA-Z0-9-_\./]+)?$/; 3 | 4 | // Santize a git ref to a format that we accept https://git-scm.com/docs/git-check-ref-format#_description 5 | export const sanitizeGitTag = (tag: string): string => 6 | tag.replace(/[^a-zA-Z0-9-_/\.:]/g, ''); 7 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/swagger2/index.ts: -------------------------------------------------------------------------------- 1 | export const SWAGGER2_HTTP_METHODS = [ 2 | 'get', 3 | 'put', 4 | 'post', 5 | 'delete', 6 | 'options', 7 | 'head', 8 | 'patch', 9 | ] as const; 10 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Severity } from './results'; 2 | import { IChange, IFact } from './openapi3/sdk/types'; 3 | 4 | export type LookupLineResult = { 5 | endLine: number; 6 | endPosition: number; 7 | startLine: number; 8 | startPosition: number; 9 | }; 10 | 11 | export type LookupLineResultWithFilepath = LookupLineResult & { 12 | filePath: string; 13 | }; 14 | 15 | export interface Result { 16 | where: string; 17 | error?: string; 18 | passed: boolean; 19 | exempted?: boolean; 20 | change: IChange | IFact; // IFact for `requirement` 21 | docsLink?: string; 22 | // new 23 | name?: string; 24 | expected?: string; // JSON string values 25 | received?: string; // JSON string values 26 | type?: 'requirement' | 'added' | 'changed' | 'removed'; 27 | severity?: Severity; 28 | 29 | // to deprecate 30 | condition?: string; 31 | effectiveOnDate?: Date; 32 | isShould: boolean; 33 | isMust: boolean; 34 | } 35 | 36 | export type ResultWithSourcemap = Result & { 37 | sourcemap?: LookupLineResultWithFilepath; 38 | }; 39 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/utilities/__tests__/group-changes.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@jest/globals'; 2 | import { 3 | factsToChangelog, 4 | OpenAPITraverser, 5 | groupChangesAndRules, 6 | } from '../../index'; 7 | import { openAPI as petStoreBase } from '../../examples/petstore-base'; 8 | import { openAPI as petStoreUpdated } from '../../examples/petstore-updated'; 9 | 10 | test('groupChangesAndRules diffs and groups changes between two open api files', () => { 11 | const baseTraverser = new OpenAPITraverser(); 12 | baseTraverser.traverse(petStoreBase); 13 | 14 | const targetTraverser = new OpenAPITraverser(); 15 | targetTraverser.traverse(petStoreUpdated); 16 | 17 | const baseFacts = [...baseTraverser.facts()]; 18 | const targetFacts = [...targetTraverser.facts()]; 19 | 20 | expect( 21 | groupChangesAndRules({ 22 | toFacts: targetFacts, 23 | changes: factsToChangelog(baseFacts, targetFacts), 24 | rules: [], 25 | }) 26 | ).toMatchSnapshot('pet store grouped changes'); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/utilities/compare-changes-by-path.ts: -------------------------------------------------------------------------------- 1 | import { IChange } from '../openapi3/sdk/types'; 2 | 3 | /** 4 | * Sort changes by conceptual path, lexicographically, ancestors first 5 | */ 6 | export function compareChangesByPath( 7 | changeA: IChange, 8 | changeB: IChange 9 | ): number { 10 | const pathA = changeA.location.conceptualPath; 11 | const pathB = changeB.location.conceptualPath; 12 | 13 | for (let i = 0; i < Math.max(pathA.length, pathB.length); i++) { 14 | let a = pathA[i]; 15 | let b = pathB[i]; 16 | 17 | if (a === b) continue; 18 | if (!b) return 1; 19 | if (!a) return -1; 20 | if (a < b) return -1; 21 | if (a > b) return 1; 22 | } 23 | 24 | return pathB.length - pathA.length; 25 | } 26 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/utilities/id.ts: -------------------------------------------------------------------------------- 1 | const ENDPOINT_ID_SEPARATOR = '-~_~-'; 2 | export const getEndpointId = (endpoint: { 3 | method: string; 4 | path: string; 5 | }): string => { 6 | return `${endpoint.method.toUpperCase()}${ENDPOINT_ID_SEPARATOR}${ 7 | endpoint.path 8 | }`; 9 | }; 10 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/utilities/traverse-spec.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPITraverser } from '../openapi3/implementations/openapi3/openapi-traverser'; 2 | import { IFact } from '../openapi3/sdk/types'; 3 | import { FlatOpenAPIV3, FlatOpenAPIV3_1 } from '../flat-openapi-types'; 4 | 5 | export const traverseSpec = ( 6 | spec: FlatOpenAPIV3.Document | FlatOpenAPIV3_1.Document 7 | ): IFact[] => { 8 | const traverser = new OpenAPITraverser(); 9 | traverser.traverse(spec); 10 | return [...traverser.facts()]; 11 | }; 12 | -------------------------------------------------------------------------------- /projects/openapi-utilities/src/utilities/truthy.ts: -------------------------------------------------------------------------------- 1 | export const isTruthyStringValue = (value: string): boolean => { 2 | const lowerCase = value.toLowerCase(); 3 | 4 | return ( 5 | lowerCase === 'true' || 6 | lowerCase === '1' || 7 | lowerCase === 'yes' || 8 | lowerCase === 'y' || 9 | lowerCase === 'on' 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /projects/optic/.env.example: -------------------------------------------------------------------------------- 1 | # staging 2 | 3 | OPTIC_ENV=staging 4 | OPTIC_TOKEN=your-token 5 | 6 | # production 7 | 8 | # OPTIC_ENV=production 9 | # OPTIC_TOKEN=your-token 10 | -------------------------------------------------------------------------------- /projects/optic/.env.production: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/.env.production -------------------------------------------------------------------------------- /projects/optic/.gitignore: -------------------------------------------------------------------------------- 1 | test_data 2 | temp 3 | ci-context.json 4 | tmp 5 | -------------------------------------------------------------------------------- /projects/optic/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/optic/Developers.md: -------------------------------------------------------------------------------- 1 | ## Running optic locally 2 | 3 | Copy the .env.example file, `cp .env.example .env`, and replace the values in the file with your own. 4 | 5 | In the root monorepo directory, run `task build`. Then in the project root directory `/projects/optic` you can run: `yarn run local:run ` 6 | 7 | # Examples 8 | 9 | ``` 10 | yarn local:run --help 11 | yarn local:run init --help 12 | ``` 13 | -------------------------------------------------------------------------------- /projects/optic/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: [ 4 | '@babel/plugin-transform-runtime', 5 | [ 6 | 'transform-inline-environment-variables', 7 | { 8 | include: ['SENTRY_URL', 'SEGMENT_KEY'], 9 | }, 10 | ], 11 | ], 12 | env: { 13 | production: { 14 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 15 | }, 16 | }, 17 | targets: { 18 | node: 16, 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /projects/optic/jest-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | }; 4 | -------------------------------------------------------------------------------- /projects/optic/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'], 8 | moduleNameMapper: { 9 | '^nimma/fallbacks$': [ 10 | '../../node_modules/nimma/dist/cjs/fallbacks/index.js', 11 | '../../../../node_modules/nimma/dist/cjs/fallbacks/index.js', 12 | ], 13 | '^nimma/legacy$': [ 14 | '../../node_modules/nimma/dist/legacy/cjs/index.js', 15 | '../../../../node_modules/nimma/dist/legacy/cjs/index.js', 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /projects/optic/specs/test-spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: number 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/bundle.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | test, 3 | expect, 4 | describe, 5 | jest, 6 | beforeEach, 7 | afterEach, 8 | } from '@jest/globals'; 9 | import { runOptic, setupWorkspace, normalizeWorkspace } from './integration'; 10 | 11 | jest.setTimeout(30000); 12 | 13 | let oldEnv: any; 14 | beforeEach(() => { 15 | oldEnv = { ...process.env }; 16 | process.env.LOG_LEVEL = 'info'; 17 | process.env.OPTIC_ENV = 'local'; 18 | }); 19 | 20 | afterEach(() => { 21 | process.env = { ...oldEnv }; 22 | }); 23 | 24 | describe('bundle', () => { 25 | test('bundles components together', async () => { 26 | const workspace = await setupWorkspace('bundle/specs'); 27 | const { combined, code } = await runOptic(workspace, 'bundle openapi.yml'); 28 | expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); 29 | expect(code).toBe(0); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-add/many-files/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-add/many-files/nested/speccopy2.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-add/many-files/not-added.yml: -------------------------------------------------------------------------------- 1 | iaminvalid: true -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-add/many-files/spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-add/one-file/spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-list/many-files/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-list/many-files/nested/speccopy2.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-list/many-files/not-added.yml: -------------------------------------------------------------------------------- 1 | iaminvalid: true -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/api-list/many-files/spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/bundle/specs/anotherschema.yml: -------------------------------------------------------------------------------- 1 | CreatedAt: 2 | type: string 3 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/bundle/specs/schemas.yml: -------------------------------------------------------------------------------- 1 | TodoRead: 2 | type: object 3 | title: Todo 4 | properties: 5 | name: 6 | type: string 7 | status: 8 | type: string 9 | created_at: 10 | $ref: 'anotherschema.yml#/CreatedAt' 11 | firstTodo: 12 | $ref: "#/TodoRead" 13 | todos: 14 | type: array 15 | items: 16 | $ref: "#/TodoRead" 17 | required: 18 | - name 19 | - status 20 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture-init/no-yml/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/src/__tests__/integration/workspaces/capture-init/no-yml/empty.txt -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture-init/yml/optic.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | - '@org/custom-ruleset' 6 | - ./rules/local.js 7 | capture: 8 | openapi.yml: 9 | server: 10 | command: node server.js 11 | url: http://localhost:3000 12 | ready_endpoint: /healthcheck 13 | requests: 14 | send: 15 | - path: / 16 | - path: /books 17 | method: GET 18 | - path: /books/asd 19 | method: GET 20 | - path: /books/def 21 | method: GET 22 | - path: /books/asd 23 | method: POST 24 | data: 25 | name: asd 26 | price: 1 27 | author_id: 6nTxAFM5ck4Hob77hGQoL 28 | - path: /authors 29 | method: GET 30 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/har/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /form: 8 | post: 9 | requestBody: 10 | required: true 11 | content: 12 | multipart/form-data: 13 | schema: 14 | type: object 15 | properties: 16 | file: 17 | type: string 18 | required: 19 | - file 20 | responses: {} 21 | /books: 22 | get: 23 | responses: 24 | "200": 25 | description: 200 response 26 | content: 27 | application/json: 28 | schema: 29 | $ref: "#/components/schemas/GetBooks200ResponseBody" 30 | components: 31 | schemas: 32 | GetBooks200ResponseBody: 33 | type: object 34 | properties: 35 | books: 36 | type: array 37 | items: 38 | type: object 39 | properties: 40 | id: 41 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/postman/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /books: 8 | get: 9 | responses: 10 | "200": 11 | description: 200 response 12 | content: 13 | application/json: 14 | schema: 15 | $ref: "#/components/schemas/GetBooks200ResponseBody" 16 | components: 17 | schemas: 18 | GetBooks200ResponseBody: 19 | type: object 20 | properties: 21 | books: 22 | type: array 23 | items: 24 | type: object 25 | properties: 26 | id: 27 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/components/books.yml: -------------------------------------------------------------------------------- 1 | GetBooks200ResponseBody: 2 | type: object 3 | properties: 4 | books: 5 | type: array 6 | items: 7 | type: object 8 | properties: 9 | id: 10 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/file.txt: -------------------------------------------------------------------------------- 1 | file-info 2 | morestuff -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-prefix-and-server-urls.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | servers: 7 | - name: server 8 | url: http://localhost:%PORT/api 9 | paths: 10 | /books: 11 | get: 12 | responses: 13 | "200": 14 | description: 200 response 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/GetBooks200ResponseBody" 19 | components: 20 | schemas: 21 | GetBooks200ResponseBody: 22 | type: object 23 | properties: 24 | books: 25 | type: array 26 | items: 27 | type: object 28 | properties: 29 | id: 30 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-prefixed-url.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /books: 8 | get: 9 | responses: 10 | "200": 11 | description: 200 response 12 | content: 13 | application/json: 14 | schema: 15 | $ref: "#/components/schemas/GetBooks200ResponseBody" 16 | components: 17 | schemas: 18 | GetBooks200ResponseBody: 19 | type: object 20 | properties: 21 | books: 22 | type: array 23 | items: 24 | type: object 25 | properties: 26 | id: 27 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-with-external-ref-spaces.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /books: 8 | get: 9 | responses: 10 | "200": 11 | description: 200 response 12 | content: 13 | application/json: 14 | schema: 15 | $ref: ./with space/books.yml#/GetBooks200ResponseBody 16 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-with-external-ref.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /books: 8 | get: 9 | responses: 10 | "200": 11 | description: 200 response 12 | content: 13 | application/json: 14 | schema: 15 | $ref: ./components/books.yml#/GetBooks200ResponseBody 16 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-with-ignore.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-path-ignore: 7 | - method: get 8 | path: /books/** 9 | - /authors 10 | paths: 11 | /books: 12 | get: 13 | responses: 14 | "200": 15 | description: 200 response 16 | content: 17 | application/json: 18 | schema: 19 | $ref: "#/components/schemas/GetBooks200ResponseBody" 20 | /authors: 21 | get: 22 | responses: 23 | "200": 24 | description: 200 response 25 | content: 26 | application/json: 27 | schema: 28 | type: object 29 | components: 30 | schemas: 31 | GetBooks200ResponseBody: 32 | type: object 33 | properties: 34 | books: 35 | type: array 36 | items: 37 | type: object 38 | properties: 39 | id: 40 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/openapi-with-server-prefix.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | servers: 7 | - name: server 8 | url: http://localhost:%PORT/api 9 | paths: 10 | /books: 11 | get: 12 | responses: 13 | "200": 14 | description: 200 response 15 | content: 16 | application/json: 17 | schema: 18 | $ref: "#/components/schemas/GetBooks200ResponseBody" 19 | components: 20 | schemas: 21 | GetBooks200ResponseBody: 22 | type: object 23 | properties: 24 | books: 25 | type: array 26 | items: 27 | type: object 28 | properties: 29 | id: 30 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/capture/with-server/with space/books.yml: -------------------------------------------------------------------------------- 1 | GetBooks200ResponseBody: 2 | type: object 3 | properties: 4 | books: 5 | type: array 6 | items: 7 | type: object 8 | properties: 9 | id: 10 | type: string -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/cloud-diff/spec-no-url.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginalaa", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginalaa", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginaaal", 19 | "responses": { 20 | "200": { 21 | "description": "hello", 22 | "content": { 23 | "application/json": { 24 | "example": "hello", 25 | "schema": { 26 | "type": "string" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "info": { 36 | "version": "0.0.0", 37 | "title": "Empty" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/cloud-diff/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "x-optic-url": "https://app.useoptic.com/organizations/org-id/apis/api-id", 4 | "paths": { 5 | "/example": { 6 | "get": { 7 | "operationId": "my-op", 8 | "responses": {} 9 | }, 10 | "post": { 11 | "operationId": "postOriginalaa", 12 | "responses": {} 13 | }, 14 | "put": { 15 | "operationId": "putOriginalaa", 16 | "responses": {} 17 | }, 18 | "patch": { 19 | "operationId": "putOriginaaal", 20 | "responses": { 21 | "200": { 22 | "description": "hello", 23 | "content": { 24 | "application/json": { 25 | "example": "hello", 26 | "schema": { 27 | "type": "string" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "info": { 37 | "version": "0.0.0", 38 | "title": "Empty" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/empty/optic.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/globs/folder-to-run/ignore/should-ignore.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'https://app.useoptic.com/organizations/org-id/apis/api-id' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/globs/folder-to-run/should-run.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'https://app.useoptic.com/organizations/org-id/apis/api-id' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/globs/specwithkey.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'https://app.useoptic.com/organizations/org-id/apis/api-id' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/optic.dev.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/random-json-with-openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "bad": { 3 | "this-isnt-a-spec": { 4 | "but has word": "openapi" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/spec-with-invalid-url.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'not-good' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/specwithkey.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "x-optic-url": "https://app.useoptic.com/organizations/org-id/apis/api-id", 4 | "paths": { 5 | "/example": { 6 | "get": { 7 | "operationId": "my-op-original", 8 | "responses": {} 9 | }, 10 | "post": { 11 | "operationId": "postOriginal", 12 | "responses": {} 13 | }, 14 | "put": { 15 | "operationId": "putOriginal", 16 | "responses": {} 17 | }, 18 | "patch": { 19 | "operationId": "putOriginal", 20 | "responses": {} 21 | } 22 | } 23 | }, 24 | "info": { 25 | "version": "0.0.0", 26 | "title": "API 1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/specwithkey.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'https://app.useoptic.com/organizations/org-id/apis/api-id' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/specwithoutkey.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "API 2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff-all/repo/specwithoutkey.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/basic-rules-dev-yml/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/basic-rules-dev-yml/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "put": { 10 | "operationId": "putOriginalaa", 11 | "responses": {} 12 | }, 13 | "patch": { 14 | "operationId": "putOriginaaal", 15 | "responses": { 16 | "200": { 17 | "description": "hello", 18 | "content": { 19 | "application/json": { 20 | "example": "hello", 21 | "schema": { 22 | "type": "number" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/invalid_name": { 31 | "get": { 32 | "operationId": "getInvalidName", 33 | "responses": {} 34 | } 35 | } 36 | }, 37 | "info": { 38 | "version": "0.0.0", 39 | "title": "Empty" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/basic-rules-dev-yml/optic.dev.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/custom-rules/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/custom-rules/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "put": { 10 | "operationId": "putOriginalaa", 11 | "responses": {} 12 | }, 13 | "patch": { 14 | "operationId": "putOriginaaal", 15 | "responses": { 16 | "200": { 17 | "description": "hello", 18 | "content": { 19 | "application/json": { 20 | "example": "hello", 21 | "schema": { 22 | "type": "number" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/invalid_name": { 31 | "get": { 32 | "operationId": "getInvalidName", 33 | "responses": {} 34 | } 35 | } 36 | }, 37 | "info": { 38 | "version": "0.0.0", 39 | "title": "Empty" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/custom-rules/optic.dev.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | - '@org/custom-ruleset' 6 | - ./rules/local.js 7 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/custom-rules/rules/cloud-mock.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/src/__tests__/integration/workspaces/diff/custom-rules/rules/cloud-mock.js -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/extends/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/extends/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "put": { 10 | "operationId": "putOriginalaa", 11 | "responses": {} 12 | }, 13 | "patch": { 14 | "operationId": "putOriginaaal", 15 | "responses": { 16 | "200": { 17 | "description": "hello", 18 | "content": { 19 | "application/json": { 20 | "example": "hello", 21 | "schema": { 22 | "type": "number" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/invalid_name": { 31 | "get": { 32 | "operationId": "getInvalidName", 33 | "responses": {} 34 | } 35 | } 36 | }, 37 | "info": { 38 | "version": "0.0.0", 39 | "title": "Empty" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/extends/optic.dev.yml: -------------------------------------------------------------------------------- 1 | extends: '@test-org/ruleset-id' 2 | ruleset: 3 | - naming: 4 | pathComponents: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/files-no-repo/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/files-no-repo/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginalaa", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginalaa", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginaaal", 19 | "responses": { 20 | "200": { 21 | "description": "hello", 22 | "content": { 23 | "application/json": { 24 | "example": "hello", 25 | "schema": { 26 | "type": "string" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "info": { 36 | "version": "0.0.0", 37 | "title": "Empty" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/petstore/ruleset.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | required_on: added 5 | requestHeaders: snake_case 6 | queryParameters: snake_case 7 | responseHeaders: snake_case 8 | cookieParameters: snake_case 9 | pathComponents: snake_case 10 | properties: snake_case 11 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/ref-resolve-headers/optic.yml: -------------------------------------------------------------------------------- 1 | external_refs: 2 | resolve_headers: 3 | - headers: 4 | custom_header: hello -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/repo/example-api-1-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginalaa", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginalaa", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginaaal", 19 | "responses": { 20 | "200": { 21 | "description": "hello", 22 | "content": { 23 | "application/json": { 24 | "example": "hello", 25 | "schema": { 26 | "type": "string" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "info": { 36 | "version": "0.0.0", 37 | "title": "API 1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/repo/example-api-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "API 1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/repo/example-api-2-updated.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginalaa", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginalaa", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginaaal", 19 | "responses": { 20 | "200": { 21 | "description": "hello", 22 | "content": { 23 | "application/json": { 24 | "example": "hello", 25 | "schema": { 26 | "type": "string" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "info": { 36 | "version": "0.0.0", 37 | "title": "API 2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/repo/example-api-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "API 2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/repo/optic.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - id: example-api-1 3 | path: ./example-api-1.json 4 | - id: example-api-2 5 | path: ./example-api-2.json 6 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/upload/spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "x-optic-url": "https://app.useoptic.com/organizations/org-id/apis/api-id", 4 | "paths": { 5 | "/example": { 6 | "get": { 7 | "operationId": "my-op", 8 | "responses": {} 9 | }, 10 | "post": { 11 | "operationId": "postOriginalaa", 12 | "responses": {} 13 | }, 14 | "put": { 15 | "operationId": "putOriginalaa", 16 | "responses": {} 17 | }, 18 | "patch": { 19 | "operationId": "putOriginaaal", 20 | "responses": { 21 | "200": { 22 | "description": "hello", 23 | "content": { 24 | "application/json": { 25 | "example": "hello", 26 | "schema": { 27 | "type": "string" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "info": { 37 | "version": "0.0.0", 38 | "title": "Empty" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/with-standard-arg/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/with-standard-arg/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op", 7 | "responses": {} 8 | }, 9 | "put": { 10 | "operationId": "putOriginalaa", 11 | "responses": {} 12 | }, 13 | "patch": { 14 | "operationId": "putOriginaaal", 15 | "responses": { 16 | "200": { 17 | "description": "hello", 18 | "content": { 19 | "application/json": { 20 | "example": "hello", 21 | "schema": { 22 | "type": "number" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | "/invalid_name": { 31 | "get": { 32 | "operationId": "getInvalidName", 33 | "responses": {} 34 | } 35 | } 36 | }, 37 | "info": { 38 | "version": "0.0.0", 39 | "title": "Empty" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/with-standard-arg/ruleset.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/with-x-optic-standard/example-api-v0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "paths": { 4 | "/example": { 5 | "get": { 6 | "operationId": "my-op-original", 7 | "responses": {} 8 | }, 9 | "post": { 10 | "operationId": "postOriginal", 11 | "responses": {} 12 | }, 13 | "put": { 14 | "operationId": "putOriginal", 15 | "responses": {} 16 | }, 17 | "patch": { 18 | "operationId": "putOriginal", 19 | "responses": {} 20 | } 21 | } 22 | }, 23 | "info": { 24 | "version": "0.0.0", 25 | "title": "Empty" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/diff/with-x-optic-standard/example-api-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "x-optic-standard": "@test-org/ruleset-id", 4 | "paths": { 5 | "/example": { 6 | "get": { 7 | "operationId": "my-op", 8 | "responses": {} 9 | }, 10 | "put": { 11 | "operationId": "putOriginalaa", 12 | "responses": {} 13 | }, 14 | "patch": { 15 | "operationId": "putOriginaaal", 16 | "responses": { 17 | "200": { 18 | "description": "hello", 19 | "content": { 20 | "application/json": { 21 | "example": "hello", 22 | "schema": { 23 | "type": "number" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | }, 31 | "/invalid_name": { 32 | "get": { 33 | "operationId": "getInvalidName", 34 | "responses": {} 35 | } 36 | } 37 | }, 38 | "info": { 39 | "version": "0.0.0", 40 | "title": "Empty" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/lint/specs/optic.dev.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - naming: 3 | required_on: always 4 | properties: camelCase 5 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/lint/specs/spec-fails-requirement.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id_thing: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/lint/specs/spec-fails-validation.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | version: 0.1.0 4 | paths: 5 | /filler_route: 6 | post: 7 | operationId: create 8 | responses: 9 | "201": 10 | description: Created successfully 11 | content: 12 | application/json: 13 | schema: 14 | type: object 15 | properties: 16 | id: 17 | type: string 18 | format: uuid 19 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 20 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/lint/specs/spec-good-spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/lint/specs/spec-with-bad-formatting.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/ruleset-publish/invalid-js-file/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'no-description', 3 | rulesetConstructor: () => { 4 | return {}; 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/ruleset-publish/no-rulesConstructor/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'no-rules', 3 | description: 'hello', 4 | configSchema: { 5 | type: 'object', 6 | properties: { 7 | validate_all: { 8 | type: 'boolean', 9 | }, 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/ruleset-publish/valid-js-file/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'the-best-ruleset', 3 | description: 'hello', 4 | configSchema: { 5 | type: 'object', 6 | properties: { 7 | validate_all: { 8 | type: 'boolean', 9 | }, 10 | }, 11 | }, 12 | rulesetConstructor: () => { 13 | return {}; 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/run/gitignore/optic.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - naming: 3 | properties: snake_case 4 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/run/multi-spec/optic.yml: -------------------------------------------------------------------------------- 1 | ruleset: 2 | - breaking-changes 3 | - naming: 4 | pathComponents: camelCase 5 | capture: 6 | openapi.yml: 7 | server: 8 | command: node server.js 9 | url: http://localhost:%PORT 10 | ready_endpoint: /healthcheck 11 | requests: 12 | send: 13 | - path: / 14 | - path: /books 15 | method: GET 16 | - path: /books/asd 17 | method: GET 18 | - path: /books/def 19 | method: GET 20 | - path: /books/asd 21 | method: POST 22 | data: 23 | name: asd 24 | price: 1 25 | author_id: 6nTxAFM5ck4Hob77hGQoL 26 | - path: /authors 27 | method: GET 28 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/spec-push/no-x-optic-url/spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: 7 | /filler_route: 8 | post: 9 | operationId: create 10 | responses: 11 | "201": 12 | description: Created successfully 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | id: 19 | type: string 20 | format: uuid 21 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 22 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/spec-push/simple/spec.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | x-optic-url: 'https://app.useoptic.com/organizations/org-id/apis/api-id' 7 | paths: 8 | /filler_route: 9 | post: 10 | operationId: create 11 | responses: 12 | "201": 13 | description: Created successfully 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | properties: 19 | id: 20 | type: string 21 | format: uuid 22 | example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 23 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/update/empty-spec/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: {} 7 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/update/existing-spec/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | paths: {} 7 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/integration/workspaces/update/prefix-server-spec/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: a spec 4 | description: The API 5 | version: 0.1.0 6 | servers: 7 | - name: book server 8 | url: http://localhost:3030/api 9 | paths: {} 10 | -------------------------------------------------------------------------------- /projects/optic/src/__tests__/optic.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - id: spec 3 | path: openapi-spec.yml 4 | 5 | capture: 6 | openapi.yml: 7 | server: 8 | url: https://echo.o3c.org 9 | requests: 10 | send: 11 | - path: /users 12 | headers: 13 | x-response-json: '[{"id":0, "name":"aidan"}]' 14 | authorization: "token {{TOKEN}}" 15 | 16 | -------------------------------------------------------------------------------- /projects/optic/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export { createOpticClient, OpticBackendClient } from './optic-backend'; 2 | export * as BackendTypes from './optic-backend-types'; 3 | -------------------------------------------------------------------------------- /projects/optic/src/client/optic-backend-types.ts: -------------------------------------------------------------------------------- 1 | export type Standard = { 2 | config: { 3 | ruleset: { name: string; config: unknown }[]; 4 | }; 5 | name: string; 6 | slug: string; 7 | organization_id: string; 8 | ruleset_id: string; 9 | created_at: string; 10 | updated_at: string; 11 | }; 12 | 13 | export type StandardConfig = { name: string; config: any }[]; 14 | 15 | export type Api = { 16 | api_id: string; 17 | organization_id: string; 18 | path: string; 19 | }; 20 | -------------------------------------------------------------------------------- /projects/optic/src/commands/api/create.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import { errorHandler } from '../../error-handler'; 3 | import { OpticCliConfig, VCS } from '../../config'; 4 | 5 | const usage = () => ` 6 | optic api create `; 7 | 8 | const helpText = ` 9 | Example usage: 10 | Add an Optic API URL to a spec file. 11 | $ optic api create 12 | `; 13 | 14 | export const registerApiCreate = (cli: Command, config: OpticCliConfig) => { 15 | cli 16 | .command('create') 17 | .configureHelp({ 18 | commandUsage: usage, 19 | }) 20 | .addHelpText('after', helpText) 21 | .description('Generate an optic url to add to your specs') 22 | .argument('', 'the name of the api') 23 | .action( 24 | errorHandler(getApiCreateAction(config), { command: 'api-create' }) 25 | ); 26 | }; 27 | 28 | const getApiCreateAction = (config: OpticCliConfig) => async (name: string) => { 29 | console.error('api create is not supported'); 30 | return; 31 | }; 32 | -------------------------------------------------------------------------------- /projects/optic/src/commands/api/default-ruleset-config.ts: -------------------------------------------------------------------------------- 1 | export function getDefaultRulesetConfig(rulesetName: string): any { 2 | if (rulesetName === 'breaking-changes') { 3 | return {}; 4 | } else if (rulesetName === 'naming') { 5 | return { 6 | required_on: 'added', 7 | requestHeaders: 'snake_case', 8 | queryParameters: 'snake_case', 9 | responseHeaders: 'snake_case', 10 | cookieParameters: 'snake_case', 11 | pathComponents: 'snake_case', 12 | properties: 'snake_case', 13 | }; 14 | } else if (rulesetName === 'examples') { 15 | return { 16 | require_request_examples: true, 17 | require_response_examples: true, 18 | require_parameter_examples: true, 19 | required_on: 'always', 20 | }; 21 | } 22 | 23 | return {}; 24 | } 25 | -------------------------------------------------------------------------------- /projects/optic/src/commands/api/get-file-candidates.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'path'; 3 | 4 | export async function getFileCandidates(opts: { 5 | startsWith: string; 6 | }): Promise { 7 | const files: string[] = []; 8 | const stack: string[] = [process.cwd()]; 9 | while (stack.length > 0) { 10 | const dir: string = stack.pop()!; 11 | for (const file of await fs.readdir(dir, { withFileTypes: true })) { 12 | const absolutePath = path.join(dir, file.name); 13 | if (!absolutePath.startsWith(opts.startsWith)) { 14 | continue; 15 | } 16 | 17 | if (file.isDirectory()) { 18 | stack.push(absolutePath); 19 | } else if (/\.(json|ya?ml)$/i.test(file.name)) { 20 | files.push(absolutePath); 21 | } 22 | } 23 | } 24 | 25 | return files; 26 | } 27 | -------------------------------------------------------------------------------- /projects/optic/src/commands/bundle/json-iterator.ts: -------------------------------------------------------------------------------- 1 | import { jsonPointerHelpers } from '@useoptic/json-pointer-helpers'; 2 | 3 | interface IteratorResult { 4 | pointer: string; 5 | value: any; 6 | } 7 | 8 | export function* jsonIterator( 9 | obj: any, 10 | path: string = jsonPointerHelpers.compile([]) 11 | ): Iterable { 12 | if (Array.isArray(obj)) { 13 | for (let i = 0; i < obj.length; i++) { 14 | const newPath = jsonPointerHelpers.append(path, i.toString()); 15 | yield* jsonIterator(obj[i], newPath); 16 | } 17 | } else if (typeof obj === 'object' && obj !== null) { 18 | for (const key in obj) { 19 | if (obj.hasOwnProperty(key)) { 20 | const newPath = jsonPointerHelpers.append(path, key); 21 | yield* jsonIterator(obj[key], newPath); 22 | } 23 | } 24 | } 25 | yield { pointer: path, value: obj }; 26 | } 27 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/actions/add-ignore-paths.ts: -------------------------------------------------------------------------------- 1 | import { ParseResult } from '../../../utils/spec-loaders'; 2 | import { SpecPatches } from '../patches/patchers/spec/patches'; 3 | import { getIgnorePathPatch } from '../patches/patchers/spec/spec'; 4 | import { jsonOpsFromSpecPatches } from '../patches/patches'; 5 | import { writePatchesToFiles } from '../write/file'; 6 | 7 | export async function addIgnorePaths( 8 | parseResult: Exclude, 9 | ignorePaths: { method: string; path: string }[] 10 | ) { 11 | const specPatches: SpecPatches = (async function* (): SpecPatches { 12 | yield getIgnorePathPatch(parseResult.jsonLike, ignorePaths); 13 | })(); 14 | 15 | const operations = await jsonOpsFromSpecPatches(specPatches); 16 | await writePatchesToFiles(operations, parseResult.sourcemap); 17 | } 18 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add-ignore-paths'; 2 | export * from './documented'; 3 | export * from './undocumented'; 4 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/patches/patch-operations.ts: -------------------------------------------------------------------------------- 1 | import { Operation as PatchOperation } from 'fast-json-patch'; 2 | 3 | export type { PatchOperation }; 4 | 5 | export enum PatchImpact { 6 | BackwardsCompatible = 'BackwardsCompatible', 7 | BackwardsIncompatible = 'BackwardsIncompatible', 8 | BackwardsCompatibilityUnknown = 'BackwardsCompatibilityUnknown', 9 | Addition = 'Addition', 10 | Refactor = 'Refactor', 11 | } 12 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/sources/__tests__/body.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from '@jest/globals'; 2 | import { CapturedBody } from '../body'; 3 | 4 | describe('CaputeredBody', () => { 5 | it('can be created and parsed', async () => { 6 | const testString = 'any string will do'; 7 | const justBody = CapturedBody.from(testString, 'text/plain'); 8 | (''); 9 | expect(await CapturedBody.text(justBody)).toEqual(testString); 10 | 11 | const testJson = { a: 1, b: '2', c: { d: 3 }, e: [null] }; 12 | const jsonBody = CapturedBody.fromJSON(testJson); 13 | 14 | expect(await CapturedBody.json(jsonBody)).toEqual(testJson); 15 | 16 | const withContentType = CapturedBody.from(testString, 'application/text'); 17 | expect(withContentType.contentType).toEqual('application/text'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/sources/body.ts: -------------------------------------------------------------------------------- 1 | export interface CapturedBody { 2 | contentType: string | null; 3 | size: number; // size in bytes, defaults to -1 4 | body: string | null; 5 | } 6 | 7 | export class CapturedBody { 8 | static from( 9 | stringValue: string | null, 10 | contentType: string | null = null 11 | ): CapturedBody { 12 | return { contentType, body: stringValue, size: stringValue?.length || 0 }; 13 | } 14 | 15 | static fromJSON(json: { [key: string]: any }, ...rest) { 16 | const asJsonString = JSON.stringify(json); 17 | return CapturedBody.from(asJsonString, 'application/json'); 18 | } 19 | 20 | static body(body: CapturedBody) { 21 | return body.body; 22 | } 23 | 24 | static async json(body) { 25 | const text = await CapturedBody.text(body); 26 | return JSON.parse(text!); 27 | } 28 | 29 | static async text(body: CapturedBody) { 30 | return body.body; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/optic/src/commands/capture/storage.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import fs from 'node:fs/promises'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | 6 | const tmpDirectory = os.tmpdir(); 7 | 8 | export async function getCaptureStorage(filePath: string): Promise { 9 | const resolvedFilepath = path.resolve(filePath); 10 | 11 | const specPathHash = crypto 12 | .createHash('md5') 13 | .update(resolvedFilepath) 14 | .digest('hex'); 15 | 16 | // TODO in the future we can reuse cached requests if paths are identical (maybe hash paths?) 17 | const trafficDirectory = path.join( 18 | tmpDirectory, 19 | 'optic', 20 | 'captures-v2', 21 | specPathHash, 22 | String(Date.now()) 23 | ); 24 | 25 | await fs.mkdir(trafficDirectory, { recursive: true }); 26 | 27 | return trafficDirectory; 28 | } 29 | -------------------------------------------------------------------------------- /projects/optic/src/commands/diff/changelog-renderers/__tests__/fixtures/params-new.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: test 4 | version: 1.0.0 5 | description: test_description 6 | paths: 7 | /test/set1: 8 | get: 9 | summary: Test1 10 | parameters: 11 | - in: header 12 | name: X-username 13 | schema: 14 | type: string 15 | responses: 16 | '200': 17 | description: OK 18 | content: 19 | application/json: 20 | schema: 21 | type: string 22 | /test/set2: 23 | post: 24 | summary: Test2 25 | responses: 26 | '200': 27 | description: OK 28 | content: 29 | application/json: 30 | schema: 31 | type: string 32 | parameters: 33 | - in: header 34 | name: X-username 35 | schema: 36 | type: string 37 | - in: query 38 | name: test_test 39 | schema: 40 | type: string 41 | -------------------------------------------------------------------------------- /projects/optic/src/commands/diff/changelog-renderers/__tests__/fixtures/params-old.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: test 4 | version: 1.0.0 5 | description: test_description 6 | paths: 7 | /test/set1: 8 | get: 9 | summary: Test1 10 | responses: 11 | '200': 12 | description: OK 13 | content: 14 | application/json: 15 | schema: 16 | type: string 17 | /test/set2: 18 | post: 19 | summary: Test2 20 | responses: 21 | '200': 22 | description: OK 23 | content: 24 | application/json: 25 | schema: 26 | type: string 27 | parameters: 28 | - in: header 29 | name: X-username 30 | schema: 31 | type: string 32 | -------------------------------------------------------------------------------- /projects/optic/src/commands/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import { OpticCliConfig } from '../../config'; 3 | import { errorHandler } from '../../error-handler'; 4 | export const registerLogin = (cli: Command, config: OpticCliConfig) => { 5 | cli 6 | .command('login', { hidden: true }) 7 | .description('Login to Optic') 8 | .action(errorHandler(getLoginAction(config), { command: 'login' })); 9 | }; 10 | 11 | const getLoginAction = (config: OpticCliConfig) => async () => { 12 | console.error('login is not supported'); 13 | return; 14 | }; 15 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/captures/capture-storage.ts: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import os from 'os'; 4 | const tmpDirectory = os.tmpdir(); 5 | import crypto from 'crypto'; 6 | 7 | export async function captureStorage(filePath: string): Promise<{ 8 | openApiExists: boolean; 9 | trafficDirectory: string; 10 | existingCaptures: number; 11 | }> { 12 | const resolvedFilepath = Path.resolve(filePath); 13 | const openApiExists: boolean = await fs.pathExists(resolvedFilepath); 14 | 15 | const specPathHash = crypto 16 | .createHash('md5') 17 | .update(resolvedFilepath) 18 | .digest('hex'); 19 | 20 | const trafficDirectory = Path.join( 21 | tmpDirectory, 22 | 'optic', 23 | 'captures', 24 | specPathHash 25 | ); 26 | 27 | await fs.ensureDir(trafficDirectory); 28 | 29 | return { 30 | openApiExists, 31 | trafficDirectory, 32 | existingCaptures: (await fs.readdir(trafficDirectory)).length, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/captures/index.ts: -------------------------------------------------------------------------------- 1 | export { getInteractions } from './getInteractions'; 2 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/captures/mac-system-proxy.ts: -------------------------------------------------------------------------------- 1 | export function parseMacNetworkLine(line: string): { 2 | enabled: boolean; 3 | host: string; 4 | port: string; 5 | authenticated: boolean; 6 | } { 7 | const match = line.match( 8 | /Enabled: (Yes|No)\nServer: ([:/\da-zA-Z.\-_]*)\nPort: (\d+)\nAuthenticated Proxy Enabled: (0|1)/ 9 | ); 10 | 11 | if (match) { 12 | const [_, enabled, host, port, authenticated] = match; 13 | 14 | return { 15 | enabled: enabled === 'Yes', 16 | host, 17 | port, 18 | authenticated: authenticated === 'Yes', 19 | }; 20 | } else { 21 | throw new Error(`Unexpected proxy config returned from networksetup`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/diffing/document.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from '@jest/globals'; 2 | import { parseAddOperations } from './document'; 3 | 4 | it('can parse from input string', () => { 5 | const toAdd = parseAddOperations(['get /todos', 'post /todos/{status}/']); 6 | expect(toAdd.unwrap()).toMatchInlineSnapshot(` 7 | [ 8 | { 9 | "methods": [ 10 | "get", 11 | ], 12 | "pathPattern": "/todos", 13 | }, 14 | { 15 | "methods": [ 16 | "post", 17 | ], 18 | "pathPattern": "/todos/{status}", 19 | }, 20 | ] 21 | `); 22 | }); 23 | 24 | it('invalid input strings empty', () => { 25 | const toAdd = parseAddOperations(['DeTTE /todos']); 26 | expect(toAdd.err).toBe(true); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/lib/__tests__/__snapshots__/forkable.iter.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`forks share backpressure and run in lock-step 1`] = ` 4 | [ 5 | "fast: this", 6 | "slow: this", 7 | "fast: is", 8 | "slow: is", 9 | "fast: a", 10 | "slow: a", 11 | "fast: sentence", 12 | "slow: sentence", 13 | ] 14 | `; 15 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/lib/shell-utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { exec } from 'child_process'; 3 | import isElevated from 'is-elevated'; 4 | 5 | export const platform: 'mac' | 'windows' | 'linux' = 6 | process.platform === 'win32' 7 | ? 'windows' 8 | : process.platform === 'darwin' 9 | ? 'mac' 10 | : 'linux'; 11 | 12 | export async function exitIfNotElevated(withError: string) { 13 | const result = await isElevated(); 14 | if (result) { 15 | return; 16 | } else { 17 | console.log(chalk.red(withError)); 18 | process.exit(2); 19 | } 20 | } 21 | 22 | export async function runCommand(command: string): Promise { 23 | return new Promise((resolve, reject) => { 24 | exec(command, (err, stdout, stderr) => { 25 | if (err) { 26 | console.error(`exec error: ${err}`); 27 | return reject(err); 28 | } 29 | resolve(stdout); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`diffOperationWithSpec yields diffs for paths with no methods 1`] = ` 4 | { 5 | "kind": "UnmatchedPath", 6 | "subject": "/orders/{orderId}/products", 7 | } 8 | `; 9 | 10 | exports[`diffOperationWithSpec yields diffs for unmatched methods 1`] = ` 11 | { 12 | "kind": "UnmatchedMethod", 13 | "pathPattern": "/orders/{orderId}/products", 14 | "subject": "post", 15 | } 16 | `; 17 | 18 | exports[`diffOperationWithSpec yields diffs for unmatched methods 2`] = ` 19 | [ 20 | { 21 | "kind": "UnmatchedMethod", 22 | "pathPattern": "/orders/{orderId}/products", 23 | "subject": "post", 24 | }, 25 | { 26 | "kind": "UnmatchedMethod", 27 | "pathPattern": "/orders/{orderId}/products", 28 | "subject": "put", 29 | }, 30 | ] 31 | `; 32 | 33 | exports[`diffOperationWithSpec yields diffs for unmatched paths 1`] = ` 34 | { 35 | "kind": "UnmatchedPath", 36 | "subject": "/orders/{orderId}/transaction", 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/index.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from '../../specs'; 2 | import { OperationDiffResult } from '../../../capture/patches/patchers/spec/types'; 3 | import { SpecOperationDiffTraverser } from './traversers'; 4 | import { FlatOpenAPIV3, FlatOpenAPIV3_1 } from '@useoptic/openapi-utilities'; 5 | 6 | export function* diffOperationWithSpec( 7 | operation: { 8 | pathPattern: string; 9 | methods: OpenAPIV3.HttpMethods[]; 10 | }, 11 | spec: FlatOpenAPIV3.Document | FlatOpenAPIV3_1.Document 12 | ): IterableIterator { 13 | const traverser = new SpecOperationDiffTraverser(); 14 | traverser.traverse(operation, spec); 15 | yield* traverser.results(); 16 | } 17 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/__snapshots__/method.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`visitMethod detects an unspecified path 1`] = ` 4 | [ 5 | { 6 | "kind": "UnmatchedMethod", 7 | "pathPattern": "/orders/{orderId}", 8 | "subject": "get", 9 | }, 10 | ] 11 | `; 12 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/__snapshots__/path.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`visitPath detects an unspecified path 1`] = ` 4 | [ 5 | { 6 | "kind": "UnmatchedPath", 7 | "subject": "/orders/{orderId}", 8 | }, 9 | ] 10 | `; 11 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/index.ts: -------------------------------------------------------------------------------- 1 | import { OperationDiffResult } from '../../../../capture/patches/patchers/spec/types'; 2 | 3 | export interface OperationDiffVisitor { 4 | (interaction: I, spec: T, context?: C): IterableIterator; 5 | } 6 | 7 | export { visitPath } from './path'; 8 | export { visitMethod } from '../../../operations/diffs/visitors/method'; 9 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/method.test.ts: -------------------------------------------------------------------------------- 1 | import { it, describe, expect } from '@jest/globals'; 2 | import { visitMethod } from './method'; 3 | import { OpenAPIV3 } from '../../../specs'; 4 | import { Some, None } from 'ts-results'; 5 | 6 | describe('visitMethod', () => { 7 | it('detects an unspecified path', () => { 8 | const operation: OpenAPIV3.OperationObject = { 9 | responses: {}, 10 | }; 11 | 12 | const matchingResults = [ 13 | ...visitMethod(OpenAPIV3.HttpMethods.GET, Some(operation), { 14 | pathPattern: '/orders/{orderId}', 15 | }), 16 | ]; 17 | expect(matchingResults).toHaveLength(0); 18 | 19 | const unmatchingResults = [ 20 | ...visitMethod(OpenAPIV3.HttpMethods.GET, None, { 21 | pathPattern: '/orders/{orderId}', 22 | }), 23 | ]; 24 | expect(unmatchingResults).toHaveLength(1); 25 | expect(unmatchingResults).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/method.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from '../../../specs'; 2 | import { 3 | OperationDiffResult, 4 | OperationDiffResultKind, 5 | } from '../../../../capture/patches/patchers/spec/types'; 6 | import { Option } from 'ts-results'; 7 | 8 | export function* visitMethod( 9 | method: OpenAPIV3.HttpMethods, 10 | spec: Option, 11 | context: { pathPattern: string } 12 | ): IterableIterator { 13 | if (spec.none) { 14 | yield { 15 | kind: OperationDiffResultKind.UnmatchedMethod, 16 | subject: method, 17 | pathPattern: context.pathPattern, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/path.test.ts: -------------------------------------------------------------------------------- 1 | import { it, describe, expect } from '@jest/globals'; 2 | import { visitPath } from './path'; 3 | import { Some, None } from 'ts-results'; 4 | 5 | describe('visitPath', () => { 6 | it('detects an unspecified path', () => { 7 | const matchingResults = [ 8 | ...visitPath('/orders/{orderId}', Some({}), { 9 | pathPattern: Some('/orders/{orderId}'), 10 | }), 11 | ]; 12 | expect(matchingResults).toHaveLength(0); 13 | 14 | const unmatchingResults = [ 15 | ...visitPath('/orders/{orderId}', None, { pathPattern: None }), 16 | ]; 17 | expect(unmatchingResults).toHaveLength(1); 18 | expect(unmatchingResults).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/operations/diffs/visitors/path.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from '../../../specs'; 2 | import { 3 | OperationDiffResult, 4 | OperationDiffResultKind, 5 | } from '../../../../capture/patches/patchers/spec/types'; 6 | import { Option } from 'ts-results'; 7 | 8 | export function* visitPath( 9 | path: string, 10 | specOption: Option, 11 | context: { pathPattern: Option } 12 | ): IterableIterator { 13 | if (specOption.none) { 14 | yield { 15 | kind: OperationDiffResultKind.UnmatchedPath, 16 | subject: path, 17 | }; 18 | } 19 | 20 | // TODO: match path's parameters against those documented 21 | // let parameterComponents = PathComponents.fromPath(path).filter( 22 | // PathComponent.isTemplate 23 | // ); 24 | // const spec = specOption.unwrap() 25 | // spec.parameters 26 | } 27 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/reporters/next-command.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export function nextCommand(task: string, runCommand: string) { 4 | return chalk.bold(`${task} $ ${chalk.dim(runCommand)}`); 5 | } 6 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/files/reconcilers/index.ts: -------------------------------------------------------------------------------- 1 | import { Operation } from '../../../../capture/patches/patchers/spec/patches'; 2 | 3 | export { applyPatch } from './stringify'; 4 | 5 | export interface SpecFileReconciler { 6 | ( 7 | filePath: string, 8 | fileContents: string, 9 | operations: Operation[], 10 | config?: Config 11 | ): Promise; 12 | } 13 | export type PatchApplyResult = 14 | | { contents: string; success: true; filePath: string } 15 | | { error: string; success: false; filePath: string }; 16 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/index.ts: -------------------------------------------------------------------------------- 1 | export { readDeferencedSpec } from './io'; 2 | 3 | export { 4 | OpenAPIV3, 5 | isFactVariant, 6 | OpenApiKind as FactVariants, 7 | } from '@useoptic/openapi-utilities'; 8 | 9 | // files 10 | export { SpecFile, SpecFilesSourcemap } from './files'; 11 | export type { SpecFileOperation } from './files'; 12 | 13 | // patches and operations 14 | export { SpecFileOperations, SpecFiles, SpecFilesAsync } from './streams/files'; 15 | 16 | // templates 17 | export { SpecTemplate } from './templates'; 18 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/io.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseOpenAPIWithSourcemap, 3 | ParseOpenAPIResult, 4 | JSONParserError, 5 | } from '@useoptic/openapi-io'; 6 | import { Result, Ok, Err } from 'ts-results'; 7 | 8 | export async function readDeferencedSpec( 9 | path: string 10 | ): Promise, JSONParserError>> { 11 | try { 12 | return Ok(await parseOpenAPIWithSourcemap(path)); 13 | } catch (err) { 14 | if (err instanceof JSONParserError) { 15 | return Err(err); 16 | } else { 17 | // all errors should be considered unrecoverable, so panic (throw) 18 | throw err; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/patches/generators/index.ts: -------------------------------------------------------------------------------- 1 | import { SpecPatch } from '../../../../capture/patches/patchers/spec/patches'; 2 | import { UndocumentedOperation } from '../../../operations'; 3 | import { missingMethodPatches } from './missing-method'; 4 | import { missingPathPatches } from './missing-path'; 5 | 6 | export { newSpecPatches } from './new-spec'; 7 | 8 | export interface UndocumentedOperationPatchGenerator { 9 | (undocumentedOperation: UndocumentedOperation); 10 | } 11 | 12 | const undocumentedOperationPatchGenerators: UndocumentedOperationPatchGenerator[] = 13 | [missingMethodPatches, missingPathPatches]; 14 | 15 | export function* undocumentedOperationPatches( 16 | undocumentedOperation: UndocumentedOperation 17 | ): IterableIterator { 18 | for (let generator of undocumentedOperationPatchGenerators) { 19 | yield* generator(undocumentedOperation); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/patches/generators/missing-method.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UndocumentedOperation, 3 | UndocumentedOperationType, 4 | } from '../../../operations'; 5 | import { createMissingMethodPatch } from '../../../../capture/patches/patchers/spec/spec'; 6 | import { SpecPatch } from '../../../../capture/patches/patchers/spec/patches'; 7 | 8 | export function* missingMethodPatches( 9 | undocumentedOperation: UndocumentedOperation 10 | ): IterableIterator { 11 | if (undocumentedOperation.type !== UndocumentedOperationType.MissingMethod) 12 | return; 13 | 14 | yield createMissingMethodPatch(undocumentedOperation); 15 | } 16 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/patches/generators/missing-path.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UndocumentedOperation, 3 | UndocumentedOperationType, 4 | } from '../../../operations'; 5 | import { createMissingPathPatches } from '../../../../capture/patches/patchers/spec/spec'; 6 | import { SpecPatch } from '../../../../capture/patches/patchers/spec/patches'; 7 | 8 | export function* missingPathPatches( 9 | undocumentedOperation: UndocumentedOperation 10 | ): IterableIterator { 11 | if (undocumentedOperation.type !== UndocumentedOperationType.MissingPath) 12 | return; 13 | 14 | yield createMissingPathPatches(undocumentedOperation); 15 | } 16 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/patches/generators/new-spec.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPIV3 } from '../../'; 2 | import { 3 | PatchImpact, 4 | SpecPatch, 5 | } from '../../../../capture/patches/patchers/spec/patches'; 6 | 7 | export function* newSpecPatches( 8 | info: OpenAPIV3.InfoObject, 9 | openAPIversion: string = '3.0.3' 10 | ): IterableIterator { 11 | const newSpec: OpenAPIV3.Document = { 12 | openapi: openAPIversion, 13 | info, 14 | // @ts-ignore 15 | 'x-optic-path-ignore': ['**/*.+(ico|png|jpeg|jpg|gif)'], 16 | paths: {}, 17 | }; 18 | 19 | yield { 20 | impact: [PatchImpact.BackwardsCompatibilityUnknown], 21 | description: `create a new spec`, 22 | path: '/', 23 | diff: undefined, 24 | groupedOperations: [ 25 | { 26 | op: 'add', 27 | path: '', 28 | value: newSpec, 29 | }, 30 | ], 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/specs/templates/index.ts: -------------------------------------------------------------------------------- 1 | import { ObservedSpecPatchGenerator } from '../patches/generators/template'; 2 | 3 | export interface SpecTemplate { 4 | name: string; 5 | patchGenerator: ObservedSpecPatchGenerator; 6 | } 7 | 8 | export class SpecTemplate { 9 | static create( 10 | name, 11 | patchGenerator: ObservedSpecPatchGenerator 12 | ): SpecTemplate { 13 | return { name, patchGenerator }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/tests/fixtures/facts.ts: -------------------------------------------------------------------------------- 1 | import Path from 'path'; 2 | import { readDeferencedSpec } from '../../specs'; 3 | 4 | const petstorePath = Path.join( 5 | __dirname, 6 | '../../../../../../openapi-utilities/inputs/openapi3/petstore0.json.flattened-without-sourcemap.json' 7 | ); 8 | 9 | export async function petstore() { 10 | const { jsonLike: spec } = (await readDeferencedSpec(petstorePath)).unwrap(); 11 | return spec; 12 | } 13 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/tests/inputs/petstore.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Swagger Petstore 4 | version: 1.0.0 5 | paths: 6 | /store/inventory: 7 | get: 8 | responses: {} 9 | /store/order: 10 | get: 11 | responses: {} 12 | servers: 13 | - url: http://petstore.swagger.io/v2 14 | - url: https://petstore.swagger.io/v2 15 | -------------------------------------------------------------------------------- /projects/optic/src/commands/oas/tests/shapes/__snapshots__/extend.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`extending existing schemas can add type to typeless shapes from body 1`] = ` 4 | { 5 | "properties": { 6 | "age": { 7 | "example": 9, 8 | "type": "number", 9 | }, 10 | "hello": { 11 | "example": "you", 12 | "type": "string", 13 | }, 14 | }, 15 | "type": "object", 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /projects/optic/src/commands/ruleset/init.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | import { OpticCliConfig } from '../../config'; 3 | import { errorHandler } from '../../error-handler'; 4 | 5 | export const registerRulesetInit = (cli: Command, config: OpticCliConfig) => { 6 | cli 7 | .command('init') 8 | .description('Initializes a new ruleset project') 9 | .argument('[name]', 'the name of the new ruleset project') 10 | .action(errorHandler(getInitAction(), { command: 'ruleset-init' })); 11 | }; 12 | 13 | const getInitAction = () => async (name?: string) => { 14 | console.log('Ruleset upload is no longer supported'); 15 | 16 | return; 17 | }; 18 | -------------------------------------------------------------------------------- /projects/optic/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const OPTIC_STANDARD_KEY = 'x-optic-standard'; 2 | export const OPTIC_URL_KEY = 'x-optic-url'; 3 | export const OPTIC_EMPTY_SPEC_KEY = 'x-optic-ci-empty-spec'; 4 | export const OPTIC_PATH_IGNORE_KEY = 'x-optic-path-ignore'; 5 | -------------------------------------------------------------------------------- /projects/optic/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { initCli } from './init'; 3 | 4 | (async () => { 5 | const cli = await initCli(); 6 | 7 | cli.parse(process.argv); 8 | })(); 9 | -------------------------------------------------------------------------------- /projects/optic/src/lib.ts: -------------------------------------------------------------------------------- 1 | export { initCli } from './init'; 2 | export { setRulesets } from './commands/diff/generate-rule-runner'; 3 | export { setGenerateContext } from './commands/diff/compute'; 4 | -------------------------------------------------------------------------------- /projects/optic/src/logger.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | 3 | const validLogLevels = new Set([ 4 | 'trace', 5 | 'debug', 6 | 'info', 7 | 'warn', 8 | 'error', 9 | 'silent', 10 | ]); 11 | const logLevel = process.env.LOG_LEVEL; 12 | if (!logLevel && process.env.ENVIRONMENT === 'test') { 13 | log.setLevel('silent'); 14 | } else if (logLevel && validLogLevels.has(logLevel)) { 15 | log.setLevel(logLevel as any); 16 | } else { 17 | log.setLevel('info'); 18 | } 19 | 20 | log.setLevel(log.getLevel()); // Applies the plugin 21 | 22 | export const logger = log; 23 | -------------------------------------------------------------------------------- /projects/optic/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ParseResult } from './utils/spec-loaders'; 2 | export type { OpticCliConfig } from './config'; 3 | export type CustomUploadFn = (spec: ParseResult) => Promise; 4 | -------------------------------------------------------------------------------- /projects/optic/src/utils/capture.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { normalize } from 'crosspath'; 3 | 4 | export type SpawnCommand = { 5 | cmd: string; 6 | args: string[]; 7 | }; 8 | 9 | // commandSplitter() splits a command and its args into a SpawnCommand--in a naive fashion. it 10 | // assumes that `command` contains a single command, rather than something more complex. since 11 | // there is no validation or safety checks performed on `command` exercise caution where you 12 | // use this. 13 | export function commandSplitter(command: string): SpawnCommand { 14 | return { 15 | cmd: command.split(' ')[0], 16 | args: command.split(' ').slice(1), 17 | }; 18 | } 19 | 20 | // returns the relative path between file and rootPath 21 | export function resolveRelativePath(rootPath: string, file: string): string { 22 | return normalize(path.relative(rootPath, path.resolve(file))); 23 | } 24 | -------------------------------------------------------------------------------- /projects/optic/src/utils/checksum.ts: -------------------------------------------------------------------------------- 1 | import { normalizeOpenApiPath } from '@useoptic/openapi-utilities/build/openapi3/implementations/openapi3/openapi-traverser'; 2 | import { createHash } from 'crypto'; 3 | import stableStringify from 'json-stable-stringify'; 4 | import { OpenAPIV3 } from '@useoptic/openapi-utilities'; 5 | 6 | export function computeChecksumForAws(file: string): string { 7 | const hash = createHash('sha256'); 8 | 9 | hash.update(file); 10 | 11 | return hash.digest('base64'); 12 | } 13 | 14 | export function computeEndpointChecksum( 15 | path: string, 16 | method: string, 17 | endpointContent: OpenAPIV3.OperationObject 18 | ): string { 19 | const hash = createHash('sha256'); 20 | 21 | const normalizedUrlPath = normalizeOpenApiPath(path); 22 | const normalizedContent = { 23 | [normalizedUrlPath]: { 24 | [method]: endpointContent, 25 | }, 26 | }; 27 | hash.update(stableStringify(normalizedContent)); 28 | 29 | return hash.digest('hex'); 30 | } 31 | -------------------------------------------------------------------------------- /projects/optic/src/utils/open-url.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import open from 'open'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | 6 | const tmpDirectory = os.tmpdir(); 7 | 8 | export const openUrl = async (url: string) => { 9 | const tmpHtmlPath = path.join(tmpDirectory, 'optic', 'tmp-web.html'); 10 | await fs.mkdir(path.dirname(tmpHtmlPath), { recursive: true }); 11 | 12 | await fs.writeFile( 13 | tmpHtmlPath, 14 | `` 15 | ); 16 | 17 | await open(tmpHtmlPath); 18 | }; 19 | -------------------------------------------------------------------------------- /projects/optic/src/utils/pathPatterns.ts: -------------------------------------------------------------------------------- 1 | function isPattern(part: string) { 2 | return part.startsWith('{') && part.endsWith('}'); 3 | } 4 | 5 | export function matchPathPattern( 6 | pathPattern: string, 7 | path: string 8 | ): { match: true; exact: boolean } | { match: false } { 9 | const patternParts = pathPattern.split('/'); 10 | const pathParts = path.split('/'); 11 | if (patternParts.length !== pathParts.length) { 12 | return { match: false }; 13 | } 14 | 15 | for (let i = 0; i < patternParts.length; i++) { 16 | const pathPart = pathParts[i]; 17 | const patternPart = patternParts[i]; 18 | if (!isPattern(patternPart) && pathPart !== patternPart) { 19 | return { match: false }; 20 | } 21 | } 22 | 23 | return { 24 | match: true, 25 | exact: patternParts.every((part) => !isPattern(part)), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /projects/optic/src/utils/render-cloud.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger'; 2 | import chalk from 'chalk'; 3 | 4 | export function renderCloudSetup() { 5 | logger.info(''); 6 | logger.info(''); 7 | logger.info( 8 | ' ' + 9 | chalk.bold.yellow( 10 | 'Finish setting up Optic by adding your OPTIC_TOKEN. Create one here: ' 11 | ) + 12 | chalk.blue.underline('https://app.useoptic.com/') 13 | ); 14 | 15 | logger.info(' → Add API Review Tools to your Pull Requests '); 16 | logger.info( 17 | chalk.dim( 18 | ' Preview Docs | Visual Diffs | Notify Consumers | Sharable links | API Changelogs | Stats' 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /projects/optic/src/utils/spinner.ts: -------------------------------------------------------------------------------- 1 | import ora, { Ora } from 'ora'; 2 | import { logger } from '../logger'; 3 | 4 | const spinnerIsDisabled = 5 | process.env.ENVIRONMENT === 'test' || logger.getLevel() === 5; 6 | 7 | export const getSpinner = (options: Parameters[0]): Ora | null => { 8 | if (spinnerIsDisabled) { 9 | const text = typeof options === 'string' ? options : options?.text ?? ''; 10 | logger.info(text); 11 | return null; 12 | } else { 13 | return ora(options); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /projects/optic/src/utils/tags.ts: -------------------------------------------------------------------------------- 1 | import { SPEC_TAG_REGEXP } from '@useoptic/openapi-utilities'; 2 | import { logger } from '../logger'; 3 | 4 | export function getTagsFromOptions(tag?: string): string[] { 5 | let tagsToAdd: string[] = []; 6 | if (tag) { 7 | const tags = tag.split(','); 8 | const invalidTags = tags.filter((tag) => !SPEC_TAG_REGEXP.test(tag)); 9 | if (invalidTags.length > 0) { 10 | logger.error( 11 | `The following tags were invalid: ${invalidTags.join( 12 | ', ' 13 | )}. Tags must only include alphanumeric characters, dashes (-, _) or colons (:)` 14 | ); 15 | } 16 | tagsToAdd.push(...tags); 17 | } 18 | 19 | return tagsToAdd; 20 | } 21 | 22 | export function getUniqueTags(tags: string[]): string[] { 23 | return [...new Set(tags)]; 24 | } 25 | -------------------------------------------------------------------------------- /projects/optic/src/utils/write-optic-config.ts: -------------------------------------------------------------------------------- 1 | import yaml from 'yaml'; 2 | import fs from 'node:fs/promises'; 3 | import path from 'path'; 4 | import { OpticCliConfig } from '../config'; 5 | 6 | // merges a yaml document into the existing optic.yml and writes it to disk. if newDocument's top-level 7 | // key already exists in the Optic config, it is overwritten. 8 | export async function updateOpticConfig( 9 | newDocument: yaml.Document.Parsed, 10 | filePath: string, 11 | config: OpticCliConfig 12 | ): Promise { 13 | let opticYml = new yaml.Document(); 14 | let configPath = config.configPath; 15 | if (configPath) { 16 | opticYml = yaml.parseDocument(await fs.readFile(configPath, 'utf8')); 17 | } else { 18 | configPath = path.join(config.root, 'optic.yml'); 19 | } 20 | 21 | opticYml.setIn(['capture', filePath], newDocument); 22 | await fs.writeFile(configPath, opticYml.toString()); 23 | 24 | return configPath; 25 | } 26 | -------------------------------------------------------------------------------- /projects/optic/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/tmp/.gitkeep -------------------------------------------------------------------------------- /projects/optic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "rootDir": "src", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "build", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "jsx": "react-jsx", 14 | "lib": ["dom", "dom.iterable", "esnext"], 15 | "allowJs": false, 16 | "allowSyntheticDefaultImports": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "node16", 19 | "moduleResolution": "node16", 20 | "resolveJsonModule": true, 21 | "noImplicitAny": false, 22 | "downlevelIteration": true, 23 | "isolatedModules": true, 24 | "emitDeclarationOnly": true 25 | }, 26 | "include": ["src"], 27 | "exclude": ["**/*.test.ts", "**/*.test.tsx", "./build/**/*", "examples/**"] 28 | } 29 | -------------------------------------------------------------------------------- /projects/optic/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /projects/optic/web/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/optic/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/web/public/favicon.ico -------------------------------------------------------------------------------- /projects/optic/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | Optic Changelog 13 | 14 | 15 | 16 |
17 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /projects/optic/web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/web/public/logo192.png -------------------------------------------------------------------------------- /projects/optic/web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opticdev/optic/24e500f8b30c7207c6141c3395a031b5e6898d1a/projects/optic/web/public/logo512.png -------------------------------------------------------------------------------- /projects/optic/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /projects/optic/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /projects/optic/web/src/app/attributes/any-attribute.tsx: -------------------------------------------------------------------------------- 1 | import Typography from '@mui/material/Typography'; 2 | import Box from '@mui/material/Box'; 3 | import BaseNode from './base-node'; 4 | import { Yaml } from './Yaml'; 5 | 6 | type AnyAttributeProperties = { 7 | name: string; 8 | value: T; 9 | changelog?: any; 10 | expandAll?: boolean; 11 | }; 12 | 13 | export default function AnyAttribute({ 14 | value, 15 | name, 16 | changelog, 17 | expandAll, 18 | }: AnyAttributeProperties) { 19 | return ( 20 | 21 | 22 | 23 | {name}: 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /projects/optic/web/src/app/attributes/change-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { changeIndicatedColors } from '../constants'; 2 | import { Typography } from '@mui/material'; 3 | 4 | export const ChangeIndicator = ({ 5 | changelog, 6 | fontSize, 7 | }: { 8 | changelog?: any; 9 | fontSize?: number; 10 | }) => { 11 | return changelog?.type === 'removed' ? ( 12 |
13 | 21 | Removed 22 | 23 |
24 | ) : changelog?.type === 'added' ? ( 25 |
26 | 34 | Added 35 | 36 |
37 | ) : null; 38 | }; 39 | -------------------------------------------------------------------------------- /projects/optic/web/src/app/attributes/url.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import { Chip } from '@mui/material'; 3 | import Typography from '@mui/material/Typography'; 4 | 5 | export default function PathUrl(props: { 6 | pathPattern: string; 7 | method: string; 8 | }) { 9 | const Title = ( 10 | 18 | {props.pathPattern} 19 | 20 | ); 21 | 22 | return ( 23 | 31 | 42 | {Title} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /projects/optic/web/src/app/issues/issues.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/system'; 2 | import type { RuleResult } from '@useoptic/openapi-utilities'; 3 | import { Severity } from '@useoptic/openapi-utilities'; 4 | import Issue from './issue'; 5 | 6 | export const Issues = ({ 7 | ruleResults, 8 | }: { 9 | ruleResults?: RuleResult[] | undefined; 10 | }) => { 11 | return ruleResults?.length ? ( 12 | 13 | {ruleResults.map((result, ix) => ( 14 | 21 | ))} 22 | 23 | ) : null; 24 | }; 25 | -------------------------------------------------------------------------------- /projects/optic/web/src/app/utils/operationId.ts: -------------------------------------------------------------------------------- 1 | // This should not be changed as the ID is used in the URLs 2 | export const getOperationId = (operation: { 3 | pathPattern: string; 4 | method: string; 5 | }) => `${operation.method}.${operation.pathPattern}`; 6 | 7 | export const parseOperationId = (operationId: string) => { 8 | const [method, ...pathSegments] = operationId.split('.'); 9 | return { method, path: pathSegments.join('.') }; 10 | }; 11 | -------------------------------------------------------------------------------- /projects/optic/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "downlevelIteration": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /projects/rulesets-base/.gitignore: -------------------------------------------------------------------------------- 1 | optic.config.js 2 | test_data 3 | -------------------------------------------------------------------------------- /projects/rulesets-base/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/rulesets-base/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | env: { 5 | production: { 6 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /projects/rulesets-base/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'], 8 | moduleNameMapper: { 9 | '^nimma/fallbacks$': [ 10 | '../../node_modules/nimma/dist/cjs/fallbacks/index.js', 11 | '../../../../node_modules/nimma/dist/cjs/fallbacks/index.js', 12 | ], 13 | '^nimma/legacy$': [ 14 | '../../node_modules/nimma/dist/legacy/cjs/index.js', 15 | '../../../../node_modules/nimma/dist/legacy/cjs/index.js', 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/custom-rulesets/__tests__/mocks/fake-custom-ruleset.ts: -------------------------------------------------------------------------------- 1 | import { Matchers, Ruleset, SpecificationRule } from '../../../index'; 2 | 3 | export const MustHaveApiVersion = new SpecificationRule({ 4 | name: 'Must have api version', 5 | rule: (specificationAssertions) => { 6 | specificationAssertions.requirement.matches({ 7 | info: { 8 | version: Matchers.string, 9 | }, 10 | }); 11 | }, 12 | }); 13 | 14 | const FakeRuleset = { 15 | name: 'Fake Ruleset', 16 | description: 'A fake ruleset for testing', 17 | configSchema: { 18 | type: 'object', 19 | properties: { 20 | required_on: { 21 | type: 'string', 22 | enum: ['always', 'added'], 23 | }, 24 | }, 25 | }, 26 | rulesetConstructor: (config: unknown) => { 27 | return new Ruleset({ name: 'fake-ruleset', rules: [MustHaveApiVersion] }); 28 | }, 29 | }; 30 | 31 | export default FakeRuleset; 32 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/custom-rulesets/__tests__/mocks/local-fake-custom-ruleset.ts: -------------------------------------------------------------------------------- 1 | import { Matchers, Ruleset, SpecificationRule } from '../../../index'; 2 | 3 | export const MustHaveApiVersion = new SpecificationRule({ 4 | name: 'Must have api version', 5 | rule: (specificationAssertions) => { 6 | specificationAssertions.requirement.matches({ 7 | info: { 8 | version: Matchers.string, 9 | }, 10 | }); 11 | }, 12 | }); 13 | 14 | const FakeRuleset = { 15 | name: 'Fake Ruleset', 16 | description: 'A fake ruleset for testing', 17 | configSchema: { 18 | type: 'object', 19 | properties: { 20 | required_on: { 21 | type: 'string', 22 | enum: ['always', 'added'], 23 | }, 24 | }, 25 | }, 26 | rulesetConstructor: (config: unknown) => { 27 | return new Ruleset({ name: 'fake-ruleset', rules: [MustHaveApiVersion] }); 28 | }, 29 | }; 30 | 31 | export default FakeRuleset; 32 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/custom-rulesets/load-ruleset.ts: -------------------------------------------------------------------------------- 1 | // Handle ESM and CJS modules 2 | export function loadRuleset(path: string) { 3 | const mod = require(path); 4 | return mod && mod.__esModule ? mod : { default: mod }; 5 | } 6 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/custom-rulesets/resolve-ruleset.ts: -------------------------------------------------------------------------------- 1 | import Ajv, { AnySchema } from 'ajv'; 2 | 3 | import { Ruleset } from '../rules/ruleset'; 4 | import { loadRuleset } from './load-ruleset'; 5 | 6 | type RulesetModule = { 7 | default: { 8 | name: string; 9 | description: string; 10 | configSchema: AnySchema; 11 | rulesetConstructor: (config: unknown) => Ruleset; 12 | }; 13 | }; 14 | 15 | export type RulesetDef = { 16 | name: string; 17 | config: unknown; 18 | }; 19 | 20 | const ajv = new Ajv(); 21 | 22 | export async function resolveRuleset( 23 | ruleset: RulesetDef, 24 | rulesetPath: string 25 | ): Promise { 26 | const rulesetMod = loadRuleset(rulesetPath) as RulesetModule; 27 | 28 | const validate = ajv.compile(rulesetMod.default.configSchema); 29 | const valid = validate(ruleset.config); 30 | if (valid) { 31 | return rulesetMod.default.rulesetConstructor(ruleset.config); 32 | } else { 33 | return `Ruleset ${ruleset.name} had configuration errors:\n${ajv.errorsText( 34 | validate.errors 35 | )}`; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { Severity, textToSev } from '@useoptic/openapi-utilities'; 2 | 3 | type RuleConfig = { 4 | message: string; 5 | severity?: 'info' | 'warn' | 'error'; 6 | } & ( 7 | | { 8 | expected?: undefined; 9 | received?: undefined; 10 | } 11 | | { 12 | expected: any; 13 | received: any; 14 | } 15 | ); 16 | 17 | export class RuleError extends Error { 18 | public type: 'rule-error'; 19 | public severity?: Severity; 20 | constructor(public details: RuleConfig) { 21 | super(details.message); 22 | this.type = 'rule-error'; 23 | this.severity = details.severity && textToSev(details.severity); 24 | // https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work 25 | Object.setPrototypeOf(this, RuleError.prototype); 26 | } 27 | 28 | toString(): string { 29 | return this.details.message; 30 | } 31 | 32 | static isInstance(v: any): v is RuleError { 33 | return v?.type === 'rule-error'; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as TestHelpers from './test-helpers'; 2 | export { prepareRulesets } from './custom-rulesets/prepare-rulesets'; 3 | export { loadRuleset } from './custom-rulesets/load-ruleset'; 4 | 5 | export * from './errors'; 6 | export * from './rules'; 7 | export * from './rule-runner'; 8 | export { SpectralRule } from './extended-rules/spectral-rule'; 9 | export { TestHelpers }; 10 | 11 | export * from './types'; 12 | export type { SpectralResult, Spectral } from './extended-rules/spectral-rule'; 13 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/rule-runner/__tests__/__snapshots__/rule-runner.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`spectral rules runs spectral rules 1`] = ` 4 | [ 5 | { 6 | "change": { 7 | "location": { 8 | "conceptualLocation": { 9 | "method": "", 10 | "path": "This Specification", 11 | }, 12 | "conceptualPath": [], 13 | "jsonPath": "", 14 | "kind": "API", 15 | }, 16 | }, 17 | "condition": "oas3-api-servers", 18 | "error": "OpenAPI "servers" must be present and non-empty array.", 19 | "isMust": true, 20 | "isShould": false, 21 | "passed": false, 22 | "where": "requirement ", 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/rule-runner/__tests__/group-facts.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@jest/globals'; 2 | import { groupFacts } from '../group-facts'; 3 | import { 4 | before as petstoreBefore, 5 | after as petstoreAfter, 6 | changes as petstoreChanges, 7 | } from './examples/petstore'; 8 | 9 | test('groupFacts for petstore example', () => { 10 | expect( 11 | groupFacts({ 12 | beforeFacts: petstoreBefore, 13 | afterFacts: petstoreAfter, 14 | changes: petstoreChanges, 15 | }) 16 | ).toMatchSnapshot(); 17 | }); 18 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/rule-runner/index.ts: -------------------------------------------------------------------------------- 1 | import { Matchers, Matcher } from './matchers/utils'; 2 | export { Matchers, Matcher }; 3 | export * from './rule-runner'; 4 | -------------------------------------------------------------------------------- /projects/rulesets-base/src/rules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ruleset'; 2 | export * from './operation-rule'; 3 | export * from './specification-rule'; 4 | export * from './request-rule'; 5 | export * from './response-rule'; 6 | export * from './response-body-rule'; 7 | export * from './property-rule'; 8 | -------------------------------------------------------------------------------- /projects/rulesets-base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "rootDir": "src", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "downlevelIteration": true, 8 | "sourceMap": true, 9 | "outDir": "build", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "lib": ["esnext"], 15 | "allowJs": false, 16 | "allowSyntheticDefaultImports": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "node16", 19 | "moduleResolution": "node16", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noImplicitAny": false 23 | }, 24 | "include": ["src"], 25 | "exclude": ["**/*.test.ts", "**/*.test.tsx", "./build/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /projects/standard-rulesets/.gitignore: -------------------------------------------------------------------------------- 1 | optic.config.js 2 | test_data 3 | -------------------------------------------------------------------------------- /projects/standard-rulesets/.npmignore: -------------------------------------------------------------------------------- 1 | *.test.ts 2 | *.test.ts.snap -------------------------------------------------------------------------------- /projects/standard-rulesets/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 3 | plugins: ['@babel/plugin-transform-runtime'], 4 | env: { 5 | production: { 6 | ignore: ['**/*.test.ts', '**/*.test.tsx'], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /projects/standard-rulesets/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | modulePathIgnorePatterns: ['mocks'], 5 | resetMocks: true, 6 | testMatch: ['/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], 7 | testPathIgnorePatterns: ['build'], 8 | moduleNameMapper: { 9 | '^nimma/fallbacks$': [ 10 | '../../node_modules/nimma/dist/cjs/fallbacks/index.js', 11 | '../../../../node_modules/nimma/dist/cjs/fallbacks/index.js', 12 | ], 13 | '^nimma/legacy$': [ 14 | '../../node_modules/nimma/dist/legacy/cjs/index.js', 15 | '../../../../node_modules/nimma/dist/legacy/cjs/index.js', 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/breaking-changes/helpers/getOperationAssertionsParameter.ts: -------------------------------------------------------------------------------- 1 | import { Assertions, OperationAssertions } from '@useoptic/rulesets-base'; 2 | import { ParameterIn, ParamAssertionType } from './types'; 3 | 4 | const unhandledCase = (x: never) => { 5 | throw new Error(`received unexpected runtime value ${x}`); 6 | }; 7 | 8 | export const getOperationAssertionsParameter = ( 9 | operationAssertions: OperationAssertions, 10 | parameterIn: ParameterIn 11 | ): Assertions => { 12 | switch (parameterIn) { 13 | case 'query': 14 | return operationAssertions.queryParameter; 15 | case 'path': 16 | return operationAssertions.pathParameter; 17 | case 'cookie': 18 | return operationAssertions.cookieParameter; 19 | case 'header': 20 | return operationAssertions.headerParameter; 21 | default: 22 | return unhandledCase(parameterIn); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/breaking-changes/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import { AssertionType } from '@useoptic/rulesets-base'; 2 | 3 | export type ParameterIn = 'cookie' | 'query' | 'header' | 'path'; 4 | export type ParamAssertionType = Extract< 5 | AssertionType, 6 | 'query-parameter' | 'path-parameter' | 'header-parameter' | 'cookie-parameter' 7 | >; 8 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/breaking-changes/preventOperationRemoval.ts: -------------------------------------------------------------------------------- 1 | import { OperationRule, RuleError } from '@useoptic/rulesets-base'; 2 | 3 | export const preventOperationRemoval = () => 4 | new OperationRule({ 5 | name: 'prevent operation removal', 6 | rule: (operationAssertions) => { 7 | operationAssertions.removed(() => { 8 | throw new RuleError({ 9 | message: 'cannot remove an operation. This is a breaking change.', 10 | }); 11 | }); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/breaking-changes/preventResponseStatusCodeRemoval.ts: -------------------------------------------------------------------------------- 1 | import { ResponseRule, RuleError } from '@useoptic/rulesets-base'; 2 | 3 | export const preventResponseStatusCodeRemoval = () => 4 | new ResponseRule({ 5 | name: 'prevent response status code removal', 6 | rule: (responseAssertions) => { 7 | responseAssertions.removed((value) => { 8 | throw new RuleError({ 9 | message: `must not remove response status code ${value.statusCode}. This is a breaking change.`, 10 | }); 11 | }); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/documentation/constants.ts: -------------------------------------------------------------------------------- 1 | export const appliesWhen = ['added', 'addedOrChanged', 'always'] as const; 2 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/documentation/requireOperationDescription.ts: -------------------------------------------------------------------------------- 1 | import { OperationRule, RuleError } from '@useoptic/rulesets-base'; 2 | import { appliesWhen } from './constants'; 3 | 4 | export const requireOperationDescription = ( 5 | applies: (typeof appliesWhen)[number] 6 | ) => 7 | new OperationRule({ 8 | name: 'require operation description', 9 | rule: (operationAssertions) => { 10 | const lifecycle = applies === 'always' ? 'requirement' : applies; 11 | operationAssertions[lifecycle]((operation) => { 12 | if (!operation.raw.description) { 13 | throw new RuleError({ 14 | message: `an operation description must be included`, 15 | }); 16 | } 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/documentation/requireOperationId.ts: -------------------------------------------------------------------------------- 1 | import { OperationRule, RuleError } from '@useoptic/rulesets-base'; 2 | import { appliesWhen } from './constants'; 3 | 4 | export const requireOperationId = (applies: (typeof appliesWhen)[number]) => 5 | new OperationRule({ 6 | name: 'require operation id', 7 | rule: (operationAssertions) => { 8 | const lifecycle = applies === 'always' ? 'requirement' : applies; 9 | operationAssertions[lifecycle]((operation) => { 10 | if (!operation.raw.operationId) { 11 | throw new RuleError({ 12 | message: `an operation id must be included`, 13 | }); 14 | } 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/documentation/requireOperationSummary.ts: -------------------------------------------------------------------------------- 1 | import { OperationRule, RuleError } from '@useoptic/rulesets-base'; 2 | import { appliesWhen } from './constants'; 3 | 4 | export const requireOperationSummary = ( 5 | applies: (typeof appliesWhen)[number] 6 | ) => 7 | new OperationRule({ 8 | name: 'require operation summary', 9 | rule: (operationAssertions) => { 10 | const lifecycle = applies === 'always' ? 'requirement' : applies; 11 | operationAssertions[lifecycle]((operation) => { 12 | if (!operation.raw.summary) { 13 | throw new RuleError({ 14 | message: `an operation summary must be included`, 15 | }); 16 | } 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/documentation/requirePropertyDescriptions.ts: -------------------------------------------------------------------------------- 1 | import { PropertyRule, RuleError } from '@useoptic/rulesets-base'; 2 | import { appliesWhen } from './constants'; 3 | 4 | export const requirePropertyDescription = ( 5 | applies: (typeof appliesWhen)[number] 6 | ) => 7 | new PropertyRule({ 8 | name: 'require property description', 9 | rule: (propertyAssertions) => { 10 | const lifecycle = applies === 'always' ? 'requirement' : applies; 11 | propertyAssertions[lifecycle]((property) => { 12 | if (!property.raw.description) { 13 | throw new RuleError({ 14 | message: `an property description must be included`, 15 | }); 16 | } 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/examples/constants.ts: -------------------------------------------------------------------------------- 1 | export const appliesWhen = ['added', 'addedOrChanged', 'always'] as const; 2 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/examples/example-ruleset.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from '@jest/globals'; 2 | import { ExamplesRuleset } from '../index'; 3 | 4 | describe('fromOpticConfig', () => { 5 | test('invalid configuration', async () => { 6 | const out = await ExamplesRuleset.fromOpticConfig({ 7 | require_parameter_examples: 123, 8 | }); 9 | expect(out).toEqual( 10 | '- ruleset/examples/require_parameter_examples must be boolean' 11 | ); 12 | }); 13 | 14 | test('valid config', async () => { 15 | const ruleset = await ExamplesRuleset.fromOpticConfig({ 16 | require_parameter_examples: true, 17 | exclude_operations_with_extension: 'x-legacy', 18 | docs_link: 'asdasd.com', 19 | }); 20 | expect(ruleset).toBeInstanceOf(ExamplesRuleset); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/examples/qualifiedContentType.ts: -------------------------------------------------------------------------------- 1 | import MIMEType from 'whatwg-mimetype'; 2 | 3 | export function qualifiedContentType(contentType: string): boolean { 4 | let parsedType = contentType && MIMEType.parse(contentType); 5 | if (parsedType) { 6 | if ( 7 | parsedType.essence === 'application/json' || 8 | parsedType.essence === 'application/xml' || 9 | parsedType.essence === 'text/json' || 10 | parsedType.essence === 'text/plain' || 11 | parsedType.subtype.endsWith('+json') || 12 | parsedType.subtype.endsWith('+xml') 13 | ) { 14 | return true; 15 | } 16 | } 17 | 18 | return false; 19 | } 20 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/index.ts: -------------------------------------------------------------------------------- 1 | import { LintGpt } from './lintgpt'; 2 | 3 | export * from './breaking-changes'; 4 | export * from './naming-changes'; 5 | export * from './examples'; 6 | export * from './spectral'; 7 | export * from './documentation'; 8 | 9 | import { BreakingChangesRuleset } from './breaking-changes'; 10 | import { NamingChangesRuleset } from './naming-changes'; 11 | import { ExamplesRuleset } from './examples'; 12 | import { SpectralRulesets } from './spectral'; 13 | import { DocumentationRuleset } from './documentation'; 14 | 15 | export const StandardRulesets = { 16 | 'breaking-changes': BreakingChangesRuleset, 17 | naming: NamingChangesRuleset, 18 | spectral: SpectralRulesets, 19 | examples: ExamplesRuleset, 20 | documentation: DocumentationRuleset, 21 | lintgpt: LintGpt, 22 | }; 23 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/lintgpt/constants.ts: -------------------------------------------------------------------------------- 1 | export const appliesWhen = ['added', 'addedOrChanged', 'always'] as const; 2 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/naming-changes/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from '@jest/globals'; 2 | import { NamingChangesRuleset } from '..'; 3 | 4 | describe('fromOpticConfig', () => { 5 | test('valid', async () => { 6 | const ruleset = await NamingChangesRuleset.fromOpticConfig({ 7 | required_on: 'always', 8 | properties: 'snake_case', 9 | exclude_operations_with_extension: 'x-legacy', 10 | docs_link: 'asdasd.com', 11 | severity: 'warn', 12 | }); 13 | expect(ruleset).toBeInstanceOf(NamingChangesRuleset); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/naming-changes/constants.ts: -------------------------------------------------------------------------------- 1 | export const casing = [ 2 | 'snake_case', 3 | 'camelCase', 4 | 'Capital-Param-Case', 5 | 'param-case', 6 | 'PascalCase', 7 | 'case-insensitive-param-case', 8 | ] as const; 9 | 10 | export const appliesWhen = ['added', 'addedOrChanged', 'always'] as const; 11 | 12 | export type NamingConfig = { 13 | requestHeaders?: (typeof casing)[number]; 14 | queryParameters?: (typeof casing)[number]; 15 | responseHeaders?: (typeof casing)[number]; 16 | cookieParameters?: (typeof casing)[number]; 17 | pathComponents?: (typeof casing)[number]; 18 | operationId?: (typeof casing)[number]; 19 | properties?: (typeof casing)[number]; 20 | }; 21 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/naming-changes/isCase.ts: -------------------------------------------------------------------------------- 1 | import { casing } from './constants'; 2 | 3 | function regexForRule(format: (typeof casing)[number]) { 4 | switch (format) { 5 | case 'camelCase': 6 | return /^[a-z][a-z0-9]*(?:[A-Z0-9][a-z0-9]+)*$/; 7 | case 'Capital-Param-Case': 8 | return /^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$/; 9 | case 'param-case': 10 | return /^[a-z0-9]+(-[a-z0-9]+)*$/; 11 | case 'PascalCase': 12 | return /^[A-Z][a-z0-9]+(?:[A-Z0-9][a-z0-9]+)*$/; 13 | case 'snake_case': 14 | return /^[a-z0-9]+(?:_[a-z0-9]+)*$/; 15 | case 'case-insensitive-param-case': 16 | return /^[a-z0-9]+(-[a-z0-9]+)*$/i; 17 | default: 18 | return /(.*?)/; 19 | } 20 | } 21 | 22 | export function isCase( 23 | example: string, 24 | format: (typeof casing)[number] 25 | ): boolean { 26 | return regexForRule(format).test(example); 27 | } 28 | -------------------------------------------------------------------------------- /projects/standard-rulesets/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { isTruthyStringValue } from '@useoptic/openapi-utilities'; 2 | import { RuleContext } from '@useoptic/rulesets-base'; 3 | 4 | function extensionIsTruthy(extension: any) { 5 | if (typeof extension === 'string') { 6 | return isTruthyStringValue(extension); 7 | } else { 8 | return extension === true; 9 | } 10 | } 11 | 12 | export const excludeOperationWithExtensionMatches = ( 13 | excludeOperationWithExtensions: string | string[] 14 | ) => { 15 | return (context: RuleContext): boolean => { 16 | return Array.isArray(excludeOperationWithExtensions) 17 | ? excludeOperationWithExtensions.some( 18 | (e) => !extensionIsTruthy((context.operation.raw as any)[e]) 19 | ) 20 | : !extensionIsTruthy( 21 | (context.operation.raw as any)[excludeOperationWithExtensions] 22 | ); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /projects/standard-rulesets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "rootDir": "src", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "downlevelIteration": true, 8 | "sourceMap": true, 9 | "outDir": "build", 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "lib": ["esnext"], 15 | "allowJs": false, 16 | "allowSyntheticDefaultImports": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "module": "node16", 19 | "moduleResolution": "node16", 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noImplicitAny": false 23 | }, 24 | "include": ["src"], 25 | "exclude": ["**/*.test.ts", "**/*.test.tsx", "./build/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", ":dependencyDashboard" 5 | ], 6 | "js": { 7 | "addLabels": ["js"] 8 | }, 9 | "packageRules": [ 10 | { 11 | "matchFiles": ["yarn.lock"], 12 | "addLabels": ["deps"] 13 | }, 14 | { 15 | "matchPaths": [".github/workflows/*"], 16 | "addLabels": ["gh-actions"] 17 | } 18 | ] 19 | } 20 | --------------------------------------------------------------------------------