From bc12dc1962c1fff4f72d079fab4bc44b1c19a0a2 Mon Sep 17 00:00:00 2001 From: John Gresty Date: Tue, 15 Oct 2024 14:53:08 +0100 Subject: [PATCH 1/3] fix: add default values to openapi docs In the event that there are endpoints defined for a version but nothing to collate, eg when all the endpoints are experimental, then we will output a stub document with null values for OpenAPI and Paths. This stub document cannot be parsed by cerberus, which will then crash on startup. This adds default values so that cerberus can parse the document, even though there are no routable paths for this version. --- internal/simplebuild/build.go | 5 +- testdata/output/2024-10-15/spec.json | 772 +++++++++++++++++++++++++++ testdata/output/2024-10-15/spec.yaml | 536 +++++++++++++++++++ testdata/output/embed.go | 6 +- 4 files changed, 1316 insertions(+), 3 deletions(-) create mode 100644 testdata/output/2024-10-15/spec.json create mode 100644 testdata/output/2024-10-15/spec.yaml diff --git a/internal/simplebuild/build.go b/internal/simplebuild/build.go index c694c8b6..669977b3 100644 --- a/internal/simplebuild/build.go +++ b/internal/simplebuild/build.go @@ -151,7 +151,10 @@ func (ops Operations) Build(startVersion vervet.Version) DocSet { output := make(DocSet, len(versionDates)) for idx, versionDate := range versionDates { output[idx] = VersionedDoc{ - Doc: &openapi3.T{}, + Doc: &openapi3.T{ + OpenAPI: "3.0.3", + Paths: openapi3.NewPaths(), + }, VersionDate: versionDate, } for path, spec := range filteredOps { diff --git a/testdata/output/2024-10-15/spec.json b/testdata/output/2024-10-15/spec.json new file mode 100644 index 00000000..278c9ef8 --- /dev/null +++ b/testdata/output/2024-10-15/spec.json @@ -0,0 +1,772 @@ +{ + "components": { + "headers": { + "DeprecationHeader": { + "description": "A header containing the deprecation date of the underlying endpoint. For more information, please refer to the deprecation header RFC:\nhttps://tools.ietf.org/id/draft-dalal-deprecation-header-01.html\n", + "example": "2021-07-01T00:00:00Z", + "schema": { + "format": "date-time", + "type": "string" + } + }, + "LocationHeader": { + "description": "A header providing a URL for the location of a resource\n", + "example": "https://example.com/resource/4", + "schema": { + "format": "url", + "type": "string" + } + }, + "RequestIdResponseHeader": { + "description": "A header containing a unique id used for tracking this request. If you are reporting an issue to Snyk it's very helpful to provide this ID.\n", + "example": "4b58e274-ec62-4fab-917b-1d2c48d6bdef", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "SunsetHeader": { + "description": "A header containing the date of when the underlying endpoint will be removed. This header is only present if the endpoint has been deprecated. Please refer to the RFC for more information:\nhttps://datatracker.ietf.org/doc/html/rfc8594\n", + "example": "2021-08-02T00:00:00Z", + "schema": { + "format": "date-time", + "type": "string" + } + }, + "VersionRequestedResponseHeader": { + "description": "A header containing the version of the endpoint requested by the caller.", + "example": "2021-06-04", + "schema": { + "$ref": "#/components/schemas/QueryVersion" + } + }, + "VersionServedResponseHeader": { + "description": "A header containing the version of the endpoint that was served by the API.", + "example": "2021-06-04", + "schema": { + "$ref": "#/components/schemas/ActualVersion" + } + }, + "VersionStageResponseHeader": { + "description": "A header containing the version stage of the endpoint. This stage describes the guarantees snyk provides surrounding stability of the endpoint.\n", + "schema": { + "enum": [ + "wip", + "experimental", + "beta", + "ga", + "deprecated", + "sunset" + ], + "example": "ga", + "type": "string" + } + } + }, + "parameters": { + "Pagination": { + "description": "The parameters used to paginate through a list of results from the API.", + "in": "query", + "name": "page", + "schema": { + "additionalProperties": false, + "properties": { + "after": { + "type": "string" + }, + "before": { + "type": "string" + }, + "size": { + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + }, + "Version": { + "description": "The requested version of the endpoint to process the request", + "example": "2021-06-04", + "in": "query", + "name": "version", + "required": true, + "schema": { + "$ref": "#/components/schemas/QueryVersion" + } + } + }, + "responses": { + "400": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorDocument" + } + } + }, + "description": "Bad Request: A parameter provided as a part of the request was invalid.", + "headers": { + "deprecation": { + "$ref": "#/components/headers/DeprecationHeader" + }, + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-lifecycle-stage": { + "$ref": "#/components/headers/VersionStageResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + }, + "sunset": { + "$ref": "#/components/headers/SunsetHeader" + } + } + }, + "401": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorDocument" + } + } + }, + "description": "Unauthorized: the request requires an authentication token or a token with more permissions.", + "headers": { + "deprecation": { + "$ref": "#/components/headers/DeprecationHeader" + }, + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-lifecycle-stage": { + "$ref": "#/components/headers/VersionStageResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + }, + "sunset": { + "$ref": "#/components/headers/SunsetHeader" + } + } + }, + "404": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorDocument" + } + } + }, + "description": "Not Found: The resource being operated on could not be found.", + "headers": { + "deprecation": { + "$ref": "#/components/headers/DeprecationHeader" + }, + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-lifecycle-stage": { + "$ref": "#/components/headers/VersionStageResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + }, + "sunset": { + "$ref": "#/components/headers/SunsetHeader" + } + } + }, + "500": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/ErrorDocument" + } + } + }, + "description": "Internal Server Error: An error was encountered while attempting to process the request.", + "headers": { + "deprecation": { + "$ref": "#/components/headers/DeprecationHeader" + }, + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-lifecycle-stage": { + "$ref": "#/components/headers/VersionStageResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + }, + "sunset": { + "$ref": "#/components/headers/SunsetHeader" + } + } + } + }, + "schemas": { + "ActualVersion": { + "description": "Resolved API version", + "pattern": "^((([0-9]{4})-([0-1][0-9]))-((3[01])|(0[1-9])|([12][0-9]))(~(wip|work-in-progress|experimental|beta))?)$", + "type": "string" + }, + "Error": { + "additionalProperties": false, + "example": { + "detail": "Not Found", + "status": "404" + }, + "properties": { + "detail": { + "description": "A human-readable explanation specific to this occurrence of the problem.", + "example": "The request was missing these required fields: ...", + "type": "string" + }, + "id": { + "description": "A unique identifier for this particular occurrence of the problem.", + "example": "f16c31b5-6129-4571-add8-d589da9be524", + "format": "uuid", + "type": "string" + }, + "meta": { + "additionalProperties": true, + "example": { + "key": "value" + }, + "type": "object" + }, + "source": { + "additionalProperties": false, + "example": { + "pointer": "/data/attributes" + }, + "properties": { + "parameter": { + "description": "A string indicating which URI query parameter caused the error.", + "example": "param1", + "type": "string" + }, + "pointer": { + "description": "A JSON Pointer [RFC6901] to the associated entity in the request document.", + "example": "/data/attributes", + "type": "string" + } + }, + "type": "object" + }, + "status": { + "description": "The HTTP status code applicable to this problem, expressed as a string value.", + "example": "400", + "pattern": "^[45]\\d\\d$", + "type": "string" + } + }, + "required": [ + "status", + "detail" + ], + "type": "object" + }, + "ErrorDocument": { + "additionalProperties": false, + "example": { + "errors": [ + { + "detail": "Permission denied for this resource", + "status": "403" + } + ], + "jsonapi": { + "version": "1.0" + } + }, + "properties": { + "errors": { + "example": [ + { + "detail": "Permission denied for this resource", + "status": "403" + } + ], + "items": { + "$ref": "#/components/schemas/Error" + }, + "minItems": 1, + "type": "array" + }, + "jsonapi": { + "$ref": "#/components/schemas/JsonApi" + } + }, + "required": [ + "jsonapi", + "errors" + ], + "type": "object" + }, + "HelloWorld": { + "additionalProperties": false, + "properties": { + "attributes": { + "additionalProperties": false, + "properties": { + "message": { + "type": "string" + }, + "requestSubject": { + "additionalProperties": false, + "properties": { + "clientId": { + "format": "uuid", + "type": "string" + }, + "publicId": { + "format": "uuid", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "publicId", + "type" + ], + "type": "object" + } + }, + "required": [ + "message", + "requestSubject" + ], + "type": "object" + }, + "id": { + "format": "uuid", + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "attributes" + ], + "type": "object" + }, + "JsonApi": { + "additionalProperties": false, + "example": { + "version": "1.0" + }, + "properties": { + "version": { + "description": "Version of the JSON API specification this server supports.", + "example": "1.0", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$", + "type": "string" + } + }, + "required": [ + "version" + ], + "type": "object" + }, + "LinkProperty": { + "example": "https://example.com/api/resource", + "oneOf": [ + { + "description": "A string containing the link’s URL.", + "example": "https://example.com/api/resource", + "type": "string" + }, + { + "additionalProperties": false, + "example": { + "href": "https://example.com/api/resource" + }, + "properties": { + "href": { + "description": "A string containing the link’s URL.", + "example": "https://example.com/api/resource", + "type": "string" + }, + "meta": { + "$ref": "#/components/schemas/Meta" + } + }, + "required": [ + "href" + ], + "type": "object" + } + ] + }, + "Links": { + "additionalProperties": false, + "properties": { + "first": { + "$ref": "#/components/schemas/LinkProperty" + }, + "last": { + "$ref": "#/components/schemas/LinkProperty" + }, + "next": { + "$ref": "#/components/schemas/LinkProperty" + }, + "prev": { + "$ref": "#/components/schemas/LinkProperty" + }, + "related": { + "$ref": "#/components/schemas/LinkProperty" + }, + "self": { + "$ref": "#/components/schemas/LinkProperty" + } + }, + "type": "object" + }, + "Meta": { + "additionalProperties": true, + "description": "Free-form object that may contain non-standard information.", + "example": { + "key1": "value1", + "key2": { + "sub_key": "sub_value" + }, + "key3": [ + "array_value1", + "array_value2" + ] + }, + "type": "object" + }, + "QueryVersion": { + "description": "Requested API version", + "pattern": "^(wip|work-in-progress|experimental|beta|((([0-9]{4})-([0-1][0-9]))-((3[01])|(0[1-9])|([12][0-9]))(~(wip|work-in-progress|experimental|beta))?))$", + "type": "string" + } + } + }, + "info": { + "title": "Registry", + "version": "3.0.0" + }, + "openapi": "3.0.3", + "paths": { + "/examples/hello-world": { + "post": { + "description": "Create a single result from the hello-world example", + "operationId": "helloWorldCreate", + "parameters": [ + { + "$ref": "#/components/parameters/Version" + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "additionalProperties": false, + "properties": { + "attributes": { + "additionalProperties": false, + "properties": { + "betaField": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message", + "betaField" + ], + "type": "object" + } + }, + "required": [ + "attributes" + ], + "type": "object" + } + } + } + }, + "responses": { + "201": { + "content": { + "application/vnd.api+json": { + "schema": { + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/HelloWorld" + }, + "jsonapi": { + "$ref": "#/components/schemas/JsonApi" + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "required": [ + "jsonapi", + "data", + "links" + ], + "type": "object" + } + } + }, + "description": "A hello world entity being requested is returned", + "headers": { + "location": { + "$ref": "#/components/headers/LocationHeader" + } + }, + "x-snyk-include-headers": { + "$ref": "../../../schemas/headers/common-response.yaml#/Common" + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "x-snyk-api-lifecycle": "released", + "x-snyk-api-owners": [ + "@snyk/api" + ], + "x-snyk-api-releases": [ + "2021-06-13~beta" + ], + "x-snyk-api-resource": "hello-world", + "x-snyk-api-stability": "beta", + "x-snyk-api-version": "2021-06-13~beta", + "x-stability-level": "beta" + } + }, + "/examples/hello-world/{id}": { + "get": { + "description": "Get a single result from the hello-world example", + "operationId": "helloWorldGetOne", + "parameters": [ + { + "$ref": "#/components/parameters/Version" + }, + { + "$ref": "#/components/parameters/Pagination" + }, + { + "description": "The id of the hello-world example entity to be retrieved.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.api+json": { + "schema": { + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/HelloWorld" + }, + "jsonapi": { + "$ref": "#/components/schemas/JsonApi" + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "required": [ + "jsonapi", + "data", + "links" + ], + "type": "object" + } + } + }, + "description": "A hello world entity being requested is returned", + "headers": { + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + }, + "x-snyk-api-lifecycle": "released", + "x-snyk-api-owners": [ + "@snyk/api" + ], + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], + "x-snyk-api-resource": "hello-world", + "x-snyk-api-stability": "beta", + "x-snyk-api-version": "2021-06-13~beta", + "x-stability-level": "beta" + } + }, + "/openapi": { + "get": { + "description": "List available versions of OpenAPI specification", + "operationId": "listAPIVersions", + "responses": { + "200": { + "content": { + "application/vnd.api+json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "List of available versions is returned", + "headers": { + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + }, + "/openapi/{version}": { + "get": { + "description": "Get OpenAPI specification effective at version.", + "operationId": "getAPIVersion", + "parameters": [ + { + "description": "The requested version of the API", + "in": "path", + "name": "version", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/vnd.api+json": { + "schema": { + "type": "object" + } + }, + "application/x-yaml": { + "schema": { + "type": "object" + } + } + }, + "description": "OpenAPI specification matching requested version is returned", + "headers": { + "snyk-request-id": { + "$ref": "#/components/headers/RequestIdResponseHeader" + }, + "snyk-version-requested": { + "$ref": "#/components/headers/VersionRequestedResponseHeader" + }, + "snyk-version-served": { + "$ref": "#/components/headers/VersionServedResponseHeader" + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + }, + "404": { + "$ref": "#/components/responses/404" + }, + "500": { + "$ref": "#/components/responses/500" + } + } + } + } + }, + "servers": [ + { + "description": "Test REST API", + "url": "https://example.com/api/rest" + } + ], + "x-snyk-api-version": "2024-10-15" +} \ No newline at end of file diff --git a/testdata/output/2024-10-15/spec.yaml b/testdata/output/2024-10-15/spec.yaml new file mode 100644 index 00000000..ce5db982 --- /dev/null +++ b/testdata/output/2024-10-15/spec.yaml @@ -0,0 +1,536 @@ +# OpenAPI spec generated by vervet, DO NOT EDIT +components: + headers: + DeprecationHeader: + description: | + A header containing the deprecation date of the underlying endpoint. For more information, please refer to the deprecation header RFC: + https://tools.ietf.org/id/draft-dalal-deprecation-header-01.html + example: "2021-07-01T00:00:00Z" + schema: + format: date-time + type: string + LocationHeader: + description: | + A header providing a URL for the location of a resource + example: https://example.com/resource/4 + schema: + format: url + type: string + RequestIdResponseHeader: + description: | + A header containing a unique id used for tracking this request. If you are reporting an issue to Snyk it's very helpful to provide this ID. + example: 4b58e274-ec62-4fab-917b-1d2c48d6bdef + schema: + format: uuid + type: string + SunsetHeader: + description: | + A header containing the date of when the underlying endpoint will be removed. This header is only present if the endpoint has been deprecated. Please refer to the RFC for more information: + https://datatracker.ietf.org/doc/html/rfc8594 + example: "2021-08-02T00:00:00Z" + schema: + format: date-time + type: string + VersionRequestedResponseHeader: + description: A header containing the version of the endpoint requested by the + caller. + example: "2021-06-04" + schema: + $ref: '#/components/schemas/QueryVersion' + VersionServedResponseHeader: + description: A header containing the version of the endpoint that was served + by the API. + example: "2021-06-04" + schema: + $ref: '#/components/schemas/ActualVersion' + VersionStageResponseHeader: + description: | + A header containing the version stage of the endpoint. This stage describes the guarantees snyk provides surrounding stability of the endpoint. + schema: + enum: + - wip + - experimental + - beta + - ga + - deprecated + - sunset + example: ga + type: string + parameters: + Pagination: + description: The parameters used to paginate through a list of results from + the API. + in: query + name: page + schema: + additionalProperties: false + properties: + after: + type: string + before: + type: string + size: + format: int32 + type: integer + type: object + Version: + description: The requested version of the endpoint to process the request + example: "2021-06-04" + in: query + name: version + required: true + schema: + $ref: '#/components/schemas/QueryVersion' + responses: + "400": + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ErrorDocument' + description: 'Bad Request: A parameter provided as a part of the request was + invalid.' + headers: + deprecation: + $ref: '#/components/headers/DeprecationHeader' + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-lifecycle-stage: + $ref: '#/components/headers/VersionStageResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + sunset: + $ref: '#/components/headers/SunsetHeader' + "401": + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ErrorDocument' + description: 'Unauthorized: the request requires an authentication token or + a token with more permissions.' + headers: + deprecation: + $ref: '#/components/headers/DeprecationHeader' + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-lifecycle-stage: + $ref: '#/components/headers/VersionStageResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + sunset: + $ref: '#/components/headers/SunsetHeader' + "404": + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ErrorDocument' + description: 'Not Found: The resource being operated on could not be found.' + headers: + deprecation: + $ref: '#/components/headers/DeprecationHeader' + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-lifecycle-stage: + $ref: '#/components/headers/VersionStageResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + sunset: + $ref: '#/components/headers/SunsetHeader' + "500": + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ErrorDocument' + description: 'Internal Server Error: An error was encountered while attempting + to process the request.' + headers: + deprecation: + $ref: '#/components/headers/DeprecationHeader' + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-lifecycle-stage: + $ref: '#/components/headers/VersionStageResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + sunset: + $ref: '#/components/headers/SunsetHeader' + schemas: + ActualVersion: + description: Resolved API version + pattern: ^((([0-9]{4})-([0-1][0-9]))-((3[01])|(0[1-9])|([12][0-9]))(~(wip|work-in-progress|experimental|beta))?)$ + type: string + Error: + additionalProperties: false + example: + detail: Not Found + status: "404" + properties: + detail: + description: A human-readable explanation specific to this occurrence of + the problem. + example: 'The request was missing these required fields: ...' + type: string + id: + description: A unique identifier for this particular occurrence of the problem. + example: f16c31b5-6129-4571-add8-d589da9be524 + format: uuid + type: string + meta: + additionalProperties: true + example: + key: value + type: object + source: + additionalProperties: false + example: + pointer: /data/attributes + properties: + parameter: + description: A string indicating which URI query parameter caused the + error. + example: param1 + type: string + pointer: + description: A JSON Pointer [RFC6901] to the associated entity in the + request document. + example: /data/attributes + type: string + type: object + status: + description: The HTTP status code applicable to this problem, expressed + as a string value. + example: "400" + pattern: ^[45]\d\d$ + type: string + required: + - status + - detail + type: object + ErrorDocument: + additionalProperties: false + example: + errors: + - detail: Permission denied for this resource + status: "403" + jsonapi: + version: "1.0" + properties: + errors: + example: + - detail: Permission denied for this resource + status: "403" + items: + $ref: '#/components/schemas/Error' + minItems: 1 + type: array + jsonapi: + $ref: '#/components/schemas/JsonApi' + required: + - jsonapi + - errors + type: object + HelloWorld: + additionalProperties: false + properties: + attributes: + additionalProperties: false + properties: + message: + type: string + requestSubject: + additionalProperties: false + properties: + clientId: + format: uuid + type: string + publicId: + format: uuid + type: string + type: + type: string + required: + - publicId + - type + type: object + required: + - message + - requestSubject + type: object + id: + format: uuid + type: string + type: + type: string + required: + - type + - id + - attributes + type: object + JsonApi: + additionalProperties: false + example: + version: "1.0" + properties: + version: + description: Version of the JSON API specification this server supports. + example: "1.0" + pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)$ + type: string + required: + - version + type: object + LinkProperty: + example: https://example.com/api/resource + oneOf: + - description: A string containing the link’s URL. + example: https://example.com/api/resource + type: string + - additionalProperties: false + example: + href: https://example.com/api/resource + properties: + href: + description: A string containing the link’s URL. + example: https://example.com/api/resource + type: string + meta: + $ref: '#/components/schemas/Meta' + required: + - href + type: object + Links: + additionalProperties: false + properties: + first: + $ref: '#/components/schemas/LinkProperty' + last: + $ref: '#/components/schemas/LinkProperty' + next: + $ref: '#/components/schemas/LinkProperty' + prev: + $ref: '#/components/schemas/LinkProperty' + related: + $ref: '#/components/schemas/LinkProperty' + self: + $ref: '#/components/schemas/LinkProperty' + type: object + Meta: + additionalProperties: true + description: Free-form object that may contain non-standard information. + example: + key1: value1 + key2: + sub_key: sub_value + key3: + - array_value1 + - array_value2 + type: object + QueryVersion: + description: Requested API version + pattern: ^(wip|work-in-progress|experimental|beta|((([0-9]{4})-([0-1][0-9]))-((3[01])|(0[1-9])|([12][0-9]))(~(wip|work-in-progress|experimental|beta))?))$ + type: string +info: + title: Registry + version: 3.0.0 +openapi: 3.0.3 +paths: + /examples/hello-world: + post: + description: Create a single result from the hello-world example + operationId: helloWorldCreate + parameters: + - $ref: '#/components/parameters/Version' + requestBody: + content: + application/vnd.api+json: + schema: + additionalProperties: false + properties: + attributes: + additionalProperties: false + properties: + betaField: + type: string + message: + type: string + required: + - message + - betaField + type: object + required: + - attributes + type: object + responses: + "201": + content: + application/vnd.api+json: + schema: + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/HelloWorld' + jsonapi: + $ref: '#/components/schemas/JsonApi' + links: + $ref: '#/components/schemas/Links' + required: + - jsonapi + - data + - links + type: object + description: A hello world entity being requested is returned + headers: + location: + $ref: '#/components/headers/LocationHeader' + x-snyk-include-headers: + $ref: ../../../schemas/headers/common-response.yaml#/Common + "400": + $ref: '#/components/responses/400' + "401": + $ref: '#/components/responses/401' + "404": + $ref: '#/components/responses/404' + "500": + $ref: '#/components/responses/500' + x-snyk-api-lifecycle: released + x-snyk-api-owners: + - '@snyk/api' + x-snyk-api-releases: + - 2021-06-13~beta + x-snyk-api-resource: hello-world + x-snyk-api-stability: beta + x-snyk-api-version: 2021-06-13~beta + x-stability-level: beta + /examples/hello-world/{id}: + get: + description: Get a single result from the hello-world example + operationId: helloWorldGetOne + parameters: + - $ref: '#/components/parameters/Version' + - $ref: '#/components/parameters/Pagination' + - description: The id of the hello-world example entity to be retrieved. + in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/vnd.api+json: + schema: + additionalProperties: false + properties: + data: + $ref: '#/components/schemas/HelloWorld' + jsonapi: + $ref: '#/components/schemas/JsonApi' + links: + $ref: '#/components/schemas/Links' + required: + - jsonapi + - data + - links + type: object + description: A hello world entity being requested is returned + headers: + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + "400": + $ref: '#/components/responses/400' + "401": + $ref: '#/components/responses/401' + "404": + $ref: '#/components/responses/404' + "500": + $ref: '#/components/responses/500' + x-snyk-api-lifecycle: released + x-snyk-api-owners: + - '@snyk/api' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta + x-snyk-api-resource: hello-world + x-snyk-api-stability: beta + x-snyk-api-version: 2021-06-13~beta + x-stability-level: beta + /openapi: + get: + description: List available versions of OpenAPI specification + operationId: listAPIVersions + responses: + "200": + content: + application/vnd.api+json: + schema: + items: + type: string + type: array + description: List of available versions is returned + headers: + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + "400": + $ref: '#/components/responses/400' + "401": + $ref: '#/components/responses/401' + "404": + $ref: '#/components/responses/404' + "500": + $ref: '#/components/responses/500' + /openapi/{version}: + get: + description: Get OpenAPI specification effective at version. + operationId: getAPIVersion + parameters: + - description: The requested version of the API + in: path + name: version + required: true + schema: + type: string + responses: + "200": + content: + application/vnd.api+json: + schema: + type: object + application/x-yaml: + schema: + type: object + description: OpenAPI specification matching requested version is returned + headers: + snyk-request-id: + $ref: '#/components/headers/RequestIdResponseHeader' + snyk-version-requested: + $ref: '#/components/headers/VersionRequestedResponseHeader' + snyk-version-served: + $ref: '#/components/headers/VersionServedResponseHeader' + "400": + $ref: '#/components/responses/400' + "401": + $ref: '#/components/responses/401' + "404": + $ref: '#/components/responses/404' + "500": + $ref: '#/components/responses/500' +servers: +- description: Test REST API + url: https://example.com/api/rest +x-snyk-api-version: "2024-10-15" diff --git a/testdata/output/embed.go b/testdata/output/embed.go index 0b561ca7..fafcc073 100644 --- a/testdata/output/embed.go +++ b/testdata/output/embed.go @@ -12,10 +12,10 @@ import "embed" //go:embed 2021-06-04~experimental/spec.yaml //go:embed 2021-06-07~experimental/spec.json //go:embed 2021-06-07~experimental/spec.yaml -//go:embed 2021-06-13~experimental/spec.json -//go:embed 2021-06-13~experimental/spec.yaml //go:embed 2021-06-13~beta/spec.json //go:embed 2021-06-13~beta/spec.yaml +//go:embed 2021-06-13~experimental/spec.json +//go:embed 2021-06-13~experimental/spec.yaml //go:embed 2021-08-20~experimental/spec.json //go:embed 2021-08-20~experimental/spec.yaml //go:embed 2023-06-01~experimental/spec.json @@ -24,6 +24,8 @@ import "embed" //go:embed 2023-06-02~experimental/spec.yaml //go:embed 2023-06-03~experimental/spec.json //go:embed 2023-06-03~experimental/spec.yaml +//go:embed 2024-10-15/spec.json +//go:embed 2024-10-15/spec.yaml // Versions contains OpenAPI specs for each distinct release version. var Versions embed.FS From 4889a49f489c5f1be31dc7874d9d735d0b7d93b1 Mon Sep 17 00:00:00 2001 From: John Gresty Date: Tue, 15 Oct 2024 16:31:31 +0100 Subject: [PATCH 2/3] fix: update tests to account for pivot date The old test only test the older compallation pipeline, which does not emit specs after the pivot date. Filter those out when testing the old pipeline. --- internal/backstage/backstage_test.go | 3 +++ internal/compiler/compiler_test.go | 14 +++++++++++- testdata/catalog-vervet-apis-with-pivot.yaml | 19 ++++++++++++++++ testdata/catalog-vervet-apis.yaml | 23 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/internal/backstage/backstage_test.go b/internal/backstage/backstage_test.go index 9e899672..0c7afcd2 100644 --- a/internal/backstage/backstage_test.go +++ b/internal/backstage/backstage_test.go @@ -132,6 +132,7 @@ func TestLoadVersionsNoApis(t *testing.T) { - Registry_2023-06-01_experimental - Registry_2023-06-02_experimental - Registry_2023-06-03_experimental + - Registry_2024-10-15_ga --- `[1:]+string(vervetAPIs)) } @@ -164,6 +165,7 @@ func TestLoadVersionsSomeApis(t *testing.T) { - Registry_2023-06-01_experimental - Registry_2023-06-02_experimental - Registry_2023-06-03_experimental + - Registry_2024-10-15_ga - someOtherApi --- `[1:]+string(vervetAPIs)) @@ -193,6 +195,7 @@ func TestDoesNotOutputStabilitiesAfterPivotDate(t *testing.T) { - Registry_2023-06-01 - Registry_2023-06-02 - Registry_2023-06-03 + - Registry_2024-10-15 --- `[1:]+string(vervetAPIs)) } diff --git a/internal/compiler/compiler_test.go b/internal/compiler/compiler_test.go index 4deb16cd..2dfdc534 100644 --- a/internal/compiler/compiler_test.go +++ b/internal/compiler/compiler_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" "text/template" + "time" qt "github.com/frankban/quicktest" @@ -157,7 +158,10 @@ func TestCompilerSmokePaths(t *testing.T) { } func assertOutputsEqual(c *qt.C, refDir, testDir string) { - err := fs.WalkDir(os.DirFS(refDir), ".", func(path string, d fs.DirEntry, err error) error { + pivotDate, err := time.Parse("2006-01-02", "2024-10-15") + c.Assert(err, qt.IsNil) + + err = fs.WalkDir(os.DirFS(refDir), ".", func(path string, d fs.DirEntry, err error) error { c.Assert(err, qt.IsNil) if d.IsDir() { return nil @@ -166,6 +170,14 @@ func assertOutputsEqual(c *qt.C, refDir, testDir string) { // only comparing compiled specs here return nil } + specDate, err := time.Parse("2006-01-02", path[:10]) + c.Assert(err, qt.IsNil) + if !specDate.Before(pivotDate) { + // After pivot date we exclusively use simplebuild so don't emit + // specs from this pipeline + return nil + } + outputFile, err := os.ReadFile(filepath.Join(testDir, path)) c.Assert(err, qt.IsNil) refFile, err := os.ReadFile(filepath.Join(refDir, path)) diff --git a/testdata/catalog-vervet-apis-with-pivot.yaml b/testdata/catalog-vervet-apis-with-pivot.yaml index fc16c5be..2eb5ea08 100644 --- a/testdata/catalog-vervet-apis-with-pivot.yaml +++ b/testdata/catalog-vervet-apis-with-pivot.yaml @@ -192,3 +192,22 @@ spec: owner: someone-else definition: $text: output/2023-06-03~experimental/spec.json +--- +# Generated by vervet, DO NOT EDIT +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: Registry_2024-10-15 + title: Registry 2024-10-15 + annotations: + api.snyk.io/generated-by: vervet + labels: + api.snyk.io/version-date: "2024-10-15" + tags: + - 2024-10 +spec: + type: openapi + lifecycle: production + owner: someone-else + definition: + $text: output/2024-10-15/spec.json diff --git a/testdata/catalog-vervet-apis.yaml b/testdata/catalog-vervet-apis.yaml index 11a3e7c4..ceaacf7c 100644 --- a/testdata/catalog-vervet-apis.yaml +++ b/testdata/catalog-vervet-apis.yaml @@ -204,3 +204,26 @@ spec: owner: someone-else definition: $text: output/2023-06-03~experimental/spec.json +--- +# Generated by vervet, DO NOT EDIT +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: Registry_2024-10-15_ga + title: Registry 2024-10-15 ga + annotations: + api.snyk.io/generated-by: vervet + labels: + api.snyk.io/version-date: "2024-10-15" + api.snyk.io/version-lifecycle: released + api.snyk.io/version-stability: ga + tags: + - 2024-10 + - ga + - released +spec: + type: openapi + lifecycle: ga + owner: someone-else + definition: + $text: output/2024-10-15/spec.json From 61c4ed168e0f0a7d76175bf17969ae381ad4cf40 Mon Sep 17 00:00:00 2001 From: John Gresty Date: Tue, 15 Oct 2024 16:40:01 +0100 Subject: [PATCH 3/3] feat: validate specs before writing in simplebuild We had an issue where invalid specs would be written if there were no paths. We would not check what we were writing was invalid so it would pass all CI checks then fail at runtime when something tried to use them. This patch ensures that will never happen by verifying what we write is valid. --- internal/simplebuild/build.go | 8 ++++++-- internal/simplebuild/output.go | 10 ++++++++-- internal/simplebuild/output_test.go | 8 +++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/simplebuild/build.go b/internal/simplebuild/build.go index 669977b3..0d94bc89 100644 --- a/internal/simplebuild/build.go +++ b/internal/simplebuild/build.go @@ -97,7 +97,7 @@ func Build( return err } - err = writer.Write(doc) + err = writer.Write(ctx, doc) if err != nil { return err } @@ -153,7 +153,11 @@ func (ops Operations) Build(startVersion vervet.Version) DocSet { output[idx] = VersionedDoc{ Doc: &openapi3.T{ OpenAPI: "3.0.3", - Paths: openapi3.NewPaths(), + Info: &openapi3.Info{ + Title: "Snyk API", + Version: "1.0.0", + }, + Paths: openapi3.NewPaths(), }, VersionDate: versionDate, } diff --git a/internal/simplebuild/output.go b/internal/simplebuild/output.go index e709d206..eb428b33 100644 --- a/internal/simplebuild/output.go +++ b/internal/simplebuild/output.go @@ -1,6 +1,7 @@ package simplebuild import ( + "context" "fmt" "io/fs" "os" @@ -59,13 +60,18 @@ func NewWriter(cfg config.Output, appendOutputFiles bool) (*DocWriter, error) { // Write writes compiled specs to a single directory in YAML and JSON formats. // Call Finalize after to populate other directories. -func (out *DocWriter) Write(doc VersionedDoc) error { +func (out *DocWriter) Write(ctx context.Context, doc VersionedDoc) error { + err := doc.Doc.Validate(ctx) + if err != nil { + return fmt.Errorf("invalid compiled document: %w", err) + } + // We write to the first directory then copy the entire directory // afterwards dir := out.paths[0] versionDir := path.Join(dir, doc.VersionDate.Format(time.DateOnly)) - err := os.MkdirAll(versionDir, 0755) + err = os.MkdirAll(versionDir, 0755) if err != nil { return fmt.Errorf("make output directory: %w", err) } diff --git a/internal/simplebuild/output_test.go b/internal/simplebuild/output_test.go index 04d719d5..18135256 100644 --- a/internal/simplebuild/output_test.go +++ b/internal/simplebuild/output_test.go @@ -1,6 +1,7 @@ package simplebuild import ( + "context" "os" "path" "path/filepath" @@ -123,6 +124,7 @@ func TestDocSet_WriteOutputs(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() if tt.setup != nil { tt.setup(t, tt.args) } @@ -130,7 +132,7 @@ func TestDocSet_WriteOutputs(t *testing.T) { writer, err := NewWriter(tt.args.cfg, tt.args.appendOutputFiles) c.Assert(err, qt.IsNil) for _, doc := range tt.docs { - err = writer.Write(doc) + err = writer.Write(ctx, doc) c.Assert(err, qt.IsNil) } err = writer.Finalize() @@ -142,4 +144,8 @@ func TestDocSet_WriteOutputs(t *testing.T) { } var minimalSpec = `--- +openapi: 3.0.3 +info: + title: minimal spec + version: 1.0.0 paths: {}`