diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..0467c00 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @oai/tsc \ No newline at end of file diff --git a/.github/templates/agenda.md b/.github/templates/agenda.md index 34481a6..85e5482 100644 --- a/.github/templates/agenda.md +++ b/.github/templates/agenda.md @@ -3,7 +3,7 @@ - Meeting link: + Meeting link: diff --git a/.github/workflows/agenda.yaml b/.github/workflows/agenda.yaml index d3f97f3..1d2a626 100644 --- a/.github/workflows/agenda.yaml +++ b/.github/workflows/agenda.yaml @@ -2,6 +2,9 @@ name: Create meeting template on: workflow_dispatch: {} + schedule: + # every two weeks on tuesday at 10AM PST (with DST) + - cron: '0 17 */14 * 2' jobs: create-discussion: @@ -16,6 +19,11 @@ jobs: echo 'AGENDA<> $GITHUB_ENV cat .github/templates/agenda.md >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV + - name: Get Next Meeting Date + id: get-next-meeting-date + run: | + NEXT_MEETING_DATE=$(date -d "next Tuesday" +%Y-%m-%d) + echo "NEXT_MEETING_DATE=$NEXT_MEETING_DATE" >> $GITHUB_ENV - name: Create discussion with agenda id: create-repository-discussion uses: octokit/graphql-action@v2.x @@ -24,7 +32,7 @@ jobs: with: variables: | body: "${{ env.AGENDA }}" - title: "Overlays Meeting" + title: "Overlays Meeting (${{ env.NEXT_MEETING_DATE }})" repositoryId: 'MDEwOlJlcG9zaXRvcnkzNTk4NjU5MDI=' categoryId: 'DIC_kwDOFXMeLs4COVB8' query: | diff --git a/.github/workflows/respec.yaml b/.github/workflows/respec.yaml index d7c93b8..e3cf9c5 100644 --- a/.github/workflows/respec.yaml +++ b/.github/workflows/respec.yaml @@ -19,9 +19,18 @@ jobs: respec: if: github.repository == 'OAI/Overlay-Specification' - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.OAI_SPEC_PUBLISHER_APPID }} + private-key: ${{ secrets.OAI_SPEC_PUBLISHER_PRIVATE_KEY }} + owner: OAI + repositories: OpenAPI-Specification + - uses: actions/checkout@v4 # checkout main branch with: fetch-depth: 0 @@ -35,7 +44,7 @@ jobs: - uses: actions/checkout@v4 # checkout gh-pages branch with: - token: ${{ secrets.OAS_REPO_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} repository: OAI/OpenAPI-Specification # TODO: change to OAI/... ref: gh-pages path: deploy @@ -46,16 +55,14 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@v7 with: - # A personal access token is required to push changes to the repository. - # This token needs to be refreshed regularly and stored in the repository secrets. - token: ${{ secrets.OAS_REPO_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} branch: update-overlay-respec-version base: gh-pages delete-branch: true path: deploy labels: Housekeeping - team-reviewers: OAI/tsc - title: Update ReSpec-rendered specification versions for Overlay + reviewers: darrelmiller,webron,earth2marsh,lornajane,mikekistler,miqui,handrews,ralfhandl + title: Overlay - Update ReSpec-rendered specification versions commit-message: Update ReSpec-rendered specification versions signoff: true body: | diff --git a/.github/workflows/schema-publish.yaml b/.github/workflows/schema-publish.yaml new file mode 100644 index 0000000..91bc36b --- /dev/null +++ b/.github/workflows/schema-publish.yaml @@ -0,0 +1,66 @@ +name: schema-publish + +# author: @ralfhandl + +# +# This workflow copies the x.y schemas to the gh-pages branch +# + +# run this on push to main +on: + push: + branches: + - main + workflow_dispatch: {} + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.OAI_SPEC_PUBLISHER_APPID }} + private-key: ${{ secrets.OAI_SPEC_PUBLISHER_PRIVATE_KEY }} + owner: OAI + repositories: OpenAPI-Specification + + - uses: actions/checkout@v4 # checkout main branch + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 # setup Node.js + with: + node-version: 20.x + + - name: Install dependencies + run: npm ci + + - uses: actions/checkout@v4 # checkout gh-pages branch + with: + token: ${{ steps.generate-token.outputs.token }} + repository: OAI/OpenAPI-Specification + ref: gh-pages + path: deploy + + - name: run main script + run: scripts/schema-publish.sh + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.generate-token.outputs.token }} + branch: publish-overlay-schema-iteration + base: gh-pages + delete-branch: true + path: deploy + labels: Housekeeping,Schema + reviewers: darrelmiller,webron,earth2marsh,lornajane,mikekistler,miqui,handrews,ralfhandl + title: Overlay - Publish Schema Iterations + commit-message: New Overlay schema iterations + signoff: true + body: | + This pull request is automatically triggered by GitHub action `schema-publish` in the OAI/Overlay-Specification repo. + The `schemas/**/*.yaml` files have changed and JSON files are automatically generated. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a74309..a627cfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,38 @@ Pull requests are also welcome, but it is recommended to create an issue first, Questions and comments are also welcome - use the GitHub Discussions feature. You will also find notes from past meetings in the Discussion tab. +## Key information + +This project is covered by our [Code of Conduct](https://github.com/OAI/OpenAPI-Specification?tab=coc-ov-file#readme). +All participants are expected to read and follow this code. + +No changes, however trivial, are ever made to the contents of published specifications (the files in the `versions/` folder). +Exceptions may be made when links to external documents have been changed by a 3rd party, in order to keep our documents accurate. + +Published versions of the specification are in the `versions/` folder. +The under-development versions of the specification are in the file `src/overlay.md` on the appropriately-versioned branch. +For example, work on the next release for 1.1 is on `v1.1-dev` in the file `src/overlay.md`. + +The [spec site](https://spec.openapis.org) is the source of truth for the OpenAPI Overlay specification as it contains all the citations and author credits. + +The OpenAPI project is almost entirely staffed by volunteers. +Please be patient with the people in this project, who all have other jobs and are active here because we believe this project has a positive impact in the world. + +## Pull Requests + +Pull requests are always welcome but please read the section below on [branching strategy](#branching-strategy) before you start. + +Pull requests must come from a fork; create a fresh branch on your fork based on the target branch for your change. + +### Branching Strategy + +Overview of branches: + +- `main` holds the published versions of the specification, utility scripts and supporting documentation. +- `dev` is for development infrastructure and other changes that apply to multiple versions of development. +- Branches named `vX.Y-dev` are the active development branches for future releases. + All changes should be applied to the _earliest_ branch where the changes are relevant in the first instance. + ## Build the HTML version to publish We use ReSpec to render the markdown specification as HTML for publishing and easier reading. diff --git a/README.md b/README.md index 29a1bcc..b4fd879 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ If you are looking for tools to use with Overlays, try these: - [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-cli/getting-started) - [overlays-js](https://github.com/lornajane/openapi-overlays-js) - [apigee-go-gen CLI](https://apigee.github.io/apigee-go-gen/transform/commands/oas-overlay/) +- [openapi-format CLI/UI](https://github.com/thim81/openapi-format) +- [oas-patch CLI](https://github.com/mcroissant/oas_patcher) +- [oas-overlay-java](https://github.com/IBM/oas-overlay-java) +- [Specmatic](https://specmatic.io/) - [Docs](https://docs.specmatic.io/documentation/contract_tests.html#overlays) +- [BinkyLabs.OpenApi.Overlays - dotnet](https://github.com/BinkyLabs/openapi-overlays-dotnet) (Is something missing from the list? Send us a pull request to add it!) diff --git a/compliant-sets/README.md b/compliant-sets/README.md new file mode 100644 index 0000000..77d34b0 --- /dev/null +++ b/compliant-sets/README.md @@ -0,0 +1,9 @@ +# OpenAPI Overlay Compliant Sets + +The folders in this directory contain sets of "known good" Overlays, along with OpenAPI descriptions before and after the Overlay. +These files are offered as examples of how a series of Overlays are expected to be applied, with the aim of supporting people building tools that apply Overlays. + +Each directory contains: +- `overlay.yaml` - the Overlay +- `openapi.yaml` - an OpenAPI description to use +- `output.yaml` - the OpenAPI description after the Overlay has been applied diff --git a/compliant-sets/add-a-license/openapi.yaml b/compliant-sets/add-a-license/openapi.yaml new file mode 100644 index 0000000..12a99c1 --- /dev/null +++ b/compliant-sets/add-a-license/openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/add-a-license/output.yaml b/compliant-sets/add-a-license/output.yaml new file mode 100644 index 0000000..8378332 --- /dev/null +++ b/compliant-sets/add-a-license/output.yaml @@ -0,0 +1,74 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town + license: + name: MIT + url: 'https://opensource.org/licenses/MIT' +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 + diff --git a/compliant-sets/add-a-license/overlay.yaml b/compliant-sets/add-a-license/overlay.yaml new file mode 100644 index 0000000..3fea356 --- /dev/null +++ b/compliant-sets/add-a-license/overlay.yaml @@ -0,0 +1,10 @@ +overlay: '1.0.0' +info: + title: Add MIT license + version: '1.0.0' +actions: + - target: '$.info' + update: + license: + name: MIT + url: https://opensource.org/licenses/MIT diff --git a/compliant-sets/description-and-summary/openapi.yaml b/compliant-sets/description-and-summary/openapi.yaml new file mode 100644 index 0000000..12a99c1 --- /dev/null +++ b/compliant-sets/description-and-summary/openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/description-and-summary/output.yaml b/compliant-sets/description-and-summary/output.yaml new file mode 100644 index 0000000..4394d8f --- /dev/null +++ b/compliant-sets/description-and-summary/output.yaml @@ -0,0 +1,71 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All of the available buildings + operationId: buildingsList + description: This is the summary of getting the buildings + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/description-and-summary/overlay.yaml b/compliant-sets/description-and-summary/overlay.yaml new file mode 100644 index 0000000..36fb1cb --- /dev/null +++ b/compliant-sets/description-and-summary/overlay.yaml @@ -0,0 +1,10 @@ +overlay: 1.0.0 +info: + title: Add a building endpoint description + version: 1.0.0 +actions: +- target: $.paths['/buildings'].get + update: + description: This is the summary of getting the buildings + summary: All of the available buildings + diff --git a/compliant-sets/remove-example/openapi.yaml b/compliant-sets/remove-example/openapi.yaml new file mode 100644 index 0000000..12a99c1 --- /dev/null +++ b/compliant-sets/remove-example/openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/remove-example/output.yaml b/compliant-sets/remove-example/output.yaml new file mode 100644 index 0000000..24bfdd6 --- /dev/null +++ b/compliant-sets/remove-example/output.yaml @@ -0,0 +1,69 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + location_id: + type: integer + example: 44 diff --git a/compliant-sets/remove-example/overlay.yaml b/compliant-sets/remove-example/overlay.yaml new file mode 100644 index 0000000..5963349 --- /dev/null +++ b/compliant-sets/remove-example/overlay.yaml @@ -0,0 +1,7 @@ +overlay: 1.0.0 +info: + title: Remove an example + version: 1.0.0 +actions: +- target: $.components.schemas.Building.properties['building'].example + remove: true diff --git a/compliant-sets/remove-matching-responses/openapi.yaml b/compliant-sets/remove-matching-responses/openapi.yaml new file mode 100644 index 0000000..8d260cc --- /dev/null +++ b/compliant-sets/remove-matching-responses/openapi.yaml @@ -0,0 +1,30 @@ +openapi: 3.1.0 +info: + title: Responses + version: 1.0.0 +paths: + /foo: + get: + responses: + '200': + description: OK + '500': + description: oops + /bar: + post: + responses: + '201': + description: Created + '500': + description: oops + default: + description: something + /baa: + post: + responses: + '201': + description: Shouted + '500': + description: oops + default: + description: something diff --git a/compliant-sets/remove-matching-responses/output.yaml b/compliant-sets/remove-matching-responses/output.yaml new file mode 100644 index 0000000..ac5444b --- /dev/null +++ b/compliant-sets/remove-matching-responses/output.yaml @@ -0,0 +1,20 @@ +openapi: 3.1.0 +info: + title: Responses + version: 1.0.0 +paths: + /foo: + get: + responses: + '200': + description: OK + /bar: + post: + responses: + '201': + description: Created + /baa: + post: + responses: + '201': + description: Shouted diff --git a/compliant-sets/remove-matching-responses/overlay.yaml b/compliant-sets/remove-matching-responses/overlay.yaml new file mode 100644 index 0000000..6607a77 --- /dev/null +++ b/compliant-sets/remove-matching-responses/overlay.yaml @@ -0,0 +1,11 @@ +overlay: 1.0.0 +info: + title: Response code removal test + version: 1.0.0 + +actions: + - target: $.paths..responses['500'] + remove: true + + - target: $.paths..responses['default'] + remove: true diff --git a/compliant-sets/remove-property/openapi.yaml b/compliant-sets/remove-property/openapi.yaml new file mode 100644 index 0000000..12a99c1 --- /dev/null +++ b/compliant-sets/remove-property/openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/remove-property/output.yaml b/compliant-sets/remove-property/output.yaml new file mode 100644 index 0000000..aaf6e0c --- /dev/null +++ b/compliant-sets/remove-property/output.yaml @@ -0,0 +1,67 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/remove-property/overlay.yaml b/compliant-sets/remove-property/overlay.yaml new file mode 100644 index 0000000..e143719 --- /dev/null +++ b/compliant-sets/remove-property/overlay.yaml @@ -0,0 +1,8 @@ +overlay: 1.0.0 +info: + title: Remove all string properties + version: 1.0.0 +actions: +- target: $.paths['/locations'].get.responses['200']..properties[?(@.type == 'string')] + remove: true + diff --git a/compliant-sets/remove-server/openapi.yaml b/compliant-sets/remove-server/openapi.yaml new file mode 100644 index 0000000..9187ea5 --- /dev/null +++ b/compliant-sets/remove-server/openapi.yaml @@ -0,0 +1,10 @@ +openapi: 3.1.0 +info: + title: API servers + version: 1.0.0 +servers: + - url: 'https://api.dev.example.com' + description: Dev + - url: 'https://api.example.com' + description: Production +paths: {} diff --git a/compliant-sets/remove-server/output.yaml b/compliant-sets/remove-server/output.yaml new file mode 100644 index 0000000..8195aea --- /dev/null +++ b/compliant-sets/remove-server/output.yaml @@ -0,0 +1,8 @@ +openapi: 3.1.0 +info: + title: API servers + version: 1.0.0 +servers: + - url: 'https://api.example.com' + description: Production +paths: {} diff --git a/compliant-sets/remove-server/overlay.yaml b/compliant-sets/remove-server/overlay.yaml new file mode 100644 index 0000000..aa3a050 --- /dev/null +++ b/compliant-sets/remove-server/overlay.yaml @@ -0,0 +1,9 @@ +overlay: 1.0.0 +info: + title: Remove dev server + version: 1.0.0 +extends: openapi-with-servers.yaml + +actions: + - target: $.servers[?( @.description == 'Dev' )] + remove: true diff --git a/compliant-sets/replace-servers-for-sandbox/openapi.yaml b/compliant-sets/replace-servers-for-sandbox/openapi.yaml new file mode 100644 index 0000000..e514c5b --- /dev/null +++ b/compliant-sets/replace-servers-for-sandbox/openapi.yaml @@ -0,0 +1,46 @@ +openapi: 3.1.0 +info: + title: Simple API + description: A basic OpenAPI description with a single endpoint. + version: 1.0.0 +servers: + - url: https://api.example.com/v1 + description: Production server + - url: https://staging.example.com/v1 + description: Staging server +paths: + /getItems: + get: + summary: Retrieve a list of items + description: Fetch a list of items from the API. + operationId: getItems + responses: + '200': + description: Successful response with items + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + description: Unique identifier for the item + name: + type: string + description: Name of the item + description: + type: string + description: Detailed information about the item + examples: + example1: + summary: Example response + value: + - id: 1 + name: Item One + description: Description for item one + - id: 2 + name: Item Two + description: Description for item two + diff --git a/compliant-sets/replace-servers-for-sandbox/output.yaml b/compliant-sets/replace-servers-for-sandbox/output.yaml new file mode 100644 index 0000000..e354868 --- /dev/null +++ b/compliant-sets/replace-servers-for-sandbox/output.yaml @@ -0,0 +1,43 @@ +openapi: 3.1.0 +info: + title: Simple API + description: A basic OpenAPI description with a single endpoint. + version: 1.0.0 +paths: + /getItems: + get: + summary: Retrieve a list of items + description: Fetch a list of items from the API. + operationId: getItems + responses: + '200': + description: Successful response with items + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + description: Unique identifier for the item + name: + type: string + description: Name of the item + description: + type: string + description: Detailed information about the item + examples: + example1: + summary: Example response + value: + - id: 1 + name: Item One + description: Description for item one + - id: 2 + name: Item Two + description: Description for item two +servers: + - url: http://api-test-tunnel.local + description: Local development server diff --git a/compliant-sets/replace-servers-for-sandbox/overlay.yaml b/compliant-sets/replace-servers-for-sandbox/overlay.yaml new file mode 100644 index 0000000..dd1ffde --- /dev/null +++ b/compliant-sets/replace-servers-for-sandbox/overlay.yaml @@ -0,0 +1,13 @@ +overlay: 1.0.0 +info: + title: Change server URL + description: Replace servers list with a single sandbox or local development URL for the API + version: 1.0.0 +actions: + - target: '$.servers' + remove: true + - target: '$' + update: + servers: + - url: http://api-test-tunnel.local + description: Local development server \ No newline at end of file diff --git a/compliant-sets/update-root/openapi.yaml b/compliant-sets/update-root/openapi.yaml new file mode 100644 index 0000000..12a99c1 --- /dev/null +++ b/compliant-sets/update-root/openapi.yaml @@ -0,0 +1,70 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/update-root/output.yaml b/compliant-sets/update-root/output.yaml new file mode 100644 index 0000000..22fbe70 --- /dev/null +++ b/compliant-sets/update-root/output.yaml @@ -0,0 +1,71 @@ +openapi: 3.1.0 +info: + version: 1.0.0 + title: Imaginary town + x-overlaid: true +servers: + - url: 'https://example.com' + description: Example server +paths: + /buildings: + get: + summary: All buildings + operationId: buildingsList + responses: + '200': + description: Return all known buildings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Building' + '/buildings/{buildingId}': + get: + summary: Specific building + operationId: buildingById + parameters: + - name: buildingId + in: path + required: true + description: Which building to return + schema: + type: string + responses: + '200': + description: Return a building + content: + application/json: + schema: + $ref: '#/components/schemas/Building' + /locations: + get: + summary: All locations + operationId: locationList + responses: + '200': + description: Returns all locations + content: + application/json: + schema: + type: array + items: + type: object + properties: + location_id: + type: integer + example: 44 + name: + type: string + example: North Village +components: + schemas: + Building: + type: object + properties: + building: + type: string + example: house + location_id: + type: integer + example: 44 diff --git a/compliant-sets/update-root/overlay.yaml b/compliant-sets/update-root/overlay.yaml new file mode 100644 index 0000000..606ec8f --- /dev/null +++ b/compliant-sets/update-root/overlay.yaml @@ -0,0 +1,9 @@ +overlay: 1.0.0 +info: + title: Structured Overlay + version: 1.0.0 +actions: +- target: "$" # Root of document + update: + info: + x-overlaid: true diff --git a/package-lock.json b/package-lock.json index a8828c5..fc1063a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@hyperjump/json-schema": "^1.9.8", + "@hyperjump/json-schema": "^1.9.9", "c8": "^10.1.2", "markdownlint-cli": "^0.41.0", "mdv": "^1.3.4", @@ -493,9 +493,9 @@ } }, "node_modules/@hyperjump/json-schema": { - "version": "1.9.8", - "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.9.8.tgz", - "integrity": "sha512-qmdMpYn8CpYR7z3fxkL6fgkDvMaAEFKtmYu3XDi6hWW2BT+rLl7T4Y4QpafEIR4wkcmCxcJf9me9FmxKpv3i9g==", + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@hyperjump/json-schema/-/json-schema-1.9.9.tgz", + "integrity": "sha512-+3aN6GaJvRzQ3H5JxO4wIuYiw6/iQLJ260DvtlaY5DDK0ti4uPmmEg56ijGsyYABj00GVTxyOkFO1BH9AN707w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2cab8cc..f7d9471 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "yargs": "^17.7.2" }, "devDependencies": { - "@hyperjump/json-schema": "^1.9.8", + "@hyperjump/json-schema": "^1.9.9", "c8": "^10.1.2", "markdownlint-cli": "^0.41.0", "mdv": "^1.3.4", @@ -41,6 +41,6 @@ "scripts": { "build": "bash ./scripts/md2html/build.sh", "format-markdown": "bash ./scripts/format-markdown.sh ./versions/*.md", - "test": "c8 --100 vitest --watch=false" + "test": "c8 --100 vitest --watch=false && bash scripts/schema-test-coverage.sh" } } diff --git a/schemas/v1.0/readme.md b/schemas/v1.0/readme.md new file mode 100644 index 0000000..6a99c33 --- /dev/null +++ b/schemas/v1.0/readme.md @@ -0,0 +1,36 @@ +# OpenAPI Overlay 1.0.x JSON Schema + +Here you can find the JSON Schema for validating Overlays of versions 1.0.x. + +As a reminder, the JSON Schema is not the source of truth for the Specification. +In cases of conflicts between the Specification itself and the JSON Schema, the +Specification wins. Also, some Specification constraints cannot be represented +with the JSON Schema so it's highly recommended to employ other methods to +ensure compliance. + +The iteration version of the JSON Schema can be found in the `$id` field. +For example, the value of `$id: https://spec.openapis.org/overlay/1.0/schema/2024-10-17` means this iteration was created on October 17, 2024. + +## Contributing + +To submit improvements to the schema, modify the `schema.yaml` and add test cases for your changes. + +The TSC will then: +- Run tests on the updated schema +- Update the iteration version +- Publish the new version + +## Tests + +The [test suite](../../tests/v1.0) is part of this package. + +```bash +npm install +npm test +``` + +You can also validate a document individually. + +```bash +node scripts/validate.mjs path/to/document/to/validate.yaml +``` \ No newline at end of file diff --git a/schemas/v1.0/schema.yaml b/schemas/v1.0/schema.yaml index 96196d5..000bfa9 100644 --- a/schemas/v1.0/schema.yaml +++ b/schemas/v1.0/schema.yaml @@ -1,5 +1,63 @@ $id: https://spec.openapis.org/overlay/1.0/schema/WORK-IN-PROGRESS $schema: https://json-schema.org/draft/2020-12/schema description: The description of Overlay v1.0.x documents - type: object +properties: + overlay: + type: string + pattern: ^1\.0\.\d+$ + info: + $ref: "#/$defs/info-object" + extends: + type: string + format: uri-reference + actions: + type: array + minItems: 1 + uniqueItems: true + items: + $ref: "#/$defs/action-object" +required: + - overlay + - info + - actions +$ref: "#/$defs/specification-extensions" +unevaluatedProperties: false +$defs: + info-object: + type: object + properties: + title: + type: string + version: + type: string + required: + - title + - version + $ref: "#/$defs/specification-extensions" + unevaluatedProperties: false + action-object: + properties: + target: + type: string + pattern: ^\$ + description: + type: string + update: + type: + - string + - boolean + - object + - array + - number + - "null" + remove: + type: boolean + default: false + required: + - target + $ref: "#/$defs/specification-extensions" + unevaluatedProperties: false + specification-extensions: + patternProperties: + ^x-: true diff --git a/scripts/md2html/build.sh b/scripts/md2html/build.sh index 215ce4a..1d8436e 100755 --- a/scripts/md2html/build.sh +++ b/scripts/md2html/build.sh @@ -15,22 +15,32 @@ cp ../../EDITORS.md history/EDITORS_v1.0.0.md # temporarily copy installed version of respec into build directory cp -p ../../node_modules/respec/builds/respec-w3c.* ../../deploy/js/ +# latest=`git describe --abbrev=0 --tags` -- introduce after release tags created latest=1.0.0 latestCopied=none -for filename in ../../versions/*.md ; do +lastMinor="-" +for filename in $(ls -1 ../../versions/[123456789].*.md | sort -r) ; do version=$(basename "$filename" .md) + minorVersion=${version:0:3} tempfile=../../deploy/overlay/v$version-tmp.html echo -e "\n=== v$version ===" + node md2html.js --maintainers ./history/EDITORS_v$version.md ${filename} > $tempfile npx respec --use-local --src $tempfile --out ../../deploy/overlay/v$version.html rm $tempfile + if [ $version = $latest ]; then if [[ ${version} != *"rc"* ]];then # version is not a Release Candidate - cp -p ../../deploy/overlay/v$version.html ../../deploy/overlay/latest.html + ( cd ../../deploy/overlay && ln -sf v$version.html latest.html ) latestCopied=v$version fi fi + + if [ ${minorVersion} != ${lastMinor} ]; then + ( cd ../../deploy/overlay && ln -sf v$version.html v$minorVersion.html ) + lastMinor=$minorVersion + fi done echo Latest tag is $latest, copied $latestCopied to latest.html diff --git a/scripts/schema-publish.sh b/scripts/schema-publish.sh new file mode 100755 index 0000000..ab2cff7 --- /dev/null +++ b/scripts/schema-publish.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +# Author: @ralfhandl + +# Run this script from the root of the repo. It is designed to be run by a GitHub workflow. + +for schemaDir in schemas/v* ; do + vVersion=$(basename "$schemaDir") + version=${vVersion:1} + echo $version + + # list of schemas to process, dependent schemas come first + schemas=(schema.yaml) + + # find the newest commit date for each schema + maxDate="" + declare -A datesHash + for schema in "${schemas[@]}"; do + if [ -f "$schemaDir/$schema" ]; then + newestCommitDate=$(git log -1 --format="%ad" --date=short "$schemaDir/$schema") + + # the newest date across a schema and all its dependencies is its date stamp + if [ "$newestCommitDate" \> "$maxDate" ]; then + maxDate=$newestCommitDate + fi + datesHash["$schema"]=$maxDate + echo $schema changed at $newestCommitDate + fi + done + + # construct sed command + sedCmd=() + for schema in "${!datesHash[@]}"; do + base=$(basename "$schema" .yaml) + sedCmd+=("-e s/$base\/WORK-IN-PROGRESS/$base\/${datesHash[$schema]}/g") + done + + # create the date-stamped schemas + for schema in "${!datesHash[@]}"; do + base=$(basename "$schema" .yaml) + target=deploy/overlay/$version/$base/${datesHash[$schema]} + + mkdir -p "deploy/overlay/$version/$base" + + sed ${sedCmd[@]} $schemaDir/$schema > $target.yaml + node scripts/yaml2json/yaml2json.js $target.yaml + rm $target.yaml + mv $target.json $target + + mv deploy/overlay/$version/$base/*.md $target.md + done + + echo "" +done diff --git a/scripts/schema-test-coverage.mjs b/scripts/schema-test-coverage.mjs new file mode 100644 index 0000000..6be4eb1 --- /dev/null +++ b/scripts/schema-test-coverage.mjs @@ -0,0 +1,133 @@ +import { readdir, readFile } from "node:fs/promises"; +import YAML from "yaml"; +import { join } from "node:path"; +import { argv } from "node:process"; +import "@hyperjump/json-schema/draft-2020-12"; +import "@hyperjump/json-schema/draft-04"; +import { + compile, + getSchema, + interpret, + Validation, + BASIC, +} from "@hyperjump/json-schema/experimental"; +import * as Instance from "@hyperjump/json-schema/instance/experimental"; + +/** + * @import { AST } from "@hyperjump/json-schema/experimental" + * @import { Json } from "@hyperjump/json-schema" + */ + +import contentTypeParser from "content-type"; +import { addMediaTypePlugin } from "@hyperjump/browser"; +import { buildSchemaDocument } from "@hyperjump/json-schema/experimental"; + +addMediaTypePlugin("application/schema+yaml", { + parse: async (response) => { + const contentType = contentTypeParser.parse( + response.headers.get("content-type") ?? "", + ); + const contextDialectId = + contentType.parameters.schema ?? contentType.parameters.profile; + + const foo = YAML.parse(await response.text()); + return buildSchemaDocument(foo, response.url, contextDialectId); + }, + fileMatcher: (path) => path.endsWith(".yaml"), +}); + +/** @type (testDirectory: string) => AsyncGenerator<[string,Json]> */ +const tests = async function* (testDirectory) { + for (const file of await readdir(testDirectory, { + recursive: true, + withFileTypes: true, + })) { + if (!file.isFile() || !file.name.endsWith(".yaml")) { + continue; + } + + const testPath = join(file.parentPath, file.name); + const testJson = await readFile(testPath, "utf8"); + + yield [testPath, YAML.parse(testJson)]; + } +}; + +/** @type (testDirectory: string) => Promise */ +const runTests = async (testDirectory) => { + for await (const [name, test] of tests(testDirectory)) { + const instance = Instance.fromJs(test); + + const result = interpret(compiled, instance, BASIC); + + if (!result.valid) { + console.log("Failed:", name, result.errors); + } + } +}; + +/** @type (ast: AST) => string[] */ +const keywordLocations = (ast) => { + /** @type string[] */ + const locations = []; + for (const schemaLocation in ast) { + if (schemaLocation === "metaData") { + continue; + } + + if (Array.isArray(ast[schemaLocation])) { + for (const keyword of ast[schemaLocation]) { + if (Array.isArray(keyword)) { + locations.push(keyword[1]); + } + } + } + } + + return locations; +}; + +/////////////////////////////////////////////////////////////////////////////// + +const schema = await getSchema(argv[2]); +const compiled = await compile(schema); + +/** @type Set */ +const visitedLocations = new Set(); +const baseInterpret = Validation.interpret; +Validation.interpret = (url, instance, ast, dynamicAnchors, quiet) => { + if (Array.isArray(ast[url])) { + for (const keywordNode of ast[url]) { + if (Array.isArray(keywordNode)) { + visitedLocations.add(keywordNode[1]); + } + } + } + return baseInterpret(url, instance, ast, dynamicAnchors, quiet); +}; + +await runTests(argv[3]); +Validation.interpret = baseInterpret; + +// console.log("Covered:", visitedLocations); + +const allKeywords = keywordLocations(compiled.ast); +const notCovered = allKeywords.filter( + (location) => !visitedLocations.has(location), +); +if (notCovered.length > 0) { + console.log("NOT Covered:", notCovered.length, "of", allKeywords.length); + const maxNotCovered = 20; + const firstNotCovered = notCovered.slice(0, maxNotCovered); + if (notCovered.length > maxNotCovered) firstNotCovered.push("..."); + console.log(firstNotCovered); + process.exitCode = 1; +} + +console.log( + "Covered:", + visitedLocations.size, + "of", + allKeywords.length, + "(" + Math.floor((visitedLocations.size / allKeywords.length) * 100) + "%)", +); diff --git a/scripts/schema-test-coverage.sh b/scripts/schema-test-coverage.sh new file mode 100755 index 0000000..8909619 --- /dev/null +++ b/scripts/schema-test-coverage.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Author: @ralfhandl + +# Run this script from the root of the repo + +echo +echo "Schema Test Coverage" +echo + +rc=0 + +for schemaDir in schemas/v* ; do + version=$(basename "$schemaDir") + echo $version + + node scripts/schema-test-coverage.mjs $schemaDir/schema.yaml tests/$version/pass || rc=1 + + echo +done + +exit $rc diff --git a/scripts/yaml2json/yaml2json.js b/scripts/yaml2json/yaml2json.js new file mode 100755 index 0000000..decb075 --- /dev/null +++ b/scripts/yaml2json/yaml2json.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +'use strict'; + +const fs = require('fs'); +const yaml = require('yaml'); + +function convert(filename) { + // console.log(filename); + const s = fs.readFileSync(filename,'utf8'); + let obj; + try { + obj = yaml.parse(s, {prettyErrors: true}); + fs.writeFileSync(filename.replace('.yaml','.json'),JSON.stringify(obj,null,2),'utf8'); + } + catch (ex) { + console.warn(' ',ex.message); + process.exitCode = 1; + } +} + +if (process.argv.length<3) { + console.warn('Usage: yaml2json {infiles}'); +} +else { + for (let i=2;i { - const contentType = contentTypeParser.parse(response.headers.get("content-type") ?? ""); - const contextDialectId = contentType.parameters.schema ?? contentType.parameters.profile; - - const foo = YAML.parse(await response.text()); - return buildSchemaDocument(foo, response.url, contextDialectId); - }, - fileMatcher: (path) => path.endsWith(".yaml") - }); + parse: async (response) => { + const contentType = contentTypeParser.parse( + response.headers.get("content-type") ?? "", + ); + const contextDialectId = + contentType.parameters.schema ?? contentType.parameters.profile; + + const foo = YAML.parse(await response.text()); + return buildSchemaDocument(foo, response.url, contextDialectId); + }, + fileMatcher: (path) => path.endsWith(".yaml"), +}); const parseYamlFromFile = (filePath) => { const schemaYaml = readFileSync(filePath, "utf8"); @@ -26,7 +32,13 @@ const parseYamlFromFile = (filePath) => { setMetaSchemaOutputFormat(BASIC); -const validateOverlay = await validate("./schemas/v1.0/schema.yaml"); +let validateOverlay; +try { + validateOverlay = await validate("./schemas/v1.0/schema.yaml"); +} catch (error) { + console.error(error.output); + process.exit(1); +} describe("v1.0", () => { describe("Pass", () => { @@ -36,7 +48,7 @@ describe("v1.0", () => { test(entry.name, () => { const instance = parseYamlFromFile(`./tests/v1.0/pass/${entry.name}`); const output = validateOverlay(instance, BASIC); - expect(output.valid).to.equal(true); + expect(output).to.deep.equal({ valid: true }); }); }); });