├── .github ├── FUNDING.yml ├── dependabot.yml ├── sponsors │ ├── apideck-dark.png │ ├── apideck-light.png │ ├── bump-sh-dark.png │ ├── bump-sh-light.png │ ├── scalar-dark.png │ ├── scalar-light.png │ ├── speakeasy-github-sponsor-dark.svg │ ├── speakeasy-github-sponsor-light.svg │ └── speakeasy.png └── workflows │ └── build.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bundler ├── bundler.go ├── bundler_composer.go ├── bundler_composer_test.go ├── bundler_test.go ├── composer_functions.go ├── composer_functions_test.go ├── detect_type.go ├── detect_type_test.go └── test │ └── specs │ ├── bundled.yaml │ ├── clash │ ├── callback_A.yaml │ ├── fishcake.yaml │ ├── paging.yaml │ ├── param_A.yaml │ ├── requestbody_A.yaml │ └── unknown.yaml │ ├── common.yaml │ ├── error.yaml │ ├── examples │ └── example_A.yaml │ ├── fishcake.yaml │ ├── main.yaml │ ├── paging.yaml │ └── smash │ ├── header_A.yaml │ ├── link_A.yaml │ ├── paging.yaml │ ├── pathItem_A.yaml │ └── response_A.yaml ├── datamodel ├── constants.go ├── document_config.go ├── document_config_test.go ├── high │ ├── base │ │ ├── base.go │ │ ├── contact.go │ │ ├── contact_test.go │ │ ├── discriminator.go │ │ ├── discriminator_test.go │ │ ├── dynamic_value.go │ │ ├── dynamic_value_test.go │ │ ├── example.go │ │ ├── example_test.go │ │ ├── external_doc.go │ │ ├── external_doc_test.go │ │ ├── info.go │ │ ├── info_test.go │ │ ├── licence_test.go │ │ ├── license.go │ │ ├── schema.go │ │ ├── schema_proxy.go │ │ ├── schema_proxy_test.go │ │ ├── schema_test.go │ │ ├── security_requirement.go │ │ ├── security_requirement_test.go │ │ ├── tag.go │ │ ├── tag_test.go │ │ ├── xml.go │ │ └── xml_test.go │ ├── node_builder.go │ ├── node_builder_test.go │ ├── nodes │ │ └── nodeentry.go │ ├── shared.go │ ├── shared_test.go │ ├── v2 │ │ ├── asyncresult.go │ │ ├── definitions.go │ │ ├── examples.go │ │ ├── header.go │ │ ├── items.go │ │ ├── operation.go │ │ ├── parameter.go │ │ ├── parameter_definitions.go │ │ ├── path_item.go │ │ ├── path_item_test.go │ │ ├── paths.go │ │ ├── response.go │ │ ├── responses.go │ │ ├── responses_definitions.go │ │ ├── scopes.go │ │ ├── security_definitions.go │ │ ├── security_scheme.go │ │ ├── swagger.go │ │ └── swagger_test.go │ └── v3 │ │ ├── asyncresult.go │ │ ├── callback.go │ │ ├── callback_test.go │ │ ├── components.go │ │ ├── components_test.go │ │ ├── document.go │ │ ├── document_test.go │ │ ├── encoding.go │ │ ├── encoding_test.go │ │ ├── header.go │ │ ├── header_test.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── media_type.go │ │ ├── media_type_test.go │ │ ├── oauth_flow.go │ │ ├── oauth_flow_test.go │ │ ├── oauth_flows.go │ │ ├── oauth_flows_test.go │ │ ├── operation.go │ │ ├── operation_test.go │ │ ├── package_test.go │ │ ├── parameter.go │ │ ├── parameter_test.go │ │ ├── path_item.go │ │ ├── path_item_test.go │ │ ├── paths.go │ │ ├── paths_test.go │ │ ├── request_body.go │ │ ├── request_body_test.go │ │ ├── response.go │ │ ├── response_test.go │ │ ├── responses.go │ │ ├── responses_test.go │ │ ├── security_scheme.go │ │ ├── security_scheme_test.go │ │ ├── server.go │ │ ├── server_test.go │ │ ├── server_variable.go │ │ └── server_variable_test.go ├── low │ ├── base │ │ ├── base.go │ │ ├── constants.go │ │ ├── contact.go │ │ ├── contact_test.go │ │ ├── context.go │ │ ├── context_test.go │ │ ├── discriminator.go │ │ ├── discriminator_test.go │ │ ├── example.go │ │ ├── example_test.go │ │ ├── external_doc.go │ │ ├── external_doc_test.go │ │ ├── info.go │ │ ├── info_test.go │ │ ├── license.go │ │ ├── license_test.go │ │ ├── schema.go │ │ ├── schema_proxy.go │ │ ├── schema_proxy_test.go │ │ ├── schema_test.go │ │ ├── security_requirement.go │ │ ├── security_requirement_test.go │ │ ├── tag.go │ │ ├── tag_test.go │ │ ├── xml.go │ │ └── xml_test.go │ ├── extraction_functions.go │ ├── extraction_functions_test.go │ ├── low.go │ ├── model_builder.go │ ├── model_builder_test.go │ ├── model_interfaces.go │ ├── node_map.go │ ├── node_map_test.go │ ├── reference.go │ ├── reference_test.go │ ├── v2 │ │ ├── constants.go │ │ ├── definitions.go │ │ ├── definitions_test.go │ │ ├── examples.go │ │ ├── examples_test.go │ │ ├── header.go │ │ ├── header_test.go │ │ ├── items.go │ │ ├── items_test.go │ │ ├── operation.go │ │ ├── operation_test.go │ │ ├── package_test.go │ │ ├── parameter.go │ │ ├── parameter_test.go │ │ ├── path_item.go │ │ ├── path_item_test.go │ │ ├── paths.go │ │ ├── paths_test.go │ │ ├── response.go │ │ ├── response_test.go │ │ ├── responses.go │ │ ├── responses_test.go │ │ ├── scopes.go │ │ ├── scopes_test.go │ │ ├── security_scheme.go │ │ ├── security_scheme_test.go │ │ ├── swagger.go │ │ └── swagger_test.go │ └── v3 │ │ ├── callback.go │ │ ├── callback_test.go │ │ ├── components.go │ │ ├── components_test.go │ │ ├── constants.go │ │ ├── create_document.go │ │ ├── create_document_test.go │ │ ├── document.go │ │ ├── encoding.go │ │ ├── encoding_test.go │ │ ├── examples_test.go │ │ ├── header.go │ │ ├── header_test.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── media_type.go │ │ ├── media_type_test.go │ │ ├── oauth_flows.go │ │ ├── oauth_flows_test.go │ │ ├── operation.go │ │ ├── operation_test.go │ │ ├── parameter.go │ │ ├── parameter_test.go │ │ ├── path_item.go │ │ ├── path_item_test.go │ │ ├── paths.go │ │ ├── paths_test.go │ │ ├── request_body.go │ │ ├── request_body_test.go │ │ ├── response.go │ │ ├── response_test.go │ │ ├── responses.go │ │ ├── security_scheme.go │ │ ├── security_scheme_test.go │ │ ├── server.go │ │ ├── server_test.go │ │ └── server_variable.go ├── schemas │ ├── oas3-schema.json │ ├── oas31-schema.json │ └── swagger2-schema.json ├── spec_info.go ├── spec_info_test.go ├── translate.go └── translate_test.go ├── document.go ├── document_examples_test.go ├── document_iteration_test.go ├── document_test.go ├── go.mod ├── go.sum ├── index ├── cache.go ├── cache_test.go ├── cache_test │ └── docusign_test.go ├── circular_reference_result.go ├── circular_reference_result_test.go ├── extract_refs.go ├── extract_refs_test.go ├── find_component.go ├── find_component_test.go ├── index_model.go ├── index_model_test.go ├── index_utils.go ├── map_index_nodes.go ├── map_index_nodes_test.go ├── resolver.go ├── resolver_test.go ├── rolodex.go ├── rolodex_file.go ├── rolodex_file_loader.go ├── rolodex_file_loader_test.go ├── rolodex_ref_extractor.go ├── rolodex_ref_extractor_test.go ├── rolodex_remote_loader.go ├── rolodex_remote_loader_test.go ├── rolodex_test.go ├── rolodex_test_data │ ├── components.yaml │ ├── dir1 │ │ ├── components.yaml │ │ ├── subdir1 │ │ │ └── shared.yaml │ │ └── utils │ │ │ └── utils.yaml │ ├── dir2 │ │ ├── components.yaml │ │ ├── subdir2 │ │ │ └── shared.yaml │ │ └── utils │ │ │ └── utils.yaml │ ├── doc1.yaml │ ├── doc2.yaml │ ├── operations.yaml │ └── paths │ │ └── paths.yaml ├── search_index.go ├── search_index_test.go ├── search_rolodex.go ├── search_rolodex_test.go ├── spec_index.go ├── spec_index_test.go ├── utility_methods.go └── utility_methods_test.go ├── json ├── json.go └── json_test.go ├── libopenapi-logo.png ├── orderedmap ├── builder.go ├── builder_test.go ├── orderedmap.go └── orderedmap_test.go ├── renderer ├── mock_generator.go ├── mock_generator_examples_test.go ├── mock_generator_test.go ├── schema_renderer.go └── schema_renderer_test.go ├── test_specs ├── advancecallbackreferences │ ├── min-callbacks.yaml │ ├── min-components.yaml │ └── min-openapi.yaml ├── all-the-components.yaml ├── asana.yaml ├── badref-burgershop.openapi.yaml ├── burgershop.openapi-modified.yaml ├── burgershop.openapi.yaml ├── circular-tests.yaml ├── digitalocean.yaml ├── docusignv3.1.json ├── first.yaml ├── k8s.json ├── minimal_remote_refs │ ├── openapi.yaml │ └── schemas │ │ └── components.openapi.yaml ├── mixedref-burgershop.openapi.yaml ├── nested_files │ ├── components │ │ ├── foo.yaml │ │ ├── parameters │ │ │ ├── header │ │ │ │ └── page-size.yaml │ │ │ └── query │ │ │ │ └── $select.yaml │ │ ├── requestBodies │ │ │ └── AccountModel.yaml │ │ ├── responses │ │ │ └── Unspecified200.yaml │ │ └── schemas │ │ │ └── AccountModel.yaml │ ├── openapi-bundled.yaml │ ├── openapi-issue-418.yaml │ ├── openapi.yaml │ └── paths │ │ └── v1_Accounts.yaml ├── nullable-examples.openapi.yaml ├── petstorev2-badref.json ├── petstorev2-complete-modified.yaml ├── petstorev2-complete.yaml ├── petstorev2.json ├── petstorev3.json ├── redocly-starter.yaml ├── ref-followed.yaml ├── roundtrip.json ├── roundtrip.yaml ├── second.yaml ├── single-definition.yaml ├── speakeasy-components.yaml ├── speakeasy-test.yaml ├── stripe-old.yaml ├── stripe.yaml ├── swagger-circular-tests.yaml ├── swagger-invalid-recursive-model.yaml ├── swagger-valid-recursive-model.yaml ├── third.yaml ├── xsoar.json └── yaml-anchor.yaml ├── utils ├── nodes.go ├── nodes_test.go ├── type_check.go ├── type_check_test.go ├── unwrap_errors.go ├── unwrap_errors_test.go ├── utils.go ├── utils_test.go ├── windows_drive.go └── windows_drive_test.go └── what-changed ├── changed.go ├── model ├── callback.go ├── callback_test.go ├── change_types.go ├── change_types_test.go ├── comparison_functions.go ├── comparison_functions_test.go ├── components.go ├── components_test.go ├── contact.go ├── contact_test.go ├── discriminator.go ├── discriminator_test.go ├── document.go ├── document_flat.go ├── document_test.go ├── encoding.go ├── encoding_test.go ├── example.go ├── example_test.go ├── examples.go ├── examples_test.go ├── extensions.go ├── extensions_test.go ├── external_docs.go ├── external_docs_test.go ├── header.go ├── header_test.go ├── info.go ├── info_test.go ├── items.go ├── items_test.go ├── license.go ├── license_test.go ├── link.go ├── link_test.go ├── media_type.go ├── media_type_test.go ├── oauth_flows.go ├── oauth_flows_test.go ├── operation.go ├── operation_test.go ├── parameter.go ├── parameter_test.go ├── path_item.go ├── path_item_test.go ├── paths.go ├── paths_test.go ├── request_body.go ├── request_body_test.go ├── response.go ├── response_test.go ├── responses.go ├── responses_test.go ├── schema.go ├── schema_test.go ├── scopes.go ├── scopes_test.go ├── security_requirement.go ├── security_requirement_test.go ├── security_scheme.go ├── security_scheme_test.go ├── server.go ├── server_test.go ├── server_variable.go ├── server_variable_test.go ├── tags.go ├── tags_test.go ├── xml.go └── xml_test.go ├── reports ├── summary.go ├── summary_test.go └── types.go ├── what_changed.go └── what_changed_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: daveshanley -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/sponsors/apideck-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/apideck-dark.png -------------------------------------------------------------------------------- /.github/sponsors/apideck-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/apideck-light.png -------------------------------------------------------------------------------- /.github/sponsors/bump-sh-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/bump-sh-dark.png -------------------------------------------------------------------------------- /.github/sponsors/bump-sh-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/bump-sh-light.png -------------------------------------------------------------------------------- /.github/sponsors/scalar-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/scalar-dark.png -------------------------------------------------------------------------------- /.github/sponsors/scalar-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/scalar-light.png -------------------------------------------------------------------------------- /.github/sponsors/speakeasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/.github/sponsors/speakeasy.png -------------------------------------------------------------------------------- /.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: | 55 | go get github.com/axw/gocov/gocov 56 | go get github.com/AlekSi/gocov-xml 57 | go install github.com/axw/gocov/gocov 58 | go install github.com/AlekSi/gocov-xml 59 | - run: | 60 | go test -v -coverprofile cover.out ./... 61 | gocov convert cover.out | gocov-xml > coverage.xml 62 | - uses: codecov/codecov-action@v4 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | files: ./coverage.xml 66 | flags: unittests 67 | fail_ci_if_error: false 68 | verbose: true 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | test-operation.yaml 2 | .idea/ 3 | *.iml -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bundler/test/specs/clash/callback_A.yaml: -------------------------------------------------------------------------------- 1 | '{$request.query.queryUrl}': 2 | post: 3 | requestBody: 4 | description: Callback payload -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /bundler/test/specs/clash/requestbody_A.yaml: -------------------------------------------------------------------------------- 1 | required: true 2 | content: 3 | application/json: 4 | schema: 5 | $ref: "./paging.yaml#/components/schemas/dtoTest" -------------------------------------------------------------------------------- /bundler/test/specs/clash/unknown.yaml: -------------------------------------------------------------------------------- 1 | description: could be meat, could be cake. only option is to inline. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bundler/test/specs/examples/example_A.yaml: -------------------------------------------------------------------------------- 1 | description: a test example 2 | value: 3 | cakes: nice 4 | iceCream: good -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bundler/test/specs/smash/header_A.yaml: -------------------------------------------------------------------------------- 1 | schema: 2 | type: object 3 | description: this is a header -------------------------------------------------------------------------------- /bundler/test/specs/smash/link_A.yaml: -------------------------------------------------------------------------------- 1 | operationRef: updateCalendarRef 2 | operationId: updateCalendar 3 | description: a test link 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bundler/test/specs/smash/pathItem_A.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | operationId: somethingHere 3 | description: a test get -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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 | 34 | // OpenAPI3SchemaData is an embedded version of the OpenAPI 3 Schema 35 | // 36 | //go:embed schemas/oas3-schema.json 37 | var OpenAPI3SchemaData string // embedded OAS3 schema 38 | 39 | // OpenAPI31SchemaData is an embedded version of the OpenAPI 3.1 Schema 40 | // 41 | //go:embed schemas/oas31-schema.json 42 | var OpenAPI31SchemaData string // embedded OAS31 schema 43 | 44 | // OpenAPI2SchemaData is an embedded version of the OpenAPI 2 (Swagger) Schema 45 | // 46 | //go:embed schemas/swagger2-schema.json 47 | var OpenAPI2SchemaData string // embedded OAS3 schema 48 | 49 | // OAS3_1Format defines documents that can only be version 3.1 50 | var OAS3_1Format = []string{OAS31} 51 | 52 | // OAS3Format defines documents that can only be version 3.0 53 | var OAS3Format = []string{OAS3} 54 | 55 | // OAS3AllFormat defines documents that compose all 3+ versions 56 | var OAS3AllFormat = []string{OAS3, OAS31} 57 | 58 | // OAS2Format defines documents that compose swagger documnets (version 2.0) 59 | var OAS2Format = []string{OAS2} 60 | 61 | // AllFormats defines all versions of OpenAPI 62 | var AllFormats = []string{OAS3, OAS31, OAS2} 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | low *lowBase.Discriminator 27 | } 28 | 29 | // NewDiscriminator will create a new high-level Discriminator from a low-level one. 30 | func NewDiscriminator(disc *lowBase.Discriminator) *Discriminator { 31 | d := new(Discriminator) 32 | d.low = disc 33 | d.PropertyName = disc.PropertyName.Value 34 | d.Mapping = low.FromReferenceMap(disc.Mapping.Value) 35 | return d 36 | } 37 | 38 | // GoLow returns the low-level Discriminator used to build the high-level one. 39 | func (d *Discriminator) GoLow() *lowBase.Discriminator { 40 | return d.low 41 | } 42 | 43 | // GoLowUntyped will return the low-level Discriminator instance that was used to create the high-level one, with no type 44 | func (d *Discriminator) GoLowUntyped() any { 45 | return d.low 46 | } 47 | 48 | // Render will return a YAML representation of the Discriminator object as a byte slice. 49 | func (d *Discriminator) Render() ([]byte, error) { 50 | return yaml.Marshal(d) 51 | } 52 | 53 | // MarshalYAML will create a ready to render YAML representation of the Discriminator object. 54 | func (d *Discriminator) MarshalYAML() (interface{}, error) { 55 | nb := high.NewNodeBuilder(d, d.low) 56 | return nb.Render(), nil 57 | } 58 | -------------------------------------------------------------------------------- /datamodel/high/base/discriminator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 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/stretchr/testify/assert" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func TestNewDiscriminator(t *testing.T) { 18 | var cNode yaml.Node 19 | 20 | yml := `propertyName: coffee 21 | mapping: 22 | fogCleaner: in the morning` 23 | 24 | _ = yaml.Unmarshal([]byte(yml), &cNode) 25 | 26 | // build low 27 | var lowDiscriminator lowbase.Discriminator 28 | _ = lowmodel.BuildModel(cNode.Content[0], &lowDiscriminator) 29 | 30 | // build high 31 | highDiscriminator := NewDiscriminator(&lowDiscriminator) 32 | 33 | assert.Equal(t, "coffee", highDiscriminator.PropertyName) 34 | assert.Equal(t, "in the morning", highDiscriminator.Mapping.GetOrZero("fogCleaner")) 35 | assert.Equal(t, 3, highDiscriminator.GoLow().FindMappingValue("fogCleaner").ValueNode.Line) 36 | 37 | // render the example as YAML 38 | rendered, _ := highDiscriminator.Render() 39 | assert.Equal(t, strings.TrimSpace(string(rendered)), yml) 40 | } 41 | 42 | func ExampleNewDiscriminator() { 43 | // create a yaml representation of a discriminator (can be JSON, doesn't matter) 44 | yml := `propertyName: coffee 45 | mapping: 46 | coffee: in the morning` 47 | 48 | // unmarshal into a *yaml.Node 49 | var node yaml.Node 50 | _ = yaml.Unmarshal([]byte(yml), &node) 51 | 52 | // build low-level model 53 | var lowDiscriminator lowbase.Discriminator 54 | _ = lowmodel.BuildModel(node.Content[0], &lowDiscriminator) 55 | 56 | // build high-level model 57 | highDiscriminator := NewDiscriminator(&lowDiscriminator) 58 | 59 | // print out a mapping defined for the discriminator. 60 | fmt.Print(highDiscriminator.Mapping.GetOrZero("coffee")) 61 | // Output: in the morning 62 | } 63 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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/high/base/tag.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 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // Tag represents a high-level Tag instance that is backed by a low-level one. 14 | // 15 | // Adds metadata to a single tag that is used by the Operation Object. It is not mandatory to have a Tag Object per 16 | // tag defined in the Operation Object instances. 17 | // - v2: https://swagger.io/specification/v2/#tagObject 18 | // - v3: https://swagger.io/specification/#tag-object 19 | type Tag struct { 20 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 21 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 22 | ExternalDocs *ExternalDoc `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` 23 | Extensions *orderedmap.Map[string, *yaml.Node] 24 | low *low.Tag 25 | } 26 | 27 | // NewTag creates a new high-level Tag instance that is backed by a low-level one. 28 | func NewTag(tag *low.Tag) *Tag { 29 | t := new(Tag) 30 | t.low = tag 31 | if !tag.Name.IsEmpty() { 32 | t.Name = tag.Name.Value 33 | } 34 | if !tag.Description.IsEmpty() { 35 | t.Description = tag.Description.Value 36 | } 37 | if !tag.ExternalDocs.IsEmpty() { 38 | t.ExternalDocs = NewExternalDoc(tag.ExternalDocs.Value) 39 | } 40 | t.Extensions = high.ExtractExtensions(tag.Extensions) 41 | return t 42 | } 43 | 44 | // GoLow returns the low-level Tag instance used to create the high-level one. 45 | func (t *Tag) GoLow() *low.Tag { 46 | return t.low 47 | } 48 | 49 | // GoLowUntyped will return the low-level Tag instance that was used to create the high-level one, with no type 50 | func (t *Tag) GoLowUntyped() any { 51 | return t.low 52 | } 53 | 54 | // Render will return a YAML representation of the Info object as a byte slice. 55 | func (t *Tag) Render() ([]byte, error) { 56 | return yaml.Marshal(t) 57 | } 58 | 59 | // Render will return a YAML representation of the Info object as a byte slice. 60 | func (t *Tag) RenderInline() ([]byte, error) { 61 | d, _ := t.MarshalYAMLInline() 62 | return yaml.Marshal(d) 63 | } 64 | 65 | // MarshalYAML will create a ready to render YAML representation of the Info object. 66 | func (t *Tag) MarshalYAML() (interface{}, error) { 67 | nb := high.NewNodeBuilder(t, t.low) 68 | return nb.Render(), nil 69 | } 70 | 71 | func (t *Tag) MarshalYAMLInline() (interface{}, error) { 72 | nb := high.NewNodeBuilder(t, t.low) 73 | nb.Resolve = true 74 | return nb.Render(), nil 75 | } 76 | -------------------------------------------------------------------------------- /datamodel/high/base/tag_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 | "strings" 10 | "testing" 11 | 12 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 13 | lowbase "github.com/pb33f/libopenapi/datamodel/low/base" 14 | "github.com/stretchr/testify/assert" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | func TestNewTag(t *testing.T) { 19 | var cNode yaml.Node 20 | 21 | yml := `name: chicken 22 | description: nuggets 23 | externalDocs: 24 | url: https://pb33f.io 25 | x-hack: code` 26 | 27 | _ = yaml.Unmarshal([]byte(yml), &cNode) 28 | 29 | var lowTag lowbase.Tag 30 | _ = lowmodel.BuildModel(cNode.Content[0], &lowTag) 31 | _ = lowTag.Build(context.Background(), nil, cNode.Content[0], nil) 32 | 33 | highTag := NewTag(&lowTag) 34 | 35 | var xHack string 36 | _ = highTag.Extensions.GetOrZero("x-hack").Decode(&xHack) 37 | 38 | assert.Equal(t, "chicken", highTag.Name) 39 | assert.Equal(t, "nuggets", highTag.Description) 40 | assert.Equal(t, "https://pb33f.io", highTag.ExternalDocs.URL) 41 | assert.Equal(t, "code", xHack) 42 | 43 | wentLow := highTag.GoLow() 44 | assert.Equal(t, 5, wentLow.FindExtension("x-hack").ValueNode.Line) 45 | assert.NotNil(t, highTag.GoLowUntyped()) 46 | 47 | // render the tag as YAML 48 | highTagBytes, _ := highTag.Render() 49 | assert.Equal(t, strings.TrimSpace(string(highTagBytes)), yml) 50 | } 51 | 52 | func TestTag_RenderInline(t *testing.T) { 53 | tag := &Tag{ 54 | Name: "cake", 55 | } 56 | 57 | tri, _ := tag.RenderInline() 58 | 59 | assert.Equal(t, "name: cake", strings.TrimSpace(string(tri))) 60 | } 61 | 62 | func ExampleNewTag() { 63 | // create an example schema object 64 | // this can be either JSON or YAML. 65 | yml := ` 66 | name: Purchases 67 | description: All kinds of purchase related operations 68 | externalDocs: 69 | url: https://pb33f.io/purchases 70 | x-hack: code` 71 | 72 | // unmarshal raw bytes 73 | var node yaml.Node 74 | _ = yaml.Unmarshal([]byte(yml), &node) 75 | 76 | // build out the low-level model 77 | var lowTag lowbase.Tag 78 | _ = lowmodel.BuildModel(node.Content[0], &lowTag) 79 | _ = lowTag.Build(context.Background(), nil, node.Content[0], nil) 80 | 81 | // build the high level tag 82 | highTag := NewTag(&lowTag) 83 | 84 | // print out the tag name 85 | fmt.Print(highTag.Name) 86 | // Output: Purchases 87 | } 88 | -------------------------------------------------------------------------------- /datamodel/high/base/xml.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 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // XML represents a high-level representation of an XML object defined by all versions of OpenAPI and backed by 14 | // low-level XML object. 15 | // 16 | // A metadata object that allows for more fine-tuned XML model definitions. 17 | // 18 | // When using arrays, XML element names are not inferred (for singular/plural forms) and the name property SHOULD be 19 | // used to add that information. See examples for expected behavior. 20 | // 21 | // v2 - https://swagger.io/specification/v2/#xmlObject 22 | // v3 - https://swagger.io/specification/#xml-object 23 | type XML struct { 24 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 25 | Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` 26 | Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` 27 | Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"` 28 | Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"` 29 | Extensions *orderedmap.Map[string, *yaml.Node] 30 | low *low.XML 31 | } 32 | 33 | // NewXML creates a new high-level XML instance from a low-level one. 34 | func NewXML(xml *low.XML) *XML { 35 | x := new(XML) 36 | x.low = xml 37 | x.Name = xml.Name.Value 38 | x.Namespace = xml.Namespace.Value 39 | x.Prefix = xml.Prefix.Value 40 | x.Attribute = xml.Attribute.Value 41 | x.Wrapped = xml.Wrapped.Value 42 | x.Extensions = high.ExtractExtensions(xml.Extensions) 43 | return x 44 | } 45 | 46 | // GoLow returns the low level XML reference used to create the high level one. 47 | func (x *XML) GoLow() *low.XML { 48 | return x.low 49 | } 50 | 51 | // GoLowUntyped will return the low-level XML instance that was used to create the high-level one, with no type 52 | func (x *XML) GoLowUntyped() any { 53 | return x.low 54 | } 55 | 56 | // Render will return a YAML representation of the XML object as a byte slice. 57 | func (x *XML) Render() ([]byte, error) { 58 | return yaml.Marshal(x) 59 | } 60 | 61 | // MarshalYAML will create a ready to render YAML representation of the XML object. 62 | func (x *XML) MarshalYAML() (interface{}, error) { 63 | nb := high.NewNodeBuilder(x, x.low) 64 | return nb.Render(), nil 65 | } 66 | -------------------------------------------------------------------------------- /datamodel/high/base/xml_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "fmt" 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/stretchr/testify/assert" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func ExampleNewXML() { 18 | // create an example schema object 19 | // this can be either JSON or YAML. 20 | yml := ` 21 | namespace: https://pb33f.io/schema 22 | name: something 23 | attribute: true 24 | prefix: sample 25 | wrapped: true` 26 | 27 | // unmarshal raw bytes 28 | var node yaml.Node 29 | _ = yaml.Unmarshal([]byte(yml), &node) 30 | 31 | // build out the low-level model 32 | var lowXML lowbase.XML 33 | _ = lowmodel.BuildModel(node.Content[0], &lowXML) 34 | _ = lowXML.Build(node.Content[0], nil) 35 | 36 | // build the high level tag 37 | highXML := NewXML(&lowXML) 38 | 39 | // print out the XML namespace 40 | fmt.Print(highXML.Namespace) 41 | // Output: https://pb33f.io/schema 42 | } 43 | 44 | func TestContact_Render(t *testing.T) { 45 | // create an example schema object 46 | // this can be either JSON or YAML. 47 | yml := `namespace: https://pb33f.io/schema 48 | name: something 49 | attribute: true 50 | prefix: sample 51 | wrapped: true` 52 | 53 | // unmarshal raw bytes 54 | var node yaml.Node 55 | _ = yaml.Unmarshal([]byte(yml), &node) 56 | 57 | // build out the low-level model 58 | var lowXML lowbase.XML 59 | _ = lowmodel.BuildModel(node.Content[0], &lowXML) 60 | _ = lowXML.Build(node.Content[0], nil) 61 | 62 | // build the high level tag 63 | highXML := NewXML(&lowXML) 64 | 65 | // print out the XML doc 66 | highXMLBytes, _ := highXML.Render() 67 | assert.Equal(t, yml, strings.TrimSpace(string(highXMLBytes))) 68 | 69 | highXML.Attribute = false 70 | highXMLBytes, _ = highXML.Render() 71 | assert.NotEqual(t, yml, strings.TrimSpace(string(highXMLBytes))) 72 | } 73 | -------------------------------------------------------------------------------- /datamodel/high/nodes/nodeentry.go: -------------------------------------------------------------------------------- 1 | package nodes 2 | 3 | import "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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/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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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/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/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/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 | "gopkg.in/yaml.v3" 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/v3/asyncresult.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | type asyncResult[T any] struct { 4 | key string 5 | result T 6 | } 7 | -------------------------------------------------------------------------------- /datamodel/high/v3/components_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/pb33f/libopenapi/datamodel/low" 12 | v3 "github.com/pb33f/libopenapi/datamodel/low/v3" 13 | "github.com/pb33f/libopenapi/index" 14 | "github.com/pb33f/libopenapi/orderedmap" 15 | "github.com/pb33f/libopenapi/utils" 16 | "github.com/stretchr/testify/assert" 17 | "gopkg.in/yaml.v3" 18 | ) 19 | 20 | func TestComponents_MarshalYAML(t *testing.T) { 21 | comp := &Components{ 22 | Responses: orderedmap.ToOrderedMap(map[string]*Response{ 23 | "200": { 24 | Description: "OK", 25 | }, 26 | }), 27 | Parameters: orderedmap.ToOrderedMap(map[string]*Parameter{ 28 | "id": { 29 | Name: "id", 30 | In: "path", 31 | }, 32 | }), 33 | RequestBodies: orderedmap.ToOrderedMap(map[string]*RequestBody{ 34 | "body": { 35 | Content: orderedmap.ToOrderedMap(map[string]*MediaType{ 36 | "application/json": { 37 | Example: utils.CreateStringNode("why?"), 38 | }, 39 | }), 40 | }, 41 | }), 42 | PathItems: orderedmap.ToOrderedMap(map[string]*PathItem{ 43 | "/ding/dong/{bing}/{bong}/go": { 44 | Get: &Operation{ 45 | Description: "get", 46 | }, 47 | }, 48 | }), 49 | } 50 | 51 | dat, _ := comp.Render() 52 | 53 | var idxNode yaml.Node 54 | _ = yaml.Unmarshal(dat, &idxNode) 55 | idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig()) 56 | 57 | var n v3.Components 58 | _ = low.BuildModel(idxNode.Content[0], &n) 59 | _ = n.Build(context.Background(), idxNode.Content[0], idx) 60 | 61 | r := NewComponents(&n) 62 | 63 | desired := `responses: 64 | "200": 65 | description: OK 66 | parameters: 67 | id: 68 | name: id 69 | in: path 70 | requestBodies: 71 | body: 72 | content: 73 | application/json: 74 | example: why? 75 | pathItems: 76 | /ding/dong/{bing}/{bong}/go: 77 | get: 78 | description: get` 79 | 80 | dat, _ = r.Render() 81 | assert.Equal(t, desired, strings.TrimSpace(string(dat))) 82 | assert.NotNil(t, r.GoLowUntyped()) 83 | } 84 | -------------------------------------------------------------------------------- /datamodel/high/v3/encoding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2023 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 | lowmodel "github.com/pb33f/libopenapi/datamodel/low" 10 | lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | // Encoding represents an OpenAPI 3+ Encoding object 16 | // - https://spec.openapis.org/oas/v3.1.0#encoding-object 17 | type Encoding struct { 18 | ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` 19 | Headers *orderedmap.Map[string, *Header] `json:"headers,omitempty" yaml:"headers,omitempty"` 20 | Style string `json:"style,omitempty" yaml:"style,omitempty"` 21 | Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` 22 | AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` 23 | low *lowv3.Encoding 24 | } 25 | 26 | // NewEncoding creates a new instance of Encoding from a low-level one. 27 | func NewEncoding(encoding *lowv3.Encoding) *Encoding { 28 | e := new(Encoding) 29 | e.low = encoding 30 | e.ContentType = encoding.ContentType.Value 31 | e.Style = encoding.Style.Value 32 | if !encoding.Explode.IsEmpty() { 33 | e.Explode = &encoding.Explode.Value 34 | } 35 | e.AllowReserved = encoding.AllowReserved.Value 36 | e.Headers = ExtractHeaders(encoding.Headers.Value) 37 | return e 38 | } 39 | 40 | // GoLow returns the low-level Encoding instance used to create the high-level one. 41 | func (e *Encoding) GoLow() *lowv3.Encoding { 42 | return e.low 43 | } 44 | 45 | // GoLowUntyped will return the low-level Encoding instance that was used to create the high-level one, with no type 46 | func (e *Encoding) GoLowUntyped() any { 47 | return e.low 48 | } 49 | 50 | // Render will return a YAML representation of the Encoding object as a byte slice. 51 | func (e *Encoding) Render() ([]byte, error) { 52 | return yaml.Marshal(e) 53 | } 54 | 55 | // MarshalYAML will create a ready to render YAML representation of the Encoding object. 56 | func (e *Encoding) MarshalYAML() (interface{}, error) { 57 | nb := high.NewNodeBuilder(e, e.low) 58 | return nb.Render(), nil 59 | } 60 | 61 | // ExtractEncoding converts hard to navigate low-level plumbing Encoding definitions, into a high-level simple map 62 | func ExtractEncoding(elements *orderedmap.Map[lowmodel.KeyReference[string], lowmodel.ValueReference[*lowv3.Encoding]]) *orderedmap.Map[string, *Encoding] { 63 | return low.FromReferenceMapWithFunc(elements, NewEncoding) 64 | } 65 | -------------------------------------------------------------------------------- /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/orderedmap" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestEncoding_MarshalYAML(t *testing.T) { 15 | explode := true 16 | encoding := &Encoding{ 17 | ContentType: "application/json", 18 | Headers: orderedmap.ToOrderedMap(map[string]*Header{ 19 | "x-pizza-time": {Description: "oh yes please"}, 20 | }), 21 | Style: "simple", 22 | Explode: &explode, 23 | } 24 | 25 | rend, _ := encoding.Render() 26 | 27 | desired := `contentType: application/json 28 | headers: 29 | x-pizza-time: 30 | description: oh yes please 31 | style: simple 32 | explode: true` 33 | 34 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 35 | 36 | explode = false 37 | encoding.Explode = &explode 38 | rend, _ = encoding.Render() 39 | 40 | desired = `contentType: application/json 41 | headers: 42 | x-pizza-time: 43 | description: oh yes please 44 | style: simple` 45 | 46 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 47 | 48 | encoding.Explode = nil 49 | rend, _ = encoding.Render() 50 | 51 | desired = `contentType: application/json 52 | headers: 53 | x-pizza-time: 54 | description: oh yes please 55 | style: simple` 56 | 57 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 58 | 59 | encoding.Explode = &explode 60 | rend, _ = encoding.Render() 61 | 62 | desired = `contentType: application/json 63 | headers: 64 | x-pizza-time: 65 | description: oh yes please 66 | style: simple` 67 | 68 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 69 | } 70 | -------------------------------------------------------------------------------- /datamodel/high/v3/header_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/pb33f/libopenapi/utils" 13 | "github.com/stretchr/testify/assert" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func TestHeader_MarshalYAML(t *testing.T) { 18 | ext := orderedmap.New[string, *yaml.Node]() 19 | ext.Set("x-burgers", utils.CreateStringNode("why not?")) 20 | 21 | header := &Header{ 22 | Description: "A header", 23 | Required: true, 24 | Deprecated: true, 25 | AllowEmptyValue: true, 26 | Style: "simple", 27 | Explode: true, 28 | AllowReserved: true, 29 | Example: utils.CreateStringNode("example"), 30 | Examples: orderedmap.ToOrderedMap(map[string]*base.Example{ 31 | "example": {Value: utils.CreateStringNode("example")}, 32 | }), 33 | Extensions: ext, 34 | } 35 | 36 | rend, _ := header.Render() 37 | 38 | desired := `description: A header 39 | required: true 40 | deprecated: true 41 | allowEmptyValue: true 42 | style: simple 43 | explode: true 44 | allowReserved: true 45 | example: example 46 | examples: 47 | example: 48 | value: example 49 | x-burgers: why not?` 50 | 51 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 52 | } 53 | -------------------------------------------------------------------------------- /datamodel/high/v3/link_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 | ) 13 | 14 | func TestLink_MarshalYAML(t *testing.T) { 15 | link := Link{ 16 | OperationRef: "somewhere", 17 | OperationId: "somewhereOutThere", 18 | Parameters: orderedmap.ToOrderedMap(map[string]string{ 19 | "over": "theRainbow", 20 | }), 21 | RequestBody: "hello?", 22 | Description: "are you there?", 23 | Server: &Server{ 24 | URL: "https://pb33f.io", 25 | }, 26 | } 27 | 28 | dat, _ := link.Render() 29 | desired := `operationRef: somewhere 30 | operationId: somewhereOutThere 31 | parameters: 32 | over: theRainbow 33 | requestBody: hello? 34 | description: are you there? 35 | server: 36 | url: https://pb33f.io` 37 | 38 | assert.Equal(t, desired, strings.TrimSpace(string(dat))) 39 | } 40 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 21 | low *low.OAuthFlows 22 | } 23 | 24 | // NewOAuthFlows creates a new high-level OAuthFlows instance from a low-level one. 25 | func NewOAuthFlows(flows *low.OAuthFlows) *OAuthFlows { 26 | o := new(OAuthFlows) 27 | o.low = flows 28 | if !flows.Implicit.IsEmpty() { 29 | o.Implicit = NewOAuthFlow(flows.Implicit.Value) 30 | } 31 | if !flows.Password.IsEmpty() { 32 | o.Password = NewOAuthFlow(flows.Password.Value) 33 | } 34 | if !flows.ClientCredentials.IsEmpty() { 35 | o.ClientCredentials = NewOAuthFlow(flows.ClientCredentials.Value) 36 | } 37 | if !flows.AuthorizationCode.IsEmpty() { 38 | o.AuthorizationCode = NewOAuthFlow(flows.AuthorizationCode.Value) 39 | } 40 | if !flows.Implicit.IsEmpty() { 41 | o.Implicit = NewOAuthFlow(flows.Implicit.Value) 42 | } 43 | o.Extensions = high.ExtractExtensions(flows.Extensions) 44 | return o 45 | } 46 | 47 | // GoLow returns the low-level OAuthFlows instance used to create the high-level one. 48 | func (o *OAuthFlows) GoLow() *low.OAuthFlows { 49 | return o.low 50 | } 51 | 52 | // GoLowUntyped will return the low-level OAuthFlows instance that was used to create the high-level one, with no type 53 | func (o *OAuthFlows) GoLowUntyped() any { 54 | return o.low 55 | } 56 | 57 | // Render will return a YAML representation of the OAuthFlows object as a byte slice. 58 | func (o *OAuthFlows) Render() ([]byte, error) { 59 | return yaml.Marshal(o) 60 | } 61 | 62 | // MarshalYAML will create a ready to render YAML representation of the OAuthFlows object. 63 | func (o *OAuthFlows) MarshalYAML() (interface{}, error) { 64 | nb := high.NewNodeBuilder(o, o.low) 65 | return nb.Render(), nil 66 | } 67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /datamodel/high/v3/request_body.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2023 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 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | // RequestBody represents a high-level OpenAPI 3+ RequestBody object, backed by a low-level one. 14 | // - https://spec.openapis.org/oas/v3.1.0#request-body-object 15 | type RequestBody struct { 16 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 17 | Content *orderedmap.Map[string, *MediaType] `json:"content,omitempty" yaml:"content,omitempty"` 18 | Required *bool `json:"required,omitempty" yaml:"required,renderZero,omitempty"` 19 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 20 | low *low.RequestBody 21 | } 22 | 23 | // NewRequestBody will create a new high-level RequestBody instance, from a low-level one. 24 | func NewRequestBody(rb *low.RequestBody) *RequestBody { 25 | r := new(RequestBody) 26 | r.low = rb 27 | r.Description = rb.Description.Value 28 | if !rb.Required.IsEmpty() { 29 | r.Required = &rb.Required.Value 30 | } 31 | r.Extensions = high.ExtractExtensions(rb.Extensions) 32 | r.Content = ExtractContent(rb.Content.Value) 33 | return r 34 | } 35 | 36 | // GoLow returns the low-level RequestBody instance used to create the high-level one. 37 | func (r *RequestBody) GoLow() *low.RequestBody { 38 | return r.low 39 | } 40 | 41 | // GoLowUntyped will return the low-level RequestBody instance that was used to create the high-level one, with no type 42 | func (r *RequestBody) GoLowUntyped() any { 43 | return r.low 44 | } 45 | 46 | // Render will return a YAML representation of the RequestBody object as a byte slice. 47 | func (r *RequestBody) Render() ([]byte, error) { 48 | return yaml.Marshal(r) 49 | } 50 | 51 | func (r *RequestBody) RenderInline() ([]byte, error) { 52 | d, _ := r.MarshalYAMLInline() 53 | return yaml.Marshal(d) 54 | } 55 | 56 | // MarshalYAML will create a ready to render YAML representation of the RequestBody object. 57 | func (r *RequestBody) MarshalYAML() (interface{}, error) { 58 | nb := high.NewNodeBuilder(r, r.low) 59 | return nb.Render(), nil 60 | } 61 | 62 | func (r *RequestBody) MarshalYAMLInline() (interface{}, error) { 63 | nb := high.NewNodeBuilder(r, r.low) 64 | nb.Resolve = true 65 | return nb.Render(), nil 66 | } 67 | -------------------------------------------------------------------------------- /datamodel/high/v3/request_body_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 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | func TestRequestBody_MarshalYAML(t *testing.T) { 17 | ext := orderedmap.New[string, *yaml.Node]() 18 | ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) 19 | 20 | rb := true 21 | req := &RequestBody{ 22 | Description: "beer", 23 | Required: &rb, 24 | Extensions: ext, 25 | } 26 | 27 | rend, _ := req.Render() 28 | 29 | desired := `description: beer 30 | required: true 31 | x-high-gravity: why not?` 32 | 33 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 34 | } 35 | 36 | func TestRequestBody_MarshalYAMLInline(t *testing.T) { 37 | ext := orderedmap.New[string, *yaml.Node]() 38 | ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) 39 | 40 | rb := true 41 | req := &RequestBody{ 42 | Description: "beer", 43 | Required: &rb, 44 | Extensions: ext, 45 | } 46 | 47 | rend, _ := req.RenderInline() 48 | 49 | desired := `description: beer 50 | required: true 51 | x-high-gravity: why not?` 52 | 53 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 54 | } 55 | 56 | func TestRequestBody_MarshalNoRequired(t *testing.T) { 57 | ext := orderedmap.New[string, *yaml.Node]() 58 | ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) 59 | 60 | rb := false 61 | req := &RequestBody{ 62 | Description: "beer", 63 | Required: &rb, 64 | Extensions: ext, 65 | } 66 | 67 | rend, _ := req.Render() 68 | 69 | desired := `description: beer 70 | required: false 71 | x-high-gravity: why not?` 72 | 73 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 74 | } 75 | 76 | func TestRequestBody_MarshalRequiredNil(t *testing.T) { 77 | ext := orderedmap.New[string, *yaml.Node]() 78 | ext.Set("x-high-gravity", utils.CreateStringNode("why not?")) 79 | 80 | req := &RequestBody{ 81 | Description: "beer", 82 | Extensions: ext, 83 | } 84 | 85 | rend, _ := req.Render() 86 | 87 | desired := `description: beer 88 | x-high-gravity: why not?` 89 | 90 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 91 | } 92 | -------------------------------------------------------------------------------- /datamodel/high/v3/security_scheme_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package v3 5 | 6 | import ( 7 | "context" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/pb33f/libopenapi/datamodel/low" 12 | v3 "github.com/pb33f/libopenapi/datamodel/low/v3" 13 | "github.com/pb33f/libopenapi/index" 14 | "github.com/stretchr/testify/assert" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | func TestSecurityScheme_MarshalYAML(t *testing.T) { 19 | ss := &SecurityScheme{ 20 | Type: "apiKey", 21 | Description: "this is a description", 22 | Name: "superSecret", 23 | In: "header", 24 | Scheme: "https", 25 | } 26 | 27 | dat, _ := ss.Render() 28 | 29 | var idxNode yaml.Node 30 | _ = yaml.Unmarshal(dat, &idxNode) 31 | idx := index.NewSpecIndexWithConfig(&idxNode, index.CreateOpenAPIIndexConfig()) 32 | 33 | var n v3.SecurityScheme 34 | _ = low.BuildModel(idxNode.Content[0], &n) 35 | _ = n.Build(context.Background(), nil, idxNode.Content[0], idx) 36 | 37 | r := NewSecurityScheme(&n) 38 | 39 | dat, _ = r.Render() 40 | 41 | desired := `type: apiKey 42 | description: this is a description 43 | name: superSecret 44 | in: header 45 | scheme: https` 46 | 47 | assert.Equal(t, desired, strings.TrimSpace(string(dat))) 48 | } 49 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | URL string `json:"url,omitempty" yaml:"url,omitempty"` 18 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 19 | Variables *orderedmap.Map[string, *ServerVariable] `json:"variables,omitempty" yaml:"variables,omitempty"` 20 | Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"` 21 | low *lowv3.Server 22 | } 23 | 24 | // NewServer will create a new high-level Server instance from a low-level one. 25 | func NewServer(server *lowv3.Server) *Server { 26 | s := new(Server) 27 | s.low = server 28 | s.Description = server.Description.Value 29 | s.URL = server.URL.Value 30 | s.Variables = low.FromReferenceMapWithFunc(server.Variables.Value, NewServerVariable) 31 | s.Extensions = high.ExtractExtensions(server.Extensions) 32 | return s 33 | } 34 | 35 | // GoLow returns the low-level Server instance that was used to create the high-level one 36 | func (s *Server) GoLow() *lowv3.Server { 37 | return s.low 38 | } 39 | 40 | // GoLowUntyped will return the low-level Server instance that was used to create the high-level one, with no type 41 | func (s *Server) GoLowUntyped() any { 42 | return s.low 43 | } 44 | 45 | // Render will return a YAML representation of the Server object as a byte slice. 46 | func (s *Server) Render() ([]byte, error) { 47 | return yaml.Marshal(s) 48 | } 49 | 50 | // MarshalYAML will create a ready to render YAML representation of the Server object. 51 | func (s *Server) MarshalYAML() (interface{}, error) { 52 | nb := high.NewNodeBuilder(s, s.low) 53 | return nb.Render(), nil 54 | } 55 | -------------------------------------------------------------------------------- /datamodel/high/v3/server_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 | ) 13 | 14 | func TestServer_MarshalYAML(t *testing.T) { 15 | server := &Server{ 16 | URL: "https://pb33f.io", 17 | Description: "the b33f", 18 | } 19 | 20 | desired := `url: https://pb33f.io 21 | description: the b33f` 22 | 23 | rend, _ := server.Render() 24 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 25 | 26 | // mutate 27 | server.Variables = orderedmap.ToOrderedMap(map[string]*ServerVariable{ 28 | "rainbow": { 29 | Enum: []string{"one", "two", "three"}, 30 | }, 31 | }) 32 | 33 | desired = `url: https://pb33f.io 34 | description: the b33f 35 | variables: 36 | rainbow: 37 | enum: 38 | - one 39 | - two 40 | - three` 41 | 42 | rend, _ = server.Render() 43 | assert.Equal(t, desired, strings.TrimSpace(string(rend))) 44 | } 45 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /datamodel/low/base/constants.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | // Constants for labels used to look up values within OpenAPI specifications. 7 | const ( 8 | VersionLabel = "version" 9 | TermsOfServiceLabel = "termsOfService" 10 | DescriptionLabel = "description" 11 | TitleLabel = "title" 12 | EmailLabel = "email" 13 | NameLabel = "name" 14 | URLLabel = "url" 15 | ServersLabel = "servers" 16 | ServerLabel = "server" 17 | TagsLabel = "tags" 18 | ExternalDocsLabel = "externalDocs" 19 | ExamplesLabel = "examples" 20 | ExampleLabel = "example" 21 | ValueLabel = "value" 22 | InfoLabel = "info" 23 | ContactLabel = "contact" 24 | LicenseLabel = "license" 25 | PropertiesLabel = "properties" 26 | DependentSchemasLabel = "dependentSchemas" 27 | PatternPropertiesLabel = "patternProperties" 28 | IfLabel = "if" 29 | ElseLabel = "else" 30 | ThenLabel = "then" 31 | PropertyNamesLabel = "propertyNames" 32 | UnevaluatedItemsLabel = "unevaluatedItems" 33 | UnevaluatedPropertiesLabel = "unevaluatedProperties" 34 | AdditionalPropertiesLabel = "additionalProperties" 35 | XMLLabel = "xml" 36 | ItemsLabel = "items" 37 | PrefixItemsLabel = "prefixItems" 38 | ContainsLabel = "contains" 39 | AllOfLabel = "allOf" 40 | AnyOfLabel = "anyOf" 41 | OneOfLabel = "oneOf" 42 | NotLabel = "not" 43 | TypeLabel = "type" 44 | DiscriminatorLabel = "discriminator" 45 | ExclusiveMinimumLabel = "exclusiveMinimum" 46 | ExclusiveMaximumLabel = "exclusiveMaximum" 47 | SchemaLabel = "schema" 48 | SchemaTypeLabel = "$schema" 49 | AnchorLabel = "$anchor" 50 | ) 51 | 52 | /* 53 | PropertyNames low.NodeReference[*SchemaProxy] 54 | UnevaluatedItems low.NodeReference[*SchemaProxy] 55 | UnevaluatedProperties low.NodeReference[*SchemaProxy] 56 | */ 57 | -------------------------------------------------------------------------------- /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 | "strings" 10 | 11 | "github.com/pb33f/libopenapi/datamodel/low" 12 | "github.com/pb33f/libopenapi/index" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | // Contact represents a low-level representation of the Contact definitions found at 18 | // 19 | // v2 - https://swagger.io/specification/v2/#contactObject 20 | // v3 - https://spec.openapis.org/oas/v3.1.0#contact-object 21 | type Contact struct { 22 | Name low.NodeReference[string] 23 | URL low.NodeReference[string] 24 | Email low.NodeReference[string] 25 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 26 | KeyNode *yaml.Node 27 | RootNode *yaml.Node 28 | index *index.SpecIndex 29 | context context.Context 30 | *low.Reference 31 | low.NodeMap 32 | } 33 | 34 | func (c *Contact) Build(ctx context.Context, keyNode, root *yaml.Node, idx *index.SpecIndex) error { 35 | c.KeyNode = keyNode 36 | c.RootNode = root 37 | c.Reference = new(low.Reference) 38 | c.Nodes = low.ExtractNodes(ctx, root) 39 | c.Extensions = low.ExtractExtensions(root) 40 | c.context = ctx 41 | c.index = idx 42 | return nil 43 | } 44 | 45 | // GetIndex will return the index.SpecIndex instance attached to the Contact object 46 | func (c *Contact) GetIndex() *index.SpecIndex { 47 | return c.index 48 | } 49 | 50 | // GetContext will return the context.Context instance used when building the Contact object 51 | func (c *Contact) GetContext() context.Context { 52 | return c.context 53 | } 54 | 55 | // GetRootNode will return the root yaml node of the Contact object 56 | func (c *Contact) GetRootNode() *yaml.Node { 57 | return c.RootNode 58 | } 59 | 60 | // GetKeyNode will return the key yaml node of the Contact object 61 | func (c *Contact) GetKeyNode() *yaml.Node { 62 | return c.KeyNode 63 | } 64 | 65 | // Hash will return a consistent SHA256 Hash of the Contact object 66 | func (c *Contact) Hash() [32]byte { 67 | var f []string 68 | if !c.Name.IsEmpty() { 69 | f = append(f, c.Name.Value) 70 | } 71 | if !c.URL.IsEmpty() { 72 | f = append(f, c.URL.Value) 73 | } 74 | if !c.Email.IsEmpty() { 75 | f = append(f, c.Email.Value) 76 | } 77 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 78 | } 79 | 80 | // GetExtensions returns all extensions for Contact 81 | func (c *Contact) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 82 | return c.Extensions 83 | } 84 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | -------------------------------------------------------------------------------- /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/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 | "strings" 9 | 10 | "gopkg.in/yaml.v3" 11 | 12 | "github.com/pb33f/libopenapi/datamodel/low" 13 | "github.com/pb33f/libopenapi/orderedmap" 14 | ) 15 | 16 | // Discriminator is only used by OpenAPI 3+ documents, it represents a polymorphic discriminator used for schemas 17 | // 18 | // When request bodies or response payloads may be one of a number of different schemas, a discriminator object can be 19 | // used to aid in serialization, deserialization, and validation. The discriminator is a specific object in a schema 20 | // which is used to inform the consumer of the document of an alternative schema based on the value associated with it. 21 | // 22 | // When using the discriminator, inline schemas will not be considered. 23 | // 24 | // v3 - https://spec.openapis.org/oas/v3.1.0#discriminator-object 25 | type Discriminator struct { 26 | PropertyName low.NodeReference[string] 27 | Mapping low.NodeReference[*orderedmap.Map[low.KeyReference[string], low.ValueReference[string]]] 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 | // calculate a hash from every property. 57 | var f []string 58 | if d.PropertyName.Value != "" { 59 | f = append(f, d.PropertyName.Value) 60 | } 61 | 62 | for v := range orderedmap.SortAlpha(d.Mapping.Value).ValuesFromOldest() { 63 | f = append(f, v.Value) 64 | } 65 | 66 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 67 | } 68 | -------------------------------------------------------------------------------- /datamodel/low/base/discriminator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package base 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | "github.com/stretchr/testify/assert" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | func TestDiscriminator_FindMappingValue(t *testing.T) { 15 | yml := `propertyName: freshCakes 16 | mapping: 17 | something: nothing` 18 | 19 | var idxNode yaml.Node 20 | mErr := yaml.Unmarshal([]byte(yml), &idxNode) 21 | assert.NoError(t, mErr) 22 | 23 | var n Discriminator 24 | err := low.BuildModel(idxNode.Content[0], &n) 25 | assert.NoError(t, err) 26 | assert.Equal(t, "nothing", n.FindMappingValue("something").Value) 27 | assert.Nil(t, n.FindMappingValue("freshCakes")) 28 | } 29 | 30 | func TestDiscriminator_Hash(t *testing.T) { 31 | left := `propertyName: freshCakes 32 | mapping: 33 | something: nothing` 34 | 35 | right := `mapping: 36 | something: nothing 37 | propertyName: freshCakes` 38 | 39 | var lNode, rNode yaml.Node 40 | _ = yaml.Unmarshal([]byte(left), &lNode) 41 | _ = yaml.Unmarshal([]byte(right), &rNode) 42 | 43 | // create low level objects 44 | var lDoc Discriminator 45 | var rDoc Discriminator 46 | _ = low.BuildModel(lNode.Content[0], &lDoc) 47 | _ = low.BuildModel(rNode.Content[0], &rDoc) 48 | lDoc.RootNode = &lNode 49 | lDoc.KeyNode = &rNode 50 | 51 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 52 | assert.NotNil(t, lDoc.GetRootNode()) 53 | assert.NotNil(t, lDoc.GetKeyNode()) 54 | } 55 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/low/base/tag_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 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func TestTag_Build(t *testing.T) { 18 | yml := `name: a tag 19 | description: a description 20 | externalDocs: 21 | url: https://pb33f.io 22 | x-coffee: tasty` 23 | 24 | var idxNode yaml.Node 25 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 26 | idx := index.NewSpecIndex(&idxNode) 27 | 28 | var n Tag 29 | err := low.BuildModel(idxNode.Content[0], &n) 30 | assert.NoError(t, err) 31 | 32 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 33 | assert.NoError(t, err) 34 | assert.Equal(t, "a tag", n.Name.Value) 35 | assert.Equal(t, "a description", n.Description.Value) 36 | assert.Equal(t, "https://pb33f.io", n.ExternalDocs.Value.URL.Value) 37 | 38 | var xCoffee string 39 | _ = n.FindExtension("x-coffee").GetValue().Decode(&xCoffee) 40 | 41 | assert.Equal(t, "tasty", xCoffee) 42 | assert.Equal(t, 1, orderedmap.Len(n.GetExtensions())) 43 | assert.NotNil(t, n.GetRootNode()) 44 | assert.Nil(t, n.GetKeyNode()) 45 | assert.NotNil(t, n.GetContext()) 46 | assert.NotNil(t, n.GetIndex()) 47 | } 48 | 49 | func TestTag_Build_Error(t *testing.T) { 50 | yml := `name: a tag 51 | description: a description 52 | externalDocs: 53 | $ref: #borko` 54 | 55 | var idxNode yaml.Node 56 | _ = yaml.Unmarshal([]byte(yml), &idxNode) 57 | idx := index.NewSpecIndex(&idxNode) 58 | 59 | var n Tag 60 | err := low.BuildModel(&idxNode, &n) 61 | assert.NoError(t, err) 62 | 63 | err = n.Build(context.Background(), nil, idxNode.Content[0], idx) 64 | assert.Error(t, err) 65 | } 66 | 67 | func TestTag_Hash(t *testing.T) { 68 | left := `name: melody 69 | description: my princess 70 | externalDocs: 71 | url: https://pb33f.io 72 | x-b33f: princess` 73 | 74 | right := `name: melody 75 | description: my princess 76 | externalDocs: 77 | url: https://pb33f.io 78 | x-b33f: princess` 79 | 80 | var lNode, rNode yaml.Node 81 | _ = yaml.Unmarshal([]byte(left), &lNode) 82 | _ = yaml.Unmarshal([]byte(right), &rNode) 83 | 84 | // create low level objects 85 | var lDoc Tag 86 | var rDoc Tag 87 | _ = low.BuildModel(lNode.Content[0], &lDoc) 88 | _ = low.BuildModel(rNode.Content[0], &rDoc) 89 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 90 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 91 | 92 | assert.Equal(t, lDoc.Hash(), rDoc.Hash()) 93 | } 94 | -------------------------------------------------------------------------------- /datamodel/low/base/xml.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | "github.com/pb33f/libopenapi/index" 11 | "github.com/pb33f/libopenapi/orderedmap" 12 | "github.com/pb33f/libopenapi/utils" 13 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | // XML represents a low-level representation of an XML object defined by all versions of OpenAPI. 17 | // 18 | // A metadata object that allows for more fine-tuned XML model definitions. 19 | // 20 | // When using arrays, XML element names are not inferred (for singular/plural forms) and the name property SHOULD be 21 | // used to add that information. See examples for expected behavior. 22 | // 23 | // v2 - https://swagger.io/specification/v2/#xmlObject 24 | // v3 - https://swagger.io/specification/#xml-object 25 | type XML struct { 26 | Name low.NodeReference[string] 27 | Namespace low.NodeReference[string] 28 | Prefix low.NodeReference[string] 29 | Attribute low.NodeReference[bool] 30 | Wrapped low.NodeReference[bool] 31 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 32 | RootNode *yaml.Node 33 | index *index.SpecIndex 34 | context context.Context 35 | *low.Reference 36 | low.NodeMap 37 | } 38 | 39 | // Build will extract extensions from the XML instance. 40 | func (x *XML) Build(root *yaml.Node, _ *index.SpecIndex) error { 41 | root = utils.NodeAlias(root) 42 | utils.CheckForMergeNodes(root) 43 | x.RootNode = root 44 | x.Reference = new(low.Reference) 45 | x.Nodes = low.ExtractNodes(nil, root) 46 | x.Extensions = low.ExtractExtensions(root) 47 | return nil 48 | } 49 | 50 | // GetExtensions returns all Tag extensions and satisfies the low.HasExtensions interface. 51 | func (x *XML) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 52 | return x.Extensions 53 | } 54 | 55 | // GetRootNode returns the root yaml node of the Tag object 56 | func (x *XML) GetRootNode() *yaml.Node { 57 | return x.RootNode 58 | } 59 | 60 | // Hash generates a SHA256 hash of the XML object using properties 61 | func (x *XML) Hash() [32]byte { 62 | var f []string 63 | if !x.Name.IsEmpty() { 64 | f = append(f, x.Name.Value) 65 | } 66 | if !x.Namespace.IsEmpty() { 67 | f = append(f, x.Namespace.Value) 68 | } 69 | if !x.Prefix.IsEmpty() { 70 | f = append(f, x.Prefix.Value) 71 | } 72 | if !x.Attribute.IsEmpty() { 73 | f = append(f, fmt.Sprint(x.Attribute.Value)) 74 | } 75 | if !x.Wrapped.IsEmpty() { 76 | f = append(f, fmt.Sprint(x.Wrapped.Value)) 77 | } 78 | f = append(f, low.HashExtensions(x.Extensions)...) 79 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 80 | } 81 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | } 35 | -------------------------------------------------------------------------------- /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 "gopkg.in/yaml.v3" 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/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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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/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/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 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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/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 | "gopkg.in/yaml.v3" 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/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/low/v3/server_variable.go: -------------------------------------------------------------------------------- 1 | package v3 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/pb33f/libopenapi/datamodel/low" 10 | "github.com/pb33f/libopenapi/orderedmap" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | // ServerVariable represents a low-level OpenAPI 3+ ServerVariable object. 15 | // 16 | // ServerVariable is an object representing a Server Variable for server URL template substitution. 17 | // - https://spec.openapis.org/oas/v3.1.0#server-variable-object 18 | // 19 | // This is the only struct that is not Buildable, it's not used by anything other than a Server instance, 20 | // and it has nothing to build that requires it to be buildable. 21 | type ServerVariable struct { 22 | Enum []low.NodeReference[string] 23 | Default low.NodeReference[string] 24 | Description low.NodeReference[string] 25 | Extensions *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] 26 | KeyNode *yaml.Node 27 | RootNode *yaml.Node 28 | *low.Reference 29 | low.NodeMap 30 | } 31 | 32 | // GetRootNode returns the root yaml node of the ServerVariable object. 33 | func (s *ServerVariable) GetRootNode() *yaml.Node { 34 | return s.RootNode 35 | } 36 | 37 | // GetKeyNode returns the key yaml node of the ServerVariable object. 38 | func (s *ServerVariable) GetKeyNode() *yaml.Node { 39 | return s.RootNode 40 | } 41 | 42 | // GetExtensions returns all extensions and satisfies the low.HasExtensions interface. 43 | func (s *ServerVariable) GetExtensions() *orderedmap.Map[low.KeyReference[string], low.ValueReference[*yaml.Node]] { 44 | return s.Extensions 45 | } 46 | 47 | // Hash will return a consistent SHA256 Hash of the ServerVariable object 48 | func (s *ServerVariable) Hash() [32]byte { 49 | var f []string 50 | keys := make([]string, len(s.Enum)) 51 | z := 0 52 | for k := range s.Enum { 53 | keys[z] = fmt.Sprint(s.Enum[k].Value) 54 | z++ 55 | } 56 | sort.Strings(keys) 57 | f = append(f, keys...) 58 | if !s.Default.IsEmpty() { 59 | f = append(f, s.Default.Value) 60 | } 61 | if !s.Description.IsEmpty() { 62 | f = append(f, s.Description.Value) 63 | } 64 | return sha256.Sum256([]byte(strings.Join(f, "|"))) 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pb33f/libopenapi 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb 7 | github.com/speakeasy-api/jsonpath v0.6.2 8 | github.com/stretchr/testify v1.10.0 9 | github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd 10 | gopkg.in/yaml.v3 v3.0.1 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/mailru/easyjson v0.7.7 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 9 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU= 14 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= 15 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 16 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ= 20 | github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd h1:dLuIF2kX9c+KknGJUdJi1Il1SDiTSK158/BB9kdgAew= 24 | github.com/wk8/go-ordered-map/v2 v2.1.9-0.20240815153524-6ea36470d1bd/go.mod h1:DbzwytT4g/odXquuOCqroKvtxxldI4nb3nuesHF/Exo= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 27 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /index/circular_reference_result.go: -------------------------------------------------------------------------------- 1 | package index 2 | 3 | import ( 4 | "strings" 5 | 6 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index/index_model_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | func TestSpecIndex_GetConfig(t *testing.T) { 15 | idx1 := NewTestSpecIndex() 16 | c := SpecIndexConfig{} 17 | idx1.config = &c 18 | assert.Equal(t, &c, idx1.GetConfig()) 19 | } 20 | 21 | func Test_MarshalJSON(t *testing.T) { 22 | rm := &ReferenceMapped{ 23 | OriginalReference: &Reference{ 24 | FullDefinition: "full definition", 25 | Path: "path", 26 | Node: &yaml.Node{ 27 | Line: 1, 28 | Column: 1, 29 | Content: []*yaml.Node{ 30 | { 31 | Line: 9, 32 | Column: 10, 33 | }, 34 | { 35 | Value: "lemon cake", 36 | }, 37 | }, 38 | }, 39 | }, 40 | Reference: &Reference{ 41 | FullDefinition: "full definition", 42 | Path: "path", 43 | Node: &yaml.Node{ 44 | Line: 2, 45 | Column: 2, 46 | }, 47 | KeyNode: &yaml.Node{ 48 | Line: 3, 49 | Column: 3, 50 | }, 51 | }, 52 | Definition: "definition", 53 | FullDefinition: "full definition", 54 | IsPolymorphic: true, 55 | } 56 | 57 | bytes, _ := json.Marshal(rm) 58 | assert.Len(t, bytes, 173) 59 | } 60 | -------------------------------------------------------------------------------- /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/libopenapi/utils" 12 | "github.com/speakeasy-api/jsonpath/pkg/jsonpath" 13 | "github.com/stretchr/testify/assert" 14 | "gopkg.in/yaml.v3" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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! -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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/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' -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /index/rolodex_test_data/operations.yaml: -------------------------------------------------------------------------------- 1 | external_operation: 2 | get: 3 | responses: 4 | 200: 5 | description: "OK" -------------------------------------------------------------------------------- /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' -------------------------------------------------------------------------------- /index/search_index_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Princess B33f Heavy Industries / Dave Shanley 2 | // SPDX-License-Identifier: MIT 3 | 4 | package index 5 | 6 | import ( 7 | "context" 8 | "os" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | func TestSpecIndex_SearchIndexForReference(t *testing.T) { 16 | petstore, _ := os.ReadFile("../test_specs/petstorev3.json") 17 | var rootNode yaml.Node 18 | _ = yaml.Unmarshal(petstore, &rootNode) 19 | 20 | c := CreateOpenAPIIndexConfig() 21 | idx := NewSpecIndexWithConfig(&rootNode, c) 22 | 23 | ref, _ := idx.SearchIndexForReference("#/components/schemas/Pet") 24 | assert.NotNil(t, ref) 25 | } 26 | 27 | func TestSpecIndex_SearchIndexForReferenceWithContext(t *testing.T) { 28 | petstore, _ := os.ReadFile("../test_specs/petstorev3.json") 29 | var rootNode yaml.Node 30 | _ = yaml.Unmarshal(petstore, &rootNode) 31 | 32 | c := CreateOpenAPIIndexConfig() 33 | idx := NewSpecIndexWithConfig(&rootNode, c) 34 | 35 | ref, _, _ := idx.SearchIndexForReferenceWithContext(context.Background(), "#/components/schemas/Pet") 36 | assert.NotNil(t, ref) 37 | } 38 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/pb33f/libopenapi/orderedmap" 9 | "gopkg.in/yaml.v3" 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 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 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 | -------------------------------------------------------------------------------- /libopenapi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pb33f/libopenapi/8fd18648b47e4a823edab8680e14e89feb897290/libopenapi-logo.png -------------------------------------------------------------------------------- /test_specs/advancecallbackreferences/min-callbacks.yaml: -------------------------------------------------------------------------------- 1 | test-callback: 2 | "/test-callback": 3 | $ref: "./min-components.yaml#/components/pathItems/test-callback" 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | anyOf: 25 | - "$ref": "#/components/schemas/Four" 26 | required: 27 | - testThing 28 | - anyOf 29 | Three: 30 | description: "test three" 31 | properties: 32 | tester: 33 | "$ref": "#/components/schemas/Four" 34 | bester: 35 | "$ref": "#/components/schemas/Seven" 36 | yester: 37 | "$ref": "#/components/schemas/Seven" 38 | required: 39 | - tester 40 | - bester 41 | - yester 42 | Four: 43 | description: "test four" 44 | properties: 45 | lemons: 46 | "$ref": "#/components/schemas/Nine" 47 | required: 48 | - lemons 49 | Five: 50 | properties: 51 | rice: 52 | "$ref": "#/components/schemas/Six" 53 | required: 54 | - rice 55 | Six: 56 | properties: 57 | mints: 58 | "$ref": "#/components/schemas/Nine" 59 | required: 60 | - mints 61 | Seven: 62 | properties: 63 | wow: 64 | "$ref": "#/components/schemas/Three" 65 | required: 66 | - wow 67 | Nine: 68 | description: done. 69 | Ten: 70 | properties: 71 | yeah: 72 | "$ref": "#/components/schemas/Ten" 73 | required: 74 | - yeah 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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' -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | Tag: "!!bool", 56 | Value: str, 57 | } 58 | return n 59 | } 60 | 61 | func CreateIntNode(str string) *yaml.Node { 62 | n := &yaml.Node{ 63 | Kind: yaml.ScalarNode, 64 | Tag: "!!int", 65 | Value: str, 66 | } 67 | return n 68 | } 69 | 70 | func CreateEmptyScalarNode() *yaml.Node { 71 | n := &yaml.Node{ 72 | Kind: yaml.ScalarNode, 73 | Tag: "!!null", 74 | Value: "", 75 | } 76 | return n 77 | } 78 | 79 | func CreateFloatNode(str string) *yaml.Node { 80 | n := &yaml.Node{ 81 | Kind: yaml.ScalarNode, 82 | Tag: "!!float", 83 | Value: str, 84 | } 85 | return n 86 | } 87 | -------------------------------------------------------------------------------- /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, "!!bool", b.Tag) 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, "!!float", f.Tag) 39 | assert.Equal(t, "3.14", f.Value) 40 | } 41 | 42 | func TestCreateIntNode(t *testing.T) { 43 | i := CreateIntNode("42") 44 | assert.Equal(t, "!!int", i.Tag) 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) 61 | assert.Equal(t, "foo", y.Value) 62 | } 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | func CheckPathOverlap(pathA, pathB, sep string) string { 17 | a := strings.Split(pathA, sep) 18 | b := strings.Split(pathB, sep) 19 | if strings.HasPrefix(a[len(a)-1], "/") && a[len(a)-1][1:] == b[0] { 20 | b = b[1:] 21 | } 22 | if a[len(a)-1] == b[0] { 23 | b = b[1:] 24 | } 25 | f := filepath.Join(pathA, strings.Join(b, sep)) 26 | 27 | return f 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /what-changed/model/change_types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley 2 | // https://pb33f.io 3 | 4 | package model 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestChange_MarshalJSON(t *testing.T) { 14 | rinseAndRepeat := func(ch *Change) map[string]any { 15 | b, err := ch.MarshalJSON() 16 | assert.NoError(t, err) 17 | 18 | var rebuilt map[string]any 19 | err = json.Unmarshal(b, &rebuilt) 20 | assert.NoError(t, err) 21 | return rebuilt 22 | } 23 | 24 | change := Change{ 25 | ChangeType: Modified, 26 | } 27 | rebuilt := rinseAndRepeat(&change) 28 | assert.Equal(t, "modified", rebuilt["changeText"]) 29 | assert.Equal(t, float64(1), rebuilt["change"]) 30 | 31 | change = Change{ 32 | ChangeType: ObjectAdded, 33 | } 34 | rebuilt = rinseAndRepeat(&change) 35 | assert.Equal(t, "object_added", rebuilt["changeText"]) 36 | assert.Equal(t, float64(3), rebuilt["change"]) 37 | 38 | change = Change{ 39 | ChangeType: ObjectRemoved, 40 | } 41 | rebuilt = rinseAndRepeat(&change) 42 | assert.Equal(t, "object_removed", rebuilt["changeText"]) 43 | assert.Equal(t, float64(4), rebuilt["change"]) 44 | 45 | change = Change{ 46 | ChangeType: PropertyAdded, 47 | } 48 | rebuilt = rinseAndRepeat(&change) 49 | assert.Equal(t, "property_added", rebuilt["changeText"]) 50 | assert.Equal(t, float64(2), rebuilt["change"]) 51 | 52 | change = Change{ 53 | ChangeType: PropertyRemoved, 54 | } 55 | rebuilt = rinseAndRepeat(&change) 56 | assert.Equal(t, "property_removed", rebuilt["changeText"]) 57 | assert.Equal(t, float64(5), rebuilt["change"]) 58 | 59 | change = Change{ 60 | Original: "gangster", 61 | } 62 | rebuilt = rinseAndRepeat(&change) 63 | assert.Equal(t, "gangster", rebuilt["original"]) 64 | 65 | change = Change{ 66 | New: "shoes", 67 | } 68 | rebuilt = rinseAndRepeat(&change) 69 | assert.Equal(t, "shoes", rebuilt["new"]) 70 | 71 | one := 1 72 | change = Change{ 73 | Context: &ChangeContext{ 74 | OriginalLine: &one, 75 | }, 76 | } 77 | rebuilt = rinseAndRepeat(&change) 78 | assert.NotNil(t, rebuilt["context"]) 79 | 80 | change = Change{ 81 | Type: "burger", 82 | } 83 | rebuilt = rinseAndRepeat(&change) 84 | assert.Equal(t, "burger", rebuilt["type"]) 85 | 86 | change = Change{ 87 | Path: "difficult", 88 | } 89 | rebuilt = rinseAndRepeat(&change) 90 | assert.Equal(t, "difficult", rebuilt["path"]) 91 | 92 | prop := &PropertyChanges{Changes: []*Change{&change}} 93 | assert.Len(t, prop.GetPropertyChanges(), 1) 94 | } 95 | -------------------------------------------------------------------------------- /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 | return c.Changes 19 | } 20 | 21 | // TotalChanges represents the total number of changes that have occurred to a Contact object 22 | func (c *ContactChanges) TotalChanges() int { 23 | return c.PropertyChanges.TotalChanges() 24 | } 25 | 26 | // TotalBreakingChanges always returns 0 for Contact objects, they are non-binding. 27 | func (c *ContactChanges) TotalBreakingChanges() int { 28 | return 0 29 | } 30 | 31 | // CompareContact will check a left (original) and right (new) Contact object for any changes. If there 32 | // were any, a pointer to a ContactChanges object is returned, otherwise if nothing changed - the function 33 | // returns nil. 34 | func CompareContact(l, r *base.Contact) *ContactChanges { 35 | var changes []*Change 36 | var props []*PropertyCheck 37 | 38 | // check URL 39 | props = append(props, &PropertyCheck{ 40 | LeftNode: l.URL.ValueNode, 41 | RightNode: r.URL.ValueNode, 42 | Label: v3.URLLabel, 43 | Changes: &changes, 44 | Breaking: false, 45 | Original: l, 46 | New: r, 47 | }) 48 | 49 | // check name 50 | props = append(props, &PropertyCheck{ 51 | LeftNode: l.Name.ValueNode, 52 | RightNode: r.Name.ValueNode, 53 | Label: v3.NameLabel, 54 | Changes: &changes, 55 | Breaking: false, 56 | Original: l, 57 | New: r, 58 | }) 59 | 60 | // check email 61 | props = append(props, &PropertyCheck{ 62 | LeftNode: l.Email.ValueNode, 63 | RightNode: r.Email.ValueNode, 64 | Label: v3.EmailLabel, 65 | Changes: &changes, 66 | Breaking: false, 67 | Original: l, 68 | New: r, 69 | }) 70 | 71 | // check everything. 72 | CheckProperties(props) 73 | 74 | dc := new(ContactChanges) 75 | dc.PropertyChanges = NewPropertyChanges(changes) 76 | if dc.TotalChanges() <= 0 { 77 | return nil 78 | } 79 | return dc 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 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 | return a.Changes 20 | } 21 | 22 | // TotalChanges represents the total number of changes made between Example instances. 23 | func (a *ExamplesChanges) TotalChanges() int { 24 | return a.PropertyChanges.TotalChanges() 25 | } 26 | 27 | // TotalBreakingChanges will always return 0. Examples cannot break a contract. 28 | func (a *ExamplesChanges) TotalBreakingChanges() int { 29 | return 0 // not supported. 30 | } 31 | 32 | // CompareExamplesV2 compares two Swagger Examples objects, returning a pointer to 33 | // ExamplesChanges if anything was found. 34 | func CompareExamplesV2(l, r *v2.Examples) *ExamplesChanges { 35 | lHashes := make(map[string]string) 36 | rHashes := make(map[string]string) 37 | lValues := make(map[string]low.ValueReference[*yaml.Node]) 38 | rValues := make(map[string]low.ValueReference[*yaml.Node]) 39 | 40 | for k, v := range l.Values.FromOldest() { 41 | lHashes[k.Value] = low.GenerateHashString(v.Value) 42 | lValues[k.Value] = v 43 | } 44 | 45 | for k, v := range r.Values.FromOldest() { 46 | rHashes[k.Value] = low.GenerateHashString(v.Value) 47 | rValues[k.Value] = v 48 | } 49 | var changes []*Change 50 | 51 | // check left example hashes 52 | for k := range lHashes { 53 | rhash := rHashes[k] 54 | if rhash == "" { 55 | CreateChange(&changes, ObjectRemoved, k, 56 | lValues[k].GetValueNode(), nil, false, 57 | lValues[k].GetValue(), nil) 58 | continue 59 | } 60 | if lHashes[k] == rHashes[k] { 61 | continue 62 | } 63 | CreateChange(&changes, Modified, k, 64 | lValues[k].GetValueNode(), rValues[k].GetValueNode(), false, 65 | lValues[k].GetValue(), lValues[k].GetValue()) 66 | 67 | } 68 | 69 | // check right example hashes 70 | for k := range rHashes { 71 | lhash := lHashes[k] 72 | if lhash == "" { 73 | CreateChange(&changes, ObjectAdded, k, 74 | nil, lValues[k].GetValueNode(), false, 75 | nil, lValues[k].GetValue()) 76 | continue 77 | } 78 | } 79 | 80 | ex := new(ExamplesChanges) 81 | ex.PropertyChanges = NewPropertyChanges(changes) 82 | if ex.TotalChanges() <= 0 { 83 | return nil 84 | } 85 | return ex 86 | } 87 | -------------------------------------------------------------------------------- /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 | var changes []*Change 20 | changes = append(changes, e.Changes...) 21 | if e.ExtensionChanges != nil { 22 | changes = append(changes, e.ExtensionChanges.GetAllChanges()...) 23 | } 24 | return changes 25 | } 26 | 27 | // TotalChanges returns a count of everything that changed 28 | func (e *ExternalDocChanges) TotalChanges() int { 29 | c := e.PropertyChanges.TotalChanges() 30 | if e.ExtensionChanges != nil { 31 | c += e.ExtensionChanges.TotalChanges() 32 | } 33 | return c 34 | } 35 | 36 | // TotalBreakingChanges always returns 0 for ExternalDoc objects, they are non-binding. 37 | func (e *ExternalDocChanges) TotalBreakingChanges() int { 38 | return 0 39 | } 40 | 41 | // CompareExternalDocs will compare a left (original) and a right (new) slice of ValueReference 42 | // nodes for any changes between them. If there are changes, then a pointer to ExternalDocChanges 43 | // is returned, otherwise if nothing changed - then nil is returned. 44 | func CompareExternalDocs(l, r *base.ExternalDoc) *ExternalDocChanges { 45 | var changes []*Change 46 | var props []*PropertyCheck 47 | 48 | // URL 49 | props = append(props, &PropertyCheck{ 50 | LeftNode: l.URL.ValueNode, 51 | RightNode: r.URL.ValueNode, 52 | Label: v3.URLLabel, 53 | Changes: &changes, 54 | Breaking: false, 55 | Original: l, 56 | New: r, 57 | }) 58 | 59 | // description. 60 | props = append(props, &PropertyCheck{ 61 | LeftNode: l.Description.ValueNode, 62 | RightNode: r.Description.ValueNode, 63 | Label: v3.DescriptionLabel, 64 | Changes: &changes, 65 | Breaking: false, 66 | Original: l, 67 | New: r, 68 | }) 69 | 70 | // check everything. 71 | CheckProperties(props) 72 | 73 | dc := new(ExternalDocChanges) 74 | dc.PropertyChanges = NewPropertyChanges(changes) 75 | 76 | // check extensions 77 | dc.ExtensionChanges = CheckExtensions(l, r) 78 | if dc.TotalChanges() <= 0 { 79 | return nil 80 | } 81 | return dc 82 | } 83 | -------------------------------------------------------------------------------- /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 | "gopkg.in/yaml.v3" 14 | ) 15 | 16 | func TestCompareRequestBodies(t *testing.T) { 17 | left := `description: something 18 | required: true 19 | content: 20 | application/json: 21 | schema: 22 | type: int` 23 | 24 | right := `description: something 25 | required: true 26 | content: 27 | application/json: 28 | schema: 29 | type: int` 30 | 31 | var lNode, rNode yaml.Node 32 | _ = yaml.Unmarshal([]byte(left), &lNode) 33 | _ = yaml.Unmarshal([]byte(right), &rNode) 34 | 35 | // create low level objects 36 | var lDoc v3.RequestBody 37 | var rDoc v3.RequestBody 38 | _ = low.BuildModel(lNode.Content[0], &lDoc) 39 | _ = low.BuildModel(rNode.Content[0], &rDoc) 40 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 41 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 42 | 43 | // compare. 44 | extChanges := CompareRequestBodies(&lDoc, &rDoc) 45 | assert.Nil(t, extChanges) 46 | } 47 | 48 | func TestCompareRequestBodies_Modified(t *testing.T) { 49 | left := `description: something 50 | required: true 51 | x-pizza: thin 52 | content: 53 | application/json: 54 | schema: 55 | type: int` 56 | 57 | right := `x-pizza: oven 58 | description: nothing 59 | required: false 60 | content: 61 | application/json: 62 | schema: 63 | type: string` 64 | 65 | var lNode, rNode yaml.Node 66 | _ = yaml.Unmarshal([]byte(left), &lNode) 67 | _ = yaml.Unmarshal([]byte(right), &rNode) 68 | 69 | // create low level objects 70 | var lDoc v3.RequestBody 71 | var rDoc v3.RequestBody 72 | _ = low.BuildModel(lNode.Content[0], &lDoc) 73 | _ = low.BuildModel(rNode.Content[0], &rDoc) 74 | _ = lDoc.Build(context.Background(), nil, lNode.Content[0], nil) 75 | _ = rDoc.Build(context.Background(), nil, rNode.Content[0], nil) 76 | 77 | // compare. 78 | extChanges := CompareRequestBodies(&lDoc, &rDoc) 79 | 80 | assert.Equal(t, 4, extChanges.TotalChanges()) 81 | assert.Len(t, extChanges.GetAllChanges(), 4) 82 | assert.Equal(t, 2, extChanges.TotalBreakingChanges()) 83 | } 84 | -------------------------------------------------------------------------------- /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 | var changes []*Change 21 | changes = append(changes, s.Changes...) 22 | if s.ExtensionChanges != nil { 23 | changes = append(changes, s.ExtensionChanges.GetAllChanges()...) 24 | } 25 | return changes 26 | } 27 | 28 | // TotalChanges returns the total changes found between two Swagger Scopes objects. 29 | func (s *ScopesChanges) TotalChanges() int { 30 | c := s.PropertyChanges.TotalChanges() 31 | if s.ExtensionChanges != nil { 32 | c += s.ExtensionChanges.TotalChanges() 33 | } 34 | return c 35 | } 36 | 37 | // TotalBreakingChanges returns the total number of breaking changes between two Swagger Scopes objects. 38 | func (s *ScopesChanges) TotalBreakingChanges() int { 39 | return s.PropertyChanges.TotalBreakingChanges() 40 | } 41 | 42 | // CompareScopes compares a left and right Swagger Scopes objects for changes. If anything is found, returns 43 | // a pointer to ScopesChanges, or returns nil if nothing is found. 44 | func CompareScopes(l, r *v2.Scopes) *ScopesChanges { 45 | if low.AreEqual(l, r) { 46 | return nil 47 | } 48 | var changes []*Change 49 | for k, v := range l.Values.FromOldest() { 50 | if r != nil && r.FindScope(k.Value) == nil { 51 | CreateChange(&changes, ObjectRemoved, v3.Scopes, 52 | v.ValueNode, nil, true, 53 | k.Value, nil) 54 | continue 55 | } 56 | if r != nil && r.FindScope(k.Value) != nil { 57 | if v.Value != r.FindScope(k.Value).Value { 58 | CreateChange(&changes, Modified, v3.Scopes, 59 | v.ValueNode, r.FindScope(k.Value).ValueNode, true, 60 | v.Value, r.FindScope(k.Value).Value) 61 | } 62 | } 63 | } 64 | for k, v := range r.Values.FromOldest() { 65 | if l != nil && l.FindScope(k.Value) == nil { 66 | CreateChange(&changes, ObjectAdded, v3.Scopes, 67 | nil, v.ValueNode, false, 68 | nil, k.Value) 69 | } 70 | } 71 | 72 | sc := new(ScopesChanges) 73 | sc.PropertyChanges = NewPropertyChanges(changes) 74 | sc.ExtensionChanges = CompareExtensions(l.Extensions, r.Extensions) 75 | return sc 76 | } 77 | -------------------------------------------------------------------------------- /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 | return s.Changes 19 | } 20 | 21 | // CompareServerVariables compares a left and right OpenAPI ServerVariable object for changes. 22 | // If anything is found, returns a pointer to a ServerVariableChanges instance, otherwise returns nil. 23 | func CompareServerVariables(l, r *v3.ServerVariable) *ServerVariableChanges { 24 | if low.AreEqual(l, r) { 25 | return nil 26 | } 27 | 28 | var props []*PropertyCheck 29 | var changes []*Change 30 | 31 | lValues := make(map[string]low.NodeReference[string]) 32 | rValues := make(map[string]low.NodeReference[string]) 33 | for i := range l.Enum { 34 | lValues[l.Enum[i].Value] = l.Enum[i] 35 | } 36 | for i := range r.Enum { 37 | rValues[r.Enum[i].Value] = r.Enum[i] 38 | } 39 | for k := range lValues { 40 | if _, ok := rValues[k]; !ok { 41 | CreateChange(&changes, ObjectRemoved, v3.EnumLabel, 42 | lValues[k].ValueNode, nil, true, 43 | lValues[k].Value, nil) 44 | continue 45 | } 46 | } 47 | for k := range rValues { 48 | if _, ok := lValues[k]; !ok { 49 | CreateChange(&changes, ObjectAdded, v3.EnumLabel, 50 | lValues[k].ValueNode, rValues[k].ValueNode, false, 51 | lValues[k].Value, rValues[k].Value) 52 | } 53 | } 54 | 55 | // default 56 | props = append(props, &PropertyCheck{ 57 | LeftNode: l.Default.ValueNode, 58 | RightNode: r.Default.ValueNode, 59 | Label: v3.DefaultLabel, 60 | Changes: &changes, 61 | Breaking: true, 62 | Original: l, 63 | New: r, 64 | }) 65 | 66 | // description 67 | props = append(props, &PropertyCheck{ 68 | LeftNode: l.Description.ValueNode, 69 | RightNode: r.Description.ValueNode, 70 | Label: v3.DescriptionLabel, 71 | Changes: &changes, 72 | Breaking: false, 73 | Original: l, 74 | New: r, 75 | }) 76 | 77 | // check everything. 78 | CheckProperties(props) 79 | sc := new(ServerVariableChanges) 80 | sc.PropertyChanges = NewPropertyChanges(changes) 81 | return sc 82 | } 83 | -------------------------------------------------------------------------------- /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 | assert.Equal(t, 43, report.ChangeReport[v3.PathsLabel].Total) 30 | assert.Equal(t, 10, report.ChangeReport[v3.PathsLabel].Breaking) 31 | assert.Equal(t, 3, report.ChangeReport[v3.TagsLabel].Total) 32 | assert.Equal(t, 1, report.ChangeReport[v3.ExternalDocsLabel].Total) 33 | assert.Equal(t, 2, report.ChangeReport[v3.WebhooksLabel].Total) 34 | assert.Equal(t, 2, report.ChangeReport[v3.ServersLabel].Total) 35 | assert.Equal(t, 1, report.ChangeReport[v3.ServersLabel].Breaking) 36 | assert.Equal(t, 1, report.ChangeReport[v3.SecurityLabel].Total) 37 | assert.Equal(t, 20, report.ChangeReport[v3.ComponentsLabel].Total) 38 | assert.Equal(t, 8, report.ChangeReport[v3.ComponentsLabel].Breaking) 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------