From 4870e57c1701dcc22bdc5870e189878a54e0a5d8 Mon Sep 17 00:00:00 2001 From: Rajkumar Natarajan Date: Sun, 31 Mar 2024 12:54:23 -0700 Subject: [PATCH 01/22] make openapi work --- resources/ring/openapi/openapi-schema.json | 1658 ++++++++++++++++++++ src/ring/openapi/openapi3.clj | 319 ++++ src/ring/openapi/openapi3_schema.clj | 187 +++ src/ring/openapi/validator.clj | 9 + src/ring/swagger/json_schema.clj | 148 +- src/ring/swagger/json_schema_dirty.clj | 12 +- src/ring/swagger/swagger2.clj | 64 +- test/ring/openapi/openapi3_test.clj | 103 ++ test/ring/swagger/json_schema_test.clj | 68 +- test/ring/swagger/swagger2_unit_test.clj | 10 +- 10 files changed, 2427 insertions(+), 151 deletions(-) create mode 100644 resources/ring/openapi/openapi-schema.json create mode 100644 src/ring/openapi/openapi3.clj create mode 100644 src/ring/openapi/openapi3_schema.clj create mode 100644 src/ring/openapi/validator.clj create mode 100644 test/ring/openapi/openapi3_test.clj diff --git a/resources/ring/openapi/openapi-schema.json b/resources/ring/openapi/openapi-schema.json new file mode 100644 index 00000000..7f1933ec --- /dev/null +++ b/resources/ring/openapi/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 + } + } +} \ No newline at end of file diff --git a/src/ring/openapi/openapi3.clj b/src/ring/openapi/openapi3.clj new file mode 100644 index 00000000..1b863f26 --- /dev/null +++ b/src/ring/openapi/openapi3.clj @@ -0,0 +1,319 @@ +(ns ring.openapi.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.openapi.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])) + +(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) 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 options :openapi)] + :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 options :openapi)] + {: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 options :openapi)]) + (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-openapi 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.openapi.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/openapi/openapi3_schema.clj b/src/ring/openapi/openapi3_schema.clj new file mode 100644 index 00000000..8db5c153 --- /dev/null +++ b/src/ring/openapi/openapi3_schema.clj @@ -0,0 +1,187 @@ +(ns ring.openapi.openapi3-schema + (:require [schema.core :as s] + [ring.swagger.swagger2-full-schema :refer [X- length-greater matches opt Contact 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 + { + :name s/Str + (opt :in) s/Any + (opt :description) s/Str + :required s/Bool + (opt :deprecated) s/Bool + (opt :allowEmptyValue) s/Bool + (opt :style) s/Any + (opt :explode) s/Bool + (opt :allowReserved) s/Bool + (opt :schema) OpenApiSchemaPart + (opt :example) Example + (opt :examples) {s/Str Example} + (opt :content) {s/Str MediaObject}}) + +(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 + }) + +(s/defschema ResponseCode (s/enum "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" "510" "502" "503" "504" "505" "506" "507" "508" "510" "511")) + +(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) s/Any #_[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 (every-pred map? #(= (:type %) "apiKey")) 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/openapi/validator.clj b/src/ring/openapi/validator.clj new file mode 100644 index 00000000..18566ce1 --- /dev/null +++ b/src/ring/openapi/validator.clj @@ -0,0 +1,9 @@ +(ns ring.openapi.validator + (:require [clojure.java.io :as io] + [scjsv.core :as v])) + +; https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v2.0/schema.json +; http://json-schema.org/draft-04/schema + +(def validate + (v/validator (slurp (io/resource "ring/openapi/openapi-schema.json")))) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index b730a4ba..44bcc5f8 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -56,10 +56,10 @@ (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])) + (convert [this options schema-type])) (defn not-supported! [e] (throw (IllegalArgumentException. @@ -77,9 +77,9 @@ (defn reference? [m] (contains? m :$ref)) -(defn reference [e] +(defn reference [e schema-type] (if-let [schema-name (s/schema-name e)] - {:$ref (str "#/definitions/" schema-name)} + {:$ref (str (if (= schema-type :openapi) "#/components/schemas/" "#/definitions/") schema-name)} (if (not *ignore-missing-mappings*) (not-supported! e)))) @@ -92,28 +92,28 @@ m)) ;; Classes -(defmethod convert-class java.lang.Integer [_ _] {:type "integer" :format "int32"}) -(defmethod convert-class java.lang.Long [_ _] {:type "integer" :format "int64"}) -(defmethod convert-class java.lang.Double [_ _] {:type "number" :format "double"}) -(defmethod convert-class java.lang.Number [_ _] {:type "number" :format "double"}) -(defmethod convert-class java.lang.String [_ _] {:type "string"}) -(defmethod convert-class java.lang.Boolean [_ _] {:type "boolean"}) -(defmethod convert-class clojure.lang.Keyword [_ _] {:type "string"}) -(defmethod convert-class clojure.lang.Symbol [_ _] {:type "string"}) -(defmethod convert-class java.util.UUID [_ _] {:type "string" :format "uuid"}) -(defmethod convert-class java.util.Date [_ _] {:type "string" :format "date-time"}) -(defmethod convert-class org.joda.time.DateTime [_ _] {:type "string" :format "date-time"}) -(defmethod convert-class org.joda.time.LocalDate [_ _] {:type "string" :format "date"}) -(defmethod convert-class org.joda.time.LocalTime [_ _] {:type "string" :format "time"}) -(defmethod convert-class java.util.regex.Pattern [_ _] {:type "string" :format "regex"}) -(defmethod convert-class java.io.File [_ _] {:type "file"}) +(defmethod convert-class java.lang.Integer [_ _ _] {:type "integer" :format "int32"}) +(defmethod convert-class java.lang.Long [_ _ _] {:type "integer" :format "int64"}) +(defmethod convert-class java.lang.Double [_ _ _] {:type "number" :format "double"}) +(defmethod convert-class java.lang.Number [_ _ _] {:type "number" :format "double"}) +(defmethod convert-class java.lang.String [_ _ _] {:type "string"}) +(defmethod convert-class java.lang.Boolean [_ _ _] {:type "boolean"}) +(defmethod convert-class clojure.lang.Keyword [_ _ _] {:type "string"}) +(defmethod convert-class clojure.lang.Symbol [_ _ _] {:type "string"}) +(defmethod convert-class java.util.UUID [_ _ _] {:type "string" :format "uuid"}) +(defmethod convert-class java.util.Date [_ _ _] {:type "string" :format "date-time"}) +(defmethod convert-class org.joda.time.DateTime [_ _ _] {:type "string" :format "date-time"}) +(defmethod convert-class org.joda.time.LocalDate [_ _ _] {:type "string" :format "date"}) +(defmethod convert-class org.joda.time.LocalTime [_ _ _] {:type "string" :format "time"}) +(defmethod convert-class java.util.regex.Pattern [_ _ _] {:type "string" :format "regex"}) +(defmethod convert-class java.io.File [_ _ _] {:type "file"}) (extension/java-time - (defmethod convert-class java.time.Instant [_ _] {:type "string" :format "date-time"}) - (defmethod convert-class java.time.LocalDate [_ _] {:type "string" :format "date"}) - (defmethod convert-class java.time.LocalTime [_ _] {:type "string" :format "time"})) + (defmethod convert-class java.time.Instant [_ _ _] {:type "string" :format "date-time"}) + (defmethod convert-class java.time.LocalDate [_ _ _] {:type "string" :format "date"}) + (defmethod convert-class java.time.LocalTime [_ _ _] {:type "string" :format "time"})) -(defmethod convert-class :default [e _] +(defmethod convert-class :default [e _ _] (if-not *ignore-missing-mappings* (not-supported! e))) @@ -127,56 +127,56 @@ (defn ->swagger ([x] - (->swagger x {})) - ([x options] + (->swagger x {} :swagger)) + ([x options schema-type] (-> x - (convert options) + (convert options schema-type) (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 schema-type] + (try (->swagger v {:key-meta key-meta} schema-type) (catch Exception e (throw - (IllegalArgumentException. - (str "error converting to swagger schema [" k " " - (try (s/explain v) (catch Exception _ v)) "]") e))))) + (IllegalArgumentException. + (str "error converting to swagger schema [" k " " + (try (s/explain v) (catch Exception _ v)) "]") e))))) -(defn- coll-schema [e options] +(defn- coll-schema [e options schema-type] (-> {:type "array" - :items (->swagger (first e) (assoc options ::no-meta true))} + :items (->swagger (first e) (assoc options ::no-meta true) schema-type)} (assoc-collection-format options))) (extend-protocol JsonSchema Object - (convert [e _] + (convert [e _ _] (not-supported! e)) Class - (convert [e options] + (convert [e options schema-type] (if-let [schema (common/record-schema e)] - (schema-object schema) - (convert-class e options))) + (schema-object schema schema-type) + (convert-class e options schema-type))) nil - (convert [_ _] + (convert [_ _ _] nil) FieldSchema - (convert [e _] - (->swagger (:schema e))) + (convert [e _ schema-type] + (->swagger (:schema e) {} schema-type)) schema.core.Predicate - (convert [e _] - (some-> e :pred-name predicate-name-to-class ->swagger)) + (convert [e _ schema-type] + (some-> e :pred-name predicate-name-to-class (->swagger {} schema-type))) schema.core.EnumSchema - (convert [e _] - (merge (->swagger (class (first (:vs e)))) {:enum (seq (:vs e))})) + (convert [e options schema-type] + (merge (->swagger (class (first (:vs e))) options schema-type) {:enum (seq (:vs e))})) schema.core.Maybe - (convert [e {:keys [in]}] + (convert [e {:keys [in]} _] (let [schema (->swagger (:schema e))] (condp contains? in #{:query :formData} (assoc schema :allowEmptyValue true) @@ -184,71 +184,71 @@ schema))) schema.core.Both - (convert [e _] + (convert [e _ _] (->swagger (first (:schemas e)))) schema.core.Either - (convert [e _] + (convert [e _ _] (->swagger (first (:schemas e)))) schema.core.Recursive - (convert [e _] + (convert [e _ _] (->swagger (:derefable e))) schema.core.EqSchema - (convert [e _] + (convert [e _ _] (merge (->swagger (class (:v e))) {:enum [(:v e)]})) schema.core.NamedSchema - (convert [e _] - (->swagger (:schema e))) + (convert [e _ schema-type] + (->swagger (:schema e) {} schema-type)) schema.core.One - (convert [e _] + (convert [e _ _] (->swagger (:schema e))) schema.core.AnythingSchema - (convert [_ {:keys [in] :as opts}] + (convert [_ {:keys [in] :as opts} schema-type] (if (and in (not= :body in)) - (->swagger (s/maybe s/Str) opts) + (->swagger (s/maybe s/Str) opts schema-type) {})) schema.core.ConditionalSchema - (convert [e _] + (convert [e _ _] {:x-oneOf (vec (keep (comp ->swagger second) (:preds-and-schemas e)))}) schema.core.CondPre - (convert [e _] + (convert [e _ _] {:x-oneOf (mapv ->swagger (:schemas e))}) schema.core.Constrained - (convert [e _] + (convert [e _ _] (->swagger (:schema e))) java.util.regex.Pattern - (convert [e _] + (convert [e _ _] {:type "string" :pattern (str e)}) ;; Collections clojure.lang.Sequential - (convert [e options] - (coll-schema e options)) + (convert [e options schema-type] + (coll-schema e options schema-type)) clojure.lang.IPersistentSet - (convert [e options] - (assoc (coll-schema e options) :uniqueItems true)) + (convert [e options schema-type] + (assoc (coll-schema e options schema-type) :uniqueItems true)) clojure.lang.IPersistentMap - (convert [e {:keys [properties?]}] + (convert [e {:keys [properties?]} schema-type] (if properties? - {:properties (properties e)} - (reference e))) + {:properties (properties e schema-type)} + (reference e schema-type))) clojure.lang.Var - (convert [e _] - (reference e))) + (convert [e _ schema-type] + (reference e schema-type))) ;; ;; Schema to Swagger Schema definitions @@ -259,14 +259,14 @@ The result is put into collection of same type as input schema. Thus linked/map should keep the order of items. Returnes nil if no properties are found." - [schema] + [schema schema-type] {: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)] - :let [v (try->swagger v k key-meta)]] + :let [v (try->swagger v k key-meta schema-type)]] (and v [k v])))] (if (seq props) props))) @@ -274,20 +274,20 @@ (defn additional-properties "Generates json-schema additional properties from a plain map schema from under key s/Keyword." - [schema] + [schema schema-type] {: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)) + (try->swagger v s/Keyword nil schema-type )) false)) (defn schema-object "Returns a JSON Schema object of a plain map schema." - [schema] + [schema schema-type] (if (common/plain-map? schema) - (let [properties (properties schema) + (let [properties (properties schema schema-type) title (if (not (s/schema-name schema)) (common/title schema)) - additional-properties (additional-properties schema) + additional-properties (additional-properties schema schema-type) meta (json-schema-meta schema) required (some->> (rsc/required-keys schema) (filter (partial contains? properties)) diff --git a/src/ring/swagger/json_schema_dirty.clj b/src/ring/swagger/json_schema_dirty.clj index 7ba1c5c1..04b3f528 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}} schema-type] (if properties? (merge {:discriminator (name (:dispatch-key e))} - (json-schema/->swagger (:schema e) {:properties? properties?})) - (json-schema/reference e))) + (json-schema/->swagger (:schema e) {:properties? properties?} schema-type)) + (json-schema/reference e schema-type))) schema.experimental.abstract_map.SchemaExtension - (convert [e _] - {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false}) + (convert [e options schema-type] + {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false} schema-type) ; 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)] (into (empty m) (remove (comp base-keys key) m))) - {:properties? true})]})) + {:properties? true} schema-type)]})) diff --git a/src/ring/swagger/swagger2.clj b/src/ring/swagger/swagger2.clj index 4245a9c4..4be8f19a 100644 --- a/src/ring/swagger/swagger2.clj +++ b/src/ring/swagger/swagger2.clj @@ -30,39 +30,39 @@ (->> schemas rsc/collect-models (rsc/handle-duplicate-schemas (:handle-duplicate-schemas-fn options)) - (map (juxt (comp str key) (comp rsjs/schema-object val))) + (map (juxt (comp str key) (comp #(rsjs/schema-object % :swagger) val))) (into (sorted-map)))) ;; ;; Paths, parameters, responses ;; -(defmulti ^:private extract-parameter (fn [in _ _] in)) +(defmulti extract-parameter (fn [in _ _] in)) (defmethod extract-parameter :body [_ model options] (if model (let [schema (rsc/peek-schema model) - schema-json (rsjs/->swagger model options)] + schema-json (rsjs/->swagger model options :swagger)] (vector - {:in "body" - :name (or (common/title schema) "") - :description (or (:description (rsjs/json-schema-meta schema)) "") - :required (not (rsjs/maybe? model)) - :schema schema-json})))) + {:in "body" + :name (or (common/title schema) "") + :description (or (:description (rsjs/json-schema-meta schema)) "") + :required (not (rsjs/maybe? model)) + :schema schema-json})))) (defmethod extract-parameter :default [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 options)] + json-schema (rsjs/->swagger v options :swagger)] :when json-schema] (merge - {:in (name in) - :name (rsjs/key-name rk) - :description "" - :required (or (= in :path) (s/required-key? k))} - json-schema)))) + {:in (name in) + :name (rsjs/key-name rk) + :description "" + :required (or (= in :path) (s/required-key? k))} + json-schema)))) (defn- default-response-description "uses option :default-response-description-fn to generate @@ -81,11 +81,11 @@ (let [responses (p/for-map [[k v] responses :let [{:keys [schema headers]} v]] k (-> v - (cond-> schema (update-in [:schema] rsjs/->swagger options)) + (cond-> schema (update-in [:schema] rsjs/->swagger options :swagger)) (cond-> headers (update-in [:headers] (fn [headers] (if headers (->> (for [[k v] headers] - [k (rsjs/->swagger v options)]) + [k (rsjs/->swagger v options :swagger)]) (into {})))))) (update-in [:description] #(or % (:description (rsjs/json-schema-meta v)) @@ -121,12 +121,12 @@ (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) + (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))] @@ -231,13 +231,13 @@ Possible values: multi, ssv, csv, tsv, pipes." ([swagger :- (s/maybe Swagger)] (swagger-json swagger nil)) ([swagger :- (s/maybe Swagger), options :- (s/maybe Options)] - (let [options (merge option-defaults options)] - (binding [rsjs/*ignore-missing-mappings* (true? (:ignore-missing-mappings? options))] - (let [[paths definitions] (-> swagger - ensure-body-and-response-schema-names - (extract-paths-and-definitions options))] - (common/deep-merge - swagger-defaults - (-> swagger - (assoc :paths paths) - (assoc :definitions definitions)))))))) + (let [options (merge option-defaults options)] + (binding [rsjs/*ignore-missing-mappings* (true? (:ignore-missing-mappings? options))] + (let [[paths definitions] (-> swagger + ensure-body-and-response-schema-names + (extract-paths-and-definitions options))] + (common/deep-merge + swagger-defaults + (-> swagger + (assoc :paths paths) + (assoc :definitions definitions)))))))) diff --git a/test/ring/openapi/openapi3_test.clj b/test/ring/openapi/openapi3_test.clj new file mode 100644 index 00000000..50659055 --- /dev/null +++ b/test/ring/openapi/openapi3_test.clj @@ -0,0 +1,103 @@ +(ns ring.openapi.openapi3-test + (:require [clojure.test :refer :all] + [ring.openapi.openapi3 :refer :all] + [schema.core :as s] + [ring.openapi.openapi3-schema :as full-schema] + [ring.swagger.json-schema :as rsjs] + [ring.swagger.extension :as extension] + [ring.openapi.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))) \ No newline at end of file diff --git a/test/ring/swagger/json_schema_test.clj b/test/ring/swagger/json_schema_test.clj index 7b80945f..ab61e7ea 100644 --- a/test/ring/swagger/json_schema_test.clj +++ b/test/ring/swagger/json_schema_test.clj @@ -19,7 +19,7 @@ (s/defrecord User [age :- s/Int, keyboard :- Keyboard]) ;; Make currency return nil for testing purporses -(defmethod rsjs/convert-class java.util.Currency [_ _] nil) +(defmethod rsjs/convert-class java.util.Currency [_ _ _] nil) (facts "type transformations" (fact "mapped to nil" @@ -82,13 +82,13 @@ (fact "uses wrapped value by default with x-nullable true" (rsjs/->swagger (s/maybe Long)) => (assoc (rsjs/->swagger Long) :x-nullable true)) (fact "adds allowEmptyValue when for query and formData as defined by the spec" - (rsjs/->swagger (s/maybe Long) {:in :query}) => (assoc (rsjs/->swagger Long) :allowEmptyValue true) - (rsjs/->swagger (s/maybe Long) {:in :formData}) => (assoc (rsjs/->swagger Long) :allowEmptyValue true)) + (rsjs/->swagger (s/maybe Long) {:in :query} :swagger) => (assoc (rsjs/->swagger Long) :allowEmptyValue true) + (rsjs/->swagger (s/maybe Long) {:in :formData} :swagger) => (assoc (rsjs/->swagger Long) :allowEmptyValue true)) (fact "uses wrapped value by default with x-nullable true with body" - (rsjs/->swagger (s/maybe Long) {:in :body}) => (assoc (rsjs/->swagger Long) :x-nullable true)) + (rsjs/->swagger (s/maybe Long) {:in :body} :swagger) => (assoc (rsjs/->swagger Long) :x-nullable true)) (fact "uses wrapped value for other parameters" - (rsjs/->swagger (s/maybe Long) {:in :header}) => (rsjs/->swagger Long) - (rsjs/->swagger (s/maybe Long) {:in :path}) => (rsjs/->swagger Long))) + (rsjs/->swagger (s/maybe Long) {:in :header} :swagger) => (rsjs/->swagger Long) + (rsjs/->swagger (s/maybe Long) {:in :path} :swagger) => (rsjs/->swagger Long))) (fact "s/defrecord" (rsjs/->swagger User) => {:type "object", @@ -122,11 +122,11 @@ (fact "s/Any" (rsjs/->swagger s/Any) => {} - (rsjs/->swagger s/Any {:in :body}) => {} - (rsjs/->swagger s/Any {:in :header}) => {:type "string"} - (rsjs/->swagger s/Any {:in :path}) => {:type "string"} - (rsjs/->swagger s/Any {:in :query}) => {:type "string", :allowEmptyValue true} - (rsjs/->swagger s/Any {:in :formData}) => {:type "string", :allowEmptyValue true}) + (rsjs/->swagger s/Any {:in :body} :swagger) => {} + (rsjs/->swagger s/Any {:in :header} :swagger) => {:type "string"} + (rsjs/->swagger s/Any {:in :path} :swagger) => {:type "string"} + (rsjs/->swagger s/Any {:in :query} :swagger) => {:type "string", :allowEmptyValue true} + (rsjs/->swagger s/Any {:in :formData} :swagger) => {:type "string", :allowEmptyValue true}) (fact "s/conditional" (rsjs/->swagger (s/conditional (constantly true) Long (constantly false) String)) @@ -150,19 +150,19 @@ )) (fact "Optional-key default metadata" - (rsjs/properties {(with-meta (s/optional-key :foo) {:default "bar"}) s/Str}) + (rsjs/properties {(with-meta (s/optional-key :foo) {:default "bar"}) s/Str} :swagger) => {:foo {:type "string" :default "bar"}} (fact "nil default is ignored" - (rsjs/properties {(with-meta (s/optional-key :foo) {:default nil}) s/Str}) + (rsjs/properties {(with-meta (s/optional-key :foo) {:default nil}) s/Str} :swagger) => {:foo {:type "string"}}) (fact "pfnk schema" - (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str "foo"}]))) + (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str "foo"}])) :swagger) => {:x {:type "string" :default "foo"}}) (fact "pfnk schema - nil default is ignored" - (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str nil}]))) + (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str nil}])) :swagger) => {:x {:type "string"}})) (fact "Describe" @@ -204,7 +204,7 @@ (rsjs/->swagger schema) => {:$ref "#/definitions/schema"}) (fact "extra metadata is present on schema objects" - (rsjs/schema-object schema) => (contains + (rsjs/schema-object schema :swagger) => (contains {:properties {:name {:type "string"} :title {:type "string"}} :minProperties 1 @@ -228,46 +228,46 @@ (facts "properties" (fact "s/Any -values are not ignored" (keys (rsjs/properties {:a s/Str - :b s/Any})) + :b s/Any} :swagger)) => [:a :b]) (fact "nil-values are ignored" - (keys (rsjs/properties {:a s/Str, :b Currency})) + (keys (rsjs/properties {:a s/Str, :b Currency} :swagger)) => [:a]) (fact "s/Keyword -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Keyword s/Str})) + s/Keyword s/Str} :swagger)) => [:a]) (fact "Class -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Str s/Str})) + s/Str s/Str} :swagger)) => [:a]) (fact "Required keyword-keys are used" (keys (rsjs/properties {:a s/Str - (s/required-key :b) s/Str})) + (s/required-key :b) s/Str} :swagger)) => [:a :b]) (fact "Required non-keyword-keys are NOT ignored" (keys (rsjs/properties {:a s/Str - (s/required-key "b") s/Str})) + (s/required-key "b") s/Str} :swagger)) => [:a "b"]) (fact "s/Any -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Any s/Str})) + s/Any s/Str} :swagger)) => [:a]) (fact "with unknown mappings" (fact "by default, exception is thrown" (rsjs/properties {:a String - :b java.util.Vector}) => (throws IllegalArgumentException)) + :b java.util.Vector} :swagger) => (throws IllegalArgumentException)) (fact "unknown fields are ignored ig *ignore-missing-mappings* is set" (binding [rsjs/*ignore-missing-mappings* true] (keys (rsjs/properties {:a String - :b java.util.Vector})) => [:a]))) + :b java.util.Vector} :swagger)) => [:a]))) (fact "Keeps the order of properties intact" (keys (rsjs/properties (linked/map :a String @@ -277,13 +277,13 @@ :e String :f String :g String - :h String))) + :h String) :swagger)) => [:a :b :c :d :e :f :g :h]) (fact "Ordered-map works with sub-schemas" (rsjs/properties (rsc/with-named-sub-schemas (linked/map :a String :b {:foo String} - :c [{:bar String}]))) + :c [{:bar String}] )) :swagger) => anything) (fact "referenced record-schemas" @@ -291,28 +291,28 @@ (s/defschema Bar {:key Foo}) (fact "can't get properties out of record schemas" - (rsjs/properties Foo)) => (throws AssertionError) + (rsjs/properties Foo :swagger)) => (throws AssertionError) (fact "nested properties work ok" - (keys (rsjs/properties Bar)) => [:key]))) + (keys (rsjs/properties Bar :swagger)) => [:key]))) (facts "additional-properties" (fact "No additional properties" - (rsjs/additional-properties {:a s/Str}) + (rsjs/additional-properties {:a s/Str} :swagger) => false) (fact "s/Keyword" - (rsjs/additional-properties {s/Keyword s/Bool}) + (rsjs/additional-properties {s/Keyword s/Bool} :swagger) => {:type "boolean"}) (fact "s/Any" - (rsjs/additional-properties {s/Any s/Str}) + (rsjs/additional-properties {s/Any s/Str} :swagger) => {:type "string"}) (fact "s/Str" - (rsjs/additional-properties {s/Str s/Bool}) + (rsjs/additional-properties {s/Str s/Bool} :swagger) => {:type "boolean"}) (fact "s/Int" - (rsjs/additional-properties {s/Int s/Str}) + (rsjs/additional-properties {s/Int s/Str} :swagger) => {:type "string"})) diff --git a/test/ring/swagger/swagger2_unit_test.clj b/test/ring/swagger/swagger2_unit_test.clj index 4a72c3fe..939d7b82 100644 --- a/test/ring/swagger/swagger2_unit_test.clj +++ b/test/ring/swagger/swagger2_unit_test.clj @@ -84,9 +84,9 @@ ;; (fact "transform simple schemas" - (rsjs/schema-object Tag) => Tag' - (rsjs/schema-object Category) => Category' - (rsjs/schema-object Pet) => Pet') + (rsjs/schema-object Tag :swagger) => Tag' + (rsjs/schema-object Category :swagger) => Category' + (rsjs/schema-object Pet :swagger) => Pet') (s/defschema RootModel {:sub {:foo Long}}) @@ -539,11 +539,11 @@ :b InvalidElement}] (fact "fail by default" - (rsjs/schema-object schema) => (throws IllegalArgumentException)) + (rsjs/schema-object schema :swagger) => (throws IllegalArgumentException)) (fact "drops bad fields from both properties & required" (binding [rsjs/*ignore-missing-mappings* true] - (rsjs/schema-object schema) + (rsjs/schema-object schema :swagger) => {:type "object" :properties {:a {:type "string"}} From 94f1d01b1231b6559f151adf29561941c0b50d86 Mon Sep 17 00:00:00 2001 From: Rajkumar Natarajan Date: Tue, 2 Apr 2024 22:24:31 -0700 Subject: [PATCH 02/22] openapi 3.0 support pass schema type as key of options --- src/ring/openapi/openapi3.clj | 6 +- src/ring/swagger/json_schema.clj | 117 ++++++++++++------------- src/ring/swagger/json_schema_dirty.clj | 14 +-- src/ring/swagger/swagger2.clj | 10 +-- test/ring/swagger/json_schema_test.clj | 22 ++--- 5 files changed, 84 insertions(+), 85 deletions(-) diff --git a/src/ring/openapi/openapi3.clj b/src/ring/openapi/openapi3.clj index 1b863f26..b5910750 100644 --- a/src/ring/openapi/openapi3.clj +++ b/src/ring/openapi/openapi3.clj @@ -39,7 +39,7 @@ (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 options :openapi)] + json-schema (rsjs/->swagger v (assoc options :schema-type :openapi))] :when json-schema] {:in (name in) :name (rsjs/key-name rk) @@ -60,7 +60,7 @@ (into {} (for [[content-type schema-input] contents] [content-type (let [schema (rsc/peek-schema schema-input) - schema-json (rsjs/->swagger schema-input options :openapi)] + schema-json (rsjs/->swagger schema-input (assoc options :schema-type :openapi))] {:name (or (common/title schema) "") :schema schema-json})])))) @@ -78,7 +78,7 @@ (cond-> headers (update-in [:headers] (fn [headers] (if headers (->> (for [[k v] headers] - [k (rsjs/->swagger v options :openapi)]) + [k (rsjs/->swagger v (assoc options :schema-type :openapi))]) (into {})))))) (update-in [:description] #(or % (:description (rsjs/json-schema-meta v)) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 44bcc5f8..ad1a5a39 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -56,10 +56,10 @@ (str (if n (str n "/")) (name x))) x)) -(defmulti convert-class (fn [c _ _] c)) +(defmulti convert-class (fn [c _] c)) (defprotocol JsonSchema - (convert [this options schema-type])) + (convert [this options])) (defn not-supported! [e] (throw (IllegalArgumentException. @@ -92,28 +92,28 @@ m)) ;; Classes -(defmethod convert-class java.lang.Integer [_ _ _] {:type "integer" :format "int32"}) -(defmethod convert-class java.lang.Long [_ _ _] {:type "integer" :format "int64"}) -(defmethod convert-class java.lang.Double [_ _ _] {:type "number" :format "double"}) -(defmethod convert-class java.lang.Number [_ _ _] {:type "number" :format "double"}) -(defmethod convert-class java.lang.String [_ _ _] {:type "string"}) -(defmethod convert-class java.lang.Boolean [_ _ _] {:type "boolean"}) -(defmethod convert-class clojure.lang.Keyword [_ _ _] {:type "string"}) -(defmethod convert-class clojure.lang.Symbol [_ _ _] {:type "string"}) -(defmethod convert-class java.util.UUID [_ _ _] {:type "string" :format "uuid"}) -(defmethod convert-class java.util.Date [_ _ _] {:type "string" :format "date-time"}) -(defmethod convert-class org.joda.time.DateTime [_ _ _] {:type "string" :format "date-time"}) -(defmethod convert-class org.joda.time.LocalDate [_ _ _] {:type "string" :format "date"}) -(defmethod convert-class org.joda.time.LocalTime [_ _ _] {:type "string" :format "time"}) -(defmethod convert-class java.util.regex.Pattern [_ _ _] {:type "string" :format "regex"}) -(defmethod convert-class java.io.File [_ _ _] {:type "file"}) +(defmethod convert-class java.lang.Integer [_ _] {:type "integer" :format "int32"}) +(defmethod convert-class java.lang.Long [_ _] {:type "integer" :format "int64"}) +(defmethod convert-class java.lang.Double [_ _] {:type "number" :format "double"}) +(defmethod convert-class java.lang.Number [_ _] {:type "number" :format "double"}) +(defmethod convert-class java.lang.String [_ _] {:type "string"}) +(defmethod convert-class java.lang.Boolean [_ _] {:type "boolean"}) +(defmethod convert-class clojure.lang.Keyword [_ _] {:type "string"}) +(defmethod convert-class clojure.lang.Symbol [_ _] {:type "string"}) +(defmethod convert-class java.util.UUID [_ _] {:type "string" :format "uuid"}) +(defmethod convert-class java.util.Date [_ _] {:type "string" :format "date-time"}) +(defmethod convert-class org.joda.time.DateTime [_ _] {:type "string" :format "date-time"}) +(defmethod convert-class org.joda.time.LocalDate [_ _] {:type "string" :format "date"}) +(defmethod convert-class org.joda.time.LocalTime [_ _] {:type "string" :format "time"}) +(defmethod convert-class java.util.regex.Pattern [_ _] {:type "string" :format "regex"}) +(defmethod convert-class java.io.File [_ _] {:type "file"}) (extension/java-time - (defmethod convert-class java.time.Instant [_ _ _] {:type "string" :format "date-time"}) - (defmethod convert-class java.time.LocalDate [_ _ _] {:type "string" :format "date"}) - (defmethod convert-class java.time.LocalTime [_ _ _] {:type "string" :format "time"})) + (defmethod convert-class java.time.Instant [_ _] {:type "string" :format "date-time"}) + (defmethod convert-class java.time.LocalDate [_ _] {:type "string" :format "date"}) + (defmethod convert-class java.time.LocalTime [_ _] {:type "string" :format "time"})) -(defmethod convert-class :default [e _ _] +(defmethod convert-class :default [e _] (if-not *ignore-missing-mappings* (not-supported! e))) @@ -127,56 +127,55 @@ (defn ->swagger ([x] - (->swagger x {} :swagger)) - ([x options schema-type] + (->swagger x {})) + ([x options] (-> x - (convert options schema-type) + (convert options) (merge-meta x options)))) (defn try->swagger [v k key-meta schema-type] - (try (->swagger v {:key-meta key-meta} schema-type) + (try (->swagger v {:key-meta key-meta :schema-type schema-type}) (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 schema-type] +(defn- coll-schema [e options] (-> {:type "array" - :items (->swagger (first e) (assoc options ::no-meta true) schema-type)} + :items (->swagger (first e) (assoc options ::no-meta true))} (assoc-collection-format options))) (extend-protocol JsonSchema Object - (convert [e _ _] + (convert [e _] (not-supported! e)) Class - (convert [e options schema-type] + (convert [e options] (if-let [schema (common/record-schema e)] - (schema-object schema schema-type) - (convert-class e options schema-type))) + (schema-object schema (:schema-type options)) + (convert-class e options))) nil - (convert [_ _ _] + (convert [_ _] nil) FieldSchema - (convert [e _ schema-type] - (->swagger (:schema e) {} schema-type)) + (convert [e _] + (->swagger (:schema e) {})) schema.core.Predicate - (convert [e _ schema-type] - (some-> e :pred-name predicate-name-to-class (->swagger {} schema-type))) + (convert [e _] + (some-> e :pred-name predicate-name-to-class (->swagger {}))) schema.core.EnumSchema - (convert [e options schema-type] - (merge (->swagger (class (first (:vs e))) options schema-type) {: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]} _] + (convert [e {:keys [in]}] (let [schema (->swagger (:schema e))] (condp contains? in #{:query :formData} (assoc schema :allowEmptyValue true) @@ -184,70 +183,70 @@ schema))) schema.core.Both - (convert [e _ _] + (convert [e _] (->swagger (first (:schemas e)))) schema.core.Either - (convert [e _ _] + (convert [e _] (->swagger (first (:schemas e)))) schema.core.Recursive - (convert [e _ _] + (convert [e _] (->swagger (:derefable e))) schema.core.EqSchema - (convert [e _ _] + (convert [e _] (merge (->swagger (class (:v e))) {:enum [(:v e)]})) schema.core.NamedSchema - (convert [e _ schema-type] - (->swagger (:schema e) {} schema-type)) + (convert [e _] + (->swagger (:schema e) {})) schema.core.One - (convert [e _ _] + (convert [e _] (->swagger (:schema e))) schema.core.AnythingSchema - (convert [_ {:keys [in] :as opts} schema-type] + (convert [_ {:keys [in] :as opts}] (if (and in (not= :body in)) - (->swagger (s/maybe s/Str) opts schema-type) + (->swagger (s/maybe s/Str) opts) {})) schema.core.ConditionalSchema - (convert [e _ _] + (convert [e _] {:x-oneOf (vec (keep (comp ->swagger second) (:preds-and-schemas e)))}) schema.core.CondPre - (convert [e _ _] + (convert [e _] {:x-oneOf (mapv ->swagger (:schemas e))}) schema.core.Constrained - (convert [e _ _] + (convert [e _] (->swagger (:schema e))) java.util.regex.Pattern - (convert [e _ _] + (convert [e _] {:type "string" :pattern (str e)}) ;; Collections clojure.lang.Sequential - (convert [e options schema-type] - (coll-schema e options schema-type)) + (convert [e options] + (coll-schema e options)) clojure.lang.IPersistentSet - (convert [e options schema-type] - (assoc (coll-schema e options schema-type) :uniqueItems true)) + (convert [e options] + (assoc (coll-schema e options) :uniqueItems true)) clojure.lang.IPersistentMap - (convert [e {:keys [properties?]} schema-type] + (convert [e {:keys [properties? schema-type]}] (if properties? {:properties (properties e schema-type)} (reference e schema-type))) clojure.lang.Var - (convert [e _ schema-type] + (convert [e {:keys [schema-type]}] (reference e schema-type))) ;; diff --git a/src/ring/swagger/json_schema_dirty.clj b/src/ring/swagger/json_schema_dirty.clj index 04b3f528..8f0ea2cb 100644 --- a/src/ring/swagger/json_schema_dirty.clj +++ b/src/ring/swagger/json_schema_dirty.clj @@ -6,18 +6,18 @@ (extend-protocol json-schema/JsonSchema schema.experimental.abstract_map.AbstractSchema - (convert [e {:keys [properties?] - :or {properties? true}} schema-type] + (convert [e {:keys [properties? schema-type] + :or {properties? true}} ] (if properties? (merge {:discriminator (name (:dispatch-key e))} - (json-schema/->swagger (:schema e) {:properties? properties?} schema-type)) + (json-schema/->swagger (:schema e) {:properties? properties? :schema-type schema-type})) (json-schema/reference e schema-type))) schema.experimental.abstract_map.SchemaExtension - (convert [e options schema-type] - {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false} schema-type) + (convert [e {:keys [schema-type] :as options}] + {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false :schema-type schema-type}) ; 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} schema-type)]})) + {:properties? true :schema-type schema-type})]})) diff --git a/src/ring/swagger/swagger2.clj b/src/ring/swagger/swagger2.clj index 4be8f19a..913ec824 100644 --- a/src/ring/swagger/swagger2.clj +++ b/src/ring/swagger/swagger2.clj @@ -37,12 +37,12 @@ ;; Paths, parameters, responses ;; -(defmulti extract-parameter (fn [in _ _] in)) +(defmulti ^:private extract-parameter (fn [in _ _] in)) (defmethod extract-parameter :body [_ model options] (if model (let [schema (rsc/peek-schema model) - schema-json (rsjs/->swagger model options :swagger)] + schema-json (rsjs/->swagger model options)] (vector {:in "body" :name (or (common/title schema) "") @@ -55,7 +55,7 @@ (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 options :swagger)] + json-schema (rsjs/->swagger v options)] :when json-schema] (merge {:in (name in) @@ -81,11 +81,11 @@ (let [responses (p/for-map [[k v] responses :let [{:keys [schema headers]} v]] k (-> v - (cond-> schema (update-in [:schema] rsjs/->swagger options :swagger)) + (cond-> schema (update-in [:schema] rsjs/->swagger options)) (cond-> headers (update-in [:headers] (fn [headers] (if headers (->> (for [[k v] headers] - [k (rsjs/->swagger v options :swagger)]) + [k (rsjs/->swagger v options)]) (into {})))))) (update-in [:description] #(or % (:description (rsjs/json-schema-meta v)) diff --git a/test/ring/swagger/json_schema_test.clj b/test/ring/swagger/json_schema_test.clj index ab61e7ea..fd486043 100644 --- a/test/ring/swagger/json_schema_test.clj +++ b/test/ring/swagger/json_schema_test.clj @@ -19,7 +19,7 @@ (s/defrecord User [age :- s/Int, keyboard :- Keyboard]) ;; Make currency return nil for testing purporses -(defmethod rsjs/convert-class java.util.Currency [_ _ _] nil) +(defmethod rsjs/convert-class java.util.Currency [_ _] nil) (facts "type transformations" (fact "mapped to nil" @@ -82,13 +82,13 @@ (fact "uses wrapped value by default with x-nullable true" (rsjs/->swagger (s/maybe Long)) => (assoc (rsjs/->swagger Long) :x-nullable true)) (fact "adds allowEmptyValue when for query and formData as defined by the spec" - (rsjs/->swagger (s/maybe Long) {:in :query} :swagger) => (assoc (rsjs/->swagger Long) :allowEmptyValue true) - (rsjs/->swagger (s/maybe Long) {:in :formData} :swagger) => (assoc (rsjs/->swagger Long) :allowEmptyValue true)) + (rsjs/->swagger (s/maybe Long) {:in :query}) => (assoc (rsjs/->swagger Long) :allowEmptyValue true) + (rsjs/->swagger (s/maybe Long) {:in :formData}) => (assoc (rsjs/->swagger Long) :allowEmptyValue true)) (fact "uses wrapped value by default with x-nullable true with body" - (rsjs/->swagger (s/maybe Long) {:in :body} :swagger) => (assoc (rsjs/->swagger Long) :x-nullable true)) + (rsjs/->swagger (s/maybe Long) {:in :body}) => (assoc (rsjs/->swagger Long) :x-nullable true)) (fact "uses wrapped value for other parameters" - (rsjs/->swagger (s/maybe Long) {:in :header} :swagger) => (rsjs/->swagger Long) - (rsjs/->swagger (s/maybe Long) {:in :path} :swagger) => (rsjs/->swagger Long))) + (rsjs/->swagger (s/maybe Long) {:in :header}) => (rsjs/->swagger Long) + (rsjs/->swagger (s/maybe Long) {:in :path}) => (rsjs/->swagger Long))) (fact "s/defrecord" (rsjs/->swagger User) => {:type "object", @@ -122,11 +122,11 @@ (fact "s/Any" (rsjs/->swagger s/Any) => {} - (rsjs/->swagger s/Any {:in :body} :swagger) => {} - (rsjs/->swagger s/Any {:in :header} :swagger) => {:type "string"} - (rsjs/->swagger s/Any {:in :path} :swagger) => {:type "string"} - (rsjs/->swagger s/Any {:in :query} :swagger) => {:type "string", :allowEmptyValue true} - (rsjs/->swagger s/Any {:in :formData} :swagger) => {:type "string", :allowEmptyValue true}) + (rsjs/->swagger s/Any {:in :body}) => {} + (rsjs/->swagger s/Any {:in :header}) => {:type "string"} + (rsjs/->swagger s/Any {:in :path}) => {:type "string"} + (rsjs/->swagger s/Any {:in :query}) => {:type "string", :allowEmptyValue true} + (rsjs/->swagger s/Any {:in :formData}) => {:type "string", :allowEmptyValue true}) (fact "s/conditional" (rsjs/->swagger (s/conditional (constantly true) Long (constantly false) String)) From 5570b26896f2148ff488ec3c29914059c0634052 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:42:08 -0500 Subject: [PATCH 03/22] options map --- src/ring/openapi/openapi3.clj | 10 +- src/ring/swagger/json_schema.clj | 114 +++++++++++++---------- src/ring/swagger/swagger2.clj | 2 +- test/ring/swagger/swagger2_unit_test.clj | 4 +- 4 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/ring/openapi/openapi3.clj b/src/ring/openapi/openapi3.clj index b5910750..77906226 100644 --- a/src/ring/openapi/openapi3.clj +++ b/src/ring/openapi/openapi3.clj @@ -27,11 +27,13 @@ (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) val))) + (map (juxt (comp str key) (comp #(rsjs/schema-object % openapi-opts) val))) (into (sorted-map)))) (defn extract-parameter [in model options] @@ -39,7 +41,7 @@ (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 (assoc options :schema-type :openapi))] + json-schema (rsjs/->swagger v (into options openapi-opts))] :when json-schema] {:in (name in) :name (rsjs/key-name rk) @@ -60,7 +62,7 @@ (into {} (for [[content-type schema-input] contents] [content-type (let [schema (rsc/peek-schema schema-input) - schema-json (rsjs/->swagger schema-input (assoc options :schema-type :openapi))] + schema-json (rsjs/->swagger schema-input (into options openapi-opts))] {:name (or (common/title schema) "") :schema schema-json})])))) @@ -78,7 +80,7 @@ (cond-> headers (update-in [:headers] (fn [headers] (if headers (->> (for [[k v] headers] - [k (rsjs/->swagger v (assoc options :schema-type :openapi))]) + [k (rsjs/->swagger v (into options openapi-opts))]) (into {})))))) (update-in [:description] #(or % (:description (rsjs/json-schema-meta v)) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index ad1a5a39..81e8d9f0 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) @@ -77,11 +81,16 @@ (defn reference? [m] (contains? m :$ref)) -(defn reference [e schema-type] - (if-let [schema-name (s/schema-name e)] - {:$ref (str (if (= schema-type :openapi) "#/components/schemas/" "#/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->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]}] @@ -109,9 +118,9 @@ (defmethod convert-class java.io.File [_ _] {:type "file"}) (extension/java-time - (defmethod convert-class java.time.Instant [_ _] {:type "string" :format "date-time"}) - (defmethod convert-class java.time.LocalDate [_ _] {:type "string" :format "date"}) - (defmethod convert-class java.time.LocalTime [_ _] {:type "string" :format "time"})) + (defmethod convert-class java.time.Instant [_ _] {:type "string" :format "date-time"}) + (defmethod convert-class java.time.LocalDate [_ _] {:type "string" :format "date"}) + (defmethod convert-class java.time.LocalTime [_ _] {:type "string" :format "time"})) (defmethod convert-class :default [e _] (if-not *ignore-missing-mappings* @@ -133,8 +142,8 @@ (convert options) (merge-meta x options)))) -(defn try->swagger [v k key-meta schema-type] - (try (->swagger v {:key-meta key-meta :schema-type schema-type}) +(defn try->swagger [v k key-meta opts] + (try (->swagger v {:key-meta key-meta ::schema-type (opts->schema-type opts)}) (catch Exception e (throw (IllegalArgumentException. @@ -155,7 +164,7 @@ Class (convert [e options] (if-let [schema (common/record-schema e)] - (schema-object schema (:schema-type options)) + (schema-object schema options) (convert-class e options))) nil @@ -240,14 +249,14 @@ (assoc (coll-schema e options) :uniqueItems true)) clojure.lang.IPersistentMap - (convert [e {:keys [properties? schema-type]}] + (convert [e {:keys [properties?] :as opts}] (if properties? - {:properties (properties e schema-type)} - (reference e schema-type))) + {:properties (properties e opts)} + (reference e opts))) clojure.lang.Var - (convert [e {:keys [schema-type]}] - (reference e schema-type))) + (convert [e opts] + (reference e opts))) ;; ;; Schema to Swagger Schema definitions @@ -258,45 +267,48 @@ The result is put into collection of same type as input schema. Thus linked/map should keep the order of items. Returnes nil if no properties are found." - [schema schema-type] - {: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)] - :let [v (try->swagger v k key-meta schema-type)]] - (and v [k v])))] - (if (seq props) - 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)] + :let [v (try->swagger v k key-meta opts)]] + (and v [k v])))] + (if (seq props) + props)))) (defn additional-properties "Generates json-schema additional properties from a plain map schema from under key s/Keyword." - [schema schema-type] - {: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 schema-type )) - 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->schema-type opts))) + false))) (defn schema-object "Returns a JSON Schema object of a plain map schema." - [schema schema-type] - (if (common/plain-map? schema) - (let [properties (properties schema schema-type) - title (if (not (s/schema-name schema)) (common/title schema)) - additional-properties (additional-properties schema schema-type) - 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/swagger2.clj b/src/ring/swagger/swagger2.clj index 913ec824..6bb31cc5 100644 --- a/src/ring/swagger/swagger2.clj +++ b/src/ring/swagger/swagger2.clj @@ -30,7 +30,7 @@ (->> schemas rsc/collect-models (rsc/handle-duplicate-schemas (:handle-duplicate-schemas-fn options)) - (map (juxt (comp str key) (comp #(rsjs/schema-object % :swagger) val))) + (map (juxt (comp str key) (comp rsjs/schema-object val))) (into (sorted-map)))) ;; diff --git a/test/ring/swagger/swagger2_unit_test.clj b/test/ring/swagger/swagger2_unit_test.clj index 939d7b82..2ade775b 100644 --- a/test/ring/swagger/swagger2_unit_test.clj +++ b/test/ring/swagger/swagger2_unit_test.clj @@ -539,11 +539,11 @@ :b InvalidElement}] (fact "fail by default" - (rsjs/schema-object schema :swagger) => (throws IllegalArgumentException)) + (rsjs/schema-object schema) => (throws IllegalArgumentException)) (fact "drops bad fields from both properties & required" (binding [rsjs/*ignore-missing-mappings* true] - (rsjs/schema-object schema :swagger) + (rsjs/schema-object schema) => {:type "object" :properties {:a {:type "string"}} From cd75cb98efa8b728505cfb51a779b782d18b369e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:44:51 -0500 Subject: [PATCH 04/22] private --- src/ring/swagger/json_schema.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 81e8d9f0..53f64ca9 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -142,7 +142,7 @@ (convert options) (merge-meta x options)))) -(defn try->swagger [v k key-meta opts] +(defn- try->swagger [v k key-meta opts] (try (->swagger v {:key-meta key-meta ::schema-type (opts->schema-type opts)}) (catch Exception e (throw From 7fd059a4cbc1acf7f55bdf34994caf729ff1f0aa Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:48:44 -0500 Subject: [PATCH 05/22] propagate options --- src/ring/swagger/json_schema.clj | 54 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 53f64ca9..fdffc713 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -146,9 +146,9 @@ (try (->swagger v {:key-meta key-meta ::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))))) + (IllegalArgumentException. + (str "error converting to swagger schema [" k " " + (try (s/explain v) (catch Exception _ v)) "]") e))))) (defn- coll-schema [e options] (-> {:type "array" @@ -172,49 +172,49 @@ 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 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)] (condp contains? in #{:query :formData} (assoc schema :allowEmptyValue true) #{nil :body} (assoc schema :x-nullable 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}] @@ -223,16 +223,16 @@ {})) schema.core.ConditionalSchema - (convert [e _] - {:x-oneOf (vec (keep (comp ->swagger second) (:preds-and-schemas e)))}) + (convert [e options] + {:x-oneOf (vec (keep #(->swagger (second %) options) (:preds-and-schemas e)))}) schema.core.CondPre - (convert [e _] - {:x-oneOf (mapv ->swagger (:schemas e))}) + (convert [e options] + {:x-oneOf (mapv #(->swagger % options) (:schemas e))}) schema.core.Constrained - (convert [e _] - (->swagger (:schema e))) + (convert [e options] + (->swagger (:schema e) options)) java.util.regex.Pattern (convert [e _] From 6d0c185db5c620cc4ad4226c57c12154fd2a7c22 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:51:56 -0500 Subject: [PATCH 06/22] dirty --- src/ring/swagger/json_schema_dirty.clj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ring/swagger/json_schema_dirty.clj b/src/ring/swagger/json_schema_dirty.clj index 8f0ea2cb..8edbc78d 100644 --- a/src/ring/swagger/json_schema_dirty.clj +++ b/src/ring/swagger/json_schema_dirty.clj @@ -6,18 +6,18 @@ (extend-protocol json-schema/JsonSchema schema.experimental.abstract_map.AbstractSchema - (convert [e {:keys [properties? schema-type] - :or {properties? true}} ] + (convert [e {:keys [properties?] + :or {properties? true} :as options}] (if properties? (merge {:discriminator (name (:dispatch-key e))} - (json-schema/->swagger (:schema e) {:properties? properties? :schema-type schema-type})) - (json-schema/reference e schema-type))) + (json-schema/->swagger (:schema e) (assoc options :properties? properties?))) + (json-schema/reference e options))) schema.experimental.abstract_map.SchemaExtension - (convert [e {:keys [schema-type] :as options}] - {:allOf [(json-schema/->swagger (:base-schema e) {:properties? false :schema-type schema-type}) + (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)] (into (empty m) (remove (comp base-keys key) m))) - {:properties? true :schema-type schema-type})]})) + (assoc options :properties? true))]})) From d2efc2df9b135bb58b6d5d08b13064cc4a079e5e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:52:58 -0500 Subject: [PATCH 07/22] revert --- src/ring/swagger/swagger2.clj | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/ring/swagger/swagger2.clj b/src/ring/swagger/swagger2.clj index 6bb31cc5..4245a9c4 100644 --- a/src/ring/swagger/swagger2.clj +++ b/src/ring/swagger/swagger2.clj @@ -44,11 +44,11 @@ (let [schema (rsc/peek-schema model) schema-json (rsjs/->swagger model options)] (vector - {:in "body" - :name (or (common/title schema) "") - :description (or (:description (rsjs/json-schema-meta schema)) "") - :required (not (rsjs/maybe? model)) - :schema schema-json})))) + {:in "body" + :name (or (common/title schema) "") + :description (or (:description (rsjs/json-schema-meta schema)) "") + :required (not (rsjs/maybe? model)) + :schema schema-json})))) (defmethod extract-parameter :default [in model options] (if model @@ -58,11 +58,11 @@ json-schema (rsjs/->swagger v options)] :when json-schema] (merge - {:in (name in) - :name (rsjs/key-name rk) - :description "" - :required (or (= in :path) (s/required-key? k))} - json-schema)))) + {:in (name in) + :name (rsjs/key-name rk) + :description "" + :required (or (= in :path) (s/required-key? k))} + json-schema)))) (defn- default-response-description "uses option :default-response-description-fn to generate @@ -121,12 +121,12 @@ (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) + (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))] @@ -231,13 +231,13 @@ Possible values: multi, ssv, csv, tsv, pipes." ([swagger :- (s/maybe Swagger)] (swagger-json swagger nil)) ([swagger :- (s/maybe Swagger), options :- (s/maybe Options)] - (let [options (merge option-defaults options)] - (binding [rsjs/*ignore-missing-mappings* (true? (:ignore-missing-mappings? options))] - (let [[paths definitions] (-> swagger - ensure-body-and-response-schema-names - (extract-paths-and-definitions options))] - (common/deep-merge - swagger-defaults - (-> swagger - (assoc :paths paths) - (assoc :definitions definitions)))))))) + (let [options (merge option-defaults options)] + (binding [rsjs/*ignore-missing-mappings* (true? (:ignore-missing-mappings? options))] + (let [[paths definitions] (-> swagger + ensure-body-and-response-schema-names + (extract-paths-and-definitions options))] + (common/deep-merge + swagger-defaults + (-> swagger + (assoc :paths paths) + (assoc :definitions definitions)))))))) From f553fece33c4e552c63db5f330dc284d13e736e2 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:53:35 -0500 Subject: [PATCH 08/22] revert --- test/ring/swagger/swagger2_unit_test.clj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ring/swagger/swagger2_unit_test.clj b/test/ring/swagger/swagger2_unit_test.clj index 2ade775b..4a72c3fe 100644 --- a/test/ring/swagger/swagger2_unit_test.clj +++ b/test/ring/swagger/swagger2_unit_test.clj @@ -84,9 +84,9 @@ ;; (fact "transform simple schemas" - (rsjs/schema-object Tag :swagger) => Tag' - (rsjs/schema-object Category :swagger) => Category' - (rsjs/schema-object Pet :swagger) => Pet') + (rsjs/schema-object Tag) => Tag' + (rsjs/schema-object Category) => Category' + (rsjs/schema-object Pet) => Pet') (s/defschema RootModel {:sub {:foo Long}}) From 9f27501ea7f3407aee4116236e318538bbc6238b Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:54:14 -0500 Subject: [PATCH 09/22] revert --- test/ring/swagger/json_schema_test.clj | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/test/ring/swagger/json_schema_test.clj b/test/ring/swagger/json_schema_test.clj index fd486043..7b80945f 100644 --- a/test/ring/swagger/json_schema_test.clj +++ b/test/ring/swagger/json_schema_test.clj @@ -150,19 +150,19 @@ )) (fact "Optional-key default metadata" - (rsjs/properties {(with-meta (s/optional-key :foo) {:default "bar"}) s/Str} :swagger) + (rsjs/properties {(with-meta (s/optional-key :foo) {:default "bar"}) s/Str}) => {:foo {:type "string" :default "bar"}} (fact "nil default is ignored" - (rsjs/properties {(with-meta (s/optional-key :foo) {:default nil}) s/Str} :swagger) + (rsjs/properties {(with-meta (s/optional-key :foo) {:default nil}) s/Str}) => {:foo {:type "string"}}) (fact "pfnk schema" - (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str "foo"}])) :swagger) + (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str "foo"}]))) => {:x {:type "string" :default "foo"}}) (fact "pfnk schema - nil default is ignored" - (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str nil}])) :swagger) + (rsjs/properties (pfnk/input-schema (p/fnk [{x :- s/Str nil}]))) => {:x {:type "string"}})) (fact "Describe" @@ -204,7 +204,7 @@ (rsjs/->swagger schema) => {:$ref "#/definitions/schema"}) (fact "extra metadata is present on schema objects" - (rsjs/schema-object schema :swagger) => (contains + (rsjs/schema-object schema) => (contains {:properties {:name {:type "string"} :title {:type "string"}} :minProperties 1 @@ -228,46 +228,46 @@ (facts "properties" (fact "s/Any -values are not ignored" (keys (rsjs/properties {:a s/Str - :b s/Any} :swagger)) + :b s/Any})) => [:a :b]) (fact "nil-values are ignored" - (keys (rsjs/properties {:a s/Str, :b Currency} :swagger)) + (keys (rsjs/properties {:a s/Str, :b Currency})) => [:a]) (fact "s/Keyword -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Keyword s/Str} :swagger)) + s/Keyword s/Str})) => [:a]) (fact "Class -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Str s/Str} :swagger)) + s/Str s/Str})) => [:a]) (fact "Required keyword-keys are used" (keys (rsjs/properties {:a s/Str - (s/required-key :b) s/Str} :swagger)) + (s/required-key :b) s/Str})) => [:a :b]) (fact "Required non-keyword-keys are NOT ignored" (keys (rsjs/properties {:a s/Str - (s/required-key "b") s/Str} :swagger)) + (s/required-key "b") s/Str})) => [:a "b"]) (fact "s/Any -keys are ignored" (keys (rsjs/properties {:a s/Str - s/Any s/Str} :swagger)) + s/Any s/Str})) => [:a]) (fact "with unknown mappings" (fact "by default, exception is thrown" (rsjs/properties {:a String - :b java.util.Vector} :swagger) => (throws IllegalArgumentException)) + :b java.util.Vector}) => (throws IllegalArgumentException)) (fact "unknown fields are ignored ig *ignore-missing-mappings* is set" (binding [rsjs/*ignore-missing-mappings* true] (keys (rsjs/properties {:a String - :b java.util.Vector} :swagger)) => [:a]))) + :b java.util.Vector})) => [:a]))) (fact "Keeps the order of properties intact" (keys (rsjs/properties (linked/map :a String @@ -277,13 +277,13 @@ :e String :f String :g String - :h String) :swagger)) + :h String))) => [:a :b :c :d :e :f :g :h]) (fact "Ordered-map works with sub-schemas" (rsjs/properties (rsc/with-named-sub-schemas (linked/map :a String :b {:foo String} - :c [{:bar String}] )) :swagger) + :c [{:bar String}]))) => anything) (fact "referenced record-schemas" @@ -291,28 +291,28 @@ (s/defschema Bar {:key Foo}) (fact "can't get properties out of record schemas" - (rsjs/properties Foo :swagger)) => (throws AssertionError) + (rsjs/properties Foo)) => (throws AssertionError) (fact "nested properties work ok" - (keys (rsjs/properties Bar :swagger)) => [:key]))) + (keys (rsjs/properties Bar)) => [:key]))) (facts "additional-properties" (fact "No additional properties" - (rsjs/additional-properties {:a s/Str} :swagger) + (rsjs/additional-properties {:a s/Str}) => false) (fact "s/Keyword" - (rsjs/additional-properties {s/Keyword s/Bool} :swagger) + (rsjs/additional-properties {s/Keyword s/Bool}) => {:type "boolean"}) (fact "s/Any" - (rsjs/additional-properties {s/Any s/Str} :swagger) + (rsjs/additional-properties {s/Any s/Str}) => {:type "string"}) (fact "s/Str" - (rsjs/additional-properties {s/Str s/Bool} :swagger) + (rsjs/additional-properties {s/Str s/Bool}) => {:type "boolean"}) (fact "s/Int" - (rsjs/additional-properties {s/Int s/Str} :swagger) + (rsjs/additional-properties {s/Int s/Str}) => {:type "string"})) From 295a22199dd899968027df58fa3f37dc28b8b874 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Apr 2024 11:55:36 -0500 Subject: [PATCH 10/22] opts --- src/ring/swagger/json_schema.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index fdffc713..9dba61ef 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -143,7 +143,7 @@ (merge-meta x options)))) (defn- try->swagger [v k key-meta opts] - (try (->swagger v {:key-meta key-meta ::schema-type (opts->schema-type opts)}) + (try (->swagger v (assoc opts :key-meta key-meta)) (catch Exception e (throw (IllegalArgumentException. @@ -288,7 +288,7 @@ {: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->schema-type opts))) + (try->swagger v s/Keyword nil opts)) false))) (defn schema-object From 10a19766e1395e6cf0bbb916fa9ef0370148f42c Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:02:32 -0500 Subject: [PATCH 11/22] fmt --- src/ring/openapi/openapi3_schema.clj | 135 +++++++++++---------------- 1 file changed, 55 insertions(+), 80 deletions(-) diff --git a/src/ring/openapi/openapi3_schema.clj b/src/ring/openapi/openapi3_schema.clj index 8db5c153..c0982206 100644 --- a/src/ring/openapi/openapi3_schema.clj +++ b/src/ring/openapi/openapi3_schema.clj @@ -1,40 +1,32 @@ (ns ring.openapi.openapi3-schema (:require [schema.core :as s] - [ring.swagger.swagger2-full-schema :refer [X- length-greater matches opt Contact Info]])) + [ring.swagger.swagger2-full-schema :refer [opt Info]])) (s/defschema Server-Variable - { - (opt :enum) [s/Str] - :default s/Str - (opt :description) s/Str - }) + {(opt :enum) [s/Str] + :default s/Str + (opt :description) s/Str}) (s/defschema Server - { - :url s/Str + {:url s/Str (opt :description) s/Str - (opt :variables) {s/Str Server-Variable} - }) + (opt :variables) {s/Str Server-Variable}}) (s/defschema ExternalDocumentation - { - (opt :description) s/Str - :url s/Str - }) + {(opt :description) s/Str + :url s/Str}) (s/defschema OpenApiSchemaPart {s/Keyword s/Any}) (s/defschema Example - { - (opt :summary) s/Str + {(opt :summary) s/Str (opt :description) s/Str (opt :value) s/Any (opt :externalValue) s/Str}) (s/defschema Header - { - (opt :description) s/Str + {(opt :description) s/Str :required s/Bool (opt :deprecated) s/Bool (opt :allowEmptyValue) s/Bool @@ -45,24 +37,20 @@ (opt :examples) {s/Str Example}}) (s/defschema Encoding - { - (opt :contentType) s/Str + {(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 :schema) OpenApiSchemaPart (opt :example) Example (opt :examples) {s/Str Example} - (opt :encoding) {s/Str Encoding} - }) + (opt :encoding) {s/Str Encoding}}) (s/defschema Parameter - { - :name s/Str + {:name s/Str (opt :in) s/Any (opt :description) s/Str :required s/Bool @@ -77,35 +65,33 @@ (opt :content) {s/Str MediaObject}}) (s/defschema RequestBody - { - (opt :description) s/Str + {(opt :description) s/Str :content {s/Str MediaObject} - (opt :required) s/Bool - }) + (opt :required) s/Bool}) (s/defschema Link - { - (opt :operationRef) s/Str + {(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 - }) + (opt :server) Server}) -(s/defschema ResponseCode (s/enum "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" "510" "502" "503" "504" "505" "506" "507" "508" "510" "511")) +(s/defschema ResponseCode + (s/enum "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" "510" "502" "503" "504" "505" "506" "507" "508" "510" "511")) (s/defschema Response - { - :description s/Str + {:description s/Str (opt :headers) {s/Str Header} (opt :content) {s/Str MediaObject} - (opt :links) {s/Str Link} - }) + (opt :links) {s/Str Link}}) (s/defschema Operation - { - (opt :tags) [s/Str] + {(opt :tags) [s/Str] (opt :summary) s/Str (opt :description) s/Str (opt :externalDocs) ExternalDocumentation @@ -115,55 +101,47 @@ (opt :responses) {ResponseCode Response} (opt :deprecated) s/Bool (opt :security) {s/Str [s/Str]} - (opt :servers) [Server] - }) + (opt :servers) [Server]}) (s/defschema Path - { - (opt :summary) s/Str + {(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 :get) Operation + (opt :put) Operation + (opt :post) Operation + (opt :delete) Operation + (opt :head) Operation + (opt :patch) Operation (opt :servers) [Server] - (opt :parameters) s/Any - }) + (opt :parameters) s/Any}) (s/defschema Callback {s/Str Path}) (s/defschema Tag - { - :name s/Str + {:name s/Str (opt :description) s/Str - (opt :externalDocs) ExternalDocumentation - }) + (opt :externalDocs) ExternalDocumentation}) (s/defschema SecuritySchemeApiKey - { - :type s/Any + {:type s/Any (opt :description) s/Str :name s/Str - :in s/Any - }) + :in s/Any}) (s/defschema SecuritySchemeHttp - { - :type s/Any + {:type s/Any (opt :description) s/Str :scheme s/Str - :bearerFormat s/Str - }) + :bearerFormat s/Str}) (s/defschema SecurityScheme - (s/conditional (every-pred map? #(= (:type %) "apiKey")) SecuritySchemeApiKey :else SecuritySchemeHttp)) + (s/conditional + #(and (map? %) (= "apiKey" (:type %))) SecuritySchemeApiKey + :else SecuritySchemeHttp)) (s/defschema Components - { - (opt :schemas) {s/Str OpenApiSchemaPart} + {(opt :schemas) {s/Str OpenApiSchemaPart} (opt :responses) {s/Str Response} (opt :parameters) {s/Str Parameter} (opt :examples) {s/Str Example} @@ -171,17 +149,14 @@ (opt :headers) {s/Str Header} (opt :securitySchemes) {s/Str SecurityScheme} (opt :links) {s/Str Link} - (opt :callbacks) {s/Str Callback} - }) + (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 - }) + {(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}) From 313badd1f2b4aec7ccab23d24e1bff26c6f94506 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:07:11 -0500 Subject: [PATCH 12/22] 510 => 501 --- src/ring/openapi/openapi3_schema.clj | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ring/openapi/openapi3_schema.clj b/src/ring/openapi/openapi3_schema.clj index c0982206..3c2ef1dc 100644 --- a/src/ring/openapi/openapi3_schema.clj +++ b/src/ring/openapi/openapi3_schema.clj @@ -77,12 +77,17 @@ (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 - (s/enum "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" "510" "502" "503" "504" "505" "506" "507" "508" "510" "511")) + (apply s/enum (map str codes))) (s/defschema Response {:description s/Str From 69cf6bab6252c03010d72933a8feba4faeab1bef Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:16:14 -0500 Subject: [PATCH 13/22] move under resources that we control --- resources/ring/{openapi => swagger}/openapi-schema.json | 2 +- src/ring/openapi/validator.clj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename resources/ring/{openapi => swagger}/openapi-schema.json (99%) diff --git a/resources/ring/openapi/openapi-schema.json b/resources/ring/swagger/openapi-schema.json similarity index 99% rename from resources/ring/openapi/openapi-schema.json rename to resources/ring/swagger/openapi-schema.json index 7f1933ec..b43f4405 100644 --- a/resources/ring/openapi/openapi-schema.json +++ b/resources/ring/swagger/openapi-schema.json @@ -1655,4 +1655,4 @@ "additionalProperties": false } } -} \ No newline at end of file +} diff --git a/src/ring/openapi/validator.clj b/src/ring/openapi/validator.clj index 18566ce1..0aeffc4b 100644 --- a/src/ring/openapi/validator.clj +++ b/src/ring/openapi/validator.clj @@ -6,4 +6,4 @@ ; http://json-schema.org/draft-04/schema (def validate - (v/validator (slurp (io/resource "ring/openapi/openapi-schema.json")))) + (v/validator (slurp (io/resource "ring/swagger/openapi-schema.json")))) From 9ca0e79e724dc4bcfb700d93133fed37d431d6ce Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:29:48 -0500 Subject: [PATCH 14/22] ring.openapi.validator => ring.swagger.openapi3-validator --- .../{openapi/validator.clj => swagger/openapi3_validator.clj} | 2 +- test/ring/openapi/openapi3_test.clj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/ring/{openapi/validator.clj => swagger/openapi3_validator.clj} (89%) diff --git a/src/ring/openapi/validator.clj b/src/ring/swagger/openapi3_validator.clj similarity index 89% rename from src/ring/openapi/validator.clj rename to src/ring/swagger/openapi3_validator.clj index 0aeffc4b..0e64310c 100644 --- a/src/ring/openapi/validator.clj +++ b/src/ring/swagger/openapi3_validator.clj @@ -1,4 +1,4 @@ -(ns ring.openapi.validator +(ns ring.swagger.openapi3-validator (:require [clojure.java.io :as io] [scjsv.core :as v])) diff --git a/test/ring/openapi/openapi3_test.clj b/test/ring/openapi/openapi3_test.clj index 50659055..3f142120 100644 --- a/test/ring/openapi/openapi3_test.clj +++ b/test/ring/openapi/openapi3_test.clj @@ -5,7 +5,7 @@ [ring.openapi.openapi3-schema :as full-schema] [ring.swagger.json-schema :as rsjs] [ring.swagger.extension :as extension] - [ring.openapi.validator :as validator] + [ring.swagger.openapi3-validator :as validator] [linked.core :as linked] [ring.util.http-status :as status] [midje.sweet :refer :all]) @@ -100,4 +100,4 @@ :lt java.time.LocalTime} swagger {:paths {"/time" {:post {:parameters {:query model}}}}}] - (validate swagger) => nil))) \ No newline at end of file + (validate swagger) => nil))) From fcbf57cef9390abcd8e6eff96c57ce7104898e98 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:33:46 -0500 Subject: [PATCH 15/22] ring.openapi.openapi3 => ring.swagger.openapi3 --- src/ring/{openapi => swagger}/openapi3.clj | 2 +- test/ring/openapi/openapi3_test.clj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/ring/{openapi => swagger}/openapi3.clj (99%) diff --git a/src/ring/openapi/openapi3.clj b/src/ring/swagger/openapi3.clj similarity index 99% rename from src/ring/openapi/openapi3.clj rename to src/ring/swagger/openapi3.clj index 77906226..0bb6647d 100644 --- a/src/ring/openapi/openapi3.clj +++ b/src/ring/swagger/openapi3.clj @@ -1,4 +1,4 @@ -(ns ring.openapi.openapi3 +(ns ring.swagger.openapi3 (:require [clojure.string :as str] [schema.core :as s] [schema-tools.core :as stc] diff --git a/test/ring/openapi/openapi3_test.clj b/test/ring/openapi/openapi3_test.clj index 3f142120..340253c8 100644 --- a/test/ring/openapi/openapi3_test.clj +++ b/test/ring/openapi/openapi3_test.clj @@ -1,6 +1,6 @@ (ns ring.openapi.openapi3-test (:require [clojure.test :refer :all] - [ring.openapi.openapi3 :refer :all] + [ring.swagger.openapi3 :refer :all] [schema.core :as s] [ring.openapi.openapi3-schema :as full-schema] [ring.swagger.json-schema :as rsjs] From 14d75981f697d687a9cf09c6d5af6798a0311dc2 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:34:43 -0500 Subject: [PATCH 16/22] ring.openapi.openapi3-test => ring.swagger.openapi3-test --- test/ring/{openapi => swagger}/openapi3_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/ring/{openapi => swagger}/openapi3_test.clj (99%) diff --git a/test/ring/openapi/openapi3_test.clj b/test/ring/swagger/openapi3_test.clj similarity index 99% rename from test/ring/openapi/openapi3_test.clj rename to test/ring/swagger/openapi3_test.clj index 340253c8..78676eac 100644 --- a/test/ring/openapi/openapi3_test.clj +++ b/test/ring/swagger/openapi3_test.clj @@ -1,4 +1,4 @@ -(ns ring.openapi.openapi3-test +(ns ring.swagger.openapi3-test (:require [clojure.test :refer :all] [ring.swagger.openapi3 :refer :all] [schema.core :as s] From 7f8d5de4747a97f79210c4148682fee109cd12e6 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:35:57 -0500 Subject: [PATCH 17/22] ring.openapi.openapi3-schema ring.swagger.openapi3-schema --- src/ring/swagger/openapi3.clj | 2 +- src/ring/{openapi => swagger}/openapi3_schema.clj | 2 +- test/ring/swagger/openapi3_test.clj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/ring/{openapi => swagger}/openapi3_schema.clj (99%) diff --git a/src/ring/swagger/openapi3.clj b/src/ring/swagger/openapi3.clj index 0bb6647d..b0151b2f 100644 --- a/src/ring/swagger/openapi3.clj +++ b/src/ring/swagger/openapi3.clj @@ -6,7 +6,7 @@ [ring.swagger.common :as common] [ring.swagger.json-schema :as rsjs] [ring.swagger.core :as rsc] - [ring.openapi.openapi3-schema :as openapi3-schema])) + [ring.swagger.openapi3-schema :as openapi3-schema])) ;; ;; Schema transformations diff --git a/src/ring/openapi/openapi3_schema.clj b/src/ring/swagger/openapi3_schema.clj similarity index 99% rename from src/ring/openapi/openapi3_schema.clj rename to src/ring/swagger/openapi3_schema.clj index 3c2ef1dc..0c997640 100644 --- a/src/ring/openapi/openapi3_schema.clj +++ b/src/ring/swagger/openapi3_schema.clj @@ -1,4 +1,4 @@ -(ns ring.openapi.openapi3-schema +(ns ring.swagger.openapi3-schema (:require [schema.core :as s] [ring.swagger.swagger2-full-schema :refer [opt Info]])) diff --git a/test/ring/swagger/openapi3_test.clj b/test/ring/swagger/openapi3_test.clj index 78676eac..d85ceee1 100644 --- a/test/ring/swagger/openapi3_test.clj +++ b/test/ring/swagger/openapi3_test.clj @@ -2,7 +2,7 @@ (:require [clojure.test :refer :all] [ring.swagger.openapi3 :refer :all] [schema.core :as s] - [ring.openapi.openapi3-schema :as full-schema] + [ring.swagger.openapi3-schema :as full-schema] [ring.swagger.json-schema :as rsjs] [ring.swagger.extension :as extension] [ring.swagger.openapi3-validator :as validator] From 57985df9bdfc8c893793817bf5d125e8d8c2ffcb Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 1 May 2024 14:42:42 -0500 Subject: [PATCH 18/22] fix docstring --- src/ring/swagger/openapi3.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ring/swagger/openapi3.clj b/src/ring/swagger/openapi3.clj index b0151b2f..e1a3f268 100644 --- a/src/ring/swagger/openapi3.clj +++ b/src/ring/swagger/openapi3.clj @@ -283,7 +283,7 @@ :handle-duplicate-schemas-fn rsc/ignore-duplicate-schemas})) (s/defn openapi-json - "Produces openapi-json output from ring-openapi spec. + "Produces openapi-json output from ring-swagger openapi3 spec. Optional second argument is a options map, supporting the following options with defaults: @@ -297,7 +297,7 @@ response descriptions from http status code. Takes a status code (Int) and returns a String. - :handle-duplicate-schemas-fn - (ring.openapi.core/ignore-duplicate-schemas), + :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 From ca63c68bbd2ab524e74b4eea5f83bcdf1a4a2ac9 Mon Sep 17 00:00:00 2001 From: Rajkumar Natarajan Date: Sat, 4 May 2024 21:52:28 -0700 Subject: [PATCH 19/22] openapi-support update urls for review comments --- src/ring/swagger/openapi3_schema.clj | 18 ++++-------------- src/ring/swagger/openapi3_validator.clj | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ring/swagger/openapi3_schema.clj b/src/ring/swagger/openapi3_schema.clj index 0c997640..b3068593 100644 --- a/src/ring/swagger/openapi3_schema.clj +++ b/src/ring/swagger/openapi3_schema.clj @@ -50,19 +50,9 @@ (opt :encoding) {s/Str Encoding}}) (s/defschema Parameter - {:name s/Str - (opt :in) s/Any - (opt :description) s/Str - :required s/Bool - (opt :deprecated) s/Bool - (opt :allowEmptyValue) s/Bool - (opt :style) s/Any - (opt :explode) s/Bool - (opt :allowReserved) s/Bool - (opt :schema) OpenApiSchemaPart - (opt :example) Example - (opt :examples) {s/Str Example} - (opt :content) {s/Str MediaObject}}) + {(opt :query) s/Any + (opt :path) s/Any + (opt :header) s/Any}) (s/defschema RequestBody {(opt :description) s/Str @@ -101,7 +91,7 @@ (opt :description) s/Str (opt :externalDocs) ExternalDocumentation (opt :operationId) s/Str - (opt :parameters) s/Any #_[Parameter] + (opt :parameters) [Parameter] (opt :requestBody) RequestBody (opt :responses) {ResponseCode Response} (opt :deprecated) s/Bool diff --git a/src/ring/swagger/openapi3_validator.clj b/src/ring/swagger/openapi3_validator.clj index 0e64310c..af926946 100644 --- a/src/ring/swagger/openapi3_validator.clj +++ b/src/ring/swagger/openapi3_validator.clj @@ -2,7 +2,7 @@ (:require [clojure.java.io :as io] [scjsv.core :as v])) -; https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v2.0/schema.json +; https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/schemas/v3.0/schema.json ; http://json-schema.org/draft-04/schema (def validate From f8b8e59daab0b6ddee90579bc3cb89894f2c9504 Mon Sep 17 00:00:00 2001 From: Rajkumar Natarajan Date: Mon, 20 May 2024 07:55:45 -0700 Subject: [PATCH 20/22] fix schema reference issue which happened during refactoring --- src/ring/swagger/json_schema.clj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 9dba61ef..104b2efe 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -14,7 +14,7 @@ (defn- opts->schema-type [opts] {:post [(keyword? %)]} - (get opts ::schema-type :swagger)) + (get opts :schema-type :swagger)) ; TODO: remove this in favor of passing it as options (def ^:dynamic *ignore-missing-mappings* false) @@ -28,7 +28,7 @@ ;; (defrecord FieldSchema [schema] - schema.core.Schema + s/Schema (spec [_] (variant/variant-spec spec/+no-precondition+ @@ -85,7 +85,7 @@ ([e] (reference e nil)) ([e opts] (if-let [schema-name (s/schema-name e)] - {:$ref (str (case (opts->schema-type (opts->schema-type opts)) + {:$ref (str (case (opts->schema-type opts) :swagger "#/definitions/" :openapi "#/components/schemas/") schema-name)} @@ -143,7 +143,9 @@ (merge-meta x options)))) (defn- try->swagger [v k key-meta opts] - (try (->swagger v (assoc opts :key-meta key-meta)) + (try (->swagger v (-> opts + (assoc :key-meta key-meta) + (assoc :schema-type (opts->schema-type opts)))) (catch Exception e (throw (IllegalArgumentException. From 0001b2d31768700a0b7670828e4ca70fb1651b9e Mon Sep 17 00:00:00 2001 From: Rajkumar Natarajan Date: Fri, 24 May 2024 06:01:14 -0700 Subject: [PATCH 21/22] fix the parameter schema --- src/ring/swagger/json_schema.clj | 16 ++++++++++++---- src/ring/swagger/openapi3_schema.clj | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 104b2efe..eb91aa62 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -187,10 +187,12 @@ schema.core.Maybe (convert [e {:keys [in] :as options}] - (let [schema (->swagger (:schema e) 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 @@ -226,11 +228,17 @@ schema.core.ConditionalSchema (convert [e options] - {:x-oneOf (vec (keep #(->swagger (second %) options) (:preds-and-schemas e)))}) + (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 options] - {:x-oneOf (mapv #(->swagger % options) (:schemas e))}) + (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 options] diff --git a/src/ring/swagger/openapi3_schema.clj b/src/ring/swagger/openapi3_schema.clj index b3068593..1610ba38 100644 --- a/src/ring/swagger/openapi3_schema.clj +++ b/src/ring/swagger/openapi3_schema.clj @@ -50,13 +50,13 @@ (opt :encoding) {s/Str Encoding}}) (s/defschema Parameter - {(opt :query) s/Any - (opt :path) s/Any - (opt :header) s/Any}) + {(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} + :content {s/Str MediaObject} (opt :required) s/Bool}) (s/defschema Link @@ -91,7 +91,7 @@ (opt :description) s/Str (opt :externalDocs) ExternalDocumentation (opt :operationId) s/Str - (opt :parameters) [Parameter] + (opt :parameters) Parameter (opt :requestBody) RequestBody (opt :responses) {ResponseCode Response} (opt :deprecated) s/Bool From a3def99e1cabace742675c65a9e60bca5f3a1ef1 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 9 Jul 2024 15:34:56 -0500 Subject: [PATCH 22/22] fix --- src/ring/swagger/json_schema.clj | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/ring/swagger/json_schema.clj b/src/ring/swagger/json_schema.clj index 24200919..eb69945a 100644 --- a/src/ring/swagger/json_schema.clj +++ b/src/ring/swagger/json_schema.clj @@ -277,7 +277,6 @@ 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." -<<<<<<< HEAD ([schema] (properties schema nil)) ([schema opts] {:pre [(common/plain-map? schema)]} @@ -285,23 +284,10 @@ (for [[k v] schema :when (s/specific-key? k) :let [key-meta (meta k) - k (s/explicit-schema-key k)] - :let [v (try->swagger v k key-meta opts)]] + k (s/explicit-schema-key k) + v (try->swagger v k key-meta opts)]] (and v [k v])))] - (if (seq props) - props)))) -======= - [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))) ->>>>>>> master + (not-empty props)))) (defn additional-properties "Generates json-schema additional properties from a plain map