├── .github ├── FUNDING.yml ├── sponsors │ ├── speakeasy.png │ ├── apideck-dark.png │ ├── bump-sh-dark.png │ ├── scalar-dark.png │ ├── scalar-light.png │ ├── apideck-light.png │ └── bump-sh-light.png ├── dependabot.yml └── workflows │ └── build.yaml ├── .gitignore ├── test_specs ├── c.yaml ├── ref_test │ ├── orig │ │ ├── c.yaml │ │ ├── b.yaml │ │ └── a.yaml │ └── mod │ │ ├── c.yaml │ │ ├── b.yaml │ │ └── a.yaml ├── c-alt.yaml ├── advancecallbackreferences │ ├── min-callbacks.yaml │ ├── min-components.yaml │ └── min-openapi.yaml ├── b.yaml ├── b-alt.yaml ├── nested_files │ ├── components │ │ ├── responses │ │ │ └── Unspecified200.yaml │ │ ├── parameters │ │ │ ├── header │ │ │ │ └── page-size.yaml │ │ │ └── query │ │ │ │ └── $select.yaml │ │ ├── requestBodies │ │ │ └── AccountModel.yaml │ │ ├── foo.yaml │ │ └── schemas │ │ │ └── AccountModel.yaml │ ├── openapi-issue-418.yaml │ ├── openapi.yaml │ └── paths │ │ └── v1_Accounts.yaml ├── single-definition.yaml ├── nullable-examples.openapi.yaml ├── third.yaml ├── a.yaml ├── a-alt.yaml ├── recursive-expression-test.yaml ├── second.yaml ├── first.yaml ├── minimal_remote_refs │ ├── openapi.yaml │ └── schemas │ │ └── components.openapi.yaml ├── ref-followed.yaml ├── yaml-anchor.yaml ├── swagger-circular-tests.yaml └── circular-tests.yaml ├── libopenapi-logo.png ├── bundler └── test │ └── specs │ ├── smash │ ├── header_A.yaml │ ├── pathItem_A.yaml │ ├── link_A.yaml │ ├── response_A.yaml │ └── paging.yaml │ ├── clash │ ├── unknown.yaml │ ├── callback_A.yaml │ ├── requestbody_A.yaml │ ├── param_A.yaml │ ├── fishcake.yaml │ └── paging.yaml │ ├── examples │ └── example_A.yaml │ ├── fishcake.yaml │ ├── paging.yaml │ ├── error.yaml │ ├── common.yaml │ └── main.yaml ├── datamodel ├── high │ ├── v2 │ │ ├── asyncresult.go │ │ ├── scopes.go │ │ ├── examples.go │ │ ├── paths.go │ │ ├── response.go │ │ ├── responses.go │ │ ├── responses_definitions.go │ │ ├── definitions.go │ │ ├── security_definitions.go │ │ ├── parameter_definitions.go │ │ ├── security_scheme.go │ │ └── path_item_test.go │ ├── v3 │ │ ├── asyncresult.go │ │ ├── package_test.go │ │ ├── oauth_flow_test.go │ │ ├── server.go │ │ ├── oauth_flow.go │ │ ├── server_variable.go │ │ ├── encoding_test.go │ │ ├── server_variable_test.go │ │ └── oauth_flows.go │ ├── nodes │ │ └── nodeentry.go │ ├── base │ │ ├── base.go │ │ ├── security_requirement_test.go │ │ ├── contact.go │ │ ├── license.go │ │ ├── external_doc_test.go │ │ ├── contact_test.go │ │ ├── external_doc.go │ │ └── discriminator.go │ └── overlay │ │ ├── info.go │ │ ├── action.go │ │ └── overlay.go ├── low │ ├── base │ │ ├── context_test.go │ │ ├── base.go │ │ ├── context.go │ │ ├── contact_test.go │ │ ├── circ_check.go │ │ ├── license_test.go │ │ ├── security_requirement_test.go │ │ ├── xml_test.go │ │ ├── circ_check_test.go │ │ ├── external_doc_test.go │ │ ├── discriminator.go │ │ └── contact.go │ ├── overlay │ │ └── constants.go │ ├── v2 │ │ ├── constants.go │ │ ├── examples_test.go │ │ ├── scopes_test.go │ │ ├── examples.go │ │ ├── package_test.go │ │ ├── scopes.go │ │ └── security_scheme_test.go │ ├── v3 │ │ ├── examples_test.go │ │ └── server_variable.go │ └── low.go ├── document_config_test.go └── constants.go ├── index ├── rolodex_test_data │ ├── operations.yaml │ ├── dir2 │ │ ├── utils │ │ │ └── utils.yaml │ │ ├── subdir2 │ │ │ └── shared.yaml │ │ └── components.yaml │ ├── dir1 │ │ ├── utils │ │ │ └── utils.yaml │ │ ├── components.yaml │ │ └── subdir1 │ │ │ └── shared.yaml │ ├── paths │ │ └── paths.yaml │ ├── components.yaml │ ├── doc2.yaml │ └── doc1.yaml ├── circular_reference_result_test.go ├── circular_reference_result.go ├── schema_id_context.go ├── rolodex_ref_extractor.go ├── schema_id_registry.go └── map_index_nodes_test.go ├── utils ├── path_part_other.go ├── path_part_windows.go ├── unwrap_errors.go ├── unwrap_errors_test.go ├── utils_bench_test.go ├── utils_regex_bench_test.go ├── nodes.go ├── windows_drive.go ├── type_check.go ├── nodes_test.go └── type_check_test.go ├── overlay ├── result.go ├── validation.go ├── errors.go └── validation_test.go ├── what-changed ├── reports │ ├── types.go │ └── summary_test.go ├── changed.go ├── model │ ├── document_flat.go │ ├── contact.go │ ├── server_variable.go │ ├── request_body_test.go │ ├── external_docs.go │ ├── license.go │ ├── examples.go │ └── scopes.go └── what_changed.go ├── go.mod ├── LICENSE ├── json └── json.go └── go.sum /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: daveshanley -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test-operation.yaml 2 | .idea/ 3 | *.iml -------------------------------------------------------------------------------- /test_specs/c.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: object C 3 | -------------------------------------------------------------------------------- /test_specs/ref_test/orig/c.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: object C 3 | -------------------------------------------------------------------------------- /libopenapi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/libopenapi-logo.png -------------------------------------------------------------------------------- /bundler/test/specs/smash/header_A.yaml: -------------------------------------------------------------------------------- 1 | schema: 2 | type: object 3 | description: this is a header -------------------------------------------------------------------------------- /test_specs/c-alt.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: object C, but this has a changed description 3 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/unknown.yaml: -------------------------------------------------------------------------------- 1 | description: could be meat, could be cake. only option is to inline. -------------------------------------------------------------------------------- /bundler/test/specs/smash/pathItem_A.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | operationId: somethingHere 3 | description: a test get -------------------------------------------------------------------------------- /.github/sponsors/speakeasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/speakeasy.png -------------------------------------------------------------------------------- /test_specs/ref_test/mod/c.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: object C, but this has a changed description 3 | -------------------------------------------------------------------------------- /.github/sponsors/apideck-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/apideck-dark.png -------------------------------------------------------------------------------- /.github/sponsors/bump-sh-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/bump-sh-dark.png -------------------------------------------------------------------------------- /.github/sponsors/scalar-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/scalar-dark.png -------------------------------------------------------------------------------- /.github/sponsors/scalar-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/scalar-light.png -------------------------------------------------------------------------------- /.github/sponsors/apideck-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/apideck-light.png -------------------------------------------------------------------------------- /.github/sponsors/bump-sh-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/HEAD/.github/sponsors/bump-sh-light.png -------------------------------------------------------------------------------- /bundler/test/specs/examples/example_A.yaml: -------------------------------------------------------------------------------- 1 | description: a test example 2 | value: 3 | cakes: nice 4 | iceCream: good -------------------------------------------------------------------------------- /datamodel/high/v2/asyncresult.go: -------------------------------------------------------------------------------- 1 | package v2 2 | 3 | type asyncResult[T any] struct { 4 | key string 5 | result T 6 | } 7 | -------------------------------------------------------------------------------- /datamodel/high/v3/asyncresult.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | type asyncResult[T any] struct { 4 | key string 5 | result T 6 | } 7 | -------------------------------------------------------------------------------- /bundler/test/specs/smash/link_A.yaml: -------------------------------------------------------------------------------- 1 | operationRef: updateCalendarRef 2 | operationId: updateCalendar 3 | description: a test link 4 | -------------------------------------------------------------------------------- /index/rolodex_test_data/operations.yaml: -------------------------------------------------------------------------------- 1 | external_operation: 2 | get: 3 | responses: 4 | 200: 5 | description: "OK" -------------------------------------------------------------------------------- /bundler/test/specs/clash/callback_A.yaml: -------------------------------------------------------------------------------- 1 | '{$request.query.queryUrl}': 2 | post: 3 | requestBody: 4 | description: Callback payload -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /utils/path_part_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package utils 5 | 6 | func pathPartEqual(a, b string) bool { 7 | return a == b 8 | } 9 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/requestbody_A.yaml: -------------------------------------------------------------------------------- 1 | required: true 2 | content: 3 | application/json: 4 | schema: 5 | $ref: "./paging.yaml#/components/schemas/dtoTest" -------------------------------------------------------------------------------- /test_specs/advancecallbackreferences/min-callbacks.yaml: -------------------------------------------------------------------------------- 1 | test-callback: 2 | "/test-callback": 3 | $ref: "./min-components.yaml#/components/pathItems/test-callback" 4 | -------------------------------------------------------------------------------- /test_specs/b.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | SchemaB: 5 | type: object 6 | properties: 7 | PropC: 8 | $ref: c.yaml -------------------------------------------------------------------------------- /test_specs/b-alt.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | SchemaB: 5 | type: object 6 | properties: 7 | PropC: 8 | $ref: c-alt.yaml -------------------------------------------------------------------------------- /test_specs/ref_test/mod/b.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | SchemaB: 5 | type: object 6 | properties: 7 | PropC: 8 | $ref: c.yaml -------------------------------------------------------------------------------- /test_specs/ref_test/orig/b.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | SchemaB: 5 | type: object 6 | properties: 7 | PropC: 8 | $ref: c.yaml -------------------------------------------------------------------------------- /test_specs/nested_files/components/responses/Unspecified200.yaml: -------------------------------------------------------------------------------- 1 | description: Unspecified 200 success object response 2 | content: 3 | application/json: 4 | schema: 5 | type: object 6 | -------------------------------------------------------------------------------- /test_specs/nested_files/components/parameters/header/page-size.yaml: -------------------------------------------------------------------------------- 1 | name: page-size 2 | description: Specify the number of results to return per page. 3 | in: header 4 | schema: 5 | type: integer 6 | -------------------------------------------------------------------------------- /bundler/test/specs/smash/response_A.yaml: -------------------------------------------------------------------------------- 1 | links: 2 | aTestLink: 3 | $ref: "./link_A.yaml" 4 | content: 5 | application/json: 6 | schema: 7 | $ref: "./paging.yaml#/components/schemas/dtoTest" -------------------------------------------------------------------------------- /test_specs/nested_files/components/requestBodies/AccountModel.yaml: -------------------------------------------------------------------------------- 1 | description: TODO 2 | required: true 3 | content: 4 | application/json: 5 | schema: 6 | $ref: ../schemas/AccountModel.yaml 7 | -------------------------------------------------------------------------------- /utils/path_part_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package utils 5 | 6 | import "strings" 7 | 8 | func pathPartEqual(a, b string) bool { 9 | return strings.EqualFold(a, b) 10 | } 11 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/param_A.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | in: query 3 | description: I am a a query param. 4 | properties: 5 | vinegar: 6 | type: string 7 | description: The type of vinegar used 8 | example: "malt" -------------------------------------------------------------------------------- /index/rolodex_test_data/dir2/utils/utils.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: I am a utility for dir2 3 | properties: 4 | message: 5 | type: object 6 | description: I am pointless dir2 utility, I am multiple levels deep. -------------------------------------------------------------------------------- /test_specs/single-definition.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | Thing: 5 | type: object 6 | description: A thing that does nothing. 7 | properties: 8 | message: 9 | type: string 10 | description: I am pointless. -------------------------------------------------------------------------------- /test_specs/advancecallbackreferences/min-components.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | pathItems: 3 | test-callback: 4 | $ref: "#/components/pathItems/test-callback-2" 5 | test-callback-2: 6 | get: 7 | responses: 8 | "200": 9 | description: OK 10 | -------------------------------------------------------------------------------- /index/rolodex_test_data/dir1/utils/utils.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: I am a utility for dir1 3 | properties: 4 | message: 5 | type: object 6 | description: I am pointless dir1. 7 | properties: 8 | shared: 9 | $ref: '../subdir1/shared.yaml#/components/schemas/SharedComponent' -------------------------------------------------------------------------------- /bundler/test/specs/fishcake.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: I am a fishcake schema 3 | properties: 4 | filling: 5 | type: string 6 | description: The filling of the fishcake 7 | example: "cod" 8 | batter: 9 | type: string 10 | description: The type of batter used 11 | example: "breadcrumb" -------------------------------------------------------------------------------- /bundler/test/specs/smash/paging.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Common schemas 4 | version: "1.0.0" 5 | servers: [] 6 | paths: [] 7 | 8 | components: 9 | schemas: 10 | dtoTest: 11 | description: Test schema (SMASH) 12 | type: object 13 | properties: 14 | fishcake: 15 | $ref: "../fishcake.yaml" 16 | -------------------------------------------------------------------------------- /index/rolodex_test_data/paths/paths.yaml: -------------------------------------------------------------------------------- 1 | /some/path: 2 | get: 3 | parameters: 4 | - $ref: '../components.yaml#/components/parameters/SomeParam' 5 | responses: 6 | '200': 7 | description: OK 8 | content: 9 | application/json: 10 | schema: 11 | $ref: '../components.yaml#/components/schemas/Ding' -------------------------------------------------------------------------------- /test_specs/nullable-examples.openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | components: 3 | schemas: 4 | Thing: 5 | type: object 6 | description: A nullable example. 7 | properties: 8 | target: 9 | nullable: true 10 | type: string 11 | enum: 12 | - staging 13 | - production 14 | example: -------------------------------------------------------------------------------- /test_specs/nested_files/components/foo.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/pb33f/libopenapi/issues/418 2 | get: 3 | parameters: 4 | - name: id 5 | in: path 6 | description: user id 7 | required: true 8 | schema: 9 | type: string 10 | responses: 11 | '200': 12 | $ref: '../openapi-issue-418.yaml#/components/responses/successResponse' -------------------------------------------------------------------------------- /test_specs/nested_files/openapi-issue-418.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | paths: 3 | '/foo': 4 | $ref: './components/foo.yaml' 5 | components: 6 | responses: 7 | successResponse: 8 | description: "success" 9 | content: 10 | application/json: 11 | schema: 12 | properties: 13 | message: 14 | type: string -------------------------------------------------------------------------------- /utils/unwrap_errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | func UnwrapErrors(err error) []error { 7 | if err == nil { 8 | return []error{} 9 | } 10 | if uw, ok := err.(interface{ Unwrap() []error }); ok { 11 | return uw.Unwrap() 12 | } else { 13 | return []error{err} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test_specs/advancecallbackreferences/min-openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Test 4 | version: 0.0.1 5 | servers: 6 | - url: https://test.com 7 | paths: 8 | /test: 9 | get: 10 | operationId: test 11 | responses: 12 | "200": 13 | description: OK 14 | callbacks: 15 | test: 16 | $ref: "./min-callbacks.yaml#/test-callback" 17 | -------------------------------------------------------------------------------- /test_specs/nested_files/components/parameters/query/$select.yaml: -------------------------------------------------------------------------------- 1 | name: $select 2 | description: Selects the columns or properties in the result set. This cannot be combined with any other query params! 3 | # https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/query-data-web-api#select-columns 4 | in: query 5 | required: false 6 | schema: 7 | type: string 8 | example: RowId,Description 9 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/fishcake.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | description: I am a fishcake schema (and a clash) 3 | properties: 4 | mixFilling: 5 | type: string 6 | description: The mixed filling of the cake 7 | example: "haddock" 8 | temp: 9 | type: number 10 | format: float 11 | minimum: 0 12 | maximum: 180 13 | description: temperature in degrees celcius 14 | example: 145 -------------------------------------------------------------------------------- /datamodel/high/nodes/nodeentry.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import "go.yaml.in/yaml/v4" 4 | 5 | // NodeEntry represents a single node used by NodeBuilder. 6 | type NodeEntry struct { 7 | Tag string 8 | Key string 9 | Value any 10 | StringValue string 11 | Line int 12 | KeyStyle yaml.Style 13 | // ValueStyle yaml.Style 14 | RenderZero bool 15 | LowValue any 16 | } 17 | -------------------------------------------------------------------------------- /index/rolodex_test_data/dir1/components.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Dir1 Test Components 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | GlobalComponent: 8 | type: object 9 | description: Dir1 Global Component 10 | properties: 11 | message: 12 | type: string 13 | description: I am pointless, but I am global dir1. 14 | SomeUtil: 15 | $ref: "utils/utils.yaml" -------------------------------------------------------------------------------- /test_specs/third.yaml: -------------------------------------------------------------------------------- 1 | title: third doc title 2 | description: third doc description 3 | type: object 4 | 5 | additionalProperties: false 6 | maxProperties: 1 7 | 8 | properties: 9 | pencils: 10 | $ref: '#/properties/property/properties/statistics' 11 | property: 12 | title: title of third prop in third doc 13 | type: object 14 | properties: 15 | statistics: 16 | $ref: 'second.yaml#/properties/property2' -------------------------------------------------------------------------------- /index/rolodex_test_data/dir1/subdir1/shared.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Dir1 Shared Components 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | SharedComponent: 8 | type: object 9 | description: Dir1 Shared Component 10 | properties: 11 | message: 12 | type: string 13 | description: I am pointless, but I am shared dir1. 14 | SomeUtil: 15 | $ref: "../utils/utils.yaml" -------------------------------------------------------------------------------- /index/rolodex_test_data/dir2/subdir2/shared.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Dir2 Shared Components 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | SharedComponent: 8 | type: object 9 | description: Dir2 Shared Component 10 | properties: 11 | utilMessage: 12 | $ref: "../utils/utils.yaml" 13 | message: 14 | type: string 15 | description: I am pointless, but I am shared dir2. -------------------------------------------------------------------------------- /overlay/result.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | // Result represents the result of applying an overlay to a target document. 7 | type Result struct { 8 | // Bytes is the raw YAML/JSON bytes of the modified document. 9 | Bytes []byte 10 | 11 | // Warnings contains non-fatal issues encountered during application. 12 | Warnings []*Warning 13 | } 14 | -------------------------------------------------------------------------------- /test_specs/nested_files/openapi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: 3.1.0 3 | info: 4 | description: Example API spec 5 | version: v1 6 | title: Example 7 | contact: 8 | name: Example 9 | email: example@example.com 10 | url: www.example.com 11 | license: 12 | name: Example 13 | url: www.example.com 14 | tags: 15 | - name: Account 16 | description: Account 17 | servers: 18 | - url: https:// 19 | paths: 20 | /api/v1/Accounts: 21 | $ref: "paths/v1_Accounts.yaml" 22 | -------------------------------------------------------------------------------- /what-changed/reports/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package reports 5 | 6 | // HasChanges represents a change model that provides a total change count and a breaking change count. 7 | type HasChanges interface { 8 | // TotalChanges represents number of all changes found 9 | TotalChanges() int 10 | 11 | // TotalBreakingChanges represents the number of contract breaking changes only. 12 | TotalBreakingChanges() int 13 | } 14 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/paging.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: Common schemas 4 | version: "1.0.0" 5 | servers: [] 6 | paths: [] 7 | 8 | components: 9 | examples: 10 | dtoErrorExample: 11 | value: 12 | errorCode: ErrUnknownError 13 | requestId: "12345" 14 | message: "An unknown error occurred" 15 | schemas: 16 | dtoTest: 17 | description: Test schema (CLASH) 18 | type: object 19 | properties: 20 | fishcake: 21 | $ref: "fishcake.yaml" 22 | -------------------------------------------------------------------------------- /index/rolodex_test_data/components.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Rolodex Test Data 4 | version: 1.0.0 5 | components: 6 | parameters: 7 | SomeParam: 8 | name: someParam 9 | in: query 10 | description: A parameter that does nothing. Ding a ling! 11 | schema: 12 | type: string 13 | schemas: 14 | Ding: 15 | type: object 16 | description: A thing that does nothing. Ding a ling! 17 | properties: 18 | message: 19 | type: string 20 | description: I am pointless. Ding Ding! -------------------------------------------------------------------------------- /bundler/test/specs/paging.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Test - paging schema 4 | version: "1.0.0" 5 | servers: [] 6 | paths: [] 7 | 8 | components: 9 | 10 | schemas: 11 | dtoTest: 12 | description: A Test schema (paging.yaml) 13 | type: string 14 | paging: 15 | description: Paging section 16 | type: object 17 | properties: 18 | test: 19 | $ref: "#/components/schemas/dtoTest" 20 | total: 21 | description: Total count 22 | type: integer 23 | example: 439 24 | -------------------------------------------------------------------------------- /test_specs/a.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: Example API 5 | termsOfService: https://example.com/terms/ 6 | contact: 7 | name: beeple 8 | email: contact@example.com 9 | url: http://example.com/contact 10 | license: 11 | name: Apache 2.0 12 | url: http://www.apache.org/licenses/LICENSE-2.0.html 13 | paths: 14 | /test: 15 | get: 16 | responses: 17 | "200": 18 | content: 19 | application/json: 20 | schema: 21 | $ref: b.yaml#/components/schemas/SchemaB -------------------------------------------------------------------------------- /test_specs/a-alt.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: Example API 5 | termsOfService: https://example.com/terms/ 6 | contact: 7 | name: beeple 8 | email: contact@example.com 9 | url: http://example.com/contact 10 | license: 11 | name: Apache 2.0 12 | url: http://www.apache.org/licenses/LICENSE-2.0.html 13 | paths: 14 | /test: 15 | get: 16 | responses: 17 | "200": 18 | content: 19 | application/json: 20 | schema: 21 | $ref: b-alt.yaml#/components/schemas/SchemaB -------------------------------------------------------------------------------- /test_specs/ref_test/mod/a.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: Example API 5 | termsOfService: https://example.com/terms/ 6 | contact: 7 | name: beeple 8 | email: contact@example.com 9 | url: http://example.com/contact 10 | license: 11 | name: Apache 2.0 12 | url: http://www.apache.org/licenses/LICENSE-2.0.html 13 | paths: 14 | /test: 15 | get: 16 | responses: 17 | "200": 18 | content: 19 | application/json: 20 | schema: 21 | $ref: b.yaml#/components/schemas/SchemaB -------------------------------------------------------------------------------- /test_specs/ref_test/orig/a.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | version: 1.0.0 4 | title: Example API 5 | termsOfService: https://example.com/terms/ 6 | contact: 7 | name: beeple 8 | email: contact@example.com 9 | url: http://example.com/contact 10 | license: 11 | name: Apache 2.0 12 | url: http://www.apache.org/licenses/LICENSE-2.0.html 13 | paths: 14 | /test: 15 | get: 16 | responses: 17 | "200": 18 | content: 19 | application/json: 20 | schema: 21 | $ref: b.yaml#/components/schemas/SchemaB -------------------------------------------------------------------------------- /test_specs/recursive-expression-test.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Recursive Expression Test 4 | version: 1 5 | servers: 6 | - url: https://dev.url 7 | variables: 8 | basePath: 9 | default: "" 10 | paths: {} 11 | components: 12 | schemas: 13 | RecursiveExpression: 14 | description: RecursiveExpression 15 | title: RecursiveExpression 16 | properties: 17 | allOf: 18 | type: array 19 | items: 20 | anyOf: 21 | - $ref: "#/components/schemas/RecursiveExpression" 22 | - type: string 23 | 24 | -------------------------------------------------------------------------------- /index/rolodex_test_data/dir2/components.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Dir2 Test Components 4 | version: 1.0.0 5 | components: 6 | schemas: 7 | GlobalComponent: 8 | type: object 9 | description: Dir2 Global Component 10 | properties: 11 | message: 12 | type: string 13 | description: I am pointless, but I am global dir2. 14 | AnotherComponent: 15 | type: object 16 | description: Dir2 Another Component 17 | properties: 18 | message: 19 | $ref: "subdir2/shared.yaml#/components/schemas/SharedComponent" 20 | SomeUtil: 21 | $ref: "utils/utils.yaml" -------------------------------------------------------------------------------- /datamodel/low/base/context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGetModelContext(t *testing.T) { 14 | assert.Nil(t, GetModelContext(nil)) 15 | assert.Nil(t, GetModelContext(context.Background())) 16 | 17 | ctx := context.WithValue(context.Background(), "modelCtx", &ModelContext{}) 18 | assert.NotNil(t, GetModelContext(ctx)) 19 | 20 | ctx = context.WithValue(context.Background(), "modelCtx", "wrong") 21 | assert.Nil(t, GetModelContext(ctx)) 22 | } 23 | -------------------------------------------------------------------------------- /datamodel/low/overlay/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | // Constants for labels used to look up values within OpenAPI Overlay specifications. 7 | // https://spec.openapis.org/overlay/v1.0.0 8 | const ( 9 | OverlayLabel = "overlay" 10 | InfoLabel = "info" 11 | ExtendsLabel = "extends" 12 | ActionsLabel = "actions" 13 | TitleLabel = "title" 14 | VersionLabel = "version" 15 | TargetLabel = "target" 16 | DescriptionLabel = "description" 17 | UpdateLabel = "update" 18 | RemoveLabel = "remove" 19 | ) 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pb33f/libopenapi 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb 7 | github.com/pb33f/jsonpath v0.7.0 8 | github.com/pb33f/ordered-map/v2 v2.3.0 9 | github.com/stretchr/testify v1.11.1 10 | go.yaml.in/yaml/v4 v4.0.0-rc.3 11 | ) 12 | 13 | require ( 14 | github.com/bahlo/generic-list-go v0.2.0 // indirect 15 | github.com/buger/jsonparser v1.1.1 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/rogpeppe/go-internal v1.9.0 // indirect 20 | golang.org/x/sync v0.19.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /test_specs/second.yaml: -------------------------------------------------------------------------------- 1 | title: second doc title 2 | description: second doc description 3 | type: object 4 | 5 | additionalProperties: false 6 | 7 | properties: 8 | 9 | property1: 10 | title: title 11 | description: property 1 description 12 | type: array 13 | items: 14 | title: item 15 | description: third description 16 | type: object 17 | additionalProperties: false 18 | properties: 19 | details: 20 | $ref: "third.yaml" 21 | 22 | property2: 23 | title: title 24 | description: property 2 description 25 | type: object 26 | additionalProperties: false 27 | properties: 28 | property: 29 | title: title 30 | description: tasty description 31 | type: integer -------------------------------------------------------------------------------- /datamodel/high/base/base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package base contains shared high-level models that are used between both versions 2 and 3 of OpenAPI. 5 | // These models are consistent across both specifications, except for the Schema. 6 | // 7 | // OpenAPI 3 contains all the same properties that an OpenAPI 2 specification does, and more. The choice 8 | // to not duplicate the schemas is to allow a graceful degradation pattern to be used. Schemas are the most complex 9 | // beats, particularly when polymorphism is used. By re-using the same superset Schema across versions, we can ensure 10 | // that all the latest features are collected, without damaging backwards compatibility. 11 | package base 12 | -------------------------------------------------------------------------------- /datamodel/low/base/base.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package base contains shared low-level models that are used between both versions 2 and 3 of OpenAPI. 5 | // These models are consistent across both specifications, except for the Schema. 6 | // 7 | // OpenAPI 3 contains all the same properties that an OpenAPI 2 specification does, and more. The choice 8 | // to not duplicate the schemas is to allow a graceful degradation pattern to be used. Schemas are the most complex 9 | // beats, particularly when polymorphism is used. By re-using the same superset Schema across versions, we can ensure 10 | // that all the latest features are collected, without damaging backwards compatibility. 11 | package base 12 | -------------------------------------------------------------------------------- /index/circular_reference_result_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCircularReferenceResult_GenerateJourneyPath(t *testing.T) { 13 | refs := []*Reference{ 14 | {Name: "chicken"}, 15 | {Name: "nuggets"}, 16 | {Name: "chicken"}, 17 | {Name: "soup"}, 18 | {Name: "chicken"}, 19 | {Name: "nuggets"}, 20 | {Name: "for"}, 21 | {Name: "me"}, 22 | {Name: "and"}, 23 | {Name: "you"}, 24 | } 25 | 26 | cr := &CircularReferenceResult{Journey: refs} 27 | assert.Equal(t, "chicken -> nuggets -> chicken -> soup -> "+ 28 | "chicken -> nuggets -> for -> me -> and -> you", cr.GenerateJourneyPath()) 29 | } 30 | -------------------------------------------------------------------------------- /datamodel/low/base/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "sync" 9 | ) 10 | 11 | // ModelContext is a struct that holds various persistent data structures for the model 12 | // that passes through the entire model building process. 13 | type ModelContext struct { 14 | SchemaCache *sync.Map 15 | } 16 | 17 | // GetModelContext will return the ModelContext from a context.Context object 18 | // if it is available, otherwise it will return nil. 19 | func GetModelContext(ctx context.Context) *ModelContext { 20 | if ctx == nil { 21 | return nil 22 | } 23 | if ctx.Value("modelCtx") == nil { 24 | return nil 25 | } 26 | if c, ok := ctx.Value("modelCtx").(*ModelContext); ok { 27 | return c 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /index/rolodex_test_data/doc2.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Rolodex Test Data 4 | version: 1.0.0 5 | paths: 6 | /nested/files3: 7 | get: 8 | responses: 9 | '200': 10 | description: OK 11 | content: 12 | application/json: 13 | schema: 14 | $ref: 'dir2/components.yaml#/components/schemas/AnotherComponent' 15 | default: 16 | description: Anything 17 | content: 18 | application/json: 19 | schema: 20 | $ref: 'dir2/components.yaml#/components/schemas/GlobalComponent' 21 | components: 22 | schemas: 23 | Thing: 24 | type: object 25 | description: A thing that does nothing. 26 | properties: 27 | message: 28 | type: string 29 | description: I am pointless. -------------------------------------------------------------------------------- /what-changed/changed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package what_changed 5 | 6 | import "github.com/pb33f/libopenapi/what-changed/model" 7 | 8 | // Changed represents an object that was changed 9 | type Changed interface { 10 | // GetAllChanges returns all top level changes made to properties in this object 11 | GetAllChanges() []*model.Change 12 | 13 | // TotalChanges returns a count of all changes made on the object, including all children 14 | TotalChanges() int 15 | 16 | // TotalBreakingChanges returns a count of all breaking changes on this object 17 | TotalBreakingChanges() int 18 | 19 | // GetPropertyChanges 20 | GetPropertyChanges() []*model.Change 21 | 22 | // PropertiesOnly will set a change object to only render properties and not the whole timeline. 23 | PropertiesOnly() 24 | } 25 | -------------------------------------------------------------------------------- /test_specs/first.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: title 4 | description: description 5 | version: 0.0.0 6 | 7 | paths: 8 | 9 | /items: 10 | get: 11 | tags: 12 | - items 13 | summary: summary 14 | description: description 15 | parameters: [] 16 | responses: 17 | '200': 18 | description: OK 19 | content: 20 | application/json: 21 | schema: 22 | title: Schema 23 | description: description 24 | type: object 25 | 26 | additionalProperties: 27 | type: object 28 | title: first title 29 | description: first description 30 | additionalProperties: false 31 | properties: 32 | second: 33 | $ref: "second.yaml" -------------------------------------------------------------------------------- /test_specs/minimal_remote_refs/openapi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: 3.1.0 3 | info: 4 | description: Example API spec 5 | version: v1 6 | title: Example 7 | contact: 8 | name: Example 9 | email: example@example.com 10 | url: www.example.com 11 | license: 12 | name: Example 13 | url: www.example.com 14 | tags: 15 | - name: Account 16 | description: Account 17 | servers: 18 | - url: https:// 19 | paths: 20 | /api/v1/Accounts: 21 | get: 22 | summary: TODO 23 | description: TODO 24 | security: 25 | - BearerAuth: [] 26 | tags: 27 | - Account 28 | operationId: listAccounts 29 | responses: 30 | "200": 31 | $ref: ./schemas/components.openapi.yaml#/components/responses/ListAccounts 32 | components: 33 | securitySchemes: 34 | BearerAuth: 35 | type: http 36 | scheme: bearer 37 | -------------------------------------------------------------------------------- /utils/unwrap_errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestUnwrapErrors(t *testing.T) { 14 | // create an array of errors 15 | errs := []error{ 16 | errors.New("first error"), 17 | errors.New("second error"), 18 | errors.New("third error"), 19 | } 20 | 21 | // join them up 22 | joined := errors.Join(errs...) 23 | assert.Error(t, joined) 24 | 25 | // unwrap them 26 | unwrapped := UnwrapErrors(joined) 27 | assert.Len(t, unwrapped, 3) 28 | } 29 | 30 | func TestUnwrapErrors_Empty(t *testing.T) { 31 | assert.Len(t, UnwrapErrors(nil), 0) 32 | } 33 | 34 | func TestUnwrapErrors_SingleError(t *testing.T) { 35 | assert.Len(t, UnwrapErrors(errors.New("single error")), 1) 36 | } 37 | -------------------------------------------------------------------------------- /index/rolodex_test_data/doc1.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Rolodex Test Data 4 | version: 1.0.0 5 | paths: 6 | /one/local: 7 | get: 8 | responses: 9 | '200': 10 | description: OK 11 | content: 12 | application/json: 13 | schema: 14 | $ref: '#/components/schemas/Thing' 15 | /one/file: 16 | get: 17 | responses: 18 | '200': 19 | description: OK 20 | content: 21 | application/json: 22 | schema: 23 | $ref: 'components.yaml#/components/schemas/Ding' 24 | /external_operation: 25 | $ref: 'operations.yaml#/external_operation' 26 | components: 27 | schemas: 28 | Thing: 29 | type: object 30 | description: A thing that does nothing. 31 | properties: 32 | message: 33 | type: string 34 | description: I am pointless. -------------------------------------------------------------------------------- /datamodel/low/v2/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | const ( 7 | DefinitionsLabel = "definitions" 8 | SecurityDefinitionsLabel = "securityDefinitions" 9 | ExamplesLabel = "examples" 10 | HeadersLabel = "headers" 11 | DefaultLabel = "default" 12 | ItemsLabel = "items" 13 | ParametersLabel = "parameters" 14 | PathsLabel = "paths" 15 | GetLabel = "get" 16 | PostLabel = "post" 17 | PatchLabel = "patch" 18 | PutLabel = "put" 19 | DeleteLabel = "delete" 20 | OptionsLabel = "options" 21 | HeadLabel = "head" 22 | SecurityLabel = "security" 23 | ScopesLabel = "scopes" 24 | ResponsesLabel = "responses" 25 | ) 26 | -------------------------------------------------------------------------------- /test_specs/ref-followed.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: All scalar types 4 | version: 1.0.0 5 | description: These types used in testing 6 | servers: 7 | - url: https://api.server.test/v1 8 | 9 | paths: 10 | /test: 11 | get: 12 | operationId: 20CBF3CA-4F9F-455E-8A3E-3C2B2CD9849A 13 | responses: 14 | "200": 15 | type: string 16 | description: This is my schema that is great! 17 | 18 | components: 19 | schemas: 20 | FBSRef: 21 | $ref: "#/components/schemas/FP" 22 | 23 | FP: 24 | type: string 25 | description: Always use full F{ 26 | example: asd asd asd 27 | pattern: '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[\+-]\d{2}:\d{2})$' 28 | 29 | UInt64: 30 | type: integer 31 | format: uint64 32 | nullable: true 33 | example: 1 34 | minimum: 1 35 | 36 | Byte: 37 | $ref: "#/components/schemas/UInt64" 38 | -------------------------------------------------------------------------------- /overlay/validation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | highoverlay "github.com/pb33f/libopenapi/datamodel/high/overlay" 8 | "go.yaml.in/yaml/v4" 9 | ) 10 | 11 | // validateOverlay checks that the overlay has all required fields. 12 | func validateOverlay(overlay *highoverlay.Overlay) error { 13 | if overlay.Overlay == "" { 14 | return ErrMissingOverlayField 15 | } 16 | if overlay.Info == nil { 17 | return ErrMissingInfo 18 | } 19 | if len(overlay.Actions) == 0 { 20 | return ErrEmptyActions 21 | } 22 | return nil 23 | } 24 | 25 | // validateTarget checks that a target node is a valid target (object or array). 26 | // Per the Overlay Spec, primitive/null targets are invalid. 27 | func validateTarget(node *yaml.Node) error { 28 | if node.Kind == yaml.ScalarNode { 29 | return ErrPrimitiveTarget 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /bundler/test/specs/error.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Test - error schema 4 | version: "1.0.0" 5 | servers: [] 6 | paths: [] 7 | 8 | components: 9 | schemas: 10 | dtoTest: 11 | description: A Test schema (error.yaml) 12 | type: string 13 | errorCode: 14 | description: ErrCode enumeration 15 | type: string 16 | enum: 17 | - ErrUnknownError 18 | - ErrEntityNotFound 19 | 20 | dtoError: 21 | example: 22 | $ref: "./clash/paging.yaml#/components/examples/dtoErrorExample" 23 | description: General error structure 24 | type: object 25 | required: 26 | - errorCode 27 | - requestId 28 | properties: 29 | errorCode: 30 | $ref: "#/components/schemas/errorCode" 31 | requestId: 32 | type: string 33 | message: 34 | type: string 35 | testBangCrash: 36 | $ref: "./clash/paging.yaml#/components/schemas/dtoTest" 37 | testBang: 38 | $ref: "#/components/schemas/dtoTest" 39 | 40 | -------------------------------------------------------------------------------- /utils/utils_bench_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | // Local regex for benchmarking 9 | var testPathCharExp = regexp.MustCompile(`^[A-Za-z0-9_\\]*$`) 10 | 11 | // Benchmark the regex-based pathCharExp.MatchString 12 | func BenchmarkPathCharExp_Regex(b *testing.B) { 13 | testCases := []string{ 14 | "simple", 15 | "SimpleCase", 16 | "with_underscore", 17 | "with-dash", 18 | "with spaces", 19 | "special!char", 20 | "123numeric", 21 | "back\\slash", 22 | } 23 | 24 | b.ResetTimer() 25 | for i := 0; i < b.N; i++ { 26 | for _, tc := range testCases { 27 | _ = testPathCharExp.MatchString(tc) 28 | } 29 | } 30 | } 31 | 32 | func BenchmarkPathCharExp_Optimized(b *testing.B) { 33 | testCases := []string{ 34 | "simple", 35 | "SimpleCase", 36 | "with_underscore", 37 | "with-dash", 38 | "with spaces", 39 | "special!char", 40 | "123numeric", 41 | "back\\slash", 42 | } 43 | 44 | b.ResetTimer() 45 | for i := 0; i < b.N; i++ { 46 | for _, tc := range testCases { 47 | _ = isPathChar(tc) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /what-changed/model/document_flat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package model 5 | 6 | type DocumentChangesFlat struct { 7 | *PropertyChanges 8 | InfoChanges []*Change `json:"info,omitempty" yaml:"info,omitempty"` 9 | PathsChanges []*Change `json:"paths,omitempty" yaml:"paths,omitempty"` 10 | TagChanges []*Change `json:"tags,omitempty" yaml:"tags,omitempty"` 11 | ExternalDocChanges []*Change `json:"externalDoc,omitempty" yaml:"externalDoc,omitempty"` 12 | WebhookChanges []*Change `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` 13 | ServerChanges []*Change `json:"servers,omitempty" yaml:"servers,omitempty"` 14 | SecurityRequirementChanges []*Change `json:"securityRequirements,omitempty" yaml:"securityRequirements,omitempty"` 15 | ComponentsChanges []*Change `json:"components,omitempty" yaml:"components,omitempty"` 16 | ExtensionChanges []*Change `json:"extensions,omitempty" yaml:"extensions,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /test_specs/nested_files/paths/v1_Accounts.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | summary: TODO 3 | description: TODO 4 | security: 5 | - BearerAuth: [] 6 | tags: 7 | - Account 8 | operationId: getAccounts 9 | parameters: 10 | - $ref: ../components/parameters/query/$select.yaml 11 | - $ref: ../components/parameters/header/page-size.yaml 12 | responses: 13 | "200": 14 | $ref: ../components/responses/Unspecified200.yaml 15 | 16 | post: 17 | summary: TODO 18 | description: TODO 19 | security: 20 | - BearerAuth: [] 21 | tags: 22 | - Account 23 | operationId: createAccounts 24 | requestBody: 25 | $ref: ../components/requestBodies/AccountModel.yaml 26 | responses: 27 | "200": 28 | $ref: ../components/responses/Unspecified200.yaml 29 | 30 | put: 31 | summary: TODO 32 | description: TODO 33 | security: 34 | - BearerAuth: [] 35 | tags: 36 | - Account 37 | operationId: updateAccounts 38 | requestBody: 39 | $ref: ../components/requestBodies/AccountModel.yaml 40 | responses: 41 | "200": 42 | $ref: ../components/responses/Unspecified200.yaml 43 | -------------------------------------------------------------------------------- /test_specs/minimal_remote_refs/schemas/components.openapi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: 3.1.0 3 | info: 4 | description: Example API component definitions 5 | version: v1 6 | title: Example 7 | contact: 8 | name: Example 9 | email: example@example.com 10 | url: www.example.com 11 | license: 12 | name: Example 13 | url: www.example.com 14 | components: 15 | schemas: 16 | Account: 17 | type: object 18 | properties: 19 | name: 20 | type: string 21 | description: > 22 | Name of the account 23 | responses: 24 | ListAccounts: 25 | description: List all accounts. 26 | content: 27 | application/json: 28 | schema: 29 | type: object 30 | properties: 31 | items: 32 | type: array 33 | items: 34 | description: > 35 | The accounts. 36 | $ref: "#/components/schemas/Account" 37 | total: 38 | type: integer 39 | description: Total number of accounts. 40 | -------------------------------------------------------------------------------- /datamodel/high/v2/scopes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | lowv2 "github.com/pb33f/libopenapi/datamodel/low/v2" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | ) 11 | 12 | // Scopes is a high-level representation of a Swagger / OpenAPI 2 OAuth2 Scopes object, that is backed by a low-level one. 13 | // 14 | // Scopes lists the available scopes for an OAuth2 security scheme. 15 | // - https://swagger.io/specification/v2/#scopesObject 16 | type Scopes struct { 17 | Values *orderedmap.Map[string, string] 18 | low *lowv2.Scopes 19 | } 20 | 21 | // NewScopes creates a new high-level instance of Scopes from a low-level one. 22 | func NewScopes(scopes *lowv2.Scopes) *Scopes { 23 | s := new(Scopes) 24 | s.low = scopes 25 | s.Values = low.FromReferenceMap(scopes.Values) 26 | return s 27 | } 28 | 29 | // GoLow returns the low-level instance of Scopes used to create the high-level one. 30 | func (s *Scopes) GoLow() *lowv2.Scopes { 31 | return s.low 32 | } 33 | -------------------------------------------------------------------------------- /datamodel/low/v3/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/pb33f/libopenapi/datamodel" 11 | ) 12 | 13 | // How to create a low-level OpenAPI 3+ Document from an OpenAPI specification 14 | func Example_createLowLevelOpenAPIDocument() { 15 | // How to create a low-level OpenAPI 3 Document 16 | 17 | // load petstore into bytes 18 | petstoreBytes, _ := os.ReadFile("../../../test_specs/petstorev3.json") 19 | 20 | // read in specification 21 | info, _ := datamodel.ExtractSpecInfo(petstoreBytes) 22 | 23 | // build low-level document model 24 | document, errs := CreateDocumentFromConfig(info, datamodel.NewDocumentConfiguration()) 25 | 26 | // if something went wrong, a slice of errors is returned 27 | if errs != nil { 28 | fmt.Printf("error: %s\n", errs.Error()) 29 | panic("cannot build document") 30 | } 31 | 32 | // print out email address from the info > contact object. 33 | fmt.Print(document.Info.Value.Contact.Value.Email.Value) 34 | // Output: apiteam@swagger.io 35 | } 36 | -------------------------------------------------------------------------------- /datamodel/high/v2/examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | lowv2 "github.com/pb33f/libopenapi/datamodel/low/v2" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // Example represents a high-level Swagger / OpenAPI 2 Example object, backed by a low level one. 14 | // Allows sharing examples for operation responses 15 | // - https://swagger.io/specification/v2/#exampleObject 16 | type Example struct { 17 | Values *orderedmap.Map[string, *yaml.Node] 18 | low *lowv2.Examples 19 | } 20 | 21 | // NewExample creates a new high-level Example instance from a low-level one. 22 | func NewExample(examples *lowv2.Examples) *Example { 23 | e := new(Example) 24 | e.low = examples 25 | if orderedmap.Len(examples.Values) > 0 { 26 | e.Values = low.FromReferenceMap(examples.Values) 27 | } 28 | return e 29 | } 30 | 31 | // GoLow returns the low-level Example used to create the high-level one. 32 | func (e *Example) GoLow() *lowv2.Examples { 33 | return e.low 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 Princess Beef Heavy Industries, LLC / Dave Shanley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test_specs/yaml-anchor.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: Example 4 | version: 0.0.1 5 | components: 6 | schemas: 7 | Example: 8 | type: object 9 | required: 10 | - id 11 | properties: 12 | id: 13 | type: string 14 | title: Name 15 | pattern: ^[a-zA-Z0-9_\-]+$ 16 | description: Name of the Example. 17 | description: 18 | type: string 19 | title: Description 20 | description: Brief description of this Example. Optional. 21 | paths: 22 | /system/examples/{id}: 23 | get: 24 | tags: 25 | &a1 26 | - Examples 27 | parameters: 28 | &id 29 | - name: id 30 | in: path 31 | required: true 32 | schema: 33 | type: string 34 | responses: 35 | &a2 36 | "200": 37 | description: a list of Example objects 38 | content: 39 | &example 40 | application/json: 41 | schema: 42 | $ref: '#/components/schemas/Example' 43 | post: 44 | tags: *a1 45 | parameters: *id 46 | responses: *a2 47 | requestBody: 48 | content: *example 49 | -------------------------------------------------------------------------------- /index/circular_reference_result.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "strings" 5 | 6 | "go.yaml.in/yaml/v4" 7 | ) 8 | 9 | // CircularReferenceResult contains a circular reference found when traversing the graph. 10 | type CircularReferenceResult struct { 11 | Journey []*Reference 12 | ParentNode *yaml.Node 13 | Start *Reference 14 | LoopIndex int 15 | LoopPoint *Reference 16 | IsArrayResult bool // if this result comes from an array loop. 17 | PolymorphicType string // which type of polymorphic loop is this? (oneOf, anyOf, allOf) 18 | IsPolymorphicResult bool // if this result comes from a polymorphic loop. 19 | IsInfiniteLoop bool // if all the definitions in the reference loop are marked as required, this is an infinite circular reference, thus is not allowed. 20 | } 21 | 22 | // GenerateJourneyPath generates a string representation of the journey taken to find the circular reference. 23 | func (c *CircularReferenceResult) GenerateJourneyPath() string { 24 | buf := strings.Builder{} 25 | for i, ref := range c.Journey { 26 | if i > 0 { 27 | buf.WriteString(" -> ") 28 | } 29 | 30 | buf.WriteString(ref.Name) 31 | } 32 | 33 | return buf.String() 34 | } 35 | -------------------------------------------------------------------------------- /datamodel/low/v2/examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/index" 12 | "github.com/stretchr/testify/assert" 13 | "go.yaml.in/yaml/v4" 14 | ) 15 | 16 | func TestExamples_Hash(t *testing.T) { 17 | yml := `something: string 18 | yes: 19 | - more 20 | - water 21 | anything: 22 | cake: burger 23 | nothing: int` 24 | 25 | var idxNode yaml.Node 26 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 27 | idx := index.NewSpecIndex(&idxNode) 28 | 29 | var n Examples 30 | _ = low.BuildModel(idxNode.Content[0], &n) 31 | _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) 32 | 33 | yml2 := `anything: 34 | cake: burger 35 | something: string 36 | nothing: int 37 | yes: 38 | - more 39 | - water` 40 | 41 | var idxNode2 yaml.Node 42 | _ = yaml.Unmarshal([]byte(yml2), &idxNode2) 43 | idx2 := index.NewSpecIndex(&idxNode2) 44 | 45 | var n2 Examples 46 | _ = low.BuildModel(idxNode2.Content[0], &n2) 47 | _ = n2.Build(context.Background(), nil, idxNode2.Content[0], idx2) 48 | 49 | assert.Equal(t, n.Hash(), n2.Hash()) 50 | } 51 | -------------------------------------------------------------------------------- /datamodel/low/low.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2004 Princess B33f Heavy Industries / Dave Shanley / Quobix 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package low contains a set of low-level models that represent OpenAPI 2 and 3 documents. 5 | // These low-level models (plumbing) are used to create high-level models, and used when deep knowledge 6 | // about the original data, positions, comments and the original node structures. 7 | // 8 | // Low-level models are not designed to be easily navigated, every single property is either a NodeReference 9 | // an KeyReference or a ValueReference. These references hold the raw value and key or value nodes that contain 10 | // the original yaml.Node trees that make up the object. 11 | // 12 | // Navigating maps that use a KeyReference as a key is tricky, because there is no easy way to provide a lookup. 13 | // Convenience methods for lookup up properties in a low-level model have therefore been provided. 14 | package low 15 | 16 | import "go.yaml.in/yaml/v4" 17 | 18 | // HasRootNode is an interface that is used to extract the root yaml.Node from a low-level model. The root node is 19 | // the top-level node that represents the entire object as represented in the original source file. 20 | type HasRootNode interface { 21 | GetRootNode() *yaml.Node 22 | } 23 | -------------------------------------------------------------------------------- /datamodel/low/v2/scopes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/index" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | "github.com/stretchr/testify/assert" 14 | "go.yaml.in/yaml/v4" 15 | ) 16 | 17 | func TestScopes_Hash(t *testing.T) { 18 | yml := `burgers: chips 19 | pizza: beans 20 | x-men: needs a reboot or a refresh` 21 | 22 | var idxNode yaml.Node 23 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 24 | idx := index.NewSpecIndex(&idxNode) 25 | 26 | var n Scopes 27 | _ = low.BuildModel(idxNode.Content[0], &n) 28 | _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) 29 | 30 | yml2 := `x-men: needs a reboot or a refresh 31 | pizza: beans 32 | burgers: chips` 33 | 34 | var idxNode2 yaml.Node 35 | _ = yaml.Unmarshal([]byte(yml2), &idxNode2) 36 | idx2 := index.NewSpecIndex(&idxNode2) 37 | 38 | var n2 Scopes 39 | _ = low.BuildModel(idxNode2.Content[0], &n2) 40 | _ = n2.Build(context.Background(), nil, idxNode2.Content[0], idx2) 41 | 42 | // hash 43 | assert.Equal(t, n.Hash(), n2.Hash()) 44 | assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) 45 | } 46 | -------------------------------------------------------------------------------- /datamodel/low/base/contact_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/stretchr/testify/assert" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | func TestContact_Hash(t *testing.T) { 16 | left := `url: https://pb33f.io 17 | description: the ranch 18 | email: buckaroo@pb33f.io 19 | x-cake: yummy` 20 | 21 | right := `url: https://pb33f.io 22 | description: the ranch 23 | email: buckaroo@pb33f.io 24 | x-beer: cold` 25 | 26 | var lNode, rNode yaml.Node 27 | _ = yaml.Unmarshal([]byte(left), &lNode) 28 | _ = yaml.Unmarshal([]byte(right), &rNode) 29 | 30 | // create low level objects 31 | var lDoc Contact 32 | var rDoc Contact 33 | _ = low.BuildModel(lNode.Content[0], &lDoc) 34 | _ = low.BuildModel(rNode.Content[0], &rDoc) 35 | 36 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 37 | 38 | c := Contact{} 39 | c.Build(context.Background(), lNode.Content[0], rNode.Content[0], nil) 40 | assert.NotNil(t, c.GetRootNode()) 41 | assert.NotNil(t, c.GetKeyNode()) 42 | assert.Equal(t, 1, c.GetExtensions().Len()) 43 | assert.Equal(t, 1, c.GetExtensions().Len()) 44 | assert.Nil(t, c.GetIndex()) 45 | assert.NotNil(t, c.GetContext()) 46 | } 47 | -------------------------------------------------------------------------------- /datamodel/high/base/security_requirement_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | 11 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 12 | lowbase "github.com/pb33f/libopenapi/datamodel/low/base" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | "github.com/stretchr/testify/assert" 15 | "go.yaml.in/yaml/v4" 16 | ) 17 | 18 | func TestNewSecurityRequirement(t *testing.T) { 19 | var cNode yaml.Node 20 | 21 | yml := `pizza: 22 | - cheese 23 | - tomato 24 | cake: 25 | - icing 26 | - sponge` 27 | 28 | _ = yaml.Unmarshal([]byte(yml), &cNode) 29 | 30 | var lowExt lowbase.SecurityRequirement 31 | _ = lowmodel.BuildModel(cNode.Content[0], &lowExt) 32 | 33 | _ = lowExt.Build(context.Background(), nil, cNode.Content[0], nil) 34 | 35 | highExt := NewSecurityRequirement(&lowExt) 36 | 37 | assert.Len(t, highExt.Requirements.GetOrZero("pizza"), 2) 38 | assert.Len(t, highExt.Requirements.GetOrZero("cake"), 2) 39 | 40 | wentLow := highExt.GoLow() 41 | assert.Equal(t, 2, orderedmap.Len(wentLow.Requirements.Value)) 42 | assert.NotNil(t, highExt.GoLowUntyped()) 43 | 44 | // render the high-level object as YAML 45 | highBytes, _ := highExt.Render() 46 | assert.Equal(t, yml, strings.TrimSpace(string(highBytes))) 47 | } 48 | -------------------------------------------------------------------------------- /datamodel/low/base/circ_check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package base 5 | 6 | // CheckSchemaProxyForCircularRefs checks if the provided SchemaProxy has any circular references, extracted from 7 | // The rolodex attached to the index. 8 | func CheckSchemaProxyForCircularRefs(s *SchemaProxy) bool { 9 | if s.GetIndex() == nil || s.GetIndex().GetRolodex() == nil { 10 | return false // no index or rolodex, so no circular references 11 | } 12 | rolo := s.GetIndex().GetRolodex() 13 | allCircs := rolo.GetRootIndex().GetCircularReferences() 14 | safeCircularRefs := rolo.GetSafeCircularReferences() 15 | ignoredCircularRefs := rolo.GetIgnoredCircularReferences() 16 | combinedCircularRefs := append(safeCircularRefs, ignoredCircularRefs...) 17 | combinedCircularRefs = append(combinedCircularRefs, allCircs...) 18 | dup := make(map[string]struct{}) 19 | for _, ref := range combinedCircularRefs { 20 | // hash the root node of the schema reference 21 | if ref.LoopPoint.FullDefinition == s.GetReference() || ref.LoopPoint.Definition == s.GetReference() { 22 | return true 23 | } 24 | // check journey, if we have any duplicated 25 | for _, ji := range ref.Journey { 26 | if _, exists := dup[ji.FullDefinition]; exists { 27 | return true // this has already been checked, it's a loop. 28 | } 29 | dup[ji.FullDefinition] = struct{}{} 30 | } 31 | } 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /datamodel/high/v3/package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/pb33f/libopenapi/utils" 11 | 12 | "github.com/pb33f/libopenapi/datamodel" 13 | lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" 14 | "github.com/pb33f/libopenapi/orderedmap" 15 | ) 16 | 17 | // An example of how to create a new high-level OpenAPI 3+ document from an OpenAPI specification. 18 | func Example_createHighLevelOpenAPIDocument() { 19 | // Load in an OpenAPI 3+ specification as a byte slice. 20 | data, _ := os.ReadFile("../../../test_specs/petstorev3.json") 21 | 22 | // Create a new *datamodel.SpecInfo from bytes. 23 | info, _ := datamodel.ExtractSpecInfo(data) 24 | 25 | var err error 26 | 27 | // Create a new low-level Document, capture any errors thrown during creation. 28 | lowDoc, err = lowv3.CreateDocumentFromConfig(info, datamodel.NewDocumentConfiguration()) 29 | 30 | // Get upset if any errors were thrown. 31 | for i := range utils.UnwrapErrors(err) { 32 | fmt.Printf("error: %v", i) 33 | } 34 | 35 | // Create a high-level Document from the low-level one. 36 | doc := NewDocument(lowDoc) 37 | 38 | // Print out some details 39 | fmt.Printf("Petstore contains %d paths and %d component schemas", 40 | orderedmap.Len(doc.Paths.PathItems), orderedmap.Len(doc.Components.Schemas)) 41 | // Output: Petstore contains 13 paths and 8 component schemas 42 | } 43 | -------------------------------------------------------------------------------- /utils/utils_regex_bench_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | // Simple regex benchmark 9 | func BenchmarkRegexMatchString(b *testing.B) { 10 | re := regexp.MustCompile(`^[A-Za-z0-9_\\]*$`) 11 | testString := "simple_test_123" 12 | 13 | b.ResetTimer() 14 | for i := 0; i < b.N; i++ { 15 | _ = re.MatchString(testString) 16 | } 17 | } 18 | 19 | // Optimized character check benchmark 20 | func BenchmarkOptimizedCharCheck(b *testing.B) { 21 | testString := "simple_test_123" 22 | 23 | b.ResetTimer() 24 | for i := 0; i < b.N; i++ { 25 | _ = isPathChar(testString) 26 | } 27 | } 28 | 29 | // Benchmark ConvertComponentIdIntoFriendlyPathSearch with various inputs 30 | func BenchmarkConvertComponentPath_Simple(b *testing.B) { 31 | path := "#/components/schemas/Pet" 32 | b.ResetTimer() 33 | for i := 0; i < b.N; i++ { 34 | _, _ = ConvertComponentIdIntoFriendlyPathSearch(path) 35 | } 36 | } 37 | 38 | func BenchmarkConvertComponentPath_Complex(b *testing.B) { 39 | path := "#/paths/~1v2~1customers~1my~1invoices~1%7Binvoice_uuid%7D/get/parameters/0" 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | _, _ = ConvertComponentIdIntoFriendlyPathSearch(path) 43 | } 44 | } 45 | 46 | func BenchmarkConvertComponentPath_VeryComplex(b *testing.B) { 47 | path := "#/paths/~1crazy~1ass~1references/get/responses/404/content/application~1xml;%20charset=utf-8/schema" 48 | b.ResetTimer() 49 | for i := 0; i < b.N; i++ { 50 | _, _ = ConvertComponentIdIntoFriendlyPathSearch(path) 51 | } 52 | } -------------------------------------------------------------------------------- /test_specs/swagger-circular-tests.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | paths: 3 | /burgers: 4 | post: 5 | responses: 6 | 200: 7 | schema: 8 | $ref: '#/definitions/Nine' 9 | definitions: 10 | One: 11 | description: "test one" 12 | properties: 13 | things: 14 | $ref: "#/definitions/Two" 15 | required: 16 | - things 17 | Two: 18 | description: "test two" 19 | properties: 20 | testThing: 21 | $ref: "#/definitions/One" 22 | required: 23 | - testThing 24 | Three: 25 | description: "test three" 26 | properties: 27 | tester: 28 | $ref: "#/definitions/Four" 29 | bester: 30 | $ref: "#/definitions/Seven" 31 | yester: 32 | $ref: "#/definitions/Seven" 33 | required: 34 | - tester 35 | - bester 36 | - yester 37 | Four: 38 | description: "test four" 39 | properties: 40 | lemons: 41 | $ref: "#/definitions/Nine" 42 | required: 43 | - lemons 44 | Five: 45 | properties: 46 | rice: 47 | $ref: "#/definitions/Six" 48 | required: 49 | - rice 50 | Six: 51 | properties: 52 | mints: 53 | $ref: "#/definitions/Nine" 54 | required: 55 | - mints 56 | Seven: 57 | properties: 58 | wow: 59 | $ref: "#/definitions/Three" 60 | required: 61 | - wow 62 | Nine: 63 | description: done. 64 | Ten: 65 | properties: 66 | yeah: 67 | $ref: "#/definitions/Ten" 68 | required: 69 | - yeah 70 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | buildWindows: 13 | name: Build Windows 14 | runs-on: windows-latest 15 | steps: 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.23 20 | id: go 21 | 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | - name: Test 29 | run: go test ./... 30 | build: 31 | name: Build Linux 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - name: Set up Go 1.x 36 | uses: actions/setup-go@v3 37 | with: 38 | go-version: 1.23 39 | id: go 40 | 41 | - name: Checkout code 42 | uses: actions/checkout@v3 43 | 44 | - name: Get dependencies 45 | run: | 46 | go get -v -t -d ./... 47 | if [ -f Gopkg.toml ]; then 48 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 49 | dep ensure 50 | fi 51 | - name: Test 52 | run: go test ./... 53 | - name: Coverage 54 | run: go test -v -coverprofile=coverage.out ./... 55 | - uses: codecov/codecov-action@v4 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | files: ./coverage.out 59 | flags: unittests 60 | fail_ci_if_error: false 61 | verbose: true 62 | -------------------------------------------------------------------------------- /datamodel/high/v3/oauth_flow_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "github.com/pb33f/libopenapi/utils" 12 | "github.com/stretchr/testify/assert" 13 | "go.yaml.in/yaml/v4" 14 | ) 15 | 16 | func TestOAuthFlow_MarshalYAML(t *testing.T) { 17 | scopes := orderedmap.New[string, string]() 18 | scopes.Set("chicken", "nuggets") 19 | scopes.Set("beefy", "soup") 20 | 21 | oflow := &OAuthFlow{ 22 | AuthorizationUrl: "https://pb33f.io", 23 | TokenUrl: "https://pb33f.io/token", 24 | RefreshUrl: "https://pb33f.io/refresh", 25 | Scopes: scopes, 26 | } 27 | 28 | rend, _ := oflow.Render() 29 | assert.NotNil(t, rend) 30 | 31 | desired := `authorizationUrl: https://pb33f.io 32 | tokenUrl: https://pb33f.io/token 33 | refreshUrl: https://pb33f.io/refresh 34 | scopes: 35 | chicken: nuggets 36 | beefy: soup` 37 | 38 | // we can't check for equality, as the scopes map will be randomly ordered when created from scratch. 39 | assert.Len(t, desired, 149) 40 | 41 | // mutate 42 | oflow.Scopes = nil 43 | ext := orderedmap.New[string, *yaml.Node]() 44 | ext.Set("x-burgers", utils.CreateStringNode("why not?")) 45 | oflow.Extensions = ext 46 | 47 | desired = `authorizationUrl: https://pb33f.io 48 | tokenUrl: https://pb33f.io/token 49 | refreshUrl: https://pb33f.io/refresh 50 | x-burgers: why not?` 51 | 52 | rend, _ = oflow.Render() 53 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 54 | } 55 | -------------------------------------------------------------------------------- /test_specs/nested_files/components/schemas/AccountModel.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | AccountId: 4 | type: string 5 | description: TODO 6 | Name: 7 | type: string 8 | description: TODO 9 | DepartmentId: 10 | type: string 11 | description: TODO 12 | ForCustomerOperators: 13 | type: boolean 14 | description: TODO 15 | ForPartIssues: 16 | type: boolean 17 | description: TODO 18 | ForPurchaseOrders: 19 | type: boolean 20 | description: TODO 21 | IsActive: 22 | type: boolean 23 | description: TODO 24 | ExternalIdentifier: 25 | type: string 26 | description: TODO 27 | Invalidated: 28 | type: boolean 29 | description: TODO 30 | ExternalEntity: 31 | type: string 32 | description: TODO 33 | ExternalFund: 34 | type: string 35 | description: TODO 36 | ExternalOrganization: 37 | type: string 38 | description: TODO 39 | ExternalAccount: 40 | type: string 41 | description: TODO 42 | ExternalProject: 43 | type: string 44 | description: TODO 45 | ExternalProgram: 46 | type: string 47 | description: TODO 48 | ExternalSource: 49 | type: string 50 | description: TODO 51 | ExternalOther1: 52 | type: string 53 | description: TODO 54 | ExternalOther2: 55 | type: string 56 | description: TODO 57 | SupportsAllTasks: 58 | type: boolean 59 | description: TODO 60 | EnforceTasks: 61 | type: boolean 62 | description: TODO 63 | MobileUniqueId: 64 | format: int32 65 | type: integer 66 | description: TODO 67 | TypeName: 68 | type: string 69 | description: TODO 70 | readOnly: true 71 | UID: 72 | type: string 73 | description: TODO 74 | -------------------------------------------------------------------------------- /utils/nodes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | import ( 7 | "go.yaml.in/yaml/v4" 8 | ) 9 | 10 | func CreateRefNode(ref string) *yaml.Node { 11 | m := CreateEmptyMapNode() 12 | nodes := make([]*yaml.Node, 2) 13 | nodes[0] = CreateStringNode("$ref") 14 | nodes[1] = CreateStringNode(ref) 15 | nodes[1].Style = yaml.SingleQuotedStyle 16 | m.Content = nodes 17 | return m 18 | } 19 | 20 | func CreateEmptyMapNode() *yaml.Node { 21 | n := &yaml.Node{ 22 | Kind: yaml.MappingNode, 23 | Tag: "!!map", 24 | } 25 | return n 26 | } 27 | 28 | func CreateYamlNode(a any) *yaml.Node { 29 | var n yaml.Node 30 | _ = n.Encode(a) 31 | 32 | return &n 33 | } 34 | 35 | func CreateEmptySequenceNode() *yaml.Node { 36 | n := &yaml.Node{ 37 | Kind: yaml.SequenceNode, 38 | Tag: "!!seq", 39 | } 40 | return n 41 | } 42 | 43 | func CreateStringNode(str string) *yaml.Node { 44 | n := &yaml.Node{ 45 | Kind: yaml.ScalarNode, 46 | Tag: "!!str", 47 | Value: str, 48 | } 49 | return n 50 | } 51 | 52 | func CreateBoolNode(str string) *yaml.Node { 53 | n := &yaml.Node{ 54 | Kind: yaml.ScalarNode, 55 | Value: str, 56 | } 57 | return n 58 | } 59 | 60 | func CreateIntNode(str string) *yaml.Node { 61 | n := &yaml.Node{ 62 | Kind: yaml.ScalarNode, 63 | Value: str, 64 | } 65 | return n 66 | } 67 | 68 | func CreateEmptyScalarNode() *yaml.Node { 69 | n := &yaml.Node{ 70 | Kind: yaml.ScalarNode, 71 | Tag: "!!null", 72 | Value: "", 73 | } 74 | return n 75 | } 76 | 77 | func CreateFloatNode(str string) *yaml.Node { 78 | n := &yaml.Node{ 79 | Kind: yaml.ScalarNode, 80 | Value: str, 81 | } 82 | return n 83 | } 84 | -------------------------------------------------------------------------------- /datamodel/high/v2/paths.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | "github.com/pb33f/libopenapi/datamodel/high" 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | v2low "github.com/pb33f/libopenapi/datamodel/low/v2" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | // Paths represents a high-level Swagger / OpenAPI Paths object, backed by a low-level one. 16 | type Paths struct { 17 | PathItems *orderedmap.Map[string, *PathItem] 18 | Extensions *orderedmap.Map[string, *yaml.Node] 19 | low *v2low.Paths 20 | } 21 | 22 | // NewPaths creates a new high-level instance of Paths from a low-level one. 23 | func NewPaths(paths *v2low.Paths) *Paths { 24 | p := new(Paths) 25 | p.low = paths 26 | p.Extensions = high.ExtractExtensions(paths.Extensions) 27 | pathItems := orderedmap.New[string, *PathItem]() 28 | 29 | translateFunc := func(pair orderedmap.Pair[low.KeyReference[string], low.ValueReference[*v2low.PathItem]]) (asyncResult[*PathItem], error) { 30 | return asyncResult[*PathItem]{ 31 | key: pair.Key().Value, 32 | result: NewPathItem(pair.Value().Value), 33 | }, nil 34 | } 35 | resultFunc := func(result asyncResult[*PathItem]) error { 36 | pathItems.Set(result.key, result.result) 37 | return nil 38 | } 39 | _ = datamodel.TranslateMapParallel[low.KeyReference[string], low.ValueReference[*v2low.PathItem], asyncResult[*PathItem]]( 40 | paths.PathItems, translateFunc, resultFunc, 41 | ) 42 | p.PathItems = pathItems 43 | return p 44 | } 45 | 46 | // GoLow returns the low-level Paths instance that backs the high level one. 47 | func (p *Paths) GoLow() *v2low.Paths { 48 | return p.low 49 | } 50 | -------------------------------------------------------------------------------- /utils/windows_drive.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | func ReplaceWindowsDriveWithLinuxPath(path string) string { 9 | if len(path) > 1 && path[1] == ':' { 10 | path = strings.ReplaceAll(path, "\\", "/") 11 | return path[2:] 12 | } 13 | return strings.ReplaceAll(path, "\\", "/") 14 | } 15 | 16 | // CheckPathOverlap joins pathA and pathB while avoiding duplicated overlapping segments. 17 | // It tolerates mixed separators in the inputs and uses OS-specific path comparison rules. 18 | func CheckPathOverlap(pathA, pathB, sep string) string { 19 | if sep == "" { 20 | sep = string(filepath.Separator) 21 | } 22 | 23 | // Split on both separators so mixed-path inputs are handled safely. 24 | split := func(p string) []string { 25 | return strings.FieldsFunc(p, func(r rune) bool { 26 | return r == '/' || r == '\\' 27 | }) 28 | } 29 | 30 | aParts := split(pathA) 31 | bParts := split(pathB) 32 | 33 | // Find the longest suffix of aParts that matches the prefix of bParts. 34 | overlap := 0 35 | maxCheck := len(aParts) 36 | if len(bParts) < maxCheck { 37 | maxCheck = len(bParts) 38 | } 39 | for i := maxCheck; i > 0; i-- { 40 | start := len(aParts) - i 41 | match := true 42 | for j := 0; j < i; j++ { 43 | if !pathPartEqual(aParts[start+j], bParts[j]) { 44 | match = false 45 | break 46 | } 47 | } 48 | if match { 49 | overlap = i 50 | break 51 | } 52 | } 53 | 54 | if overlap > 0 { 55 | bParts = bParts[overlap:] 56 | } 57 | 58 | // sep is used to build the tail; filepath.Join normalizes separators for the OS. 59 | tail := strings.Join(bParts, sep) 60 | if tail == "" { 61 | if pathA == "" { 62 | return "" 63 | } 64 | return filepath.Clean(pathA) 65 | } 66 | 67 | return filepath.Join(pathA, tail) 68 | } 69 | -------------------------------------------------------------------------------- /utils/type_check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | import "fmt" 7 | 8 | // AreValuesCorrectlyTyped will look through an array of unknown values and check they match 9 | // against the supplied type as a string. The return value is empty if everything is OK, or it 10 | // contains failures in the form of a value as a key and a message as to why it's not valid 11 | func AreValuesCorrectlyTyped(valType string, values interface{}) map[string]string { 12 | var arr []interface{} 13 | if _, ok := values.([]interface{}); !ok { 14 | return nil 15 | } 16 | arr = values.([]interface{}) 17 | 18 | results := make(map[string]string) 19 | for _, v := range arr { 20 | switch v := v.(type) { 21 | case string: 22 | if valType != "string" { 23 | results[v] = fmt.Sprintf("enum value '%v' is a "+ 24 | "string, but it's defined as a '%v'", v, valType) 25 | } 26 | case int64: 27 | if valType != "integer" && valType != "number" { 28 | results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+ 29 | "integer, but it's defined as a '%v'", v, valType) 30 | } 31 | case int: 32 | if valType != "integer" && valType != "number" { 33 | results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+ 34 | "integer, but it's defined as a '%v'", v, valType) 35 | } 36 | case float64: 37 | if valType != "number" { 38 | results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+ 39 | "number, but it's defined as a '%v'", v, valType) 40 | } 41 | case bool: 42 | if valType != "boolean" { 43 | results[fmt.Sprintf("%v", v)] = fmt.Sprintf("enum value '%v' is a "+ 44 | "boolean, but it's defined as a '%v'", v, valType) 45 | } 46 | } 47 | } 48 | return results 49 | } 50 | -------------------------------------------------------------------------------- /datamodel/high/v2/response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | "github.com/pb33f/libopenapi/datamodel/high/base" 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | lowv2 "github.com/pb33f/libopenapi/datamodel/low/v2" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | // Response is a representation of a high-level Swagger / OpenAPI 2 Response object, backed by a low-level one. 16 | // Response describes a single response from an API Operation 17 | // - https://swagger.io/specification/v2/#responseObject 18 | type Response struct { 19 | Description string 20 | Schema *base.SchemaProxy 21 | Headers *orderedmap.Map[string, *Header] 22 | Examples *Example 23 | Extensions *orderedmap.Map[string, *yaml.Node] 24 | low *lowv2.Response 25 | } 26 | 27 | // NewResponse creates a new high-level instance of Response from a low level one. 28 | func NewResponse(response *lowv2.Response) *Response { 29 | r := new(Response) 30 | r.low = response 31 | r.Extensions = high.ExtractExtensions(response.Extensions) 32 | if !response.Description.IsEmpty() { 33 | r.Description = response.Description.Value 34 | } 35 | if !response.Schema.IsEmpty() { 36 | r.Schema = base.NewSchemaProxy(&response.Schema) 37 | } 38 | if !response.Headers.IsEmpty() { 39 | r.Headers = low.FromReferenceMapWithFunc(response.Headers.Value, NewHeader) 40 | } 41 | if !response.Examples.IsEmpty() { 42 | r.Examples = NewExample(response.Examples.Value) 43 | } 44 | return r 45 | } 46 | 47 | // GoLow will return the low-level Response instance used to create the high level one. 48 | func (r *Response) GoLow() *lowv2.Response { 49 | return r.low 50 | } 51 | -------------------------------------------------------------------------------- /datamodel/low/base/license_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/stretchr/testify/assert" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | func TestLicense_Hash(t *testing.T) { 16 | left := `url: https://pb33f.io 17 | description: the ranch 18 | x-happy: dance` 19 | 20 | right := `url: https://pb33f.io 21 | description: the ranch 22 | x-drink: beer` 23 | 24 | var lNode, rNode yaml.Node 25 | _ = yaml.Unmarshal([]byte(left), &lNode) 26 | _ = yaml.Unmarshal([]byte(right), &rNode) 27 | 28 | // create low level objects 29 | var lDoc License 30 | var rDoc License 31 | _ = low.BuildModel(lNode.Content[0], &lDoc) 32 | _ = low.BuildModel(rNode.Content[0], &rDoc) 33 | 34 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 35 | 36 | l := License{} 37 | l.Build(context.Background(), lNode.Content[0], rNode.Content[0], nil) 38 | assert.NotNil(t, l.GetRootNode()) 39 | assert.NotNil(t, l.GetKeyNode()) 40 | assert.Equal(t, 1, l.GetExtensions().Len()) 41 | assert.Nil(t, l.GetIndex()) 42 | assert.NotNil(t, l.GetContext()) 43 | } 44 | 45 | func TestLicense_WithIdentifier_Hash(t *testing.T) { 46 | left := `identifier: MIT 47 | description: the ranch` 48 | 49 | right := `identifier: MIT 50 | description: the ranch` 51 | 52 | var lNode, rNode yaml.Node 53 | _ = yaml.Unmarshal([]byte(left), &lNode) 54 | _ = yaml.Unmarshal([]byte(right), &rNode) 55 | 56 | // create low level objects 57 | var lDoc License 58 | var rDoc License 59 | err := low.BuildModel(lNode.Content[0], &lDoc) 60 | assert.NoError(t, err) 61 | 62 | err = low.BuildModel(rNode.Content[0], &rDoc) 63 | assert.NoError(t, err) 64 | 65 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 66 | } 67 | -------------------------------------------------------------------------------- /overlay/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | 10 | highoverlay "github.com/pb33f/libopenapi/datamodel/high/overlay" 11 | ) 12 | 13 | // Warning represents a non-fatal issue encountered during overlay application. 14 | type Warning struct { 15 | Action *highoverlay.Action 16 | Target string 17 | Message string 18 | } 19 | 20 | func (w *Warning) String() string { 21 | return fmt.Sprintf("overlay warning: target '%s': %s", w.Target, w.Message) 22 | } 23 | 24 | // OverlayError represents an error that occurred during an overlay application. 25 | type OverlayError struct { 26 | Action *highoverlay.Action 27 | Cause error 28 | } 29 | 30 | func (e *OverlayError) Error() string { 31 | if e.Action != nil { 32 | return fmt.Sprintf("overlay error at target '%s': %v", e.Action.Target, e.Cause) 33 | } 34 | return fmt.Sprintf("overlay error: %v", e.Cause) 35 | } 36 | 37 | func (e *OverlayError) Unwrap() error { 38 | return e.Cause 39 | } 40 | 41 | // Sentinel errors for overlay operations. 42 | var ( 43 | // Parsing errors 44 | ErrInvalidOverlay = errors.New("invalid overlay document") 45 | ErrMissingOverlayField = errors.New("missing required 'overlay' field") 46 | ErrMissingInfo = errors.New("missing required 'info' field") 47 | ErrMissingActions = errors.New("missing required 'actions' field") 48 | ErrEmptyActions = errors.New("actions array must contain at least one action") 49 | 50 | // JSONPath errors 51 | ErrInvalidJSONPath = errors.New("invalid JSONPath expression") 52 | ErrPrimitiveTarget = errors.New("JSONPath target resolved to primitive/null; must be object or array") 53 | 54 | // Application errors 55 | ErrNoTargetDocument = errors.New("no target document provided") 56 | ) 57 | -------------------------------------------------------------------------------- /what-changed/reports/summary_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package reports 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi" 11 | v3 "github.com/pb33f/libopenapi/datamodel/low/v3" 12 | "github.com/pb33f/libopenapi/what-changed/model" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func createDiff() *model.DocumentChanges { 17 | burgerShopOriginal, _ := os.ReadFile("../../test_specs/burgershop.openapi.yaml") 18 | burgerShopUpdated, _ := os.ReadFile("../../test_specs/burgershop.openapi-modified.yaml") 19 | originalDoc, _ := libopenapi.NewDocument(burgerShopOriginal) 20 | updatedDoc, _ := libopenapi.NewDocument(burgerShopUpdated) 21 | documentChanges, _ := libopenapi.CompareDocuments(originalDoc, updatedDoc) 22 | return documentChanges 23 | } 24 | 25 | func TestCreateSummary_OverallReport(t *testing.T) { 26 | changes := createDiff() 27 | report := CreateOverallReport(changes) 28 | assert.Equal(t, 1, report.ChangeReport[v3.InfoLabel].Total) 29 | // Callbacks are now properly counted as individual expression changes 30 | // instead of a single PropertyAdded/PropertyRemoved change 31 | assert.Equal(t, 45, report.ChangeReport[v3.PathsLabel].Total) 32 | assert.Equal(t, 10, report.ChangeReport[v3.PathsLabel].Breaking) 33 | assert.Equal(t, 3, report.ChangeReport[v3.TagsLabel].Total) 34 | assert.Equal(t, 1, report.ChangeReport[v3.ExternalDocsLabel].Total) 35 | assert.Equal(t, 2, report.ChangeReport[v3.WebhooksLabel].Total) 36 | assert.Equal(t, 2, report.ChangeReport[v3.ServersLabel].Total) 37 | assert.Equal(t, 1, report.ChangeReport[v3.ServersLabel].Breaking) 38 | assert.Equal(t, 1, report.ChangeReport[v3.SecurityLabel].Total) 39 | assert.Equal(t, 20, report.ChangeReport[v3.ComponentsLabel].Total) 40 | assert.Equal(t, 7, report.ChangeReport[v3.ComponentsLabel].Breaking) 41 | } 42 | -------------------------------------------------------------------------------- /utils/nodes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCreateBoolNode(t *testing.T) { 13 | b := CreateBoolNode("true") 14 | assert.Equal(t, "", b.Tag) // No explicit tag - let YAML infer type 15 | assert.Equal(t, "true", b.Value) 16 | } 17 | 18 | func TestCreateEmptyMapNode(t *testing.T) { 19 | m := CreateEmptyMapNode() 20 | assert.Equal(t, "!!map", m.Tag) 21 | assert.Len(t, m.Content, 0) 22 | } 23 | 24 | func TestCreateEmptySequenceNode(t *testing.T) { 25 | s := CreateEmptySequenceNode() 26 | assert.Equal(t, "!!seq", s.Tag) 27 | assert.Len(t, s.Content, 0) 28 | } 29 | 30 | func TestCreateEmptyScalarNode(t *testing.T) { 31 | s := CreateEmptyScalarNode() 32 | assert.Equal(t, "!!null", s.Tag) 33 | assert.Equal(t, "", s.Value) 34 | } 35 | 36 | func TestCreateFloatNode(t *testing.T) { 37 | f := CreateFloatNode("3.14") 38 | assert.Equal(t, "", f.Tag) // No explicit tag - let YAML infer type 39 | assert.Equal(t, "3.14", f.Value) 40 | } 41 | 42 | func TestCreateIntNode(t *testing.T) { 43 | i := CreateIntNode("42") 44 | assert.Equal(t, "", i.Tag) // No explicit tag - let YAML infer type 45 | assert.Equal(t, "42", i.Value) 46 | } 47 | 48 | func TestCreateRefNode(t *testing.T) { 49 | r := CreateRefNode("#/components/schemas/MySchema") 50 | assert.Equal(t, "!!map", r.Tag) 51 | assert.Len(t, r.Content, 2) 52 | assert.Equal(t, "!!str", r.Content[0].Tag) 53 | assert.Equal(t, "$ref", r.Content[0].Value) 54 | assert.Equal(t, "!!str", r.Content[1].Tag) 55 | assert.Equal(t, "#/components/schemas/MySchema", r.Content[1].Value) 56 | } 57 | 58 | func TestCreateYamlNode(t *testing.T) { 59 | y := CreateYamlNode("foo") 60 | assert.Equal(t, "!!str", y.Tag) // Encode() sets appropriate tag 61 | assert.Equal(t, "foo", y.Value) 62 | } 63 | -------------------------------------------------------------------------------- /datamodel/high/v2/responses.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | "github.com/pb33f/libopenapi/datamodel/high" 9 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 10 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | // Responses is a high-level representation of a Swagger / OpenAPI 2 Responses object, backed by a low level one. 16 | type Responses struct { 17 | Codes *orderedmap.Map[string, *Response] 18 | Default *Response 19 | Extensions *orderedmap.Map[string, *yaml.Node] 20 | low *low.Responses 21 | } 22 | 23 | // NewResponses will create a new high-level instance of Responses from a low-level one. 24 | func NewResponses(responses *low.Responses) *Responses { 25 | r := new(Responses) 26 | r.low = responses 27 | r.Extensions = high.ExtractExtensions(responses.Extensions) 28 | 29 | if !responses.Default.IsEmpty() { 30 | r.Default = NewResponse(responses.Default.Value) 31 | } 32 | 33 | if orderedmap.Len(responses.Codes) > 0 { 34 | resp := orderedmap.New[string, *Response]() 35 | translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Response]]) (asyncResult[*Response], error) { 36 | return asyncResult[*Response]{ 37 | key: pair.Key().Value, 38 | result: NewResponse(pair.Value().Value), 39 | }, nil 40 | } 41 | resultFunc := func(value asyncResult[*Response]) error { 42 | resp.Set(value.key, value.result) 43 | return nil 44 | } 45 | _ = datamodel.TranslateMapParallel(responses.Codes, translateFunc, resultFunc) 46 | r.Codes = resp 47 | } 48 | 49 | return r 50 | } 51 | 52 | // GoLow will return the low-level object used to create the high-level one. 53 | func (r *Responses) GoLow() *low.Responses { 54 | return r.low 55 | } 56 | -------------------------------------------------------------------------------- /bundler/test/specs/common.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: Common schemas 4 | version: "1.0.0" 5 | servers: [] 6 | paths: [] 7 | 8 | components: 9 | pathItems: 10 | bing: 11 | get: 12 | description: Bing path item 13 | callbacks: 14 | testCallback: 15 | get: 16 | description: Test callback 17 | operationId: testCallback 18 | 19 | links: 20 | testLink: 21 | description: Test link 22 | operationId: testLink 23 | parameters: 24 | request-id: "x837ant-000007" 25 | query: "test" 26 | requestBodies: 27 | testBody: 28 | description: Test request body 29 | headers: 30 | request-id: 31 | description: Request ID 32 | type: string 33 | required: true 34 | example: "x837ant-000007" 35 | schemas: 36 | lemons: 37 | description: fresh 38 | type: array 39 | items: 40 | type: object 41 | 42 | dtoTest: 43 | description: Test schema (original - common.yaml) 44 | type: object 45 | required: 46 | - id 47 | properties: 48 | id: 49 | type: string 50 | spacing: 51 | $ref: "smash/paging.yaml#/components/schemas/dtoTest" 52 | paging: 53 | $ref: "paging.yaml#/components/schemas/paging" 54 | 55 | responses: 56 | 404: 57 | description: Not found response 58 | 59 | 403: 60 | description: Forbidden response 61 | content: 62 | application/json: 63 | schema: 64 | $ref: "error.yaml#/components/schemas/dtoError" 65 | examples: 66 | "example1": 67 | value: 68 | errorCode: ErrOperationForbidden 69 | requestId: "x837ant-000007" 70 | message: Forbidden 71 | 72 | parameters: 73 | query: 74 | description: Query param 75 | name: query 76 | in: query 77 | required: false 78 | schema: 79 | type: string 80 | 81 | -------------------------------------------------------------------------------- /datamodel/high/base/contact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/base" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // Contact represents a high-level representation of the Contact definitions found at 14 | // 15 | // v2 - https://swagger.io/specification/v2/#contactObject 16 | // v3 - https://spec.openapis.org/oas/v3.1.0#contact-object 17 | type Contact struct { 18 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 19 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 20 | Email string `json:"email,omitempty" yaml:"email,omitempty"` 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *low.Contact `json:"-" yaml:"-"` // low-level representation 23 | } 24 | 25 | // NewContact will create a new Contact instance using a low-level Contact 26 | func NewContact(contact *low.Contact) *Contact { 27 | c := new(Contact) 28 | c.low = contact 29 | c.URL = contact.URL.Value 30 | c.Name = contact.Name.Value 31 | c.Email = contact.Email.Value 32 | c.Extensions = high.ExtractExtensions(contact.Extensions) 33 | return c 34 | } 35 | 36 | // GoLow returns the low level Contact object used to create the high-level one. 37 | func (c *Contact) GoLow() *low.Contact { 38 | return c.low 39 | } 40 | 41 | // GoLowUntyped will return the low-level Contact instance that was used to create the high-level one, with no type 42 | func (c *Contact) GoLowUntyped() any { 43 | return c.low 44 | } 45 | 46 | func (c *Contact) Render() ([]byte, error) { 47 | return yaml.Marshal(c) 48 | } 49 | 50 | func (c *Contact) MarshalYAML() (interface{}, error) { 51 | nb := high.NewNodeBuilder(c, c.low) 52 | return nb.Render(), nil 53 | } 54 | -------------------------------------------------------------------------------- /test_specs/circular-tests.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0 2 | paths: 3 | /burgers: 4 | post: 5 | requestBody: 6 | content: 7 | application/json: 8 | schema: 9 | $ref: '#/components/schemas/Nine' 10 | components: 11 | schemas: 12 | One: 13 | description: "test one" 14 | properties: 15 | things: 16 | "$ref": "#/components/schemas/Two" 17 | required: 18 | - things 19 | Two: 20 | description: "test two" 21 | properties: 22 | testThing: 23 | "$ref": "#/components/schemas/One" 24 | oneOf: 25 | - "$ref": "#/components/schemas/Three" 26 | allOf: 27 | - "$ref": "#/components/schemas/Three" 28 | anyOf: 29 | - "$ref": "#/components/schemas/Three" 30 | required: 31 | - testThing 32 | - anyOf 33 | Three: 34 | description: "test three" 35 | properties: 36 | tester: 37 | "$ref": "#/components/schemas/Four" 38 | bester: 39 | "$ref": "#/components/schemas/Seven" 40 | yester: 41 | "$ref": "#/components/schemas/Seven" 42 | required: 43 | - tester 44 | - bester 45 | - yester 46 | Four: 47 | description: "test four" 48 | properties: 49 | lemons: 50 | "$ref": "#/components/schemas/Nine" 51 | required: 52 | - lemons 53 | Five: 54 | properties: 55 | rice: 56 | "$ref": "#/components/schemas/Six" 57 | required: 58 | - rice 59 | Six: 60 | properties: 61 | mints: 62 | "$ref": "#/components/schemas/Nine" 63 | required: 64 | - mints 65 | Seven: 66 | properties: 67 | wow: 68 | "$ref": "#/components/schemas/Three" 69 | required: 70 | - wow 71 | Nine: 72 | description: done. 73 | Ten: 74 | properties: 75 | yeah: 76 | "$ref": "#/components/schemas/Ten" 77 | required: 78 | - yeah 79 | -------------------------------------------------------------------------------- /what-changed/what_changed.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package what_changed 5 | // 6 | // what changed is a feature that performs an accurate and deep analysis of what has changed between two OpenAPI 7 | // documents. The report generated outlines every single change made between two specifications (left and right) 8 | // rendered in the document hierarchy, so exploring it is the same as exploring the document model. 9 | // 10 | // There are two main functions, one of generating a report for Swagger documents (OpenAPI 2) 11 | // And OpenAPI 3+ documents. 12 | // 13 | // This package uses a combined model for OpenAPI and Swagger changes, it does not break them out into separate 14 | // versions like the datamodel package. The reason for this is to prevent sprawl across versions and to provide 15 | // a single API and model for any application that wants to use this feature. 16 | package what_changed 17 | 18 | import ( 19 | "github.com/pb33f/libopenapi/datamodel/low/v2" 20 | "github.com/pb33f/libopenapi/datamodel/low/v3" 21 | "github.com/pb33f/libopenapi/what-changed/model" 22 | ) 23 | 24 | // CompareOpenAPIDocuments will compare left (original) and right (updated) OpenAPI 3+ documents and extract every change 25 | // made across the entire specification. The report outlines every property changed, everything that was added, 26 | // or removed and which of those changes were breaking. 27 | func CompareOpenAPIDocuments(original, updated *v3.Document) *model.DocumentChanges { 28 | return model.CompareDocuments(original, updated) 29 | } 30 | 31 | // CompareSwaggerDocuments will compare left (original) and a right (updated) Swagger documents and extract every change 32 | // made across the entire specification. The report outlines every property changes, everything that was added, 33 | // or removed and which of those changes were breaking. 34 | func CompareSwaggerDocuments(original, updated *v2.Swagger) *model.DocumentChanges { 35 | return model.CompareDocuments(original, updated) 36 | } 37 | -------------------------------------------------------------------------------- /index/schema_id_context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess Beef Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "context" 8 | ) 9 | 10 | // ResolvingIdsKey is the context key for tracking $id values currently being resolved. 11 | const ResolvingIdsKey ContextKey = "resolvingIds" 12 | 13 | // SchemaIdScopeKey is the context key for tracking the current $id scope during extraction. 14 | const SchemaIdScopeKey ContextKey = "schemaIdScope" 15 | 16 | // GetSchemaIdScope returns the current $id scope from the context. 17 | func GetSchemaIdScope(ctx context.Context) *SchemaIdScope { 18 | if v := ctx.Value(SchemaIdScopeKey); v != nil { 19 | return v.(*SchemaIdScope) 20 | } 21 | return nil 22 | } 23 | 24 | // WithSchemaIdScope returns a new context with the given $id scope. 25 | func WithSchemaIdScope(ctx context.Context, scope *SchemaIdScope) context.Context { 26 | return context.WithValue(ctx, SchemaIdScopeKey, scope) 27 | } 28 | 29 | // GetResolvingIds returns the set of $id values currently being resolved in the call chain. 30 | func GetResolvingIds(ctx context.Context) map[string]bool { 31 | if v := ctx.Value(ResolvingIdsKey); v != nil { 32 | return v.(map[string]bool) 33 | } 34 | return nil 35 | } 36 | 37 | // AddResolvingId adds a $id to the resolving set in the context. 38 | // Returns a new context with the updated set (copy-on-write for thread safety). 39 | func AddResolvingId(ctx context.Context, id string) context.Context { 40 | existing := GetResolvingIds(ctx) 41 | newSet := make(map[string]bool, len(existing)+1) 42 | for k, v := range existing { 43 | newSet[k] = v 44 | } 45 | newSet[id] = true 46 | return context.WithValue(ctx, ResolvingIdsKey, newSet) 47 | } 48 | 49 | // IsIdBeingResolved checks if a $id is currently being resolved in the call chain. 50 | // Used to detect and prevent circular $id resolution. 51 | func IsIdBeingResolved(ctx context.Context, id string) bool { 52 | ids := GetResolvingIds(ctx) 53 | if ids == nil { 54 | return false 55 | } 56 | return ids[id] 57 | } 58 | -------------------------------------------------------------------------------- /datamodel/low/base/security_requirement_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "github.com/stretchr/testify/assert" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | func TestSecurityRequirement_Build(t *testing.T) { 16 | yml := `one: 17 | - two 18 | - three 19 | four: 20 | - five 21 | - six` 22 | 23 | var sr SecurityRequirement 24 | var idxNode yaml.Node 25 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 26 | 27 | yml2 := `four: 28 | - six 29 | - five 30 | one: 31 | - three 32 | - two` 33 | 34 | var sr2 SecurityRequirement 35 | var idxNode2 yaml.Node 36 | _ = yaml.Unmarshal([]byte(yml2), &idxNode2) 37 | 38 | _ = sr.Build(context.Background(), nil, idxNode.Content[0], nil) 39 | _ = sr2.Build(context.Background(), nil, idxNode2.Content[0], nil) 40 | 41 | assert.Equal(t, 2, orderedmap.Len(sr.Requirements.Value)) 42 | assert.Equal(t, []string{"one", "four"}, sr.GetKeys()) 43 | assert.Len(t, sr.FindRequirement("one"), 2) 44 | assert.Equal(t, sr.Hash(), sr2.Hash()) 45 | assert.Nil(t, sr.FindRequirement("i-do-not-exist")) 46 | assert.NotNil(t, sr.GetRootNode()) 47 | assert.Nil(t, sr.GetKeyNode()) 48 | assert.NotNil(t, sr.GetContext()) 49 | assert.Nil(t, sr.GetIndex()) 50 | } 51 | 52 | func TestSecurityRequirement_TestEmptyReq(t *testing.T) { 53 | yml := `one: 54 | - two 55 | - {}` 56 | 57 | var sr SecurityRequirement 58 | var idxNode yaml.Node 59 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 60 | 61 | _ = sr.Build(context.Background(), nil, idxNode.Content[0], nil) 62 | 63 | assert.Equal(t, 1, orderedmap.Len(sr.Requirements.Value)) 64 | assert.Equal(t, []string{"one"}, sr.GetKeys()) 65 | assert.True(t, sr.ContainsEmptyRequirement) 66 | } 67 | 68 | func TestSecurityRequirement_TestEmptyContent(t *testing.T) { 69 | var sr SecurityRequirement 70 | _ = sr.Build(context.Background(), nil, &yaml.Node{}, nil) 71 | assert.True(t, sr.ContainsEmptyRequirement) 72 | } 73 | -------------------------------------------------------------------------------- /datamodel/high/v2/responses_definitions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 9 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | ) 12 | 13 | // ResponsesDefinitions is a high-level representation of a Swagger / OpenAPI 2 Responses Definitions object. 14 | // that is backed by a low-level one. 15 | // 16 | // ResponsesDefinitions is an object to hold responses to be reused across operations. Response definitions can be 17 | // referenced to the ones defined here. It does not define global operation responses 18 | // - https://swagger.io/specification/v2/#responsesDefinitionsObject 19 | type ResponsesDefinitions struct { 20 | Definitions *orderedmap.Map[string, *Response] 21 | low *low.ResponsesDefinitions 22 | } 23 | 24 | // NewResponsesDefinitions will create a new high-level instance of ResponsesDefinitions from a low-level one. 25 | func NewResponsesDefinitions(responsesDefinitions *low.ResponsesDefinitions) *ResponsesDefinitions { 26 | rd := new(ResponsesDefinitions) 27 | rd.low = responsesDefinitions 28 | responses := orderedmap.New[string, *Response]() 29 | translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Response]]) (asyncResult[*Response], error) { 30 | return asyncResult[*Response]{ 31 | key: pair.Key().Value, 32 | result: NewResponse(pair.Value().Value), 33 | }, nil 34 | } 35 | resultFunc := func(value asyncResult[*Response]) error { 36 | responses.Set(value.key, value.result) 37 | return nil 38 | } 39 | 40 | _ = datamodel.TranslateMapParallel(responsesDefinitions.Definitions, translateFunc, resultFunc) 41 | rd.Definitions = responses 42 | return rd 43 | } 44 | 45 | // GoLow returns the low-level ResponsesDefinitions used to create the high-level one. 46 | func (r *ResponsesDefinitions) GoLow() *low.ResponsesDefinitions { 47 | return r.low 48 | } 49 | -------------------------------------------------------------------------------- /datamodel/high/v2/definitions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | highbase "github.com/pb33f/libopenapi/datamodel/high/base" 9 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 10 | lowbase "github.com/pb33f/libopenapi/datamodel/low/base" 11 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | ) 14 | 15 | // Definitions is a high-level represents of a Swagger / OpenAPI 2 Definitions object, backed by a low-level one. 16 | // 17 | // An object to hold data types that can be consumed and produced by operations. These data types can be primitives, 18 | // arrays or models. 19 | // - https://swagger.io/specification/v2/#definitionsObject 20 | type Definitions struct { 21 | Definitions *orderedmap.Map[string, *highbase.SchemaProxy] 22 | low *low.Definitions 23 | } 24 | 25 | // NewDefinitions will create a new high-level instance of a Definition from a low-level one. 26 | func NewDefinitions(definitions *low.Definitions) *Definitions { 27 | rd := new(Definitions) 28 | rd.low = definitions 29 | defs := orderedmap.New[string, *highbase.SchemaProxy]() 30 | translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*lowbase.SchemaProxy]]) (asyncResult[*highbase.SchemaProxy], error) { 31 | return asyncResult[*highbase.SchemaProxy]{ 32 | key: pair.Key().Value, 33 | result: highbase.NewSchemaProxy(&lowmodel.NodeReference[*lowbase.SchemaProxy]{ 34 | Value: pair.Value().Value, 35 | }), 36 | }, nil 37 | } 38 | resultFunc := func(value asyncResult[*highbase.SchemaProxy]) error { 39 | defs.Set(value.key, value.result) 40 | return nil 41 | } 42 | _ = datamodel.TranslateMapParallel(definitions.Schemas, translateFunc, resultFunc) 43 | rd.Definitions = defs 44 | return rd 45 | } 46 | 47 | // GoLow returns the low-level Definitions object used to create the high-level one. 48 | func (d *Definitions) GoLow() *low.Definitions { 49 | return d.low 50 | } 51 | -------------------------------------------------------------------------------- /datamodel/high/v2/security_definitions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 9 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | ) 12 | 13 | // SecurityDefinitions is a high-level representation of a Swagger / OpenAPI 2 Security Definitions object, that 14 | // is backed by a low-level one. 15 | // 16 | // A declaration of the security schemes available to be used in the specification. This does not enforce the security 17 | // schemes on the operations and only serves to provide the relevant details for each scheme 18 | // - https://swagger.io/specification/v2/#securityDefinitionsObject 19 | type SecurityDefinitions struct { 20 | Definitions *orderedmap.Map[string, *SecurityScheme] 21 | low *low.SecurityDefinitions 22 | } 23 | 24 | // NewSecurityDefinitions creates a new high-level instance of a SecurityDefinitions from a low-level one. 25 | func NewSecurityDefinitions(definitions *low.SecurityDefinitions) *SecurityDefinitions { 26 | sd := new(SecurityDefinitions) 27 | sd.low = definitions 28 | schemes := orderedmap.New[string, *SecurityScheme]() 29 | translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.SecurityScheme]]) (asyncResult[*SecurityScheme], error) { 30 | return asyncResult[*SecurityScheme]{ 31 | key: pair.Key().Value, 32 | result: NewSecurityScheme(pair.Value().Value), 33 | }, nil 34 | } 35 | resultFunc := func(value asyncResult[*SecurityScheme]) error { 36 | schemes.Set(value.key, value.result) 37 | return nil 38 | } 39 | _ = datamodel.TranslateMapParallel(definitions.Definitions, translateFunc, resultFunc) 40 | 41 | sd.Definitions = schemes 42 | return sd 43 | } 44 | 45 | // GoLow returns the low-level SecurityDefinitions instance used to create the high-level one. 46 | func (sd *SecurityDefinitions) GoLow() *low.SecurityDefinitions { 47 | return sd.low 48 | } 49 | -------------------------------------------------------------------------------- /datamodel/high/overlay/info.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/overlay" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // Info represents a high-level Overlay Info Object. 14 | // https://spec.openapis.org/overlay/v1.0.0#info-object 15 | type Info struct { 16 | Title string `json:"title,omitempty" yaml:"title,omitempty"` 17 | Version string `json:"version,omitempty" yaml:"version,omitempty"` 18 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 19 | low *low.Info 20 | } 21 | 22 | // NewInfo creates a new high-level Info instance from a low-level one. 23 | func NewInfo(info *low.Info) *Info { 24 | i := new(Info) 25 | i.low = info 26 | if !info.Title.IsEmpty() { 27 | i.Title = info.Title.Value 28 | } 29 | if !info.Version.IsEmpty() { 30 | i.Version = info.Version.Value 31 | } 32 | i.Extensions = high.ExtractExtensions(info.Extensions) 33 | return i 34 | } 35 | 36 | // GoLow returns the low-level Info instance used to create the high-level one. 37 | func (i *Info) GoLow() *low.Info { 38 | return i.low 39 | } 40 | 41 | // GoLowUntyped returns the low-level Info instance with no type. 42 | func (i *Info) GoLowUntyped() any { 43 | return i.low 44 | } 45 | 46 | // Render returns a YAML representation of the Info object as a byte slice. 47 | func (i *Info) Render() ([]byte, error) { 48 | return yaml.Marshal(i) 49 | } 50 | 51 | // MarshalYAML creates a ready to render YAML representation of the Info object. 52 | func (i *Info) MarshalYAML() (interface{}, error) { 53 | m := orderedmap.New[string, any]() 54 | if i.Title != "" { 55 | m.Set("title", i.Title) 56 | } 57 | if i.Version != "" { 58 | m.Set("version", i.Version) 59 | } 60 | for pair := i.Extensions.First(); pair != nil; pair = pair.Next() { 61 | m.Set(pair.Key(), pair.Value()) 62 | } 63 | return m, nil 64 | } 65 | -------------------------------------------------------------------------------- /datamodel/low/v2/examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "crypto/sha256" 9 | "strings" 10 | 11 | "github.com/pb33f/libopenapi/datamodel/low" 12 | "github.com/pb33f/libopenapi/index" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | "github.com/pb33f/libopenapi/utils" 15 | "go.yaml.in/yaml/v4" 16 | ) 17 | 18 | // Examples represents a low-level Swagger / OpenAPI 2 Example object. 19 | // Allows sharing examples for operation responses 20 | // - https://swagger.io/specification/v2/#exampleObject 21 | type Examples struct { 22 | Values *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 23 | } 24 | 25 | // FindExample attempts to locate an example value, using a key label. 26 | func (e *Examples) FindExample(name string) *low.ValueReference[*yaml.Node] { 27 | return low.FindItemInOrderedMap(name, e.Values) 28 | } 29 | 30 | // Build will extract all examples and will attempt to unmarshal content into a map or slice based on type. 31 | func (e *Examples) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex) error { 32 | root = utils.NodeAlias(root) 33 | utils.CheckForMergeNodes(root) 34 | var keyNode, currNode *yaml.Node 35 | e.Values = orderedmap.New[low.KeyReference[string], low.ValueReference[*yaml.Node]]() 36 | for i := range root.Content { 37 | if i%2 == 0 { 38 | keyNode = root.Content[i] 39 | continue 40 | } 41 | currNode = root.Content[i] 42 | 43 | e.Values.Set( 44 | low.KeyReference[string]{ 45 | Value: keyNode.Value, 46 | KeyNode: keyNode, 47 | }, 48 | low.ValueReference[*yaml.Node]{ 49 | Value: currNode, 50 | ValueNode: currNode, 51 | }, 52 | ) 53 | } 54 | return nil 55 | } 56 | 57 | // Hash will return a consistent SHA256 Hash of the Examples object 58 | func (e *Examples) Hash() [32]byte { 59 | var f []string 60 | for v := range orderedmap.SortAlpha(e.Values).ValuesFromOldest() { 61 | f = append(f, low.GenerateHashString(v.Value)) 62 | } 63 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 64 | } 65 | -------------------------------------------------------------------------------- /datamodel/high/v2/parameter_definitions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel" 8 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 9 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | ) 12 | 13 | // ParameterDefinitions is a high-level representation of a Swagger / OpenAPI 2 Parameters Definitions object 14 | // that is backed by a low-level one. 15 | // 16 | // ParameterDefinitions holds parameters to be reused across operations. Parameter definitions can be 17 | // referenced to the ones defined here. It does not define global operation parameters 18 | // - https://swagger.io/specification/v2/#parametersDefinitionsObject 19 | type ParameterDefinitions struct { 20 | Definitions *orderedmap.Map[string, *Parameter] 21 | low *low.ParameterDefinitions 22 | } 23 | 24 | // NewParametersDefinitions creates a new instance of a high-level ParameterDefinitions, from a low-level one. 25 | // Every parameter is extracted asynchronously due to the potential depth 26 | func NewParametersDefinitions(parametersDefinitions *low.ParameterDefinitions) *ParameterDefinitions { 27 | pd := new(ParameterDefinitions) 28 | pd.low = parametersDefinitions 29 | params := orderedmap.New[string, *Parameter]() 30 | translateFunc := func(pair orderedmap.Pair[lowmodel.KeyReference[string], lowmodel.ValueReference[*low.Parameter]]) (asyncResult[*Parameter], error) { 31 | return asyncResult[*Parameter]{ 32 | key: pair.Key().Value, 33 | result: NewParameter(pair.Value().Value), 34 | }, nil 35 | } 36 | resultFunc := func(value asyncResult[*Parameter]) error { 37 | params.Set(value.key, value.result) 38 | return nil 39 | } 40 | _ = datamodel.TranslateMapParallel(parametersDefinitions.Definitions, translateFunc, resultFunc) 41 | pd.Definitions = params 42 | return pd 43 | } 44 | 45 | // GoLow returns the low-level ParameterDefinitions instance that backs the low-level one. 46 | func (p *ParameterDefinitions) GoLow() *low.ParameterDefinitions { 47 | return p.low 48 | } 49 | -------------------------------------------------------------------------------- /utils/type_check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package utils 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAreValuesCorrectlyTyped(t *testing.T) { 13 | assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"hi"}), 0) 14 | assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1}), 1) 15 | assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{"nice", 123, int64(12345)}), 2) 16 | assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{1.2, "burgers"}), 1) 17 | assert.Len(t, AreValuesCorrectlyTyped("string", []interface{}{true, false, "what"}), 2) 18 | 19 | assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{1, 2, 3, 4}), 0) 20 | assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"no way!"}), 1) 21 | assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{"nice", 123, int64(12345)}), 1) 22 | assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{999, 1.2, "burgers"}), 2) 23 | assert.Len(t, AreValuesCorrectlyTyped("integer", []interface{}{true, false, "what"}), 3) 24 | 25 | assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{1.2345}), 0) 26 | assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"no way!"}), 1) 27 | assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{"nice", 123, 2.353}), 1) 28 | assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{999, 1.2, "burgers"}), 1) 29 | assert.Len(t, AreValuesCorrectlyTyped("number", []interface{}{true, false, "what"}), 3) 30 | 31 | assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, true}), 0) 32 | assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"no way!"}), 1) 33 | assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{"nice", 123, 2.353, true}), 3) 34 | assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, true, "burgers"}), 1) 35 | assert.Len(t, AreValuesCorrectlyTyped("boolean", []interface{}{true, false, "what", 1.2, 4}), 3) 36 | assert.Nil(t, AreValuesCorrectlyTyped("boolean", []string{"hi"})) 37 | } 38 | -------------------------------------------------------------------------------- /what-changed/model/contact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low/base" 8 | "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | ) 10 | 11 | // ContactChanges Represent changes to a Contact object that is a child of Info, part of an OpenAPI document. 12 | type ContactChanges struct { 13 | *PropertyChanges 14 | } 15 | 16 | // GetAllChanges returns a slice of all changes made between Callback objects 17 | func (c *ContactChanges) GetAllChanges() []*Change { 18 | if c == nil { 19 | return nil 20 | } 21 | return c.Changes 22 | } 23 | 24 | // TotalChanges represents the total number of changes that have occurred to a Contact object 25 | func (c *ContactChanges) TotalChanges() int { 26 | if c == nil { 27 | return 0 28 | } 29 | return c.PropertyChanges.TotalChanges() 30 | } 31 | 32 | // TotalBreakingChanges returns the total number of breaking changes in Contact objects. 33 | func (c *ContactChanges) TotalBreakingChanges() int { 34 | if c == nil { 35 | return 0 36 | } 37 | return c.PropertyChanges.TotalBreakingChanges() 38 | } 39 | 40 | // CompareContact will check a left (original) and right (new) Contact object for any changes. If there 41 | // were any, a pointer to a ContactChanges object is returned, otherwise if nothing changed - the function 42 | // returns nil. 43 | func CompareContact(l, r *base.Contact) *ContactChanges { 44 | var changes []*Change 45 | props := make([]*PropertyCheck, 0, 3) 46 | 47 | props = append(props, 48 | NewPropertyCheck(CompContact, PropURL, 49 | l.URL.ValueNode, r.URL.ValueNode, 50 | v3.URLLabel, &changes, l, r), 51 | NewPropertyCheck(CompContact, PropName, 52 | l.Name.ValueNode, r.Name.ValueNode, 53 | v3.NameLabel, &changes, l, r), 54 | NewPropertyCheck(CompContact, PropEmail, 55 | l.Email.ValueNode, r.Email.ValueNode, 56 | v3.EmailLabel, &changes, l, r), 57 | ) 58 | 59 | CheckProperties(props) 60 | 61 | dc := new(ContactChanges) 62 | dc.PropertyChanges = NewPropertyChanges(changes) 63 | if dc.TotalChanges() <= 0 { 64 | return nil 65 | } 66 | return dc 67 | } 68 | -------------------------------------------------------------------------------- /datamodel/low/v2/package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/pb33f/libopenapi/utils" 11 | 12 | "github.com/pb33f/libopenapi/datamodel" 13 | ) 14 | 15 | // How to create a low-level Swagger / OpenAPI 2 Document from a specification 16 | func Example_createLowLevelSwaggerDocument() { 17 | // How to create a low-level OpenAPI 2 Document 18 | 19 | // load petstore into bytes 20 | petstoreBytes, _ := os.ReadFile("../../../test_specs/petstorev2.json") 21 | 22 | // read in specification 23 | info, _ := datamodel.ExtractSpecInfo(petstoreBytes) 24 | 25 | // build low-level document model 26 | document, err := CreateDocumentFromConfig(info, datamodel.NewDocumentConfiguration()) 27 | 28 | // if something went wrong, a slice of errors is returned 29 | errs := utils.UnwrapErrors(err) 30 | if len(errs) > 0 { 31 | for i := range errs { 32 | fmt.Printf("error: %s\n", errs[i].Error()) 33 | } 34 | panic("cannot build document") 35 | } 36 | 37 | // print out email address from the info > contact object. 38 | fmt.Print(document.Info.Value.Contact.Value.Email.Value) 39 | // Output: apiteam@swagger.io 40 | } 41 | 42 | // How to create a low-level Swagger / OpenAPI 2 Document from a specification 43 | func Example_createDocument() { 44 | // How to create a low-level OpenAPI 2 Document 45 | 46 | // load petstore into bytes 47 | petstoreBytes, _ := os.ReadFile("../../../test_specs/petstorev2.json") 48 | 49 | // read in specification 50 | info, _ := datamodel.ExtractSpecInfo(petstoreBytes) 51 | 52 | // build low-level document model 53 | document, err := CreateDocumentFromConfig(info, datamodel.NewDocumentConfiguration()) 54 | 55 | // if something went wrong, a slice of errors is returned 56 | errs := utils.UnwrapErrors(err) 57 | if len(errs) > 0 { 58 | for i := range errs { 59 | fmt.Printf("error: %s\n", errs[i].Error()) 60 | } 61 | panic("cannot build document") 62 | } 63 | 64 | // print out email address from the info > contact object. 65 | fmt.Print(document.Info.Value.Contact.Value.Email.Value) 66 | // Output: apiteam@swagger.io 67 | } 68 | -------------------------------------------------------------------------------- /datamodel/high/base/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/base" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // License is a high-level representation of a License object as defined by OpenAPI 2 and OpenAPI 3 14 | // 15 | // v2 - https://swagger.io/specification/v2/#licenseObject 16 | // v3 - https://spec.openapis.org/oas/v3.1.0#license-object 17 | type License struct { 18 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 19 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 20 | Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *low.License 23 | } 24 | 25 | // NewLicense will create a new high-level License instance from a low-level one. 26 | func NewLicense(license *low.License) *License { 27 | l := new(License) 28 | l.low = license 29 | l.Extensions = high.ExtractExtensions(license.Extensions) 30 | if !license.URL.IsEmpty() { 31 | l.URL = license.URL.Value 32 | } 33 | if !license.Name.IsEmpty() { 34 | l.Name = license.Name.Value 35 | } 36 | if !license.Identifier.IsEmpty() { 37 | l.Identifier = license.Identifier.Value 38 | } 39 | return l 40 | } 41 | 42 | // GoLow will return the low-level License used to create the high-level one. 43 | func (l *License) GoLow() *low.License { 44 | return l.low 45 | } 46 | 47 | // GoLowUntyped will return the low-level License instance that was used to create the high-level one, with no type 48 | func (l *License) GoLowUntyped() any { 49 | return l.low 50 | } 51 | 52 | // Render will return a YAML representation of the License object as a byte slice. 53 | func (l *License) Render() ([]byte, error) { 54 | return yaml.Marshal(l) 55 | } 56 | 57 | // MarshalYAML will create a ready to render YAML representation of the License object. 58 | func (l *License) MarshalYAML() (interface{}, error) { 59 | nb := high.NewNodeBuilder(l, l.low) 60 | return nb.Render(), nil 61 | } 62 | -------------------------------------------------------------------------------- /datamodel/high/base/external_doc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | 11 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 12 | lowbase "github.com/pb33f/libopenapi/datamodel/low/base" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | "github.com/stretchr/testify/assert" 15 | "go.yaml.in/yaml/v4" 16 | ) 17 | 18 | func TestNewExternalDoc(t *testing.T) { 19 | var cNode yaml.Node 20 | 21 | yml := `description: hack code 22 | url: https://pb33f.io 23 | x-hack: code` 24 | 25 | _ = yaml.Unmarshal([]byte(yml), &cNode) 26 | 27 | var lowExt lowbase.ExternalDoc 28 | _ = lowmodel.BuildModel(cNode.Content[0], &lowExt) 29 | 30 | _ = lowExt.Build(context.Background(), nil, cNode.Content[0], nil) 31 | 32 | highExt := NewExternalDoc(&lowExt) 33 | 34 | var xHack string 35 | _ = highExt.Extensions.GetOrZero("x-hack").Decode(&xHack) 36 | 37 | assert.Equal(t, "hack code", highExt.Description) 38 | assert.Equal(t, "https://pb33f.io", highExt.URL) 39 | assert.Equal(t, "code", xHack) 40 | 41 | wentLow := highExt.GoLow() 42 | assert.Equal(t, 2, wentLow.URL.ValueNode.Line) 43 | assert.Equal(t, 1, orderedmap.Len(highExt.GetExtensions())) 44 | 45 | // render the high-level object as YAML 46 | rendered, _ := highExt.Render() 47 | assert.Equal(t, strings.TrimSpace(string(rendered)), yml) 48 | } 49 | 50 | func TestExampleNewExternalDoc(t *testing.T) { 51 | // create a new external documentation spec reference 52 | // this can be YAML or JSON. 53 | yml := `description: hack code docs 54 | url: https://pb33f.io/docs 55 | x-hack: code` 56 | 57 | // unmarshal the raw bytes into a *yaml.Node 58 | var node yaml.Node 59 | _ = yaml.Unmarshal([]byte(yml), &node) 60 | 61 | // build low-level ExternalDoc 62 | var lowExt lowbase.ExternalDoc 63 | _ = lowmodel.BuildModel(node.Content[0], &lowExt) 64 | 65 | // build out low-level properties (like extensions) 66 | _ = lowExt.Build(context.Background(), nil, node.Content[0], nil) 67 | 68 | // create new high-level ExternalDoc 69 | highExt := NewExternalDoc(&lowExt) 70 | 71 | var xHack string 72 | _ = highExt.Extensions.GetOrZero("x-hack").Decode(&xHack) 73 | 74 | assert.Equal(t, "code", xHack) 75 | } 76 | -------------------------------------------------------------------------------- /datamodel/high/base/contact_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "testing" 10 | 11 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 12 | lowbase "github.com/pb33f/libopenapi/datamodel/low/base" 13 | "github.com/stretchr/testify/assert" 14 | "go.yaml.in/yaml/v4" 15 | ) 16 | 17 | func TestNewContact(t *testing.T) { 18 | var cNode yaml.Node 19 | 20 | yml := `name: pizza 21 | url: https://pb33f.io 22 | email: buckaroo@pb33f.io` 23 | 24 | _ = yaml.Unmarshal([]byte(yml), &cNode) 25 | 26 | // build low 27 | var lowContact lowbase.Contact 28 | _ = lowmodel.BuildModel(cNode.Content[0], &lowContact) 29 | 30 | // build high 31 | highContact := NewContact(&lowContact) 32 | 33 | assert.Equal(t, "pizza", highContact.Name) 34 | assert.Equal(t, "https://pb33f.io", highContact.URL) 35 | assert.Equal(t, "buckaroo@pb33f.io", highContact.Email) 36 | assert.Equal(t, 1, highContact.GoLow().Name.KeyNode.Line) 37 | } 38 | 39 | func ExampleNewContact() { 40 | // define a Contact using yaml (or JSON, it doesn't matter) 41 | yml := `name: Buckaroo 42 | url: https://pb33f.io 43 | email: buckaroo@pb33f.io` 44 | 45 | // unmarshal yaml into a *yaml.Node instance 46 | var cNode yaml.Node 47 | _ = yaml.Unmarshal([]byte(yml), &cNode) 48 | 49 | // build low 50 | var lowContact lowbase.Contact 51 | _ = lowmodel.BuildModel(cNode.Content[0], &lowContact) 52 | 53 | // build high 54 | highContact := NewContact(&lowContact) 55 | fmt.Print(highContact.Name) 56 | // Output: Buckaroo 57 | } 58 | 59 | func TestContact_MarshalYAML(t *testing.T) { 60 | yml := `name: Buckaroo 61 | url: https://pb33f.io 62 | email: buckaroo@pb33f.io 63 | ` 64 | // unmarshal yaml into a *yaml.Node instance 65 | var cNode yaml.Node 66 | _ = yaml.Unmarshal([]byte(yml), &cNode) 67 | 68 | // build low 69 | var lowContact lowbase.Contact 70 | _ = lowmodel.BuildModel(cNode.Content[0], &lowContact) 71 | _ = lowContact.Build(context.Background(), nil, cNode.Content[0], nil) 72 | 73 | // build high 74 | highContact := NewContact(&lowContact) 75 | 76 | // marshal high back to yaml, should be the same as the original, in same order. 77 | bytes, _ := highContact.Render() 78 | assert.Equal(t, yml, string(bytes)) 79 | } 80 | -------------------------------------------------------------------------------- /index/rolodex_ref_extractor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | Local RefType = iota 13 | File 14 | HTTP 15 | ) 16 | 17 | type RefType int 18 | 19 | type ExtractedRef struct { 20 | Location string 21 | Type RefType 22 | } 23 | 24 | // GetFile returns the file path of the reference. 25 | func (r *ExtractedRef) GetFile() string { 26 | switch r.Type { 27 | case File, HTTP: 28 | location := strings.Split(r.Location, "#/") 29 | return location[0] 30 | default: 31 | return r.Location 32 | } 33 | } 34 | 35 | // GetReference returns the reference path of the reference. 36 | func (r *ExtractedRef) GetReference() string { 37 | switch r.Type { 38 | case File, HTTP: 39 | location := strings.Split(r.Location, "#/") 40 | return fmt.Sprintf("#/%s", location[1]) 41 | default: 42 | return r.Location 43 | } 44 | } 45 | 46 | // ExtractFileType returns the file extension of the reference. 47 | func ExtractFileType(ref string) FileExtension { 48 | if strings.HasSuffix(ref, ".yaml") { 49 | return YAML 50 | } 51 | if strings.HasSuffix(ref, ".yml") { 52 | return YAML 53 | } 54 | if strings.HasSuffix(ref, ".json") { 55 | return JSON 56 | } 57 | if strings.HasSuffix(ref, ".js") { 58 | return JS 59 | } 60 | if strings.HasSuffix(ref, ".go") { 61 | return GO 62 | } 63 | if strings.HasSuffix(ref, ".ts") { 64 | return TS 65 | } 66 | if strings.HasSuffix(ref, ".cs") { 67 | return CS 68 | } 69 | if strings.HasSuffix(ref, ".c") { 70 | return C 71 | } 72 | if strings.HasSuffix(ref, ".cpp") { 73 | return CPP 74 | } 75 | if strings.HasSuffix(ref, ".php") { 76 | return PHP 77 | } 78 | if strings.HasSuffix(ref, ".py") { 79 | return PY 80 | } 81 | if strings.HasSuffix(ref, ".html") { 82 | return HTML 83 | } 84 | if strings.HasSuffix(ref, ".md") { 85 | return MD 86 | } 87 | if strings.HasSuffix(ref, ".java") { 88 | return JAVA 89 | } 90 | if strings.HasSuffix(ref, ".rs") { 91 | return RS 92 | } 93 | if strings.HasSuffix(ref, ".zig") { 94 | return ZIG 95 | } 96 | if strings.HasSuffix(ref, ".rb") { 97 | return RB 98 | } 99 | return UNSUPPORTED 100 | } 101 | -------------------------------------------------------------------------------- /datamodel/high/v3/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | "github.com/pb33f/libopenapi/datamodel/low" 9 | lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "go.yaml.in/yaml/v4" 12 | ) 13 | 14 | // Server represents a high-level OpenAPI 3+ Server object, that is backed by a low level one. 15 | // - https://spec.openapis.org/oas/v3.1.0#server-object 16 | type Server struct { 17 | Name string `json:"name,omitempty" yaml:"name,omitempty"` // OpenAPI 3.2+ name field for documentation 18 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 19 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 20 | Variables *orderedmap.Map[string, *ServerVariable] `json:"variables,omitempty" yaml:"variables,omitempty"` 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *lowv3.Server 23 | } 24 | 25 | // NewServer will create a new high-level Server instance from a low-level one. 26 | func NewServer(server *lowv3.Server) *Server { 27 | s := new(Server) 28 | s.low = server 29 | s.Name = server.Name.Value 30 | s.Description = server.Description.Value 31 | s.URL = server.URL.Value 32 | s.Variables = low.FromReferenceMapWithFunc(server.Variables.Value, NewServerVariable) 33 | s.Extensions = high.ExtractExtensions(server.Extensions) 34 | return s 35 | } 36 | 37 | // GoLow returns the low-level Server instance that was used to create the high-level one 38 | func (s *Server) GoLow() *lowv3.Server { 39 | return s.low 40 | } 41 | 42 | // GoLowUntyped will return the low-level Server instance that was used to create the high-level one, with no type 43 | func (s *Server) GoLowUntyped() any { 44 | return s.low 45 | } 46 | 47 | // Render will return a YAML representation of the Server object as a byte slice. 48 | func (s *Server) Render() ([]byte, error) { 49 | return yaml.Marshal(s) 50 | } 51 | 52 | // MarshalYAML will create a ready to render YAML representation of the Server object. 53 | func (s *Server) MarshalYAML() (interface{}, error) { 54 | nb := high.NewNodeBuilder(s, s.low) 55 | return nb.Render(), nil 56 | } 57 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/pb33f/libopenapi/orderedmap" 9 | "go.yaml.in/yaml/v4" 10 | ) 11 | 12 | // YAMLNodeToJSON converts yaml/json stored in a yaml.Node to json ordered matching the original yaml/json 13 | func YAMLNodeToJSON(node *yaml.Node, indentation string) ([]byte, error) { 14 | v, err := handleYAMLNode(node) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | return json.MarshalIndent(v, "", indentation) 20 | } 21 | 22 | func handleYAMLNode(node *yaml.Node) (any, error) { 23 | switch node.Kind { 24 | case yaml.DocumentNode: 25 | return handleYAMLNode(node.Content[0]) 26 | case yaml.SequenceNode: 27 | return handleSequenceNode(node) 28 | case yaml.MappingNode: 29 | return handleMappingNode(node) 30 | case yaml.ScalarNode: 31 | return handleScalarNode(node) 32 | case yaml.AliasNode: 33 | return handleYAMLNode(node.Alias) 34 | default: 35 | return nil, fmt.Errorf("unknown node kind: %v", node.Kind) 36 | } 37 | } 38 | 39 | func handleMappingNode(node *yaml.Node) (any, error) { 40 | v := orderedmap.New[string, any]() 41 | for i, n := range node.Content { 42 | if i%2 == 0 { 43 | continue 44 | } 45 | keyNode := node.Content[i-1] 46 | kv, err := handleYAMLNode(keyNode) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if reflect.TypeOf(kv).Kind() != reflect.String { 52 | keyData, err := json.Marshal(kv) 53 | if err != nil { 54 | return nil, err // unreachable code in test case, but kept for safety 55 | } 56 | kv = string(keyData) 57 | } 58 | 59 | vv, err := handleYAMLNode(n) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | v.Set(fmt.Sprintf("%v", kv), vv) 65 | } 66 | 67 | return v, nil 68 | } 69 | 70 | func handleSequenceNode(node *yaml.Node) (any, error) { 71 | var s []yaml.Node 72 | 73 | if err := node.Decode(&s); err != nil { 74 | return nil, err // unreachable code in test case, but kept for safety 75 | } 76 | 77 | v := make([]any, len(s)) 78 | for i, n := range s { 79 | vv, err := handleYAMLNode(&n) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | v[i] = vv 85 | } 86 | 87 | return v, nil 88 | } 89 | 90 | func handleScalarNode(node *yaml.Node) (any, error) { 91 | var v any 92 | 93 | if err := node.Decode(&v); err != nil { 94 | return nil, err 95 | } 96 | 97 | return v, nil 98 | } 99 | -------------------------------------------------------------------------------- /datamodel/low/v3/server_variable.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | import ( 4 | "crypto/sha256" 5 | "sort" 6 | 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | "github.com/pb33f/libopenapi/orderedmap" 9 | "go.yaml.in/yaml/v4" 10 | ) 11 | 12 | // ServerVariable represents a low-level OpenAPI 3+ ServerVariable object. 13 | // 14 | // ServerVariable is an object representing a Server Variable for server URL template substitution. 15 | // - https://spec.openapis.org/oas/v3.1.0#server-variable-object 16 | // 17 | // This is the only struct that is not Buildable, it's not used by anything other than a Server instance, 18 | // and it has nothing to build that requires it to be buildable. 19 | type ServerVariable struct { 20 | Enum []low.NodeReference[string] 21 | Default low.NodeReference[string] 22 | Description low.NodeReference[string] 23 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 24 | KeyNode *yaml.Node 25 | RootNode *yaml.Node 26 | *low.Reference 27 | low.NodeMap 28 | } 29 | 30 | // GetRootNode returns the root yaml node of the ServerVariable object. 31 | func (s *ServerVariable) GetRootNode() *yaml.Node { 32 | return s.RootNode 33 | } 34 | 35 | // GetKeyNode returns the key yaml node of the ServerVariable object. 36 | func (s *ServerVariable) GetKeyNode() *yaml.Node { 37 | return s.RootNode 38 | } 39 | 40 | // GetExtensions returns all extensions and satisfies the low.HasExtensions interface. 41 | func (s *ServerVariable) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 42 | return s.Extensions 43 | } 44 | 45 | // Hash will return a consistent SHA256 Hash of the ServerVariable object 46 | func (s *ServerVariable) Hash() [32]byte { 47 | // Use string builder pool 48 | sb := low.GetStringBuilder() 49 | defer low.PutStringBuilder(sb) 50 | 51 | // Pre-allocate and sort enum values 52 | if len(s.Enum) > 0 { 53 | keys := make([]string, len(s.Enum)) 54 | for i := range s.Enum { 55 | keys[i] = s.Enum[i].Value 56 | } 57 | sort.Strings(keys) 58 | for _, key := range keys { 59 | sb.WriteString(key) 60 | sb.WriteByte('|') 61 | } 62 | } 63 | 64 | if !s.Default.IsEmpty() { 65 | sb.WriteString(s.Default.Value) 66 | sb.WriteByte('|') 67 | } 68 | if !s.Description.IsEmpty() { 69 | sb.WriteString(s.Description.Value) 70 | sb.WriteByte('|') 71 | } 72 | return sha256.Sum256([]byte(sb.String())) 73 | } 74 | -------------------------------------------------------------------------------- /datamodel/high/v3/oauth_flow.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | "github.com/pb33f/libopenapi/datamodel/low" 9 | lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "go.yaml.in/yaml/v4" 12 | ) 13 | 14 | // OAuthFlow represents a high-level OpenAPI 3+ OAuthFlow object that is backed by a low-level one. 15 | // - https://spec.openapis.org/oas/v3.1.0#oauth-flow-object 16 | type OAuthFlow struct { 17 | AuthorizationUrl string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` 18 | TokenUrl string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` 19 | RefreshUrl string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` 20 | Scopes *orderedmap.Map[string, string] `json:"scopes,renderZero" yaml:"scopes,renderZero"` 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *lowv3.OAuthFlow 23 | } 24 | 25 | // NewOAuthFlow creates a new high-level OAuthFlow instance from a low-level one. 26 | func NewOAuthFlow(flow *lowv3.OAuthFlow) *OAuthFlow { 27 | o := new(OAuthFlow) 28 | o.low = flow 29 | o.TokenUrl = flow.TokenUrl.Value 30 | o.AuthorizationUrl = flow.AuthorizationUrl.Value 31 | o.RefreshUrl = flow.RefreshUrl.Value 32 | o.Scopes = low.FromReferenceMap(flow.Scopes.Value) 33 | o.Extensions = high.ExtractExtensions(flow.Extensions) 34 | return o 35 | } 36 | 37 | // GoLow returns the low-level OAuthFlow instance used to create the high-level one. 38 | func (o *OAuthFlow) GoLow() *lowv3.OAuthFlow { 39 | return o.low 40 | } 41 | 42 | // GoLowUntyped will return the low-level Discriminator instance that was used to create the high-level one, with no type 43 | func (o *OAuthFlow) GoLowUntyped() any { 44 | return o.low 45 | } 46 | 47 | // Render will return a YAML representation of the OAuthFlow object as a byte slice. 48 | func (o *OAuthFlow) Render() ([]byte, error) { 49 | return yaml.Marshal(o) 50 | } 51 | 52 | // MarshalYAML will create a ready to render YAML representation of the OAuthFlow object. 53 | func (o *OAuthFlow) MarshalYAML() (interface{}, error) { 54 | nb := high.NewNodeBuilder(o, o.low) 55 | return nb.Render(), nil 56 | } 57 | -------------------------------------------------------------------------------- /what-changed/model/server_variable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | ) 10 | 11 | // ServerVariableChanges represents changes found between two OpenAPI ServerVariable Objects 12 | type ServerVariableChanges struct { 13 | *PropertyChanges 14 | } 15 | 16 | // GetAllChanges returns a slice of all changes made between SecurityRequirement objects 17 | func (s *ServerVariableChanges) GetAllChanges() []*Change { 18 | if s == nil { 19 | return nil 20 | } 21 | return s.Changes 22 | } 23 | 24 | // CompareServerVariables compares a left and right OpenAPI ServerVariable object for changes. 25 | // If anything is found, returns a pointer to a ServerVariableChanges instance, otherwise returns nil. 26 | func CompareServerVariables(l, r *v3.ServerVariable) *ServerVariableChanges { 27 | if low.AreEqual(l, r) { 28 | return nil 29 | } 30 | 31 | var changes []*Change 32 | 33 | lValues := make(map[string]low.NodeReference[string]) 34 | rValues := make(map[string]low.NodeReference[string]) 35 | for i := range l.Enum { 36 | lValues[l.Enum[i].Value] = l.Enum[i] 37 | } 38 | for i := range r.Enum { 39 | rValues[r.Enum[i].Value] = r.Enum[i] 40 | } 41 | for k := range lValues { 42 | if _, ok := rValues[k]; !ok { 43 | CreateChange(&changes, ObjectRemoved, v3.EnumLabel, 44 | lValues[k].ValueNode, nil, BreakingRemoved(CompServerVariable, PropEnum), 45 | lValues[k].Value, nil) 46 | continue 47 | } 48 | } 49 | for k := range rValues { 50 | if _, ok := lValues[k]; !ok { 51 | CreateChange(&changes, ObjectAdded, v3.EnumLabel, 52 | lValues[k].ValueNode, rValues[k].ValueNode, BreakingAdded(CompServerVariable, PropEnum), 53 | lValues[k].Value, rValues[k].Value) 54 | } 55 | } 56 | 57 | props := make([]*PropertyCheck, 0, 2) 58 | props = append(props, 59 | NewPropertyCheck(CompServerVariable, PropDefault, 60 | l.Default.ValueNode, r.Default.ValueNode, 61 | v3.DefaultLabel, &changes, l, r), 62 | NewPropertyCheck(CompServerVariable, PropDescription, 63 | l.Description.ValueNode, r.Description.ValueNode, 64 | v3.DescriptionLabel, &changes, l, r), 65 | ) 66 | 67 | CheckProperties(props) 68 | sc := new(ServerVariableChanges) 69 | sc.PropertyChanges = NewPropertyChanges(changes) 70 | return sc 71 | } 72 | -------------------------------------------------------------------------------- /datamodel/high/base/external_doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/base" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // ExternalDoc represents a high-level External Documentation object as defined by OpenAPI 2 and 3 14 | // 15 | // Allows referencing an external resource for extended documentation. 16 | // 17 | // v2 - https://swagger.io/specification/v2/#externalDocumentationObject 18 | // v3 - https://spec.openapis.org/oas/v3.1.0#external-documentation-object 19 | type ExternalDoc struct { 20 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 21 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 22 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 23 | low *low.ExternalDoc 24 | } 25 | 26 | // NewExternalDoc will create a new high-level External Documentation object from a low-level one. 27 | func NewExternalDoc(extDoc *low.ExternalDoc) *ExternalDoc { 28 | d := new(ExternalDoc) 29 | d.low = extDoc 30 | if !extDoc.Description.IsEmpty() { 31 | d.Description = extDoc.Description.Value 32 | } 33 | if !extDoc.URL.IsEmpty() { 34 | d.URL = extDoc.URL.Value 35 | } 36 | d.Extensions = high.ExtractExtensions(extDoc.Extensions) 37 | return d 38 | } 39 | 40 | // GoLow returns the low-level ExternalDoc instance used to create the high-level one. 41 | func (e *ExternalDoc) GoLow() *low.ExternalDoc { 42 | return e.low 43 | } 44 | 45 | // GoLowUntyped will return the low-level ExternalDoc instance that was used to create the high-level one, with no type 46 | func (e *ExternalDoc) GoLowUntyped() any { 47 | return e.low 48 | } 49 | 50 | func (e *ExternalDoc) GetExtensions() *orderedmap.Map[string, *yaml.Node] { 51 | return e.Extensions 52 | } 53 | 54 | // Render will return a YAML representation of the ExternalDoc object as a byte slice. 55 | func (e *ExternalDoc) Render() ([]byte, error) { 56 | return yaml.Marshal(e) 57 | } 58 | 59 | // MarshalYAML will create a ready to render YAML representation of the ExternalDoc object. 60 | func (e *ExternalDoc) MarshalYAML() (interface{}, error) { 61 | nb := high.NewNodeBuilder(e, e.low) 62 | return nb.Render(), nil 63 | } 64 | -------------------------------------------------------------------------------- /datamodel/low/base/xml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package base 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | "github.com/pb33f/libopenapi/index" 11 | "github.com/stretchr/testify/assert" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | func TestXML_Build(t *testing.T) { 16 | yml := `name: a thing 17 | namespace: somewhere 18 | wrapped: true` 19 | 20 | var idxNode yaml.Node 21 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 22 | idx := index.NewSpecIndex(&idxNode) 23 | 24 | var n XML 25 | err := low.BuildModel(idxNode.Content[0], &n) 26 | assert.NoError(t, err) 27 | 28 | err = n.Build(&idxNode, idx) 29 | assert.NoError(t, err) 30 | assert.Equal(t, "a thing", n.Name.Value) 31 | assert.Equal(t, "somewhere", n.Namespace.Value) 32 | assert.True(t, n.Wrapped.Value) 33 | assert.NotNil(t, n.GetRootNode()) 34 | assert.NotNil(t, n.GetIndex()) 35 | } 36 | 37 | func TestXML_Build_WithNodeType(t *testing.T) { 38 | yml := `name: myElement 39 | namespace: http://example.com/ns 40 | nodeType: element 41 | wrapped: false` 42 | 43 | var idxNode yaml.Node 44 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 45 | idx := index.NewSpecIndex(&idxNode) 46 | 47 | var n XML 48 | err := low.BuildModel(idxNode.Content[0], &n) 49 | assert.NoError(t, err) 50 | 51 | err = n.Build(&idxNode, idx) 52 | assert.NoError(t, err) 53 | assert.Equal(t, "myElement", n.Name.Value) 54 | assert.Equal(t, "http://example.com/ns", n.Namespace.Value) 55 | assert.Equal(t, "element", n.NodeType.Value) 56 | assert.False(t, n.Wrapped.Value) 57 | 58 | // test that Hash includes nodeType 59 | hash1 := n.Hash() 60 | n.NodeType.Value = "attribute" 61 | hash2 := n.Hash() 62 | assert.NotEqual(t, hash1, hash2) 63 | } 64 | 65 | func TestXML_Build_WithAttributeAndNodeType(t *testing.T) { 66 | // test backward compatibility - both attribute and nodeType present 67 | yml := `name: myAttr 68 | attribute: true 69 | nodeType: attribute` 70 | 71 | var idxNode yaml.Node 72 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 73 | idx := index.NewSpecIndex(&idxNode) 74 | 75 | var n XML 76 | err := low.BuildModel(idxNode.Content[0], &n) 77 | assert.NoError(t, err) 78 | 79 | err = n.Build(&idxNode, idx) 80 | assert.NoError(t, err) 81 | assert.Equal(t, "myAttr", n.Name.Value) 82 | assert.True(t, n.Attribute.Value) 83 | assert.Equal(t, "attribute", n.NodeType.Value) 84 | } 85 | -------------------------------------------------------------------------------- /datamodel/high/v3/server_variable.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // ServerVariable represents a high-level OpenAPI 3+ ServerVariable object, that is backed by a low-level one. 14 | // 15 | // ServerVariable is an object representing a Server Variable for server URL template substitution. 16 | // - https://spec.openapis.org/oas/v3.1.0#server-variable-object 17 | type ServerVariable struct { 18 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` 19 | Default string `json:"default,omitempty" yaml:"default,omitempty"` 20 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *low.ServerVariable 23 | } 24 | 25 | // NewServerVariable will return a new high-level instance of a ServerVariable from a low-level one. 26 | func NewServerVariable(variable *low.ServerVariable) *ServerVariable { 27 | v := new(ServerVariable) 28 | v.low = variable 29 | var enums []string 30 | for _, enum := range variable.Enum { 31 | if enum.Value != "" { 32 | enums = append(enums, enum.Value) 33 | } 34 | } 35 | v.Default = variable.Default.Value 36 | v.Description = variable.Description.Value 37 | v.Enum = enums 38 | v.Extensions = high.ExtractExtensions(variable.Extensions) 39 | return v 40 | } 41 | 42 | // GoLow returns the low-level ServerVariable used to create the high\-level one. 43 | func (s *ServerVariable) GoLow() *low.ServerVariable { 44 | return s.low 45 | } 46 | 47 | // GoLowUntyped will return the low-level ServerVariable instance that was used to create the high-level one, with no type 48 | func (s *ServerVariable) GoLowUntyped() any { 49 | return s.low 50 | } 51 | 52 | // Render will return a YAML representation of the ServerVariable object as a byte slice. 53 | func (s *ServerVariable) Render() ([]byte, error) { 54 | return yaml.Marshal(s) 55 | } 56 | 57 | // MarshalYAML will create a ready to render YAML representation of the ServerVariable object. 58 | func (s *ServerVariable) MarshalYAML() (interface{}, error) { 59 | nb := high.NewNodeBuilder(s, s.low) 60 | return nb.Render(), nil 61 | } 62 | -------------------------------------------------------------------------------- /datamodel/high/v2/security_scheme.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/v2" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // SecurityScheme is a high-level representation of a Swagger / OpenAPI 2 SecurityScheme object 14 | // backed by a low-level one. 15 | // 16 | // SecurityScheme allows the definition of a security scheme that can be used by the operations. Supported schemes are 17 | // basic authentication, an API key (either as a header or as a query parameter) and OAuth2's common flows 18 | // (implicit, password, application and access code) 19 | // - https://swagger.io/specification/v2/#securityDefinitionsObject 20 | type SecurityScheme struct { 21 | Type string 22 | Description string 23 | Name string 24 | In string 25 | Flow string 26 | AuthorizationUrl string 27 | TokenUrl string 28 | Scopes *Scopes 29 | Extensions *orderedmap.Map[string, *yaml.Node] 30 | low *low.SecurityScheme 31 | } 32 | 33 | // NewSecurityScheme creates a new instance of SecurityScheme from a low-level one. 34 | func NewSecurityScheme(securityScheme *low.SecurityScheme) *SecurityScheme { 35 | s := new(SecurityScheme) 36 | s.low = securityScheme 37 | s.Extensions = high.ExtractExtensions(securityScheme.Extensions) 38 | if !securityScheme.Type.IsEmpty() { 39 | s.Type = securityScheme.Type.Value 40 | } 41 | if !securityScheme.Description.IsEmpty() { 42 | s.Description = securityScheme.Description.Value 43 | } 44 | if !securityScheme.Name.IsEmpty() { 45 | s.Name = securityScheme.Name.Value 46 | } 47 | if !securityScheme.In.IsEmpty() { 48 | s.In = securityScheme.In.Value 49 | } 50 | if !securityScheme.Flow.IsEmpty() { 51 | s.Flow = securityScheme.Flow.Value 52 | } 53 | if !securityScheme.AuthorizationUrl.IsEmpty() { 54 | s.AuthorizationUrl = securityScheme.AuthorizationUrl.Value 55 | } 56 | if !securityScheme.TokenUrl.IsEmpty() { 57 | s.TokenUrl = securityScheme.TokenUrl.Value 58 | } 59 | if !securityScheme.Scopes.IsEmpty() { 60 | s.Scopes = NewScopes(securityScheme.Scopes.Value) 61 | } 62 | return s 63 | } 64 | 65 | // GoLow returns the low-level SecurityScheme that was used to create the high-level one. 66 | func (s *SecurityScheme) GoLow() *low.SecurityScheme { 67 | return s.low 68 | } 69 | -------------------------------------------------------------------------------- /datamodel/high/v2/path_item_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | lowV2 "github.com/pb33f/libopenapi/datamodel/low/v2" 12 | "github.com/pb33f/libopenapi/index" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | "github.com/stretchr/testify/assert" 15 | "go.yaml.in/yaml/v4" 16 | ) 17 | 18 | func TestPathItem_GetOperations(t *testing.T) { 19 | yml := `get: 20 | description: get 21 | put: 22 | description: put 23 | post: 24 | description: post 25 | patch: 26 | description: patch 27 | delete: 28 | description: delete 29 | head: 30 | description: head 31 | options: 32 | description: options 33 | ` 34 | 35 | var idxNode yaml.Node 36 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 37 | idx := index.NewSpecIndex(&idxNode) 38 | 39 | var n lowV2.PathItem 40 | _ = low.BuildModel(&idxNode, &n) 41 | _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) 42 | 43 | r := NewPathItem(&n) 44 | 45 | assert.Equal(t, 7, orderedmap.Len(r.GetOperations())) 46 | } 47 | 48 | func TestPathItem_GetOperations_NoLow(t *testing.T) { 49 | pi := &PathItem{ 50 | Delete: &Operation{}, 51 | Post: &Operation{}, 52 | Get: &Operation{}, 53 | } 54 | ops := pi.GetOperations() 55 | 56 | expectedOrderOfOps := []string{"get", "post", "delete"} 57 | actualOrder := []string{} 58 | 59 | for op := range ops.KeysFromOldest() { 60 | actualOrder = append(actualOrder, op) 61 | } 62 | 63 | assert.Equal(t, expectedOrderOfOps, actualOrder) 64 | } 65 | 66 | func TestPathItem_GetOperations_LowWithUnsetOperations(t *testing.T) { 67 | pi := &PathItem{ 68 | Delete: &Operation{}, 69 | Post: &Operation{}, 70 | Get: &Operation{}, 71 | low: &lowV2.PathItem{}, 72 | } 73 | ops := pi.GetOperations() 74 | 75 | expectedOrderOfOps := []string{"get", "post", "delete"} 76 | actualOrder := []string{} 77 | 78 | for op := range ops.KeysFromOldest() { 79 | actualOrder = append(actualOrder, op) 80 | } 81 | 82 | assert.Equal(t, expectedOrderOfOps, actualOrder) 83 | } 84 | 85 | func TestPathItem_NewPathItem_WithParameters(t *testing.T) { 86 | pi := NewPathItem(&lowV2.PathItem{ 87 | Parameters: low.NodeReference[[]low.ValueReference[*lowV2.Parameter]]{ 88 | Value: []low.ValueReference[*lowV2.Parameter]{ 89 | { 90 | Value: &lowV2.Parameter{}, 91 | }, 92 | }, 93 | ValueNode: &yaml.Node{}, 94 | }, 95 | }) 96 | assert.NotNil(t, pi.Parameters) 97 | } 98 | -------------------------------------------------------------------------------- /datamodel/low/v2/scopes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "crypto/sha256" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/pb33f/libopenapi/datamodel/low" 13 | "github.com/pb33f/libopenapi/index" 14 | "github.com/pb33f/libopenapi/orderedmap" 15 | "github.com/pb33f/libopenapi/utils" 16 | "go.yaml.in/yaml/v4" 17 | ) 18 | 19 | // Scopes is a low-level representation of a Swagger / OpenAPI 2 OAuth2 Scopes object. 20 | // 21 | // Scopes lists the available scopes for an OAuth2 security scheme. 22 | // - https://swagger.io/specification/v2/#scopesObject 23 | type Scopes struct { 24 | Values *orderedmap.Map[low.KeyReference[string], low.ValueReference[string]] 25 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 26 | } 27 | 28 | // GetExtensions returns all Scopes extensions and satisfies the low.HasExtensions interface. 29 | func (s *Scopes) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 30 | return s.Extensions 31 | } 32 | 33 | // FindScope will attempt to locate a scope string using a key. 34 | func (s *Scopes) FindScope(scope string) *low.ValueReference[string] { 35 | return low.FindItemInOrderedMap[string](scope, s.Values) 36 | } 37 | 38 | // Build will extract scope values and extensions from node. 39 | func (s *Scopes) Build(_ context.Context, _, root *yaml.Node, _ *index.SpecIndex) error { 40 | root = utils.NodeAlias(root) 41 | utils.CheckForMergeNodes(root) 42 | s.Extensions = low.ExtractExtensions(root) 43 | valueMap := orderedmap.New[low.KeyReference[string], low.ValueReference[string]]() 44 | if utils.IsNodeMap(root) { 45 | for k := range root.Content { 46 | if k%2 == 0 { 47 | if strings.Contains(root.Content[k].Value, "x-") { 48 | continue 49 | } 50 | valueMap.Set( 51 | low.KeyReference[string]{ 52 | Value: root.Content[k].Value, 53 | KeyNode: root.Content[k], 54 | }, 55 | low.ValueReference[string]{ 56 | Value: root.Content[k+1].Value, 57 | ValueNode: root.Content[k+1], 58 | }, 59 | ) 60 | } 61 | } 62 | s.Values = valueMap 63 | } 64 | return nil 65 | } 66 | 67 | // Hash will return a consistent SHA256 Hash of the Scopes object 68 | func (s *Scopes) Hash() [32]byte { 69 | var f []string 70 | for k, v := range orderedmap.SortAlpha(s.Values).FromOldest() { 71 | f = append(f, fmt.Sprintf("%s-%s", k.Value, v.Value)) 72 | } 73 | f = append(f, low.HashExtensions(s.Extensions)...) 74 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 75 | } 76 | -------------------------------------------------------------------------------- /what-changed/model/request_body_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/datamodel/low/v3" 12 | "github.com/stretchr/testify/assert" 13 | "go.yaml.in/yaml/v4" 14 | ) 15 | 16 | func TestCompareRequestBodies(t *testing.T) { 17 | // Clear hash cache to ensure deterministic results in concurrent test environments 18 | low.ClearHashCache() 19 | left := `description: something 20 | required: true 21 | content: 22 | application/json: 23 | schema: 24 | type: int` 25 | 26 | right := `description: something 27 | required: true 28 | content: 29 | application/json: 30 | schema: 31 | type: int` 32 | 33 | var lNode, rNode yaml.Node 34 | _ = yaml.Unmarshal([]byte(left), &lNode) 35 | _ = yaml.Unmarshal([]byte(right), &rNode) 36 | 37 | // create low level objects 38 | var lDoc v3.RequestBody 39 | var rDoc v3.RequestBody 40 | _ = low.BuildModel(lNode.Content[0], &lDoc) 41 | _ = low.BuildModel(rNode.Content[0], &rDoc) 42 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 43 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 44 | 45 | // compare. 46 | extChanges := CompareRequestBodies(&lDoc, &rDoc) 47 | assert.Nil(t, extChanges) 48 | } 49 | 50 | func TestCompareRequestBodies_Modified(t *testing.T) { 51 | // Clear hash cache to ensure deterministic results in concurrent test environments 52 | low.ClearHashCache() 53 | cleanHashCacheForTest(t) 54 | 55 | left := `description: something 56 | required: true 57 | x-pizza: thin 58 | content: 59 | application/json: 60 | schema: 61 | type: int` 62 | 63 | right := `x-pizza: oven 64 | description: nothing 65 | required: false 66 | content: 67 | application/json: 68 | schema: 69 | type: string` 70 | 71 | var lNode, rNode yaml.Node 72 | _ = yaml.Unmarshal([]byte(left), &lNode) 73 | _ = yaml.Unmarshal([]byte(right), &rNode) 74 | 75 | // create low level objects 76 | var lDoc v3.RequestBody 77 | var rDoc v3.RequestBody 78 | _ = low.BuildModel(lNode.Content[0], &lDoc) 79 | _ = low.BuildModel(rNode.Content[0], &rDoc) 80 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 81 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 82 | 83 | // compare. 84 | extChanges := CompareRequestBodies(&lDoc, &rDoc) 85 | 86 | assert.Equal(t, 4, extChanges.TotalChanges()) 87 | assert.Len(t, extChanges.GetAllChanges(), 4) 88 | assert.Equal(t, 2, extChanges.TotalBreakingChanges()) 89 | } 90 | -------------------------------------------------------------------------------- /datamodel/low/base/circ_check_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "github.com/pb33f/libopenapi/datamodel/low" 9 | "github.com/pb33f/libopenapi/index" 10 | "github.com/stretchr/testify/assert" 11 | "go.yaml.in/yaml/v4" 12 | "testing" 13 | ) 14 | 15 | func TestCheckSchemaProxyForCircularRefs(t *testing.T) { 16 | 17 | rolo := index.NewRolodex(&index.SpecIndexConfig{}) 18 | dummyNode := &yaml.Node{Content: []*yaml.Node{{Content: []*yaml.Node{}}}} 19 | 20 | ref := low.Reference{} 21 | ref.SetReference("minty-fresh", dummyNode) 22 | 23 | schema := &SchemaProxy{ 24 | Reference: ref, 25 | } 26 | rootIndex := index.NewSpecIndex(dummyNode) 27 | _ = schema.Build(context.Background(), dummyNode, dummyNode, rootIndex) 28 | 29 | assert.False(t, CheckSchemaProxyForCircularRefs(schema)) // no rolodex yet. 30 | 31 | rootIndex.SetRolodex(rolo) 32 | rolo.SetRootNode(dummyNode) 33 | rolo.SetRootIndex(rootIndex) 34 | rolo.SetSafeCircularReferences([]*index.CircularReferenceResult{ 35 | { 36 | LoopPoint: &index.Reference{ 37 | FullDefinition: "minty-fresh", 38 | }, 39 | }, 40 | }) 41 | 42 | assert.True(t, CheckSchemaProxyForCircularRefs(schema)) // is circular 43 | 44 | ref = low.Reference{} 45 | ref.SetReference("tasty-burger", dummyNode) 46 | schema = &SchemaProxy{ 47 | Reference: ref, 48 | } 49 | _ = schema.Build(context.Background(), dummyNode, dummyNode, rootIndex) 50 | 51 | assert.False(t, CheckSchemaProxyForCircularRefs(schema)) // not circular 52 | } 53 | 54 | func TestCheckSchemaProxyForCircularRefs_JourneyCheck(t *testing.T) { 55 | 56 | rolo := index.NewRolodex(&index.SpecIndexConfig{}) 57 | dummyNode := &yaml.Node{Content: []*yaml.Node{{Content: []*yaml.Node{}}}} 58 | 59 | ref := low.Reference{} 60 | ref.SetReference("minty-fresh", dummyNode) 61 | 62 | schema := &SchemaProxy{ 63 | Reference: ref, 64 | } 65 | rootIndex := index.NewSpecIndex(dummyNode) 66 | _ = schema.Build(context.Background(), dummyNode, dummyNode, rootIndex) 67 | 68 | rootIndex.SetRolodex(rolo) 69 | rolo.SetRootNode(dummyNode) 70 | rolo.SetRootIndex(rootIndex) 71 | rolo.SetSafeCircularReferences([]*index.CircularReferenceResult{ 72 | { 73 | LoopPoint: &index.Reference{ 74 | FullDefinition: "not-minty-fresh", 75 | }, 76 | Journey: []*index.Reference{ 77 | { 78 | FullDefinition: "minty-fresh", 79 | }, 80 | { 81 | FullDefinition: "minty-fresh", 82 | }, 83 | }, 84 | }, 85 | }) 86 | 87 | assert.True(t, CheckSchemaProxyForCircularRefs(schema)) // no rolodex yet. 88 | 89 | } 90 | -------------------------------------------------------------------------------- /datamodel/low/base/external_doc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/index" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | "github.com/stretchr/testify/assert" 14 | "go.yaml.in/yaml/v4" 15 | ) 16 | 17 | func TestExternalDoc_FindExtension(t *testing.T) { 18 | yml := `x-fish: cake` 19 | 20 | var idxNode yaml.Node 21 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 22 | idx := index.NewSpecIndex(&idxNode) 23 | 24 | var n ExternalDoc 25 | err := low.BuildModel(&idxNode, &n) 26 | assert.NoError(t, err) 27 | 28 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 29 | assert.NoError(t, err) 30 | 31 | var xFish string 32 | _ = n.FindExtension("x-fish").Value.Decode(&xFish) 33 | 34 | assert.Equal(t, "cake", xFish) 35 | assert.NotNil(t, n.GetRootNode()) 36 | assert.Nil(t, n.GetKeyNode()) 37 | assert.NotNil(t, n.GetContext()) 38 | assert.NotNil(t, n.GetIndex()) 39 | } 40 | 41 | func TestExternalDoc_Build(t *testing.T) { 42 | yml := `url: https://pb33f.io 43 | description: the ranch 44 | x-b33f: princess` 45 | 46 | var idxNode yaml.Node 47 | mErr := yaml.Unmarshal([]byte(yml), &idxNode) 48 | assert.NoError(t, mErr) 49 | idx := index.NewSpecIndex(&idxNode) 50 | 51 | var n ExternalDoc 52 | err := low.BuildModel(idxNode.Content[0], &n) 53 | assert.NoError(t, err) 54 | 55 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 56 | assert.NoError(t, err) 57 | assert.Equal(t, "https://pb33f.io", n.URL.Value) 58 | assert.Equal(t, "the ranch", n.Description.Value) 59 | 60 | var xB33f string 61 | _ = n.FindExtension("x-b33f").Value.Decode(&xB33f) 62 | assert.Equal(t, "princess", xB33f) 63 | } 64 | 65 | func TestExternalDoc_Hash(t *testing.T) { 66 | left := `url: https://pb33f.io 67 | description: the ranch 68 | x-b33f: princess` 69 | 70 | right := `url: https://pb33f.io 71 | x-b33f: princess 72 | description: the ranch` 73 | 74 | var lNode, rNode yaml.Node 75 | _ = yaml.Unmarshal([]byte(left), &lNode) 76 | _ = yaml.Unmarshal([]byte(right), &rNode) 77 | 78 | // create low level objects 79 | var lDoc ExternalDoc 80 | var rDoc ExternalDoc 81 | _ = low.BuildModel(lNode.Content[0], &lDoc) 82 | _ = low.BuildModel(rNode.Content[0], &rDoc) 83 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 84 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 85 | 86 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 87 | assert.Equal(t, 1, orderedmap.Len(lDoc.GetExtensions())) 88 | } 89 | -------------------------------------------------------------------------------- /index/schema_id_registry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess Beef Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "fmt" 8 | "log/slog" 9 | ) 10 | 11 | // schemaIdRegistrationResult holds the result of a schema ID registration attempt. 12 | type schemaIdRegistrationResult struct { 13 | registered bool // true if successfully registered 14 | duplicate bool // true if a duplicate was found (first-wins policy applied) 15 | key string // the key used for registration 16 | } 17 | 18 | // registerSchemaIdToRegistry is the common registration logic for both SpecIndex and Rolodex. 19 | // Returns the registration result. Duplicates are logged but not treated as errors. 20 | func registerSchemaIdToRegistry( 21 | registry map[string]*SchemaIdEntry, 22 | entry *SchemaIdEntry, 23 | logger *slog.Logger, 24 | registryName string, 25 | ) (*schemaIdRegistrationResult, error) { 26 | if entry == nil { 27 | return nil, fmt.Errorf("cannot register nil SchemaIdEntry") 28 | } 29 | 30 | if err := ValidateSchemaId(entry.Id); err != nil { 31 | if logger != nil { 32 | logger.Warn("invalid $id value, skipping registration", 33 | "registry", registryName, 34 | "id", entry.Id, 35 | "error", err.Error(), 36 | "line", entry.Line, 37 | "column", entry.Column) 38 | } 39 | return nil, err 40 | } 41 | 42 | key := entry.GetKey() 43 | 44 | if existing, ok := registry[key]; ok { 45 | if logger != nil { 46 | existingPath := "" 47 | newPath := "" 48 | if existing.Index != nil { 49 | existingPath = existing.Index.GetSpecAbsolutePath() 50 | } 51 | if entry.Index != nil { 52 | newPath = entry.Index.GetSpecAbsolutePath() 53 | } 54 | logger.Warn("duplicate $id detected, keeping first registration", 55 | "registry", registryName, 56 | "id", key, 57 | "first_location", fmt.Sprintf("%s:%d:%d", existingPath, existing.Line, existing.Column), 58 | "duplicate_location", fmt.Sprintf("%s:%d:%d", newPath, entry.Line, entry.Column)) 59 | } 60 | return &schemaIdRegistrationResult{registered: false, duplicate: true, key: key}, nil 61 | } 62 | 63 | registry[key] = entry 64 | return &schemaIdRegistrationResult{registered: true, duplicate: false, key: key}, nil 65 | } 66 | 67 | // copySchemaIdRegistry creates a defensive copy of a schema ID registry. 68 | func copySchemaIdRegistry(registry map[string]*SchemaIdEntry) map[string]*SchemaIdEntry { 69 | if registry == nil { 70 | return make(map[string]*SchemaIdEntry) 71 | } 72 | result := make(map[string]*SchemaIdEntry, len(registry)) 73 | for k, v := range registry { 74 | result[k] = v 75 | } 76 | return result 77 | } 78 | -------------------------------------------------------------------------------- /overlay/validation_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | "testing" 8 | 9 | highoverlay "github.com/pb33f/libopenapi/datamodel/high/overlay" 10 | "github.com/stretchr/testify/assert" 11 | "go.yaml.in/yaml/v4" 12 | ) 13 | 14 | func TestValidateOverlay_Valid(t *testing.T) { 15 | overlay := &highoverlay.Overlay{ 16 | Overlay: "1.0.0", 17 | Info: &highoverlay.Info{ 18 | Title: "Test", 19 | Version: "1.0.0", 20 | }, 21 | Actions: []*highoverlay.Action{ 22 | {Target: "$.info"}, 23 | }, 24 | } 25 | 26 | err := validateOverlay(overlay) 27 | assert.NoError(t, err) 28 | } 29 | 30 | func TestValidateOverlay_MissingOverlayField(t *testing.T) { 31 | overlay := &highoverlay.Overlay{ 32 | Info: &highoverlay.Info{ 33 | Title: "Test", 34 | Version: "1.0.0", 35 | }, 36 | Actions: []*highoverlay.Action{ 37 | {Target: "$.info"}, 38 | }, 39 | } 40 | 41 | err := validateOverlay(overlay) 42 | assert.ErrorIs(t, err, ErrMissingOverlayField) 43 | } 44 | 45 | func TestValidateOverlay_MissingInfo(t *testing.T) { 46 | overlay := &highoverlay.Overlay{ 47 | Overlay: "1.0.0", 48 | Actions: []*highoverlay.Action{ 49 | {Target: "$.info"}, 50 | }, 51 | } 52 | 53 | err := validateOverlay(overlay) 54 | assert.ErrorIs(t, err, ErrMissingInfo) 55 | } 56 | 57 | func TestValidateOverlay_EmptyActions(t *testing.T) { 58 | overlay := &highoverlay.Overlay{ 59 | Overlay: "1.0.0", 60 | Info: &highoverlay.Info{ 61 | Title: "Test", 62 | Version: "1.0.0", 63 | }, 64 | Actions: []*highoverlay.Action{}, 65 | } 66 | 67 | err := validateOverlay(overlay) 68 | assert.ErrorIs(t, err, ErrEmptyActions) 69 | } 70 | 71 | func TestValidateTarget_Scalar(t *testing.T) { 72 | node := &yaml.Node{ 73 | Kind: yaml.ScalarNode, 74 | Value: "test", 75 | } 76 | 77 | err := validateTarget(node) 78 | assert.ErrorIs(t, err, ErrPrimitiveTarget) 79 | } 80 | 81 | func TestValidateTarget_Mapping(t *testing.T) { 82 | node := &yaml.Node{ 83 | Kind: yaml.MappingNode, 84 | } 85 | 86 | err := validateTarget(node) 87 | assert.NoError(t, err) 88 | } 89 | 90 | func TestValidateTarget_Sequence(t *testing.T) { 91 | node := &yaml.Node{ 92 | Kind: yaml.SequenceNode, 93 | } 94 | 95 | err := validateTarget(node) 96 | assert.NoError(t, err) 97 | } 98 | 99 | func TestValidateTarget_Document(t *testing.T) { 100 | node := &yaml.Node{ 101 | Kind: yaml.DocumentNode, 102 | } 103 | 104 | err := validateTarget(node) 105 | assert.NoError(t, err) 106 | } 107 | -------------------------------------------------------------------------------- /what-changed/model/external_docs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low/base" 8 | "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | ) 10 | 11 | // ExternalDocChanges represents changes made to any ExternalDoc object from an OpenAPI document. 12 | type ExternalDocChanges struct { 13 | *PropertyChanges 14 | ExtensionChanges *ExtensionChanges `json:"extensions,omitempty" yaml:"extensions,omitempty"` 15 | } 16 | 17 | // GetAllChanges returns a slice of all changes made between Example objects 18 | func (e *ExternalDocChanges) GetAllChanges() []*Change { 19 | if e == nil { 20 | return nil 21 | } 22 | var changes []*Change 23 | changes = append(changes, e.Changes...) 24 | if e.ExtensionChanges != nil { 25 | changes = append(changes, e.ExtensionChanges.GetAllChanges()...) 26 | } 27 | return changes 28 | } 29 | 30 | // TotalChanges returns a count of everything that changed 31 | func (e *ExternalDocChanges) TotalChanges() int { 32 | if e == nil { 33 | return 0 34 | } 35 | c := e.PropertyChanges.TotalChanges() 36 | if e.ExtensionChanges != nil { 37 | c += e.ExtensionChanges.TotalChanges() 38 | } 39 | return c 40 | } 41 | 42 | // TotalBreakingChanges returns the total number of breaking changes in ExternalDoc objects. 43 | func (e *ExternalDocChanges) TotalBreakingChanges() int { 44 | if e == nil { 45 | return 0 46 | } 47 | c := e.PropertyChanges.TotalBreakingChanges() 48 | if e.ExtensionChanges != nil { 49 | c += e.ExtensionChanges.TotalBreakingChanges() 50 | } 51 | return c 52 | } 53 | 54 | // CompareExternalDocs will compare a left (original) and a right (new) slice of ValueReference 55 | // nodes for any changes between them. If there are changes, then a pointer to ExternalDocChanges 56 | // is returned, otherwise if nothing changed - then nil is returned. 57 | func CompareExternalDocs(l, r *base.ExternalDoc) *ExternalDocChanges { 58 | var changes []*Change 59 | props := make([]*PropertyCheck, 0, 2) 60 | 61 | props = append(props, 62 | NewPropertyCheck(CompExternalDocs, PropURL, 63 | l.URL.ValueNode, r.URL.ValueNode, 64 | v3.URLLabel, &changes, l, r), 65 | NewPropertyCheck(CompExternalDocs, PropDescription, 66 | l.Description.ValueNode, r.Description.ValueNode, 67 | v3.DescriptionLabel, &changes, l, r), 68 | ) 69 | 70 | CheckProperties(props) 71 | 72 | dc := new(ExternalDocChanges) 73 | dc.PropertyChanges = NewPropertyChanges(changes) 74 | 75 | // check extensions 76 | dc.ExtensionChanges = CheckExtensions(l, r) 77 | if dc.TotalChanges() <= 0 { 78 | return nil 79 | } 80 | return dc 81 | } 82 | -------------------------------------------------------------------------------- /datamodel/low/base/discriminator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "crypto/sha256" 8 | 9 | "go.yaml.in/yaml/v4" 10 | 11 | "github.com/pb33f/libopenapi/datamodel/low" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | ) 14 | 15 | // Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas 16 | // 17 | // When request bodies or response payloads may be one of a number of different schemas, a discriminator object can be 18 | // used to aid in serialization, deserialization, and validation. The discriminator is a specific object in a schema 19 | // which is used to inform the consumer of the document of an alternative schema based on the value associated with it. 20 | // 21 | // When using the discriminator, inline schemas will not be considered. 22 | // 23 | // v3 - https://spec.openapis.org/oas/v3.1.0#discriminator-object 24 | type Discriminator struct { 25 | PropertyName low.NodeReference[string] 26 | Mapping low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] 27 | DefaultMapping low.NodeReference[string] // OpenAPI 3.2+ defaultMapping for fallback schema 28 | KeyNode *yaml.Node 29 | RootNode *yaml.Node 30 | low.Reference 31 | low.NodeMap 32 | } 33 | 34 | // GetRootNode will return the root yaml node of the Discriminator object 35 | func (d *Discriminator) GetRootNode() *yaml.Node { 36 | return d.RootNode 37 | } 38 | 39 | // GetKeyNode will return the key yaml node of the Discriminator object 40 | func (d *Discriminator) GetKeyNode() *yaml.Node { 41 | return d.KeyNode 42 | } 43 | 44 | // FindMappingValue will return a ValueReference containing the string mapping value 45 | func (d *Discriminator) FindMappingValue(key string) *low.ValueReference[string] { 46 | for k, v := range d.Mapping.Value.FromOldest() { 47 | if k.Value == key { 48 | return &v 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | // Hash will return a consistent SHA256 Hash of the Discriminator object 55 | func (d *Discriminator) Hash() [32]byte { 56 | // Use string builder pool 57 | sb := low.GetStringBuilder() 58 | defer low.PutStringBuilder(sb) 59 | 60 | if d.PropertyName.Value != "" { 61 | sb.WriteString(d.PropertyName.Value) 62 | sb.WriteByte('|') 63 | } 64 | 65 | for v := range orderedmap.SortAlpha(d.Mapping.Value).ValuesFromOldest() { 66 | sb.WriteString(v.Value) 67 | sb.WriteByte('|') 68 | } 69 | 70 | if d.DefaultMapping.Value != "" { 71 | sb.WriteString(d.DefaultMapping.Value) 72 | sb.WriteByte('|') 73 | } 74 | 75 | return sha256.Sum256([]byte(sb.String())) 76 | } 77 | -------------------------------------------------------------------------------- /datamodel/document_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package datamodel 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewClosedDocumentConfiguration(t *testing.T) { 13 | cfg := NewDocumentConfiguration() 14 | assert.NotNil(t, cfg) 15 | } 16 | 17 | func TestNewDocumentConfiguration_DefaultSiblingRefTransformation(t *testing.T) { 18 | cfg := NewDocumentConfiguration() 19 | assert.True(t, cfg.TransformSiblingRefs, "TransformSiblingRefs should be enabled by default for OpenAPI 3.1 compliance") 20 | } 21 | 22 | func TestDocumentConfiguration_SiblingRefTransformationDisabled(t *testing.T) { 23 | cfg := NewDocumentConfiguration() 24 | cfg.TransformSiblingRefs = false 25 | assert.False(t, cfg.TransformSiblingRefs, "TransformSiblingRefs should be configurable") 26 | } 27 | 28 | func TestNewDocumentConfiguration_DefaultPropertyMerging(t *testing.T) { 29 | cfg := NewDocumentConfiguration() 30 | assert.True(t, cfg.MergeReferencedProperties, "MergeReferencedProperties should be enabled by default") 31 | assert.Equal(t, PreserveLocal, cfg.PropertyMergeStrategy, "PropertyMergeStrategy should default to PreserveLocal") 32 | } 33 | 34 | func TestDocumentConfiguration_PropertyMergeStrategies(t *testing.T) { 35 | cfg := NewDocumentConfiguration() 36 | 37 | t.Run("preserve local strategy", func(t *testing.T) { 38 | cfg.PropertyMergeStrategy = PreserveLocal 39 | assert.Equal(t, PreserveLocal, cfg.PropertyMergeStrategy) 40 | }) 41 | 42 | t.Run("overwrite with remote strategy", func(t *testing.T) { 43 | cfg.PropertyMergeStrategy = OverwriteWithRemote 44 | assert.Equal(t, OverwriteWithRemote, cfg.PropertyMergeStrategy) 45 | }) 46 | 47 | t.Run("reject conflicts strategy", func(t *testing.T) { 48 | cfg.PropertyMergeStrategy = RejectConflicts 49 | assert.Equal(t, RejectConflicts, cfg.PropertyMergeStrategy) 50 | }) 51 | } 52 | 53 | func TestDocumentConfiguration_PropertyMergingDisabled(t *testing.T) { 54 | cfg := NewDocumentConfiguration() 55 | cfg.MergeReferencedProperties = false 56 | assert.False(t, cfg.MergeReferencedProperties, "MergeReferencedProperties should be configurable") 57 | } 58 | 59 | func TestDocumentConfiguration_BackwardsCompatibility(t *testing.T) { 60 | // verify that all new features can be disabled to preserve existing behavior 61 | cfg := NewDocumentConfiguration() 62 | cfg.TransformSiblingRefs = false 63 | cfg.MergeReferencedProperties = false 64 | 65 | assert.False(t, cfg.TransformSiblingRefs) 66 | assert.False(t, cfg.MergeReferencedProperties) 67 | // when disabled, should behave exactly like pre-enhancement versions 68 | } 69 | -------------------------------------------------------------------------------- /datamodel/high/overlay/action.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/overlay" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // Action represents a high-level Overlay Action Object. 14 | // https://spec.openapis.org/overlay/v1.0.0#action-object 15 | type Action struct { 16 | Target string `json:"target,omitempty" yaml:"target,omitempty"` 17 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 18 | Update *yaml.Node `json:"update,omitempty" yaml:"update,omitempty"` 19 | Remove bool `json:"remove,omitempty" yaml:"remove,omitempty"` 20 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 21 | low *low.Action 22 | } 23 | 24 | // NewAction creates a new high-level Action instance from a low-level one. 25 | func NewAction(action *low.Action) *Action { 26 | a := new(Action) 27 | a.low = action 28 | if !action.Target.IsEmpty() { 29 | a.Target = action.Target.Value 30 | } 31 | if !action.Description.IsEmpty() { 32 | a.Description = action.Description.Value 33 | } 34 | if !action.Update.IsEmpty() { 35 | a.Update = action.Update.Value 36 | } 37 | if !action.Remove.IsEmpty() { 38 | a.Remove = action.Remove.Value 39 | } 40 | a.Extensions = high.ExtractExtensions(action.Extensions) 41 | return a 42 | } 43 | 44 | // GoLow returns the low-level Action instance used to create the high-level one. 45 | func (a *Action) GoLow() *low.Action { 46 | return a.low 47 | } 48 | 49 | // GoLowUntyped returns the low-level Action instance with no type. 50 | func (a *Action) GoLowUntyped() any { 51 | return a.low 52 | } 53 | 54 | // Render returns a YAML representation of the Action object as a byte slice. 55 | func (a *Action) Render() ([]byte, error) { 56 | return yaml.Marshal(a) 57 | } 58 | 59 | // MarshalYAML creates a ready to render YAML representation of the Action object. 60 | func (a *Action) MarshalYAML() (interface{}, error) { 61 | m := orderedmap.New[string, any]() 62 | if a.Target != "" { 63 | m.Set("target", a.Target) 64 | } 65 | if a.Description != "" { 66 | m.Set("description", a.Description) 67 | } 68 | if a.Update != nil { 69 | m.Set("update", a.Update) 70 | } 71 | if a.Remove { 72 | m.Set("remove", a.Remove) 73 | } 74 | for pair := a.Extensions.First(); pair != nil; pair = pair.Next() { 75 | m.Set(pair.Key(), pair.Value()) 76 | } 77 | return m, nil 78 | } 79 | -------------------------------------------------------------------------------- /datamodel/low/v2/security_scheme_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v2 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/index" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | "github.com/stretchr/testify/assert" 14 | "go.yaml.in/yaml/v4" 15 | ) 16 | 17 | func TestSecurityScheme_Build_Borked(t *testing.T) { 18 | yml := `scopes: 19 | $ref: break` 20 | 21 | var idxNode yaml.Node 22 | mErr := yaml.Unmarshal([]byte(yml), &idxNode) 23 | assert.NoError(t, mErr) 24 | idx := index.NewSpecIndex(&idxNode) 25 | 26 | var n SecurityScheme 27 | err := low.BuildModel(&idxNode, &n) 28 | assert.NoError(t, err) 29 | 30 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 31 | assert.Error(t, err) 32 | } 33 | 34 | func TestSecurityScheme_Build_Scopes(t *testing.T) { 35 | yml := `scopes: 36 | some:thing: here 37 | something: there` 38 | 39 | var idxNode yaml.Node 40 | mErr := yaml.Unmarshal([]byte(yml), &idxNode) 41 | assert.NoError(t, mErr) 42 | idx := index.NewSpecIndex(&idxNode) 43 | 44 | var n SecurityScheme 45 | err := low.BuildModel(&idxNode, &n) 46 | assert.NoError(t, err) 47 | 48 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 49 | assert.NoError(t, err) 50 | assert.Equal(t, 2, orderedmap.Len(n.Scopes.Value.Values)) 51 | } 52 | 53 | func TestSecurityScheme_Hash(t *testing.T) { 54 | yml := `type: secure 55 | description: a very secure thing 56 | name: securityPerson 57 | in: my heart 58 | flow: watery 59 | authorizationUrl: https://pb33f.io 60 | tokenUrl: https://pb33f.io/token 61 | scopes: 62 | fish:monkey 63 | x-beer: not for a while` 64 | 65 | var idxNode yaml.Node 66 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 67 | idx := index.NewSpecIndex(&idxNode) 68 | 69 | var n SecurityScheme 70 | _ = low.BuildModel(idxNode.Content[0], &n) 71 | _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) 72 | 73 | yml2 := `in: my heart 74 | scopes: 75 | fish:monkey 76 | name: securityPerson 77 | type: secure 78 | flow: watery 79 | description: a very secure thing 80 | tokenUrl: https://pb33f.io/token 81 | x-beer: not for a while 82 | authorizationUrl: https://pb33f.io 83 | ` 84 | 85 | var idxNode2 yaml.Node 86 | _ = yaml.Unmarshal([]byte(yml2), &idxNode2) 87 | idx2 := index.NewSpecIndex(&idxNode2) 88 | 89 | var n2 SecurityScheme 90 | _ = low.BuildModel(idxNode2.Content[0], &n2) 91 | _ = n2.Build(context.Background(), nil, idxNode2.Content[0], idx2) 92 | 93 | // hash 94 | assert.Equal(t, n.Hash(), n2.Hash()) 95 | assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) 96 | } 97 | -------------------------------------------------------------------------------- /datamodel/high/base/discriminator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | "github.com/pb33f/libopenapi/datamodel/low" 9 | lowBase "github.com/pb33f/libopenapi/datamodel/low/base" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "go.yaml.in/yaml/v4" 12 | ) 13 | 14 | // Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas 15 | // 16 | // When request bodies or response payloads may be one of a number of different schemas, a discriminator object can be 17 | // used to aid in serialization, deserialization, and validation. The discriminator is a specific object in a schema 18 | // which is used to inform the consumer of the document of an alternative schema based on the value associated with it. 19 | // 20 | // When using the discriminator, inline schemas will not be considered. 21 | // 22 | // v3 - https://spec.openapis.org/oas/v3.1.0#discriminator-object 23 | type Discriminator struct { 24 | PropertyName string `json:"propertyName,omitempty" yaml:"propertyName,omitempty"` 25 | Mapping *orderedmap.Map[string, string] `json:"mapping,omitempty" yaml:"mapping,omitempty"` 26 | DefaultMapping string `json:"defaultMapping,omitempty" yaml:"defaultMapping,omitempty"` // OpenAPI 3.2+ defaultMapping for fallback schema 27 | low *lowBase.Discriminator 28 | } 29 | 30 | // NewDiscriminator will create a new high-level Discriminator from a low-level one. 31 | func NewDiscriminator(disc *lowBase.Discriminator) *Discriminator { 32 | d := new(Discriminator) 33 | d.low = disc 34 | d.PropertyName = disc.PropertyName.Value 35 | d.Mapping = low.FromReferenceMap(disc.Mapping.Value) 36 | d.DefaultMapping = disc.DefaultMapping.Value 37 | return d 38 | } 39 | 40 | // GoLow returns the low-level Discriminator used to build the high-level one. 41 | func (d *Discriminator) GoLow() *lowBase.Discriminator { 42 | return d.low 43 | } 44 | 45 | // GoLowUntyped will return the low-level Discriminator instance that was used to create the high-level one, with no type 46 | func (d *Discriminator) GoLowUntyped() any { 47 | return d.low 48 | } 49 | 50 | // Render will return a YAML representation of the Discriminator object as a byte slice. 51 | func (d *Discriminator) Render() ([]byte, error) { 52 | return yaml.Marshal(d) 53 | } 54 | 55 | // MarshalYAML will create a ready to render YAML representation of the Discriminator object. 56 | func (d *Discriminator) MarshalYAML() (interface{}, error) { 57 | nb := high.NewNodeBuilder(d, d.low) 58 | return nb.Render(), nil 59 | } 60 | -------------------------------------------------------------------------------- /bundler/test/specs/main.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1.0" 2 | info: 3 | title: API Title 4 | version: "1.0.0" 5 | servers: [] 6 | 7 | paths: 8 | /bong: 9 | $ref: "smash/pathItem_A.yaml" 10 | /bing: 11 | $ref: "common.yaml#/components/pathItems/bing" 12 | /test/{testId}: 13 | patch: 14 | operationId: Patch Test 15 | requestBody: 16 | $ref: "clash/requestbody_A.yaml" 17 | post: 18 | operationId: Post Test 19 | requestBody: 20 | $ref: "common.yaml#/components/requestBodies/testBody" 21 | responses: 22 | 403: 23 | # this can only be inlined, there is no way to know what type of object this is. 24 | $ref: "clash/unknown.yaml" 25 | 404: 26 | description: another test 27 | content: 28 | application/json: 29 | examples: 30 | lemonTest: 31 | $ref: "examples/example_A.yaml" 32 | schema: 33 | $ref: "common.yaml#/components/schemas/lemons" 34 | 200: 35 | description: Test 36 | content: 37 | application/json: 38 | schema: 39 | $ref: "fishcake.yaml" 40 | get: 41 | operationId: GetTest 42 | callbacks: 43 | doSomething: 44 | $ref: "clash/callback_A.yaml" 45 | onData: 46 | "{$request.query.callbackUrl}/data": 47 | $ref: "common.yaml#/components/callbacks/testCallback" 48 | parameters: 49 | - $ref: "common.yaml#/components/parameters/query" 50 | - $ref: "clash/param_A.yaml" 51 | responses: 52 | 500: 53 | $ref: "smash/response_A.yaml" 54 | 404: 55 | $ref: "common.yaml#/components/responses/404" 56 | 200: 57 | links: 58 | testLink: 59 | $ref: "common.yaml#/components/links/testLink" 60 | headers: 61 | request-id: 62 | $ref: "common.yaml#/components/headers/request-id" 63 | lost-pepsi: 64 | $ref: "smash/header_A.yaml" 65 | description: Test 200 66 | content: 67 | application/json: 68 | schema: 69 | $ref: "common.yaml#/components/schemas/dtoTest" 70 | 403: 71 | $ref: "common.yaml#/components/responses/403" 72 | 73 | /test2: 74 | post: 75 | requestBody: 76 | $ref: "common.yaml#/components/requestBodies/testBody" 77 | get: 78 | operationId: GetTest2 79 | responses: 80 | 200: 81 | description: Test 82 | content: 83 | application/json: 84 | schema: 85 | $ref: "paging.yaml#/components/schemas/paging" 86 | 87 | -------------------------------------------------------------------------------- /datamodel/high/v3/encoding_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/high/base" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "github.com/stretchr/testify/assert" 13 | "go.yaml.in/yaml/v4" 14 | ) 15 | 16 | func TestEncoding_MarshalYAML(t *testing.T) { 17 | explode := true 18 | encoding := &Encoding{ 19 | ContentType: "application/json", 20 | Headers: orderedmap.ToOrderedMap(map[string]*Header{ 21 | "x-pizza-time": {Description: "oh yes please"}, 22 | }), 23 | Style: "simple", 24 | Explode: &explode, 25 | } 26 | 27 | rend, _ := encoding.Render() 28 | 29 | desired := `contentType: application/json 30 | headers: 31 | x-pizza-time: 32 | description: oh yes please 33 | style: simple 34 | explode: true` 35 | 36 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 37 | 38 | explode = false 39 | encoding.Explode = &explode 40 | rend, _ = encoding.Render() 41 | 42 | desired = `contentType: application/json 43 | headers: 44 | x-pizza-time: 45 | description: oh yes please 46 | style: simple` 47 | 48 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 49 | 50 | encoding.Explode = nil 51 | rend, _ = encoding.Render() 52 | 53 | desired = `contentType: application/json 54 | headers: 55 | x-pizza-time: 56 | description: oh yes please 57 | style: simple` 58 | 59 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 60 | 61 | encoding.Explode = &explode 62 | rend, _ = encoding.Render() 63 | 64 | desired = `contentType: application/json 65 | headers: 66 | x-pizza-time: 67 | description: oh yes please 68 | style: simple` 69 | 70 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 71 | } 72 | 73 | func TestEncoding_MarshalYAMLInlineWithContext(t *testing.T) { 74 | explode := true 75 | encoding := &Encoding{ 76 | ContentType: "application/json", 77 | Headers: orderedmap.ToOrderedMap(map[string]*Header{ 78 | "x-pizza-time": {Description: "oh yes please"}, 79 | }), 80 | Style: "simple", 81 | Explode: &explode, 82 | } 83 | 84 | ctx := base.NewInlineRenderContext() 85 | node, err := encoding.MarshalYAMLInlineWithContext(ctx) 86 | assert.NoError(t, err) 87 | assert.NotNil(t, node) 88 | 89 | rend, _ := yaml.Marshal(node) 90 | 91 | desired := `contentType: application/json 92 | headers: 93 | x-pizza-time: 94 | description: oh yes please 95 | style: simple 96 | explode: true` 97 | 98 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 99 | } 100 | -------------------------------------------------------------------------------- /what-changed/model/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low/base" 8 | v3 "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | ) 10 | 11 | // LicenseChanges represent changes to a License object that is a child of Info object. Part of an OpenAPI document 12 | type LicenseChanges struct { 13 | *PropertyChanges 14 | ExtensionChanges *ExtensionChanges `json:"extensions,omitempty" yaml:"extensions,omitempty"` 15 | } 16 | 17 | // GetAllChanges returns a slice of all changes made between License objects 18 | func (l *LicenseChanges) GetAllChanges() []*Change { 19 | if l == nil { 20 | return nil 21 | } 22 | var changes []*Change 23 | changes = append(changes, l.Changes...) 24 | if l.ExtensionChanges != nil { 25 | changes = append(changes, l.ExtensionChanges.GetAllChanges()...) 26 | } 27 | return changes 28 | } 29 | 30 | // TotalChanges represents the total number of changes made to a License instance. 31 | func (l *LicenseChanges) TotalChanges() int { 32 | if l == nil { 33 | return 0 34 | } 35 | c := l.PropertyChanges.TotalChanges() 36 | 37 | if l.ExtensionChanges != nil { 38 | c += l.ExtensionChanges.TotalChanges() 39 | } 40 | return c 41 | } 42 | 43 | // TotalBreakingChanges returns the total number of breaking changes in License objects. 44 | func (l *LicenseChanges) TotalBreakingChanges() int { 45 | if l == nil { 46 | return 0 47 | } 48 | c := l.PropertyChanges.TotalBreakingChanges() 49 | if l.ExtensionChanges != nil { 50 | c += l.ExtensionChanges.TotalBreakingChanges() 51 | } 52 | return c 53 | } 54 | 55 | // CompareLicense will check a left (original) and right (new) License object for any changes. If there 56 | // were any, a pointer to a LicenseChanges object is returned, otherwise if nothing changed - the function 57 | // returns nil. 58 | func CompareLicense(l, r *base.License) *LicenseChanges { 59 | var changes []*Change 60 | props := make([]*PropertyCheck, 0, 3) 61 | 62 | props = append(props, 63 | NewPropertyCheck(CompLicense, PropURL, 64 | l.URL.ValueNode, r.URL.ValueNode, 65 | v3.URLLabel, &changes, l, r), 66 | NewPropertyCheck(CompLicense, PropName, 67 | l.Name.ValueNode, r.Name.ValueNode, 68 | v3.NameLabel, &changes, l, r), 69 | NewPropertyCheck(CompLicense, PropIdentifier, 70 | l.Identifier.ValueNode, r.Identifier.ValueNode, 71 | v3.Identifier, &changes, l, r), 72 | ) 73 | 74 | CheckProperties(props) 75 | 76 | lc := new(LicenseChanges) 77 | lc.PropertyChanges = NewPropertyChanges(changes) 78 | lc.ExtensionChanges = CompareExtensions(l.Extensions, r.Extensions) 79 | if lc.TotalChanges() <= 0 { 80 | return nil 81 | } 82 | return lc 83 | } 84 | -------------------------------------------------------------------------------- /datamodel/high/v3/server_variable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "github.com/stretchr/testify/assert" 12 | "go.yaml.in/yaml/v4" 13 | ) 14 | 15 | func TestServerVariable_MarshalYAML(t *testing.T) { 16 | svar := &ServerVariable{ 17 | Enum: []string{"one", "two", "three"}, 18 | Description: "money day", 19 | } 20 | 21 | desired := `enum: 22 | - one 23 | - two 24 | - three 25 | description: money day` 26 | 27 | svarRend, _ := svar.Render() 28 | 29 | assert.Equal(t, desired, strings.TrimSpace(string(svarRend))) 30 | 31 | // mutate 32 | 33 | svar.Default = "is moments away" 34 | 35 | desired = `enum: 36 | - one 37 | - two 38 | - three 39 | default: is moments away 40 | description: money day` 41 | 42 | svarRend, _ = svar.Render() 43 | 44 | assert.Equal(t, desired, strings.TrimSpace(string(svarRend))) 45 | } 46 | 47 | func TestServerVariableExtension_MarshalYAML(t *testing.T) { 48 | createExtension := func(value interface{}) *yaml.Node { 49 | node := &yaml.Node{} 50 | err := node.Encode(value) 51 | if err != nil { 52 | // Trate o erro conforme necessário 53 | } 54 | return node 55 | } 56 | 57 | svar := &ServerVariable{ 58 | Extensions: orderedmap.New[string, *yaml.Node](), 59 | } 60 | transform := []map[string]interface{}{ 61 | { 62 | "type": "translate", 63 | "allowMissing": true, 64 | "translations": []map[string]string{ 65 | {"from": "pt-br", "to": "en-us"}, 66 | }, 67 | }, 68 | } 69 | svar.Extensions.Set("x-transforms", createExtension(transform)) 70 | 71 | desired := `x-transforms: 72 | - allowMissing: true 73 | translations: 74 | - from: pt-br 75 | to: en-us 76 | type: translate` 77 | 78 | svarRend, _ := svar.Render() 79 | 80 | assert.Equal(t, desired, strings.TrimSpace(string(svarRend))) 81 | 82 | // mutate 83 | 84 | svar.Default = "es-mx" 85 | transform = []map[string]interface{}{ 86 | { 87 | "type": "translate", 88 | "allowMissing": true, 89 | "translations": []map[string]string{ 90 | {"from": "es-mx", "to": "en-us"}, 91 | }, 92 | }, 93 | } 94 | svar.Extensions.Set("x-transforms", createExtension(transform)) 95 | 96 | desired = `default: es-mx 97 | x-transforms: 98 | - allowMissing: true 99 | translations: 100 | - from: es-mx 101 | to: en-us 102 | type: translate` 103 | 104 | svarRend, _ = svar.Render() 105 | 106 | assert.Equal(t, desired, strings.TrimSpace(string(svarRend))) 107 | } 108 | -------------------------------------------------------------------------------- /what-changed/model/examples.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | v2 "github.com/pb33f/libopenapi/datamodel/low/v2" 9 | "go.yaml.in/yaml/v4" 10 | ) 11 | 12 | // ExamplesChanges represents changes made between Swagger Examples objects (Not OpenAPI 3). 13 | type ExamplesChanges struct { 14 | *PropertyChanges 15 | } 16 | 17 | // GetAllChanges returns a slice of all changes made between Examples objects 18 | func (a *ExamplesChanges) GetAllChanges() []*Change { 19 | if a == nil { 20 | return nil 21 | } 22 | return a.Changes 23 | } 24 | 25 | // TotalChanges represents the total number of changes made between Example instances. 26 | func (a *ExamplesChanges) TotalChanges() int { 27 | if a == nil { 28 | return 0 29 | } 30 | return a.PropertyChanges.TotalChanges() 31 | } 32 | 33 | // TotalBreakingChanges will always return 0. Examples cannot break a contract. 34 | func (a *ExamplesChanges) TotalBreakingChanges() int { 35 | return 0 // not supported. 36 | } 37 | 38 | // CompareExamplesV2 compares two Swagger Examples objects, returning a pointer to 39 | // ExamplesChanges if anything was found. 40 | func CompareExamplesV2(l, r *v2.Examples) *ExamplesChanges { 41 | lHashes := make(map[string]string) 42 | rHashes := make(map[string]string) 43 | lValues := make(map[string]low.ValueReference[*yaml.Node]) 44 | rValues := make(map[string]low.ValueReference[*yaml.Node]) 45 | 46 | for k, v := range l.Values.FromOldest() { 47 | lHashes[k.Value] = low.GenerateHashString(v.Value) 48 | lValues[k.Value] = v 49 | } 50 | 51 | for k, v := range r.Values.FromOldest() { 52 | rHashes[k.Value] = low.GenerateHashString(v.Value) 53 | rValues[k.Value] = v 54 | } 55 | var changes []*Change 56 | 57 | // check left example hashes 58 | for k := range lHashes { 59 | rhash := rHashes[k] 60 | if rhash == "" { 61 | CreateChange(&changes, ObjectRemoved, k, 62 | lValues[k].GetValueNode(), nil, false, 63 | lValues[k].GetValue(), nil) 64 | continue 65 | } 66 | if lHashes[k] == rHashes[k] { 67 | continue 68 | } 69 | CreateChange(&changes, Modified, k, 70 | lValues[k].GetValueNode(), rValues[k].GetValueNode(), false, 71 | lValues[k].GetValue(), lValues[k].GetValue()) 72 | 73 | } 74 | 75 | // check right example hashes 76 | for k := range rHashes { 77 | lhash := lHashes[k] 78 | if lhash == "" { 79 | CreateChange(&changes, ObjectAdded, k, 80 | nil, lValues[k].GetValueNode(), false, 81 | nil, lValues[k].GetValue()) 82 | continue 83 | } 84 | } 85 | 86 | ex := new(ExamplesChanges) 87 | ex.PropertyChanges = NewPropertyChanges(changes) 88 | if ex.TotalChanges() <= 0 { 89 | return nil 90 | } 91 | return ex 92 | } 93 | -------------------------------------------------------------------------------- /index/map_index_nodes_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "os" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/pb33f/jsonpath/pkg/jsonpath" 12 | "github.com/pb33f/libopenapi/utils" 13 | "github.com/stretchr/testify/assert" 14 | "go.yaml.in/yaml/v4" 15 | ) 16 | 17 | func TestSpecIndex_MapNodes(t *testing.T) { 18 | petstore, _ := os.ReadFile("../test_specs/petstorev3.json") 19 | var rootNode yaml.Node 20 | _ = yaml.Unmarshal(petstore, &rootNode) 21 | 22 | index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig()) 23 | 24 | <-index.nodeMapCompleted 25 | 26 | // look up a node and make sure they match exactly (same pointer) 27 | path, _ := jsonpath.NewPath("$.paths['/pet'].put") 28 | nodes := path.Query(&rootNode) 29 | 30 | keyNode, valueNode := utils.FindKeyNodeTop("operationId", nodes[0].Content) 31 | mappedKeyNode, _ := index.GetNode(keyNode.Line, keyNode.Column) 32 | mappedValueNode, _ := index.GetNode(valueNode.Line, valueNode.Column) 33 | 34 | assert.Equal(t, keyNode, mappedKeyNode) 35 | assert.Equal(t, valueNode, mappedValueNode) 36 | 37 | // make sure the pointers are the same 38 | p1 := reflect.ValueOf(keyNode).Pointer() 39 | p2 := reflect.ValueOf(mappedKeyNode).Pointer() 40 | assert.Equal(t, p1, p2) 41 | 42 | // check missing line 43 | var ok bool 44 | mappedKeyNode, ok = index.GetNode(999, 999) 45 | assert.False(t, ok) 46 | assert.Nil(t, mappedKeyNode) 47 | 48 | mappedKeyNode, ok = index.GetNode(12, 999) 49 | assert.False(t, ok) 50 | assert.Nil(t, mappedKeyNode) 51 | 52 | index.nodeMap[15] = nil 53 | mappedKeyNode, ok = index.GetNode(15, 999) 54 | assert.False(t, ok) 55 | assert.Nil(t, mappedKeyNode) 56 | } 57 | 58 | func BenchmarkSpecIndex_MapNodes(b *testing.B) { 59 | petstore, _ := os.ReadFile("../test_specs/petstorev3.json") 60 | var rootNode yaml.Node 61 | _ = yaml.Unmarshal(petstore, &rootNode) 62 | path, _ := jsonpath.NewPath("$.paths['/pet'].put") 63 | 64 | for i := 0; i < b.N; i++ { 65 | 66 | index := NewSpecIndexWithConfig(&rootNode, CreateOpenAPIIndexConfig()) 67 | 68 | <-index.nodeMapCompleted 69 | 70 | // look up a node and make sure they match exactly (same pointer) 71 | nodes := path.Query(&rootNode) 72 | 73 | keyNode, valueNode := utils.FindKeyNodeTop("operationId", nodes[0].Content) 74 | mappedKeyNode, _ := index.GetNode(keyNode.Line, keyNode.Column) 75 | mappedValueNode, _ := index.GetNode(valueNode.Line, valueNode.Column) 76 | 77 | assert.Equal(b, keyNode, mappedKeyNode) 78 | assert.Equal(b, valueNode, mappedValueNode) 79 | 80 | // make sure the pointers are the same 81 | p1 := reflect.ValueOf(keyNode).Pointer() 82 | p2 := reflect.ValueOf(mappedKeyNode).Pointer() 83 | assert.Equal(b, p1, p2) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /datamodel/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package datamodel contains two sets of models, high and low. 5 | // 6 | // The low-level (or plumbing) models are designed to capture every single detail about specification, including 7 | // all lines, columns, positions, tags, comments and essentially everything you would ever want to know. 8 | // Positions of every key, value and meta-data that is lost when blindly un-marshaling JSON/YAML into a struct. 9 | // 10 | // The high model (porcelain) is a much simpler representation of the low model, keys are simple strings and indices 11 | // are numbers. When developing consumers of the model, the high model is really what you want to use instead of the 12 | // low model, it's much easier to navigate and is designed for easy consumption. 13 | // 14 | // The high model requires the low model to be built. Every high model has a 'GoLow' method that allows the consumer 15 | // to 'drop down' from the porcelain API to the plumbing API, which gives instant access to everything low. 16 | package datamodel 17 | 18 | import ( 19 | _ "embed" 20 | ) 21 | 22 | // Constants used by utilities to determine the version of OpenAPI that we're referring to. 23 | const ( 24 | // OAS2 represents Swagger Documents 25 | OAS2 = "oas2" 26 | 27 | // OAS3 represents OpenAPI 3.0+ Documents 28 | OAS3 = "oas3" 29 | 30 | // OAS31 represents OpenAPI 3.1 Documents 31 | OAS31 = "oas3_1" 32 | 33 | // OAS32 represents OpenAPI 3.2+ Documents 34 | OAS32 = "oas3_2" 35 | ) 36 | 37 | // OpenAPI3SchemaData is an embedded version of the OpenAPI 3 Schema 38 | // 39 | //go:embed schemas/oas3-schema.json 40 | var OpenAPI3SchemaData string // embedded OAS3 schema 41 | 42 | // OpenAPI31SchemaData is an embedded version of the OpenAPI 3.1 Schema 43 | // 44 | //go:embed schemas/oas31-schema.json 45 | var OpenAPI31SchemaData string // embedded OAS31 schema 46 | 47 | //go:embed schemas/oas32-schema.json 48 | var OpenAPI32SchemaData string // embedded OAS32 schema 49 | 50 | // OpenAPI2SchemaData is an embedded version of the OpenAPI 2 (Swagger) Schema 51 | // 52 | //go:embed schemas/swagger2-schema.json 53 | var OpenAPI2SchemaData string // embedded OAS3 schema 54 | 55 | // OAS3_1Format defines documents that can only be version 3.1 56 | var OAS3_1Format = []string{OAS31} 57 | 58 | var OAS3_2Format = []string{OAS32} 59 | 60 | // OAS3Format defines documents that can only be version 3.0 61 | var OAS3Format = []string{OAS3} 62 | 63 | // OAS3AllFormat defines documents that compose all 3+ versions 64 | var OAS3AllFormat = []string{OAS3, OAS31, OAS32} 65 | 66 | // OAS2Format defines documents that compose swagger documents (version 2.0) 67 | var OAS2Format = []string{OAS2} 68 | 69 | // AllFormats defines all versions of OpenAPI 70 | var AllFormats = []string{OAS3, OAS31, OAS32, OAS2} 71 | -------------------------------------------------------------------------------- /datamodel/low/base/contact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "context" 8 | "crypto/sha256" 9 | 10 | "github.com/pb33f/libopenapi/datamodel/low" 11 | "github.com/pb33f/libopenapi/index" 12 | "github.com/pb33f/libopenapi/orderedmap" 13 | "go.yaml.in/yaml/v4" 14 | ) 15 | 16 | // Contact represents a low-level representation of the Contact definitions found at 17 | // 18 | // v2 - https://swagger.io/specification/v2/#contactObject 19 | // v3 - https://spec.openapis.org/oas/v3.1.0#contact-object 20 | type Contact struct { 21 | Name low.NodeReference[string] 22 | URL low.NodeReference[string] 23 | Email low.NodeReference[string] 24 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 25 | KeyNode *yaml.Node 26 | RootNode *yaml.Node 27 | index *index.SpecIndex 28 | context context.Context 29 | *low.Reference 30 | low.NodeMap 31 | } 32 | 33 | func (c *Contact) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { 34 | c.KeyNode = keyNode 35 | c.RootNode = root 36 | c.Reference = new(low.Reference) 37 | c.Nodes = low.ExtractNodes(ctx, root) 38 | c.Extensions = low.ExtractExtensions(root) 39 | c.context = ctx 40 | c.index = idx 41 | return nil 42 | } 43 | 44 | // GetIndex will return the index.SpecIndex instance attached to the Contact object 45 | func (c *Contact) GetIndex() *index.SpecIndex { 46 | return c.index 47 | } 48 | 49 | // GetContext will return the context.Context instance used when building the Contact object 50 | func (c *Contact) GetContext() context.Context { 51 | return c.context 52 | } 53 | 54 | // GetRootNode will return the root yaml node of the Contact object 55 | func (c *Contact) GetRootNode() *yaml.Node { 56 | return c.RootNode 57 | } 58 | 59 | // GetKeyNode will return the key yaml node of the Contact object 60 | func (c *Contact) GetKeyNode() *yaml.Node { 61 | return c.KeyNode 62 | } 63 | 64 | // Hash will return a consistent SHA256 Hash of the Contact object 65 | func (c *Contact) Hash() [32]byte { 66 | // Use string builder pool 67 | sb := low.GetStringBuilder() 68 | defer low.PutStringBuilder(sb) 69 | 70 | if !c.Name.IsEmpty() { 71 | sb.WriteString(c.Name.Value) 72 | sb.WriteByte('|') 73 | } 74 | if !c.URL.IsEmpty() { 75 | sb.WriteString(c.URL.Value) 76 | sb.WriteByte('|') 77 | } 78 | if !c.Email.IsEmpty() { 79 | sb.WriteString(c.Email.Value) 80 | sb.WriteByte('|') 81 | } 82 | 83 | // Note: Extensions are not included in the hash for Contact 84 | return sha256.Sum256([]byte(sb.String())) 85 | } 86 | 87 | // GetExtensions returns all extensions for Contact 88 | func (c *Contact) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 89 | return c.Extensions 90 | } 91 | -------------------------------------------------------------------------------- /datamodel/high/overlay/overlay.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package overlay 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/overlay" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // Overlay represents a high-level OpenAPI Overlay document. 14 | // https://spec.openapis.org/overlay/v1.0.0 15 | type Overlay struct { 16 | Overlay string `json:"overlay,omitempty" yaml:"overlay,omitempty"` 17 | Info *Info `json:"info,omitempty" yaml:"info,omitempty"` 18 | Extends string `json:"extends,omitempty" yaml:"extends,omitempty"` 19 | Actions []*Action `json:"actions,omitempty" yaml:"actions,omitempty"` 20 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 21 | low *low.Overlay 22 | } 23 | 24 | // NewOverlay creates a new high-level Overlay instance from a low-level one. 25 | func NewOverlay(overlay *low.Overlay) *Overlay { 26 | o := new(Overlay) 27 | o.low = overlay 28 | if !overlay.Overlay.IsEmpty() { 29 | o.Overlay = overlay.Overlay.Value 30 | } 31 | if !overlay.Info.IsEmpty() { 32 | o.Info = NewInfo(overlay.Info.Value) 33 | } 34 | if !overlay.Extends.IsEmpty() { 35 | o.Extends = overlay.Extends.Value 36 | } 37 | if !overlay.Actions.IsEmpty() { 38 | actions := make([]*Action, 0, len(overlay.Actions.Value)) 39 | for _, action := range overlay.Actions.Value { 40 | actions = append(actions, NewAction(action.Value)) 41 | } 42 | o.Actions = actions 43 | } 44 | o.Extensions = high.ExtractExtensions(overlay.Extensions) 45 | return o 46 | } 47 | 48 | // GoLow returns the low-level Overlay instance used to create the high-level one. 49 | func (o *Overlay) GoLow() *low.Overlay { 50 | return o.low 51 | } 52 | 53 | // GoLowUntyped returns the low-level Overlay instance with no type. 54 | func (o *Overlay) GoLowUntyped() any { 55 | return o.low 56 | } 57 | 58 | // Render returns a YAML representation of the Overlay object as a byte slice. 59 | func (o *Overlay) Render() ([]byte, error) { 60 | return yaml.Marshal(o) 61 | } 62 | 63 | // MarshalYAML creates a ready to render YAML representation of the Overlay object. 64 | func (o *Overlay) MarshalYAML() (interface{}, error) { 65 | m := orderedmap.New[string, any]() 66 | if o.Overlay != "" { 67 | m.Set("overlay", o.Overlay) 68 | } 69 | if o.Info != nil { 70 | m.Set("info", o.Info) 71 | } 72 | if o.Extends != "" { 73 | m.Set("extends", o.Extends) 74 | } 75 | if len(o.Actions) > 0 { 76 | m.Set("actions", o.Actions) 77 | } 78 | for pair := o.Extensions.First(); pair != nil; pair = pair.Next() { 79 | m.Set(pair.Key(), pair.Value()) 80 | } 81 | return m, nil 82 | } 83 | -------------------------------------------------------------------------------- /what-changed/model/scopes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package model 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/low" 8 | v2 "github.com/pb33f/libopenapi/datamodel/low/v2" 9 | v3 "github.com/pb33f/libopenapi/datamodel/low/v3" 10 | ) 11 | 12 | // ScopesChanges represents changes between two Swagger Scopes Objects 13 | type ScopesChanges struct { 14 | *PropertyChanges 15 | ExtensionChanges *ExtensionChanges `json:"extensions,omitempty" yaml:"extensions,omitempty"` 16 | } 17 | 18 | // GetAllChanges returns a slice of all changes made between Scopes objects 19 | func (s *ScopesChanges) GetAllChanges() []*Change { 20 | if s == nil { 21 | return nil 22 | } 23 | var changes []*Change 24 | changes = append(changes, s.Changes...) 25 | if s.ExtensionChanges != nil { 26 | changes = append(changes, s.ExtensionChanges.GetAllChanges()...) 27 | } 28 | return changes 29 | } 30 | 31 | // TotalChanges returns the total changes found between two Swagger Scopes objects. 32 | func (s *ScopesChanges) TotalChanges() int { 33 | if s == nil { 34 | return 0 35 | } 36 | c := s.PropertyChanges.TotalChanges() 37 | if s.ExtensionChanges != nil { 38 | c += s.ExtensionChanges.TotalChanges() 39 | } 40 | return c 41 | } 42 | 43 | // TotalBreakingChanges returns the total number of breaking changes between two Swagger Scopes objects. 44 | func (s *ScopesChanges) TotalBreakingChanges() int { 45 | return s.PropertyChanges.TotalBreakingChanges() 46 | } 47 | 48 | // CompareScopes compares a left and right Swagger Scopes objects for changes. If anything is found, returns 49 | // a pointer to ScopesChanges, or returns nil if nothing is found. 50 | func CompareScopes(l, r *v2.Scopes) *ScopesChanges { 51 | if low.AreEqual(l, r) { 52 | return nil 53 | } 54 | var changes []*Change 55 | for k, v := range l.Values.FromOldest() { 56 | if r != nil && r.FindScope(k.Value) == nil { 57 | CreateChange(&changes, ObjectRemoved, v3.Scopes, 58 | v.ValueNode, nil, BreakingRemoved(CompOAuthFlow, PropScopes), 59 | k.Value, nil) 60 | continue 61 | } 62 | if r != nil && r.FindScope(k.Value) != nil { 63 | if v.Value != r.FindScope(k.Value).Value { 64 | CreateChange(&changes, Modified, v3.Scopes, 65 | v.ValueNode, r.FindScope(k.Value).ValueNode, BreakingModified(CompOAuthFlow, PropScopes), 66 | v.Value, r.FindScope(k.Value).Value) 67 | } 68 | } 69 | } 70 | for k, v := range r.Values.FromOldest() { 71 | if l != nil && l.FindScope(k.Value) == nil { 72 | CreateChange(&changes, ObjectAdded, v3.Scopes, 73 | nil, v.ValueNode, BreakingAdded(CompOAuthFlow, PropScopes), 74 | nil, k.Value) 75 | } 76 | } 77 | 78 | sc := new(ScopesChanges) 79 | sc.PropertyChanges = NewPropertyChanges(changes) 80 | sc.ExtensionChanges = CompareExtensions(l.Extensions, r.Extensions) 81 | return sc 82 | } 83 | -------------------------------------------------------------------------------- /datamodel/high/v3/oauth_flows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "github.com/pb33f/libopenapi/datamodel/high" 8 | low "github.com/pb33f/libopenapi/datamodel/low/v3" 9 | "github.com/pb33f/libopenapi/orderedmap" 10 | "go.yaml.in/yaml/v4" 11 | ) 12 | 13 | // OAuthFlows represents a high-level OpenAPI 3+ OAuthFlows object that is backed by a low-level one. 14 | // - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object 15 | type OAuthFlows struct { 16 | Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` 17 | Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` 18 | ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` 19 | AuthorizationCode *OAuthFlow `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` 20 | Device *OAuthFlow `json:"device,omitempty" yaml:"device,omitempty"` // OpenAPI 3.2+ device flow 21 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 22 | low *low.OAuthFlows 23 | } 24 | 25 | // NewOAuthFlows creates a new high-level OAuthFlows instance from a low-level one. 26 | func NewOAuthFlows(flows *low.OAuthFlows) *OAuthFlows { 27 | o := new(OAuthFlows) 28 | o.low = flows 29 | if !flows.Implicit.IsEmpty() { 30 | o.Implicit = NewOAuthFlow(flows.Implicit.Value) 31 | } 32 | if !flows.Password.IsEmpty() { 33 | o.Password = NewOAuthFlow(flows.Password.Value) 34 | } 35 | if !flows.ClientCredentials.IsEmpty() { 36 | o.ClientCredentials = NewOAuthFlow(flows.ClientCredentials.Value) 37 | } 38 | if !flows.AuthorizationCode.IsEmpty() { 39 | o.AuthorizationCode = NewOAuthFlow(flows.AuthorizationCode.Value) 40 | } 41 | if !flows.Device.IsEmpty() { 42 | o.Device = NewOAuthFlow(flows.Device.Value) 43 | } 44 | o.Extensions = high.ExtractExtensions(flows.Extensions) 45 | return o 46 | } 47 | 48 | // GoLow returns the low-level OAuthFlows instance used to create the high-level one. 49 | func (o *OAuthFlows) GoLow() *low.OAuthFlows { 50 | return o.low 51 | } 52 | 53 | // GoLowUntyped will return the low-level OAuthFlows instance that was used to create the high-level one, with no type 54 | func (o *OAuthFlows) GoLowUntyped() any { 55 | return o.low 56 | } 57 | 58 | // Render will return a YAML representation of the OAuthFlows object as a byte slice. 59 | func (o *OAuthFlows) Render() ([]byte, error) { 60 | return yaml.Marshal(o) 61 | } 62 | 63 | // MarshalYAML will create a ready to render YAML representation of the OAuthFlows object. 64 | func (o *OAuthFlows) MarshalYAML() (interface{}, error) { 65 | nb := high.NewNodeBuilder(o, o.low) 66 | return nb.Render(), nil 67 | } 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU= 13 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= 14 | github.com/pb33f/jsonpath v0.7.0 h1:3oG6yu1RqNoMZpqnRjBMqi8fSIXWoDAKDrsB0QGTcoU= 15 | github.com/pb33f/jsonpath v0.7.0/go.mod h1:/+JlSIjWA2ijMVYGJ3IQPF4Q1nLMYbUTYNdk0exCDPQ= 16 | github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ= 17 | github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 21 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 22 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 23 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 24 | go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= 25 | go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= 26 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 27 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 30 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | --------------------------------------------------------------------------------