diff --git a/resources/ring/swagger/openapi-schema.json b/resources/ring/swagger/openapi-schema.json new file mode 100644 index 00000000..b43f4405 --- /dev/null +++ b/resources/ring/swagger/openapi-schema.json @@ -0,0 +1,1658 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "The description of OpenAPI v3.0.x documents, as defined by https://spec.openapis.org/oas/v3.0.3", + "type": "object", + "required": [ + "openapi", + "info", + "paths" + ], + "properties": { + "openapi": { + "type": "string", + "pattern": "^3\\.0\\.\\d(-.+)?$" + }, + "info": { + "$ref": "#/definitions/Info" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "uniqueItems": true + }, + "paths": { + "$ref": "#/definitions/Paths" + }, + "components": { + "$ref": "#/definitions/Components" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "patternProperties": { + "^\\$ref$": { + "type": "string" + } + } + }, + "Info": { + "type": "object", + "required": [ + "title", + "version" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "termsOfService": { + "type": "string" + }, + "contact": { + "$ref": "#/definitions/Contact" + }, + "license": { + "$ref": "#/definitions/License" + }, + "version": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "License": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Server": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ServerVariable" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ServerVariable": { + "type": "object", + "required": [ + "default" + ], + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Components": { + "type": "object", + "properties": { + "schemas": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "responses": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Response" + } + ] + } + } + }, + "parameters": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Parameter" + } + ] + } + } + }, + "examples": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Example" + } + ] + } + } + }, + "requestBodies": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/RequestBody" + } + ] + } + } + }, + "headers": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Header" + } + ] + } + } + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "links": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Link" + } + ] + } + } + }, + "callbacks": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/Callback" + } + ] + } + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { + "type": "integer", + "minimum": 0 + }, + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { + "type": "integer", + "minimum": 0 + }, + "minProperties": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "required": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "enum": { + "type": "array", + "items": { + }, + "minItems": 1, + "uniqueItems": false + }, + "type": { + "type": "string", + "enum": [ + "array", + "boolean", + "integer", + "number", + "object", + "string" + ] + }, + "not": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "allOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "oneOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "anyOf": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "properties": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + }, + { + "type": "boolean" + } + ], + "default": true + }, + "description": { + "type": "string" + }, + "format": { + "type": "string" + }, + "default": { + }, + "nullable": { + "type": "boolean", + "default": false + }, + "discriminator": { + "$ref": "#/definitions/Discriminator" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "example": { + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/XML" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Discriminator": { + "type": "object", + "required": [ + "propertyName" + ], + "properties": { + "propertyName": { + "type": "string" + }, + "mapping": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "XML": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string", + "format": "uri" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "links": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Link" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "MediaType": { + "type": "object", + "properties": { + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "encoding": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Encoding" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + } + ] + }, + "Example": { + "type": "object", + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "value": { + }, + "externalValue": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Header": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string", + "enum": [ + "simple" + ], + "default": "simple" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + } + ] + }, + "Paths": { + "type": "object", + "patternProperties": { + "^\\/": { + "$ref": "#/definitions/PathItem" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "PathItem": { + "type": "object", + "properties": { + "$ref": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + } + }, + "patternProperties": { + "^(get|put|post|delete|options|head|patch|trace)$": { + "$ref": "#/definitions/Operation" + }, + "^x-": { + } + }, + "additionalProperties": false + }, + "Operation": { + "type": "object", + "required": [ + "responses" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + }, + "operationId": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Parameter" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "uniqueItems": true + }, + "requestBody": { + "oneOf": [ + { + "$ref": "#/definitions/RequestBody" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "responses": { + "$ref": "#/definitions/Responses" + }, + "callbacks": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Callback" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "servers": { + "type": "array", + "items": { + "$ref": "#/definitions/Server" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Responses": { + "type": "object", + "properties": { + "default": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "patternProperties": { + "^[1-5](?:\\d{2}|XX)$": { + "oneOf": [ + { + "$ref": "#/definitions/Response" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "^x-": { + } + }, + "minProperties": 1, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "Tag": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/ExternalDocumentation" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExternalDocumentation": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ExampleXORExamples": { + "description": "Example and examples are mutually exclusive", + "not": { + "required": [ + "example", + "examples" + ] + } + }, + "SchemaXORContent": { + "description": "Schema and content are mutually exclusive, at least one is required", + "not": { + "required": [ + "schema", + "content" + ] + }, + "oneOf": [ + { + "required": [ + "schema" + ] + }, + { + "required": [ + "content" + ], + "description": "Some properties are not allowed if content is present", + "allOf": [ + { + "not": { + "required": [ + "style" + ] + } + }, + { + "not": { + "required": [ + "explode" + ] + } + }, + { + "not": { + "required": [ + "allowReserved" + ] + } + }, + { + "not": { + "required": [ + "example" + ] + } + }, + { + "not": { + "required": [ + "examples" + ] + } + } + ] + } + ] + }, + "Parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string" + }, + "description": { + "type": "string" + }, + "required": { + "type": "boolean", + "default": false + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "allowEmptyValue": { + "type": "boolean", + "default": false + }, + "style": { + "type": "string" + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/Schema" + }, + { + "$ref": "#/definitions/Reference" + } + ] + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + }, + "minProperties": 1, + "maxProperties": 1 + }, + "example": { + }, + "examples": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Example" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "required": [ + "name", + "in" + ], + "allOf": [ + { + "$ref": "#/definitions/ExampleXORExamples" + }, + { + "$ref": "#/definitions/SchemaXORContent" + }, + { + "$ref": "#/definitions/ParameterLocation" + } + ] + }, + "ParameterLocation": { + "description": "Parameter location", + "oneOf": [ + { + "description": "Parameter in path", + "required": [ + "required" + ], + "properties": { + "in": { + "enum": [ + "path" + ] + }, + "style": { + "enum": [ + "matrix", + "label", + "simple" + ], + "default": "simple" + }, + "required": { + "enum": [ + true + ] + } + } + }, + { + "description": "Parameter in query", + "properties": { + "in": { + "enum": [ + "query" + ] + }, + "style": { + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ], + "default": "form" + } + } + }, + { + "description": "Parameter in header", + "properties": { + "in": { + "enum": [ + "header" + ] + }, + "style": { + "enum": [ + "simple" + ], + "default": "simple" + } + } + }, + { + "description": "Parameter in cookie", + "properties": { + "in": { + "enum": [ + "cookie" + ] + }, + "style": { + "enum": [ + "form" + ], + "default": "form" + } + } + } + ] + }, + "RequestBody": { + "type": "object", + "required": [ + "content" + ], + "properties": { + "description": { + "type": "string" + }, + "content": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/MediaType" + } + }, + "required": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/APIKeySecurityScheme" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/OAuth2SecurityScheme" + }, + { + "$ref": "#/definitions/OpenIdConnectSecurityScheme" + } + ] + }, + "APIKeySecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "oneOf": [ + { + "description": "Bearer", + "properties": { + "scheme": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + }, + { + "description": "Non Bearer", + "not": { + "required": [ + "bearerFormat" + ] + }, + "properties": { + "scheme": { + "not": { + "type": "string", + "pattern": "^[Bb][Ee][Aa][Rr][Ee][Rr]$" + } + } + } + } + ] + }, + "OAuth2SecurityScheme": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flows": { + "$ref": "#/definitions/OAuthFlows" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OpenIdConnectSecurityScheme": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "openIdConnectUrl": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "OAuthFlows": { + "type": "object", + "properties": { + "implicit": { + "$ref": "#/definitions/ImplicitOAuthFlow" + }, + "password": { + "$ref": "#/definitions/PasswordOAuthFlow" + }, + "clientCredentials": { + "$ref": "#/definitions/ClientCredentialsFlow" + }, + "authorizationCode": { + "$ref": "#/definitions/AuthorizationCodeOAuthFlow" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ImplicitOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "PasswordOAuthFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "ClientCredentialsFlow": { + "type": "object", + "required": [ + "tokenUrl", + "scopes" + ], + "properties": { + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "AuthorizationCodeOAuthFlow": { + "type": "object", + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ], + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri-reference" + }, + "tokenUrl": { + "type": "string", + "format": "uri-reference" + }, + "refreshUrl": { + "type": "string", + "format": "uri-reference" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + }, + "Link": { + "type": "object", + "properties": { + "operationId": { + "type": "string" + }, + "operationRef": { + "type": "string", + "format": "uri-reference" + }, + "parameters": { + "type": "object", + "additionalProperties": { + } + }, + "requestBody": { + }, + "description": { + "type": "string" + }, + "server": { + "$ref": "#/definitions/Server" + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false, + "not": { + "description": "Operation Id and Operation Ref are mutually exclusive", + "required": [ + "operationId", + "operationRef" + ] + } + }, + "Callback": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/PathItem" + }, + "patternProperties": { + "^x-": { + } + } + }, + "Encoding": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + }, + "headers": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/Header" + }, + { + "$ref": "#/definitions/Reference" + } + ] + } + }, + "style": { + "type": "string", + "enum": [ + "form", + "spaceDelimited", + "pipeDelimited", + "deepObject" + ] + }, + "explode": { + "type": "boolean" + }, + "allowReserved": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + } + }, + "additionalProperties": false + } + } +} diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index c54d8830..eb69945a 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -12,6 +12,10 @@ (declare properties) (declare schema-object) +(defn- opts->schema-type [opts] + {:post [(keyword? %)]} + (get opts :schema-type :swagger)) + ; TODO: remove this in favor of passing it as options (def ^:dynamic *ignore-missing-mappings* false) @@ -24,7 +28,7 @@ ;; (defrecord FieldSchema [schema] - schema.core.Schema + s/Schema (spec [_] (variant/variant-spec spec/+no-precondition+ @@ -56,7 +60,7 @@ (str (if n (str n "/")) (name x))) x)) -(defmulti convert-class (fn [c options] c)) +(defmulti convert-class (fn [c _] c)) (defprotocol JsonSchema (convert [this options])) @@ -77,11 +81,16 @@ (defn reference? [m] (contains? m :$ref)) -(defn reference [e] - (if-let [schema-name (s/schema-name e)] - {:$ref (str "#/definitions/" schema-name)} - (if (not *ignore-missing-mappings*) - (not-supported! e)))) +(defn reference + ([e] (reference e nil)) + ([e opts] + (if-let [schema-name (s/schema-name e)] + {:$ref (str (case (opts->schema-type opts) + :swagger "#/definitions/" + :openapi "#/components/schemas/") + schema-name)} + (if (not *ignore-missing-mappings*) + (not-supported! e))))) (defn merge-meta [m x {:keys [::no-meta :key-meta]}] @@ -133,15 +142,16 @@ (convert options) (merge-meta x options)))) -(defn- try->swagger [v k key-meta] - (try (->swagger v {:key-meta key-meta}) +(defn- try->swagger [v k key-meta opts] + (try (->swagger v (-> opts + (assoc :key-meta key-meta) + (assoc :schema-type (opts->schema-type opts)))) (catch Exception e (throw (IllegalArgumentException. (str "error converting to swagger schema [" k " " (try (s/explain v) (catch Exception _ v)) "]") e))))) - (defn- coll-schema [e options] (-> {:type "array" :items (->swagger (first e) (assoc options ::no-meta true))} @@ -156,7 +166,7 @@ Class (convert [e options] (if-let [schema (common/record-schema e)] - (schema-object schema) + (schema-object schema options) (convert-class e options))) nil @@ -164,49 +174,51 @@ nil) FieldSchema - (convert [e _] - (->swagger (:schema e))) + (convert [e options] + (->swagger (:schema e) options)) schema.core.Predicate - (convert [e _] - (some-> e :pred-name predicate-name-to-class ->swagger)) + (convert [e options] + (some-> e :pred-name predicate-name-to-class (->swagger options))) schema.core.EnumSchema - (convert [e _] - (merge (->swagger (class (first (:vs e)))) {:enum (seq (:vs e))})) + (convert [e options] + (merge (->swagger (class (first (:vs e))) options) {:enum (seq (:vs e))})) schema.core.Maybe - (convert [e {:keys [in]}] - (let [schema (->swagger (:schema e))] + (convert [e {:keys [in] :as options}] + (let [schema (->swagger (:schema e) options) + schema-type (opts->schema-type options) + nullable-key (if (= schema-type :openapi) :nullable :x-nullable)] (condp contains? in #{:query :formData} (assoc schema :allowEmptyValue true) - #{nil :body} (assoc schema :x-nullable true) + #{nil :body} (assoc schema nullable-key true) schema))) schema.core.Both - (convert [e _] - (->swagger (first (:schemas e)))) + (convert [e options] + (->swagger (first (:schemas e)) options)) schema.core.Either - (convert [e _] - (->swagger (first (:schemas e)))) + (convert [e options] + (->swagger (first (:schemas e)) options)) schema.core.Recursive - (convert [e _] - (->swagger (:derefable e))) + (convert [e options] + (->swagger (:derefable e) options)) schema.core.EqSchema - (convert [e _] - (merge (->swagger (class (:v e))) + (convert [e options] + (merge (->swagger (class (:v e)) options) {:enum [(:v e)]})) schema.core.NamedSchema - (convert [e _] - (->swagger (:schema e))) + (convert [e options] + (->swagger (:schema e) options)) schema.core.One - (convert [e _] - (->swagger (:schema e))) + (convert [e options] + (->swagger (:schema e) options)) schema.core.AnythingSchema (convert [_ {:keys [in] :as opts}] @@ -215,16 +227,22 @@ {})) schema.core.ConditionalSchema - (convert [e _] - {:x-oneOf (vec (keep (comp ->swagger second) (:preds-and-schemas e)))}) + (convert [e options] + (let [schema-type (opts->schema-type options) + schema (vec (keep #(->swagger (second %) options) (:preds-and-schemas e))) + schema-key (if (= schema-type :openapi) :oneOf :x-oneOf)] + {schema-key schema})) schema.core.CondPre - (convert [e _] - {:x-oneOf (mapv ->swagger (:schemas e))}) + (convert [e options] + (let [schema-type (opts->schema-type options) + schema (mapv #(->swagger % options) (:schemas e)) + schema-key (if (= schema-type :openapi) :oneOf :x-oneOf)] + {schema-key schema})) schema.core.Constrained - (convert [e _] - (->swagger (:schema e))) + (convert [e options] + (->swagger (:schema e) options)) java.util.regex.Pattern (convert [e _] @@ -241,14 +259,14 @@ (assoc (coll-schema e options) :uniqueItems true)) clojure.lang.IPersistentMap - (convert [e {:keys [properties?]}] + (convert [e {:keys [properties?] :as opts}] (if properties? - {:properties (properties e)} - (reference e))) + {:properties (properties e opts)} + (reference e opts))) clojure.lang.Var - (convert [e _] - (reference e))) + (convert [e opts] + (reference e opts))) ;; ;; Schema to Swagger Schema definitions @@ -259,44 +277,47 @@ The result is put into collection of same type as input schema. Thus linked/map should keep the order of items. Returns nil if no properties are found." - [schema] - {:pre [(common/plain-map? schema)]} - (let [props (into (empty schema) - (for [[k v] schema - :when (s/specific-key? k) - :let [key-meta (meta k) - k (s/explicit-schema-key k) - v (try->swagger v k key-meta)]] - (and v [k v])))] - (not-empty props))) + ([schema] (properties schema nil)) + ([schema opts] + {:pre [(common/plain-map? schema)]} + (let [props (into (empty schema) + (for [[k v] schema + :when (s/specific-key? k) + :let [key-meta (meta k) + k (s/explicit-schema-key k) + v (try->swagger v k key-meta opts)]] + (and v [k v])))] + (not-empty props)))) (defn additional-properties "Generates json-schema additional properties from a plain map schema from under key s/Keyword." - [schema] - {:pre [(common/plain-map? schema)]} - (if-let [extra-key (s/find-extra-keys-schema schema)] - (let [v (get schema extra-key)] - (try->swagger v s/Keyword nil)) - false)) + ([schema] (additional-properties schema nil)) + ([schema opts] + {:pre [(common/plain-map? schema)]} + (if-let [extra-key (s/find-extra-keys-schema schema)] + (let [v (get schema extra-key)] + (try->swagger v s/Keyword nil opts)) + false))) (defn schema-object "Returns a JSON Schema object of a plain map schema." - [schema] - (if (common/plain-map? schema) - (let [properties (properties schema) - title (if (not (s/schema-name schema)) (common/title schema)) - additional-properties (additional-properties schema) - meta (json-schema-meta schema) - required (some->> (rsc/required-keys schema) - (filter (partial contains? properties)) - seq - vec)] - (common/remove-empty-keys - (merge - meta - {:type "object" - :title title - :properties properties - :additionalProperties additional-properties - :required required}))))) + ([schema] (schema-object schema nil)) + ([schema opts] + (if (common/plain-map? schema) + (let [properties (properties schema opts) + title (if (not (s/schema-name schema)) (common/title schema)) + additional-properties (additional-properties schema opts) + meta (json-schema-meta schema) + required (some->> (rsc/required-keys schema) + (filter (partial contains? properties)) + seq + vec)] + (common/remove-empty-keys + (merge + meta + {:type "object" + :title title + :properties properties + :additionalProperties additional-properties + :required required})))))) diff --git a/src/ring/swagger/json_schema_dirty.clj b/src/ring/swagger/json_schema_dirty.clj index 7ba1c5c1..8edbc78d 100644 --- a/src/ring/swagger/json_schema_dirty.clj +++ b/src/ring/swagger/json_schema_dirty.clj @@ -7,17 +7,17 @@ (extend-protocol json-schema/JsonSchema schema.experimental.abstract_map.AbstractSchema (convert [e {:keys [properties?] - :or {properties? true}}] + :or {properties? true} :as options}] (if properties? (merge {:discriminator (name (:dispatch-key e))} - (json-schema/->swagger (:schema e) {:properties? properties?})) - (json-schema/reference e))) + (json-schema/->swagger (:schema e) (assoc options :properties? properties?))) + (json-schema/reference e options))) schema.experimental.abstract_map.SchemaExtension - (convert [e _] - {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false}) + (convert [e options] + {:allOf [(json-schema/->swagger (:base-schema e) (assoc options :properties? false)) ; Find which keys are also in base-schema and don't include them in these properties (json-schema/->swagger (let [base-keys (set (keys (:schema (:base-schema e)))) - m (:extended-schema e)] + m (:extended-schema e)] (into (empty m) (remove (comp base-keys key) m))) - {:properties? true})]})) + (assoc options :properties? true))]})) diff --git a/src/ring/swagger/openapi3.clj b/src/ring/swagger/openapi3.clj new file mode 100644 index 00000000..e1a3f268 --- /dev/null +++ b/src/ring/swagger/openapi3.clj @@ -0,0 +1,321 @@ +(ns ring.swagger.openapi3 + (:require [clojure.string :as str] + [schema.core :as s] + [schema-tools.core :as stc] + [plumbing.core :as p] + [ring.swagger.common :as common] + [ring.swagger.json-schema :as rsjs] + [ring.swagger.core :as rsc] + [ring.swagger.openapi3-schema :as openapi3-schema])) + +;; +;; Schema transformations +;; + +(defn extract-models [swagger] + (let [route-meta (->> swagger + :paths + vals + (map vals) + flatten) + body-models (->> route-meta + (map (comp :requestBody))) + response-models (->> route-meta + (map :responses) + (mapcat vals) + (map :content) + (map vals))] + [body-models response-models])) + +(def ^:private openapi-opts {:schema-type :openapi}) + +(defn transform-models [schemas options] + (->> schemas + rsc/collect-models + (rsc/handle-duplicate-schemas (:handle-duplicate-schemas-fn options)) + (map (juxt (comp str key) (comp #(rsjs/schema-object % openapi-opts) val))) + (into (sorted-map)))) + +(defn extract-parameter [in model options] + (if model + (for [[k v] (-> model common/value-of stc/schema-value rsc/strict-schema) + :when (s/specific-key? k) + :let [rk (s/explicit-schema-key k) + json-schema (rsjs/->swagger v (into options openapi-opts))] + :when json-schema] + {:in (name in) + :name (rsjs/key-name rk) + :description "" + :required (or (= in :path) (s/required-key? k)) + :schema json-schema}))) + +(defn- default-response-description + "uses option :default-response-description-fn to generate + a default response description for status code" + [status options] + (if-let [generator (:default-response-description-fn options)] + (generator status) + "")) + +(defn convert-content-schema [contents options] + (if contents + (into {} (for [[content-type schema-input] contents] + [content-type + (let [schema (rsc/peek-schema schema-input) + schema-json (rsjs/->swagger schema-input (into options openapi-opts))] + {:name (or (common/title schema) "") + :schema schema-json})])))) + + +(defn convert-parameters [parameters options] + (into [] (mapcat (fn [[in model]] + (extract-parameter in model (assoc options :in in))) + parameters))) + +(defn convert-responses [responses options] + (let [responses (p/for-map [[k v] responses + :let [{:keys [content headers]} v]] + k (-> v + (cond-> content (assoc :content (convert-content-schema content options))) + (cond-> headers (update-in [:headers] (fn [headers] + (if headers + (->> (for [[k v] headers] + [k (rsjs/->swagger v (into options openapi-opts))]) + (into {})))))) + (update-in [:description] #(or % + (:description (rsjs/json-schema-meta v)) + (:description v) + (default-response-description k options))) + common/remove-empty-keys))] + (if-not (empty? responses) + responses + {:default {:description ""}}))) + +(defn convert-operation + "Returns a map with methods as keys and the Operation + maps with parameters and responses transformed to comply + with Swagger spec as values" + [operation options] + (p/for-map [[k v] operation] + k (-> v + (common/update-in-or-remove-key [:parameters] #(convert-parameters % options) empty?) + (common/update-in-or-remove-key [:requestBody :content] #(convert-content-schema % options) empty?) + (update-in [:responses] convert-responses options)))) + +(defn swagger-path + "Replaces Compojure/Clout style path params in uri with Swagger style + path params. + + Does not support wildcard-paths or inline-regexes. + + The regex is copied from Clout." + [uri] + ;; TODO: In 1.0, leave it to client libs to build swagger style path template + ;; Currently everyone needs to build Clout path is just extra step for all but + ;; compojure-api. + (str/replace uri #":([\p{L}_][\p{L}_0-9-]*)" "{$1}")) + +(defn extract-paths-and-definitions [swagger options] + (let [original-paths (or (:paths swagger) {}) + paths (reduce-kv + (fn [acc k v] + (assoc acc + (swagger-path k) + (convert-operation v options))) + (empty original-paths) + original-paths) + definitions (-> swagger + extract-models + (transform-models options))] + [paths definitions])) + +(defn process-contents [content prefix] + (into {} (for [[content-type schema] content] + [content-type (rsc/with-named-sub-schemas schema prefix)]))) + +(defn ensure-body-sub-schemas [route] + (update-in route [:requestBody :content] + #(process-contents % "Body"))) + +(defn ensure-response-sub-schemas [route] + (if-let [responses (get-in route [:responses])] + (let [schema-codes (reduce (fn [acc [k {:keys [content]}]] + (if content (conj acc k) acc)) + [] responses) + transformed (reduce (fn [acc code] + (update-in acc [:responses code :content] #(process-contents % "Response"))) + route schema-codes)] + transformed) + route)) + +(defn get-response-ref [v] + (some-> (-> v + :content + vals + first + :schema + :$ref) + (str/replace "/schemas/" "/responses/"))) + +(defn to-responses-defn [responses] + (into {} (for [[method status-ref-map] responses] [method (into {} (for [[status [references]] status-ref-map] [status {:$ref references}]))]))) + +(defn endpoint-processor2 [endpoint] + (let [backup (reduce-kv (fn [acc method definition] + (let [body-acc (if (:requestBody definition) + (let [body-name (-> (get-in definition [:requestBody :content]) + vals + first + :name)] + (-> acc + (update-in [:requestBodySchemas] conj {(keyword body-name) (:requestBody definition)}) + (update-in [:requestBodyDefinitions method] conj (str "#/components/requestBodies/" body-name)))) acc) + responses-acc (reduce-kv (fn [acc-res k v] + (let [response-path (get-response-ref v) + response-name (last (.split response-path "/")) + response-path-val (keyword response-name)] + (-> acc-res + (update-in [:responses method k] conj response-path) + (update-in [:responses-schema] conj {response-path-val v})))) body-acc (:responses definition))] + responses-acc)) + {} endpoint) + responses-map (to-responses-defn (:responses backup)) + response-refs-updated (reduce-kv (fn [acc http-method v] + (assoc-in acc [http-method :responses] v)) endpoint responses-map) + req-body-refs-updated (reduce-kv (fn [acc http-method [schema-reference]] + (assoc-in acc [http-method :requestBody] {:$ref schema-reference})) response-refs-updated (:requestBodyDefinitions backup))] + {:requestBodySchemas (:requestBodySchemas backup) :responses-schema (:responses-schema backup) :endpoint req-body-refs-updated})) + +(defn remove-body-name [{:keys [content]}] + {:content (into {} (for [[k v] content] [k (dissoc v :name)]))}) + +(defn move-schemas [swagger] + (let [paths (or (:paths swagger) {}) + map-req-resp-schemas (for [[k v] paths] [k (endpoint-processor2 v)]) + updated-paths (into {} (for [[k v] map-req-resp-schemas] [k (:endpoint v)])) + all-schemas (for [[_ v] map-req-resp-schemas] [(dissoc v :endpoint)]) + request-bodies (into {} (flatten (mapv (fn [x] (map :requestBodySchemas x)) (vec all-schemas)))) + request-bodies (into {} (for [[body-name schema] request-bodies] [body-name (remove-body-name schema)])) + responses-schema (into {} (flatten (map (fn [x] (map :responses-schema x)) (vec all-schemas)))) + swagger-new (-> swagger + (assoc :paths updated-paths) + (assoc-in [:components :responses] responses-schema) + (assoc-in [:components :requestBodies] request-bodies))] + (clojure.pprint/pprint request-bodies) + swagger-new)) + +;; +;; Public API +;; + +;; +;; Transforming the spec +;; + +(defn transform-operations + "Transforms the operations under the :paths of a ring-swagger spec by applying (f operation) + to all operations. If the function returns nil, the given operation is removed." + [f swagger] + (let [initial-paths (:paths swagger) + transformed (for [[path endpoints] initial-paths + [method endpoint] endpoints + :let [endpoint (f endpoint)]] + [[path method] endpoint]) + paths (reduce (fn [acc [kv endpoint]] + (if endpoint + (assoc-in acc kv endpoint) + acc)) (empty initial-paths) transformed)] + (assoc-in swagger [:paths] paths))) + +(defn ensure-body-and-response-schema-names + "Takes a ring-swagger spec and returns a new version + with a generated names for all anonymous nested schemas + that come as body parameters or response models." + [swagger] + (->> swagger + (transform-operations ensure-body-sub-schemas) + (transform-operations ensure-response-sub-schemas))) + +;; +;; Schema +;; + +(def openapi-defaults {:openapi "3.0.3" + :info {:title "Swagger API" + :version "0.0.1"}}) +;; +;; Swagger Spec +;; + +(defn security-processor [endpoint] + (let [backup (reduce-kv (fn [acc method definition] + (if (:security definition) + (let [security (:security definition) + security-schemas (into {} (for [[k v] security] [k (dissoc v :scopes)])) + security-path (into {} (for [[k v] security] [k (:scopes v)])) + result (-> acc + (update-in [:security-paths method] conj security-path) + (update-in [:security-schemes] conj security-schemas))] + result) acc)) {} endpoint) + new-endpoint (reduce-kv (fn [acc http-method & security] + (assoc-in acc [http-method :security] (vec (flatten security)))) endpoint (:security-paths backup))] + {:security-schemes (:security-schemes backup) :endpoint new-endpoint})) + +(defn security-operations [swagger] + (let [paths (or (:paths swagger) {}) + map-req-resp-schemas (for [[k v] paths] [k (security-processor v)]) + updated-paths (into {} (for [[k v] map-req-resp-schemas] [k (:endpoint v)])) + security-schemes (into {} (flatten (for [[_ v] map-req-resp-schemas] [(:security-schemes v)]))) + swagger-new (-> swagger + (assoc :paths updated-paths) + (assoc-in [:components :securitySchemes] security-schemes))] + swagger-new)) + +(def OpenApi openapi3-schema/OpenApi) + +(def Options {(s/optional-key :ignore-missing-mappings?) s/Bool + (s/optional-key :default-response-description-fn) (s/=> s/Str s/Int) + (s/optional-key :handle-duplicate-schemas-fn) s/Any}) + +(def option-defaults + (s/validate Options {:ignore-missing-mappings? false + :default-response-description-fn (constantly "") + :handle-duplicate-schemas-fn rsc/ignore-duplicate-schemas})) + +(s/defn openapi-json + "Produces openapi-json output from ring-swagger openapi3 spec. + Optional second argument is a options map, supporting + the following options with defaults: + + :ignore-missing-mappings? - (false) boolean whether to silently ignore + missing schema to JSON Schema mappings. if + set to false, IllegalArgumentException is + thrown if a Schema can't be presented as + JSON Schema. + + :default-response-description-fn - ((constantly \"\")) - a fn to generate default + response descriptions from http status code. + Takes a status code (Int) and returns a String. + + :handle-duplicate-schemas-fn - (ring.swagger.core/ignore-duplicate-schemas), + a function to handle possible duplicate schema + definitions. Takes schema-name and set of found + attached schema values as parameters. Returns + sequence of schema-name and selected schema value. + + :collection-format - Sets the collectionFormat for query and formData + parameters. + Possible values: multi, ssv, csv, tsv, pipes." + ([openapi :- (s/maybe OpenApi)] (openapi-json openapi nil)) + ([openapi :- (s/maybe OpenApi), options :- (s/maybe Options)] + (let [options (merge option-defaults options)] + (binding [rsjs/*ignore-missing-mappings* (true? (:ignore-missing-mappings? options))] + (let [[paths definitions] (-> openapi + ensure-body-and-response-schema-names + (extract-paths-and-definitions options))] + (common/deep-merge + openapi-defaults + (-> openapi + (assoc :paths paths) + (assoc-in [:components :schemas] definitions) + (security-operations)))))))) diff --git a/src/ring/swagger/openapi3_schema.clj b/src/ring/swagger/openapi3_schema.clj new file mode 100644 index 00000000..1610ba38 --- /dev/null +++ b/src/ring/swagger/openapi3_schema.clj @@ -0,0 +1,157 @@ +(ns ring.swagger.openapi3-schema + (:require [schema.core :as s] + [ring.swagger.swagger2-full-schema :refer [opt Info]])) + +(s/defschema Server-Variable + {(opt :enum) [s/Str] + :default s/Str + (opt :description) s/Str}) + +(s/defschema Server + {:url s/Str + (opt :description) s/Str + (opt :variables) {s/Str Server-Variable}}) + +(s/defschema ExternalDocumentation + {(opt :description) s/Str + :url s/Str}) + +(s/defschema OpenApiSchemaPart + {s/Keyword s/Any}) + +(s/defschema Example + {(opt :summary) s/Str + (opt :description) s/Str + (opt :value) s/Any + (opt :externalValue) s/Str}) + +(s/defschema Header + {(opt :description) s/Str + :required s/Bool + (opt :deprecated) s/Bool + (opt :allowEmptyValue) s/Bool + (opt :style) s/Any + (opt :explode) s/Bool + (opt :schema) OpenApiSchemaPart + (opt :example) s/Any + (opt :examples) {s/Str Example}}) + +(s/defschema Encoding + {(opt :contentType) s/Str + (opt :headers) {s/Str Header} + (opt :style) s/Any + (opt :explode) s/Bool + (opt :allowReserved) s/Bool}) + +(s/defschema MediaObject + {(opt :schema) OpenApiSchemaPart + (opt :example) Example + (opt :examples) {s/Str Example} + (opt :encoding) {s/Str Encoding}}) + +(s/defschema Parameter + {(opt :query) {s/Any s/Any} + (opt :path) {s/Any s/Any} + (opt :header) {s/Any s/Any}}) + +(s/defschema RequestBody + {(opt :description) s/Str + :content {s/Str MediaObject} + (opt :required) s/Bool}) + +(s/defschema Link + {(opt :operationRef) s/Str + (opt :operationId) s/Str + (opt :parameters) {s/Str s/Any} + (opt :requestBody) s/Any + (opt :description) s/Str + (opt :server) Server}) + +(def ^:private codes [100 101 102 103 + 200 201 202 203 204 205 206 207 208 226 + 300 301 302 303 304 305 306 307 308 + 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 428 429 431 451 + 500 501 502 503 504 505 506 507 508 510 511]) + +(assert (apply distinct? codes)) +(assert (apply < codes)) + +(s/defschema ResponseCode + (apply s/enum (map str codes))) + +(s/defschema Response + {:description s/Str + (opt :headers) {s/Str Header} + (opt :content) {s/Str MediaObject} + (opt :links) {s/Str Link}}) + +(s/defschema Operation + {(opt :tags) [s/Str] + (opt :summary) s/Str + (opt :description) s/Str + (opt :externalDocs) ExternalDocumentation + (opt :operationId) s/Str + (opt :parameters) Parameter + (opt :requestBody) RequestBody + (opt :responses) {ResponseCode Response} + (opt :deprecated) s/Bool + (opt :security) {s/Str [s/Str]} + (opt :servers) [Server]}) + +(s/defschema Path + {(opt :summary) s/Str + (opt :description) s/Str + (opt :get) Operation + (opt :put) Operation + (opt :post) Operation + (opt :delete) Operation + (opt :head) Operation + (opt :patch) Operation + (opt :servers) [Server] + (opt :parameters) s/Any}) + +(s/defschema Callback + {s/Str Path}) + +(s/defschema Tag + {:name s/Str + (opt :description) s/Str + (opt :externalDocs) ExternalDocumentation}) + +(s/defschema SecuritySchemeApiKey + {:type s/Any + (opt :description) s/Str + :name s/Str + :in s/Any}) + +(s/defschema SecuritySchemeHttp + {:type s/Any + (opt :description) s/Str + :scheme s/Str + :bearerFormat s/Str}) + +(s/defschema SecurityScheme + (s/conditional + #(and (map? %) (= "apiKey" (:type %))) SecuritySchemeApiKey + :else SecuritySchemeHttp)) + +(s/defschema Components + {(opt :schemas) {s/Str OpenApiSchemaPart} + (opt :responses) {s/Str Response} + (opt :parameters) {s/Str Parameter} + (opt :examples) {s/Str Example} + (opt :requestBodies) {s/Keyword RequestBody} + (opt :headers) {s/Str Header} + (opt :securitySchemes) {s/Str SecurityScheme} + (opt :links) {s/Str Link} + (opt :callbacks) {s/Str Callback}}) + +(s/defschema OpenApi + {(opt :openapi) (s/conditional string? (s/pred #(re-matches #"^3\.\d\.\d$" %))) + (opt :info) Info + (opt :servers) [Server] + (opt :paths) {s/Str Path} + (opt :components) Components + (opt :security) {s/Str [s/Str]} + (opt :tags) [Tag] + (opt :externalDocs) ExternalDocumentation}) diff --git a/src/ring/swagger/openapi3_validator.clj b/src/ring/swagger/openapi3_validator.clj new file mode 100644 index 00000000..af926946 --- /dev/null +++ b/src/ring/swagger/openapi3_validator.clj @@ -0,0 +1,9 @@ +(ns ring.swagger.openapi3-validator + (:require [clojure.java.io :as io] + [scjsv.core :as v])) + +; https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json +; http://json-schema.org/draft-04/schema + +(def validate + (v/validator (slurp (io/resource "ring/swagger/openapi-schema.json")))) diff --git a/test/ring/swagger/openapi3_test.clj b/test/ring/swagger/openapi3_test.clj new file mode 100644 index 00000000..d85ceee1 --- /dev/null +++ b/test/ring/swagger/openapi3_test.clj @@ -0,0 +1,103 @@ +(ns ring.swagger.openapi3-test + (:require [clojure.test :refer :all] + [ring.swagger.openapi3 :refer :all] + [schema.core :as s] + [ring.swagger.openapi3-schema :as full-schema] + [ring.swagger.json-schema :as rsjs] + [ring.swagger.extension :as extension] + [ring.swagger.openapi3-validator :as validator] + [linked.core :as linked] + [ring.util.http-status :as status] + [midje.sweet :refer :all]) + (:import (java.time Instant) + [java.util Date UUID] + [java.util.regex Pattern] + [org.joda.time DateTime LocalDate LocalTime])) + +(s/defschema Anything {s/Keyword s/Any}) +(s/defschema Nothing {}) + +(s/defschema LegOfPet {:length Long}) + +(s/defschema Pet {:id Long + :name String + :leg LegOfPet + (s/optional-key :weight) Double}) + +(s/defschema Parrot {:name String + :type {:name String}}) + +(s/defschema Turtle {:name String + :tags (s/if map? {s/Keyword s/Keyword} [String])}) + +(s/defschema NotFound {:message s/Str}) + +(defn validate-swagger-json [swagger & [options]] + (s/with-fn-validation + (validator/validate (openapi-json swagger options)))) + +(defn validate [swagger & [options]] + (s/with-fn-validation + (if-let [input-errors (s/check OpenApi swagger)] + {:input-errors input-errors} + (if-let [output-errors (validate-swagger-json swagger options)] + {:output-errors output-errors})))) + +(def a-complete-swagger + {:swagger "3.0.0" + :info {:version "version" + :title "title" + :description "description" + :termsOfService "jeah" + :contact {:name "name" + :url "http://someurl.com" + :email "tommi@example.com"} + :license {:name "name" + :url "http://someurl.com"}} + :servers [{:url "somehost:8080"}] + :externalDocs {:url "http://someurl.com" + :description "more info"} + :tags [{:name "pet", + :description "Everything about your Pets", + :externalDocs {:description "Find out more", :url "http://swagger.io"}} + {:name "store", + :description "Access to Petstore orders"} + {:name "user", + :description "Operations about user", + :externalDocs {:description "Find out more about our store", :url "http://swagger.io"}}] + :paths {"/api/:id" {:get {:tags ["pet"] + :summary "summary" + :description "description" + :operationId "operationId" + :parameters {:query (merge Anything {:x Long :y Long}) + :path {:id String} + :header Anything} + :responses {200 {:description "ok" + :content {"application/json" {:schema nil}}} + 400 {:description "not found" + :content {"application/json" {:schema NotFound}}}}}} + "/api/parrots" {:get {:responses {200 {:content {"application/json" {:schema Parrot}} + :description ""}}}}}}) +;; +;; facts +;; + +(fact "empty spec" + (let [swagger {}] + (validate swagger) => nil)) + +(fact "minimalistic spec" + (let [swagger {:paths {"/ping" {:get {}}}}] + (validate swagger) => nil)) + +#_(fact "more complete spec" + (validate a-complete-swagger) => nil) + +(extension/java-time + (fact "spec with java.time" + (let [model {:i Instant + :ld java.time.LocalDate + :lt java.time.LocalTime} + swagger {:paths {"/time" {:post {:parameters {:query model}}}}}] + + (validate swagger) => nil)))