diff --git a/Demos/Data/samples/1password.json b/Demos/Data/samples/1password.json new file mode 100644 index 0000000..7a076cf --- /dev/null +++ b/Demos/Data/samples/1password.json @@ -0,0 +1,518 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Events API", + "description": "1Password Events API Specification.", + "version": "1.0.0", + "x-apisguru-categories": [ + "security" + ], + "x-logo": { + "url": "https://api.apis.guru/v2/cache/logo/https_upload.wikimedia.org_wikipedia_commons_thumb_e_e3_1password-logo.svg_1280px-1password-logo.svg.png" + }, + "x-origin": [ + { + "format": "openapi", + "url": "https://i.1password.com/media/1password-events-reporting/1password-events-api.yaml", + "version": "3.0" + } + ], + "x-providerName": "1password.com", + "x-serviceName": "events" + }, + "servers": [ + { + "description": "1Password", + "url": "https://events.1password.com" + }, + { + "description": "1Password CA", + "url": "https://events.1password.ca" + }, + { + "description": "1Password EU", + "url": "https://events.1password.eu" + }, + { + "description": "1Password Enterprise", + "url": "https://events.ent.1password.com" + } + ], + "paths": { + "/api/auth/introspect": { + "get": { + "summary": "Performs introspection of the provided Bearer JWT token", + "operationId": "getAuthIntrospect", + "responses": { + "200": { + "$ref": "#/components/responses/IntrospectResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedErrorResponse" + }, + "default": { + "$ref": "#/components/responses/GenericErrorResponse" + } + }, + "security": [ + { + "jwtsa": [] + } + ], + "tags": [ + "auth" + ] + } + }, + "/api/v1/itemusages": { + "post": { + "description": "This endpoint requires your JSON Web Token to have the *itemusages* feature.", + "operationId": "getItemUsages", + "requestBody": { + "$ref": "#/components/requestBodies/ItemUsagesRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/ItemUsagesResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedErrorResponse" + }, + "default": { + "$ref": "#/components/responses/GenericErrorResponse" + } + }, + "security": [ + { + "jwtsa": [] + } + ], + "summary": "Retrieves item usages", + "tags": [ + "api-v1" + ] + } + }, + "/api/v1/signinattempts": { + "post": { + "description": "This endpoint requires your JSON Web Token to have the *signinattempts* feature.", + "operationId": "getSignInAttempts", + "requestBody": { + "$ref": "#/components/requestBodies/SignInAttemptsRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/SignInAttemptsResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedErrorResponse" + }, + "default": { + "$ref": "#/components/responses/GenericErrorResponse" + } + }, + "security": [ + { + "jwtsa": [] + } + ], + "summary": "Retrieves sign-in attempts", + "tags": [ + "api-v1" + ] + } + } + }, + "components": { + "examples": { + "Cursor": { + "summary": "Used for continued calling with a cursor", + "value": { + "cursor": "aGVsbG8hIGlzIGl0IG1lIHlvdSBhcmUgbG9va2luZyBmb3IK" + } + }, + "ResetCursor": { + "summary": "Used for reseting the cursor", + "value": { + "limit": 100, + "start_time": "2021-06-11T16:32:50-03:00" + } + } + }, + "requestBodies": { + "CursorRequest": { + "content": { + "application/json": { + "examples": { + "Continuing cursor": { + "$ref": "#/components/examples/Cursor" + }, + "Resetting cursor": { + "$ref": "#/components/examples/ResetCursor" + } + }, + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Cursor" + }, + { + "$ref": "#/components/schemas/ResetCursor" + } + ] + } + } + } + }, + "ItemUsagesRequest": { + "$ref": "#/components/requestBodies/CursorRequest" + }, + "SignInAttemptsRequest": { + "$ref": "#/components/requestBodies/CursorRequest" + } + }, + "responses": { + "GenericErrorResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Generic error" + }, + "IntrospectResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Introspection" + } + } + }, + "description": "Introspection object" + }, + "ItemUsagesResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemUsageItems" + } + } + }, + "description": "Item usages response object" + }, + "SignInAttemptsResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignInAttemptItems" + } + } + }, + "description": "Sign-in attempts response object" + }, + "UnauthorizedErrorResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unauthorized" + } + }, + "schemas": { + "Client": { + "description": "Metadata gathered about the client", + "properties": { + "app_name": { + "example": "1Password Extension", + "type": "string" + }, + "app_version": { + "example": "20127", + "type": "string" + }, + "ip_address": { + "example": "13.227.95.22", + "type": "string" + }, + "os_name": { + "example": "MacOSX", + "type": "string" + }, + "os_version": { + "example": "10.15.6", + "type": "string" + }, + "platform_name": { + "example": "Chrome", + "type": "string" + }, + "platform_version": { + "description": "Depending on the platform used, this can be the version of the browser that the client extension is installed, the model of computer that the native application is installed or the machine's CPU version that the CLI was installed", + "type": "string" + } + } + }, + "Cursor": { + "description": "Cursor", + "properties": { + "cursor": { + "description": "Cursor to fetch more data if available or continue the polling process if required", + "example": "aGVsbG8hIGlzIGl0IG1lIHlvdSBhcmUgbG9va2luZyBmb3IK", + "type": "string" + } + } + }, + "CursorCollection": { + "allOf": [ + { + "$ref": "#/components/schemas/Cursor" + }, + { + "properties": { + "has_more": { + "description": "Whether there may still be more data to fetch using the returned cursor. If true, the subsequent request could still be empty.", + "type": "boolean" + } + } + } + ], + "description": "Common cursor properties for collection responses" + }, + "DateTimeRFC3339": { + "example": "2020-06-11T16:32:50-03:00", + "format": "date-time", + "type": "string" + }, + "Details": { + "description": "Additional information about the sign-in attempt", + "properties": { + "value": { + "description": "For firewall prevented sign-ins, the value is the chosen continent, country, etc. that blocked the sign-in attempt", + "example": "Europe", + "type": "string" + } + } + }, + "Error": { + "properties": { + "Error": { + "properties": { + "Message": { + "description": "The error message.", + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "Introspection": { + "properties": { + "Features": { + "example": [ + "itemusages", + "signinattempts" + ], + "items": { + "type": "string" + }, + "type": "array" + }, + "IssuedAt": { + "$ref": "#/components/schemas/DateTimeRFC3339" + }, + "UUID": { + "type": "string" + } + }, + "type": "object" + }, + "ItemUsage": { + "description": "A single item usage object", + "properties": { + "client": { + "$ref": "#/components/schemas/Client" + }, + "item_uuid": { + "$ref": "#/components/schemas/UUID" + }, + "timestamp": { + "$ref": "#/components/schemas/DateTimeRFC3339" + }, + "used_version": { + "type": "integer" + }, + "user": { + "$ref": "#/components/schemas/User" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + }, + "vault_uuid": { + "$ref": "#/components/schemas/UUID" + } + } + }, + "ItemUsageItems": { + "allOf": [ + { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/ItemUsage" + }, + "type": "array" + } + } + }, + { + "$ref": "#/components/schemas/CursorCollection" + } + ], + "description": "An object wrapping cursor properties and a list of items usages" + }, + "ResetCursor": { + "description": "Reset cursor", + "properties": { + "end_time": { + "$ref": "#/components/schemas/DateTimeRFC3339" + }, + "limit": { + "maximum": 1000, + "minimum": 1, + "type": "number" + }, + "start_time": { + "$ref": "#/components/schemas/DateTimeRFC3339" + } + } + }, + "SignInAttempt": { + "description": "A single sign-in attempt object", + "properties": { + "category": { + "enum": [ + "success", + "credentials_failed", + "mfa_failed", + "modern_version_failed", + "firewall_failed", + "firewall_reported_success" + ], + "example": "firewall_failed", + "type": "string" + }, + "client": { + "$ref": "#/components/schemas/Client" + }, + "country": { + "description": "Country ISO Code", + "example": "France", + "type": "string" + }, + "details": { + "$ref": "#/components/schemas/Details" + }, + "session_uuid": { + "$ref": "#/components/schemas/UUID" + }, + "target_user": { + "$ref": "#/components/schemas/User" + }, + "timestamp": { + "$ref": "#/components/schemas/DateTimeRFC3339" + }, + "type": { + "enum": [ + "credentials_ok", + "mfa_ok", + "password_secret_bad", + "mfa_missing", + "totp_disabled", + "totp_bad", + "totp_timeout", + "u2f_disabled", + "u2f_bad", + "u2f_timout", + "duo_disabled", + "duo_bad", + "duo_timeout", + "duo_native_bad", + "platform_secret_disabled", + "platform_secret_bad", + "platform_secret_proxy", + "code_disabled", + "code_bad", + "code_timeout", + "ip_blocked", + "continent_blocked", + "country_blocked", + "anonymous_blocked", + "all_blocked", + "modern_version_missing", + "modern_version_old" + ], + "example": "continent_blocked", + "type": "string" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + } + }, + "SignInAttemptItems": { + "allOf": [ + { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/SignInAttempt" + }, + "type": "array" + } + } + }, + { + "$ref": "#/components/schemas/CursorCollection" + } + ], + "description": "An object wrapping cursor properties and a list of sign-in attempts" + }, + "UUID": { + "example": "56YE2TYN2VFYRLNSHKPW5NVT5E", + "type": "string" + }, + "User": { + "description": "User object", + "properties": { + "email": { + "format": "email", + "type": "string" + }, + "name": { + "description": "Full name", + "example": "Jack O'Neill", + "type": "string" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + } + } + }, + "securitySchemes": { + "jwtsa": { + "bearerFormat": "JWT-SA", + "description": "A JWT SA token issued to this service", + "scheme": "bearer", + "type": "http" + } + } + } +} \ No newline at end of file diff --git a/Demos/Data/samples/ablyio.json b/Demos/Data/samples/ablyio.json new file mode 100644 index 0000000..bda2770 --- /dev/null +++ b/Demos/Data/samples/ablyio.json @@ -0,0 +1,2038 @@ +{ + "openapi": "3.0.1", + "servers": [ + { + "url": "https://rest.ably.io" + } + ], + "info": { + "contact": { + "email": "support@ably.io", + "name": "Ably Support", + "url": "https://www.ably.io/contact", + "x-twitter": "ablyrealtime" + }, + "description": "The [REST API specification](https://www.ably.io/documentation/rest-api) for Ably.", + "title": "Platform API", + "version": "1.1.0", + "x-apisguru-categories": [ + "cloud" + ], + "x-logo": { + "url": "https://api.apis.guru/v2/cache/logo/https_twitter.com_ablyrealtime_profile_image" + }, + "x-origin": [ + { + "format": "openapi", + "url": "https://raw.githubusercontent.com/ably/open-specs/main/definitions/platform-v1.yaml", + "version": "3.0" + } + ], + "x-providerName": "ably.io", + "x-serviceName": "platform" + }, + "security": [ + { + "basicAuth": [] + }, + { + "bearerAuth": [] + } + ], + "paths": { + "/channels": { + "get": { + "description": "Enumerate all active channels of the application", + "operationId": "getMetadataOfAllChannels", + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "default": 100, + "type": "integer" + } + }, + { + "description": "Optionally limits the query to only those channels whose name starts with the given prefix", + "in": "query", + "name": "prefix", + "schema": { + "type": "string" + } + }, + { + "description": "optionally specifies whether to return just channel names (by=id) or ChannelDetails (by=value)", + "in": "query", + "name": "by", + "schema": { + "enum": [ + "value", + "id" + ], + "type": "string" + } + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "items": { + "$ref": "#/components/schemas/ChannelDetails" + }, + "type": "array" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "application/x-msgpack": { + "schema": { + "oneOf": [ + { + "items": { + "$ref": "#/components/schemas/ChannelDetails" + }, + "type": "array" + }, + { + "items": { + "type": "string" + }, + "type": "array" + } + ] + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "OK", + "headers": { + "link": { + "$ref": "#/components/headers/Link" + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Enumerate all active channels of the application", + "tags": [ + "Status" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/channels/{channel_id}": { + "get": { + "description": "Get metadata of a channel", + "operationId": "getMetadataOfChannel", + "parameters": [ + { + "$ref": "#/components/parameters/channelId" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChannelDetails" + } + } + }, + "description": "OK", + "headers": { + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Get metadata of a channel", + "tags": [ + "Status" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/channels/{channel_id}/messages": { + "get": { + "description": "Get message history for a channel", + "operationId": "getMessagesByChannel", + "parameters": [ + { + "$ref": "#/components/parameters/channelId" + }, + { + "$ref": "#/components/parameters/filterStart" + }, + { + "$ref": "#/components/parameters/filterLimit" + }, + { + "$ref": "#/components/parameters/filterEnd" + }, + { + "$ref": "#/components/parameters/filterDirection" + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + } + }, + "application/x-msgpack": { + "schema": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "type": "array" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "OK", + "headers": { + "link": { + "$ref": "#/components/headers/Link" + }, + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + }, + "default": { + "description": "Error", + "headers": { + "x-ably-errorcode": { + "$ref": "#/components/headers/ErrorCode" + }, + "x-ably-errormessage": { + "$ref": "#/components/headers/ErrorMessage" + }, + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + } + }, + "summary": "Get message history for a channel", + "tags": [ + "History" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "post": { + "description": "Publish a message to the specified channel", + "operationId": "publishMessagesToChannel", + "parameters": [ + { + "$ref": "#/components/parameters/channelId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/Message" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "properties": { + "channel": { + "type": "string" + }, + "messageId": { + "type": "string" + } + }, + "type": "object" + } + }, + "application/x-msgpack": { + "schema": { + "properties": { + "channel": { + "type": "string" + }, + "messageId": { + "type": "string" + } + }, + "type": "object" + } + }, + "text/html": { + "schema": { + "properties": { + "channel": { + "type": "string" + }, + "messageId": { + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "OK", + "headers": { + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Publish a message to a channel", + "tags": [ + "Publishing" + ] + } + }, + "/channels/{channel_id}/presence": { + "get": { + "description": "Get presence on a channel", + "operationId": "getPresenceOfChannel", + "parameters": [ + { + "$ref": "#/components/parameters/channelId" + }, + { + "in": "query", + "name": "clientId", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "connectionId", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "default": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PresenceMessage" + }, + "type": "array" + } + }, + "application/x-msgpack": { + "schema": { + "items": { + "$ref": "#/components/schemas/PresenceMessage" + }, + "type": "array" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "OK", + "headers": { + "link": { + "$ref": "#/components/headers/Link" + }, + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Get presence of a channel", + "tags": [ + "Status" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/channels/{channel_id}/presence/history": { + "get": { + "description": "Get presence on a channel", + "operationId": "getPresenceHistoryOfChannel", + "parameters": [ + { + "$ref": "#/components/parameters/channelId" + }, + { + "$ref": "#/components/parameters/filterStart" + }, + { + "$ref": "#/components/parameters/filterLimit" + }, + { + "$ref": "#/components/parameters/filterEnd" + }, + { + "$ref": "#/components/parameters/filterDirection" + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PresenceMessage" + }, + "type": "array" + } + }, + "application/x-msgpack": { + "schema": { + "items": { + "$ref": "#/components/schemas/PresenceMessage" + }, + "type": "array" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "OK", + "headers": { + "link": { + "$ref": "#/components/headers/Link" + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Get presence history of a channel", + "tags": [ + "History" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/keys/{keyName}/requestToken": { + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "post": { + "description": "This is the means by which clients obtain access tokens to use the service. You can see how to construct an Ably TokenRequest in the [Ably TokenRequest spec](https://www.ably.io/documentation/rest-api/token-request-spec) documentation, although we recommend you use an Ably SDK rather to create a TokenRequest, as the construction of a TokenRequest is complex. The resulting token response object contains the token properties as defined in Ably TokenRequest spec. Authentication is not required if using a Signed TokenRequest.", + "operationId": "requestAccessToken", + "parameters": [ + { + "$ref": "#/components/parameters/key_name" + } + ], + "requestBody": { + "content": { + "application/json": { + "example": { + "capability": { + "channel1": [ + "publish", + "subscribe" + ], + "wildcard:channels:*": [ + "publish" + ] + }, + "keyName": "YourKey.Name", + "timestamp": "1559124196551" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/TokenRequest" + }, + { + "$ref": "#/components/schemas/SignedTokenRequest" + } + ] + } + } + } + }, + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/TokenDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Request an access token", + "tags": [ + "Authentication" + ] + } + }, + "/push/channelSubscriptions": { + "delete": { + "description": "Delete a device details object.", + "operationId": "deletePushDeviceDetails", + "parameters": [ + { + "description": "Filter to restrict to subscriptions associated with that channel.", + "in": "query", + "name": "channel", + "schema": { + "type": "string" + } + }, + { + "description": "Must be set when clientId is empty, cannot be used with clientId.", + "in": "query", + "name": "deviceId", + "schema": { + "type": "string" + } + }, + { + "description": "Must be set when deviceId is empty, cannot be used with deviceId.", + "in": "query", + "name": "clientId", + "schema": { + "type": "string" + } + } + ], + "responses": { + "2XX": { + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Delete a registered device's update token", + "tags": [ + "Push" + ] + }, + "get": { + "description": "Get a list of push notification subscriptions to channels.", + "operationId": "getPushSubscriptionsOnChannels", + "parameters": [ + { + "description": "Filter to restrict to subscriptions associated with that channel.", + "in": "query", + "name": "channel", + "schema": { + "type": "string" + } + }, + { + "description": "Optional filter to restrict to devices associated with that deviceId. Cannot be used with clientId.", + "in": "query", + "name": "deviceId", + "schema": { + "type": "string" + } + }, + { + "description": "Optional filter to restrict to devices associated with that clientId. Cannot be used with deviceId.", + "in": "query", + "name": "clientId", + "schema": { + "type": "string" + } + }, + { + "description": "The maximum number of records to return.", + "in": "query", + "name": "limit", + "schema": { + "default": 100, + "maximum": 1000, + "type": "integer" + } + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "List channel subscriptions", + "tags": [ + "Push" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "post": { + "description": "Subscribe either a single device or all devices associated with a client ID to receive push notifications from messages sent to a channel.", + "operationId": "subscribePushDeviceToChannel", + "requestBody": { + "content": { + "application/json": { + "example": { + "channel": "my:channel", + "clientId": "myClientId" + }, + "schema": { + "oneOf": [ + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "deviceId": { + "description": "Must be set when clientId is empty, cannot be used with clientId.", + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "clientId": { + "description": "Must be set when deviceId is empty, cannot be used with deviceId.", + "type": "string" + } + }, + "type": "object" + } + ] + } + }, + "application/x-msgpack": { + "example": { + "channel": "my:channel", + "clientId": "myClientId" + }, + "schema": { + "oneOf": [ + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "deviceId": { + "description": "Must be set when clientId is empty, cannot be used with clientId.", + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "clientId": { + "description": "Must be set when deviceId is empty, cannot be used with deviceId.", + "type": "string" + } + }, + "type": "object" + } + ] + } + }, + "application/x-www-form-urlencoded": { + "example": { + "channel": "my:channel", + "clientId": "myClientId" + }, + "schema": { + "oneOf": [ + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "deviceId": { + "description": "Must be set when clientId is empty, cannot be used with clientId.", + "type": "string" + } + }, + "type": "object" + }, + { + "properties": { + "channel": { + "description": "Channel name.", + "type": "string" + }, + "clientId": { + "description": "Must be set when deviceId is empty, cannot be used with deviceId.", + "type": "string" + } + }, + "type": "object" + } + ] + } + } + } + }, + "responses": { + "2XX": { + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Subscribe a device to a channel", + "tags": [ + "Push" + ] + } + }, + "/push/channels": { + "get": { + "description": "Returns a paginated response of channel names.", + "operationId": "getChannelsWithPushSubscribers", + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "application/x-msgpack": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "text/html": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "List all channels with at least one subscribed device", + "tags": [ + "Push" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/push/deviceRegistrations": { + "delete": { + "description": "Unregisters devices. All their subscriptions for receiving push notifications through channels will also be deleted.", + "operationId": "unregisterAllPushDevices", + "parameters": [ + { + "description": "Optional filter to restrict to devices associated with that deviceId. Cannot be used with clientId.", + "in": "query", + "name": "deviceId", + "schema": { + "type": "string" + } + }, + { + "description": "Optional filter to restrict to devices associated with that clientId. Cannot be used with deviceId.", + "in": "query", + "name": "clientId", + "schema": { + "type": "string" + } + } + ], + "responses": { + "2XX": { + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Unregister matching devices for push notifications", + "tags": [ + "Push" + ] + }, + "get": { + "description": "List of device details of devices registed for push notifications.", + "operationId": "getRegisteredPushDevices", + "parameters": [ + { + "description": "Optional filter to restrict to devices associated with that deviceId.", + "in": "query", + "name": "deviceId", + "schema": { + "type": "string" + } + }, + { + "description": "Optional filter to restrict to devices associated with that clientId.", + "in": "query", + "name": "clientId", + "schema": { + "type": "string" + } + }, + { + "description": "The maximum number of records to return.", + "in": "query", + "name": "limit", + "schema": { + "default": 100, + "maximum": 1000, + "type": "integer" + } + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "List devices registered for receiving push notifications", + "tags": [ + "Push" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "post": { + "description": "Register a device’s details, including the information necessary to deliver push notifications to it. Requires \"push-admin\" capability.", + "operationId": "registerPushDevice", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + } + }, + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Register a device for receiving push notifications", + "tags": [ + "Push" + ] + } + }, + "/push/deviceRegistrations/{device_id}": { + "delete": { + "description": "Unregisters a single device by its device ID. All its subscriptions for receiving push notifications through channels will also be deleted.", + "operationId": "unregisterPushDevice", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "2XX": { + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Unregister a single device for push notifications", + "tags": [ + "Push" + ] + }, + "get": { + "description": "Get the full details of a device.", + "operationId": "getPushDeviceDetails", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Get a device registration", + "tags": [ + "Push" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "patch": { + "description": "Specific attributes of an existing registration can be updated. Only clientId, metadata and push.recipient are mutable.", + "operationId": "patchPushDeviceDetails", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + } + }, + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Update a device registration", + "tags": [ + "Push" + ] + }, + "put": { + "description": "Device registrations can be upserted (the existing registration is replaced entirely) with a PUT operation. Only clientId, metadata and push.recipient are mutable.", + "operationId": "putPushDeviceDetails", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + } + }, + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Update a device registration", + "tags": [ + "Push" + ] + } + }, + "/push/deviceRegistrations/{device_id}/resetUpdateToken": { + "get": { + "description": "Gets an updated device details object.", + "operationId": "updatePushDeviceDetails", + "parameters": [ + { + "$ref": "#/components/parameters/deviceId" + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/DeviceDetails" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Reset a registered device's update token", + "tags": [ + "Push" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/push/publish": { + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ], + "post": { + "description": "A convenience endpoint to deliver a push notification payload to a single device or set of devices identified by their client identifier.", + "operationId": "publishPushNotificationToDevices", + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "push": { + "$ref": "#/components/schemas/Push" + }, + "recipient": { + "$ref": "#/components/schemas/Recipient" + } + }, + "required": [ + "recipient" + ], + "type": "object" + } + }, + "application/x-msgpack": { + "schema": { + "properties": { + "push": { + "$ref": "#/components/schemas/Push" + }, + "recipient": { + "$ref": "#/components/schemas/Recipient" + } + }, + "required": [ + "recipient" + ], + "type": "object" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "properties": { + "push": { + "$ref": "#/components/schemas/Push" + }, + "recipient": { + "$ref": "#/components/schemas/Recipient" + } + }, + "required": [ + "recipient" + ], + "type": "object" + } + } + } + }, + "responses": { + "2XX": { + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Publish a push notification to device(s)", + "tags": [ + "Push" + ] + } + }, + "/stats": { + "get": { + "description": "The Ably system can be queried to obtain usage statistics for a given application, and results are provided aggregated across all channels in use in the application in the specified period. Stats may be used to track usage against account quotas.", + "operationId": "getStats", + "parameters": [ + { + "$ref": "#/components/parameters/filterStart" + }, + { + "$ref": "#/components/parameters/filterLimit" + }, + { + "$ref": "#/components/parameters/filterEnd" + }, + { + "$ref": "#/components/parameters/filterDirection" + }, + { + "description": "Specifies the unit of aggregation in the returned results.", + "in": "query", + "name": "unit", + "schema": { + "default": "minute", + "enum": [ + "minute", + "hour", + "day", + "month" + ], + "type": "string" + } + } + ], + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "summary": "Retrieve usage statistics for an application", + "tags": [ + "Stats" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + }, + "/time": { + "get": { + "description": "This returns the service time in milliseconds since the epoch.", + "operationId": "getTime", + "responses": { + "2XX": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "application/x-msgpack": { + "schema": { + "items": { + "type": "integer" + }, + "type": "array" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "security": [], + "summary": "Get the service time", + "tags": [ + "Stats" + ] + }, + "parameters": [ + { + "$ref": "#/components/parameters/versionHeader" + }, + { + "$ref": "#/components/parameters/responseFormat" + } + ] + } + }, + "components": { + "headers": { + "ErrorCode": { + "description": "The error code.", + "schema": { + "type": "integer" + } + }, + "ErrorMessage": { + "description": "The error message.", + "schema": { + "type": "string" + } + }, + "Link": { + "description": "Links to related resources, in the format defined by [RFC 5988](https://tools.ietf.org/html/rfc5988#section-5). This will potentially include a link with relation type `next`, `first`, and `current`, where appropiate.", + "required": true, + "schema": { + "pattern": "(<(.*)?>; rel=\\\"(first|current|last)?\\\",)*(<(.*)?>; rel=\\\"(first|current|last)?\\\")+", + "type": "string" + } + }, + "ServerId": { + "description": "The ID for the server communicated with.", + "required": true, + "schema": { + "type": "string" + } + } + }, + "parameters": { + "channelId": { + "description": "The [Channel's ID](https://www.ably.io/documentation/rest/channels).", + "in": "path", + "name": "channel_id", + "required": true, + "schema": { + "type": "string" + } + }, + "deviceId": { + "description": "Device's ID.", + "in": "path", + "name": "device_id", + "required": true, + "schema": { + "type": "string" + } + }, + "filterDirection": { + "in": "query", + "name": "direction", + "schema": { + "default": "backwards", + "enum": [ + "forwards", + "backwards" + ], + "type": "string" + } + }, + "filterEnd": { + "in": "query", + "name": "end", + "schema": { + "default": "now", + "type": "string" + } + }, + "filterLimit": { + "in": "query", + "name": "limit", + "schema": { + "default": "100", + "type": "integer" + } + }, + "filterStart": { + "in": "query", + "name": "start", + "schema": { + "type": "string" + } + }, + "key_name": { + "description": "The [key name](https://www.ably.io/documentation/rest-api/token-request-spec#api-key-format) comprises of the app ID and key ID of an API key.", + "in": "path", + "name": "keyName", + "required": true, + "schema": { + "type": "string" + } + }, + "responseFormat": { + "description": "The response format you would like", + "in": "query", + "name": "format", + "schema": { + "enum": [ + "json", + "jsonp", + "msgpack", + "html" + ], + "type": "string" + } + }, + "versionHeader": { + "description": "The version of the API you wish to use.", + "in": "header", + "name": "X-Ably-Version", + "schema": { + "type": "string" + } + } + }, + "responses": { + "Error": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/x-msgpack": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "text/html": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error", + "headers": { + "x-ably-errorcode": { + "$ref": "#/components/headers/ErrorCode" + }, + "x-ably-errormessage": { + "$ref": "#/components/headers/ErrorMessage" + }, + "x-ably-serverid": { + "$ref": "#/components/headers/ServerId" + } + } + } + }, + "schemas": { + "ChannelDetails": { + "properties": { + "channelId": { + "description": "The required name of the channel including any qualifier, if any.", + "type": "string" + }, + "isGlobalMaster": { + "description": "In events relating to the activity of a channel in a specific region, this optionally identifies whether or not that region is responsible for global coordination of the channel.", + "type": "boolean" + }, + "region": { + "description": "In events relating to the activity of a channel in a specific region, this optionally identifies the region.", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/ChannelStatus" + } + }, + "required": [ + "channelId" + ], + "type": "object" + }, + "ChannelStatus": { + "description": "A ChannelStatus instance.", + "properties": { + "isActive": { + "description": "A required boolean value indicating whether the channel that is the subject of the event is active. For events indicating regional activity of a channel this indicates activity in that region, not global activity.", + "type": "boolean" + }, + "occupancy": { + "$ref": "#/components/schemas/Occupancy" + } + }, + "required": [ + "isActive" + ], + "type": "object" + }, + "DeviceDetails": { + "properties": { + "clientId": { + "description": "Optional trusted client identifier for the device.", + "type": "string" + }, + "deviceSecret": { + "description": "Secret value for the device.", + "type": "string" + }, + "formFactor": { + "description": "Form factor of the push device.", + "enum": [ + "phone", + "tablet", + "desktop", + "tv", + "watch", + "car", + "embedded" + ], + "type": "string" + }, + "id": { + "description": "Unique identifier for the device generated by the device itself.", + "type": "string" + }, + "metadata": { + "description": "Optional metadata object for this device. The metadata for a device may only be set by clients with push-admin privileges and will be used more extensively in the future with smart notifications.", + "type": "object" + }, + "platform": { + "description": "Platform of the push device.", + "enum": [ + "ios", + "android" + ], + "type": "string" + }, + "push.recipient": { + "$ref": "#/components/schemas/Recipient" + }, + "push.state": { + "description": "the current state of the push device.", + "enum": [ + "Active", + "Failing", + "Failed" + ], + "readOnly": true, + "type": "string" + } + }, + "type": "object" + }, + "Error": { + "description": "Returned error from failed REST.", + "properties": { + "code": { + "description": "Error code.", + "type": "integer" + }, + "href": { + "description": "Link to help with error.", + "type": "string" + }, + "message": { + "description": "Message explaining the error's cause.", + "type": "string" + }, + "serverId": { + "description": "Server ID with which error was encountered.", + "type": "string" + }, + "statusCode": { + "description": "Status error code.", + "type": "integer" + } + }, + "type": "object" + }, + "Extras": { + "description": "Extras object. Currently only allows for [push](https://www.ably.io/documentation/general/push/publish#channel-broadcast-example) extra.", + "properties": { + "push": { + "$ref": "#/components/schemas/Push" + } + }, + "type": "object" + }, + "Message": { + "description": "Message object.", + "properties": { + "clientId": { + "description": "The [client ID](https://www.ably.io/documentation/core-features/authentication#identified-clients) of the publisher of this message.", + "type": "string" + }, + "connectionId": { + "description": "The connection ID of the publisher of this message.", + "type": "string" + }, + "data": { + "description": "The string encoded payload, with the encoding specified below.", + "type": "string" + }, + "encoding": { + "description": "This will typically be empty as all messages received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute will contain the remaining transformations not applied to the data payload.", + "type": "string" + }, + "extras": { + "$ref": "#/components/schemas/Extras" + }, + "id": { + "description": "A Unique ID that can be specified by the publisher for [idempotent publishing](https://www.ably.io/documentation/rest/messages#idempotent).", + "readOnly": true, + "type": "string" + }, + "name": { + "description": "The event name, if provided.", + "type": "string" + }, + "timestamp": { + "description": "Timestamp when the message was received by the Ably, as milliseconds since the epoch.", + "format": "int64", + "readOnly": true, + "type": "integer" + } + }, + "type": "object" + }, + "Notification": { + "properties": { + "body": { + "description": "Text below title on the expanded notification.", + "type": "string" + }, + "collapseKey": { + "description": "Platform-specific, used to group notifications together.", + "type": "string" + }, + "icon": { + "description": "Platform-specific icon for the notification.", + "type": "string" + }, + "sound": { + "description": "Platform-specific sound for the notification.", + "type": "string" + }, + "title": { + "description": "Title to display at the notification.", + "type": "string" + } + }, + "type": "object" + }, + "Occupancy": { + "description": "An Occupancy instance indicating the occupancy of a channel. For events indicating regional activity of a channel this indicates activity in that region, not global activity.", + "properties": { + "presenceConnections": { + "description": "The number of connections that are authorised to enter members into the presence channel.", + "type": "integer" + }, + "presenceMembers": { + "description": "The number of members currently entered into the presence channel.", + "type": "integer" + }, + "presenceSubscribers": { + "description": "The number of connections that are authorised to subscribe to presence messages.", + "type": "integer" + }, + "publishers": { + "description": "The number of connections attached to the channel that are authorised to publish.", + "type": "integer" + }, + "subscribers": { + "description": "The number of connections attached that are authorised to subscribe to messages.", + "type": "integer" + } + }, + "type": "object" + }, + "PresenceMessage": { + "properties": { + "action": { + "description": "The event signified by a PresenceMessage.", + "enum": [ + "ABSENT", + "PRESENT", + "ENTER", + "LEAVE", + "UPDATE" + ], + "readOnly": true, + "type": "string" + }, + "clientId": { + "description": "The client ID of the publisher of this presence update.", + "type": "string" + }, + "connectionId": { + "description": "The connection ID of the publisher of this presence update.", + "type": "string" + }, + "data": { + "description": "The presence update payload, if provided.", + "type": "string" + }, + "encoding": { + "description": "This will typically be empty as all presence updates received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute will contain the remaining transformations not applied to the data payload.", + "type": "string" + }, + "extras": { + "$ref": "#/components/schemas/Extras" + }, + "id": { + "description": "Unique ID assigned by Ably to this presence update.", + "readOnly": true, + "type": "string" + }, + "timestamp": { + "description": "Timestamp when the presence update was received by Ably, as milliseconds since the epoch.", + "format": "int64", + "readOnly": true, + "type": "integer" + } + }, + "type": "object" + }, + "Push": { + "properties": { + "apns": { + "description": "Extends and overrides generic values when delivering via APNs. [See examples](https://www.ably.io/documentation/general/push/publish#payload-structure)", + "properties": { + "notification": { + "$ref": "#/components/schemas/Notification" + } + }, + "type": "object" + }, + "data": { + "description": "Arbitrary [key-value string-to-string payload](https://www.ably.io/documentation/general/push/publish#channel-broadcast-example).", + "type": "string" + }, + "fcm": { + "description": "Extends and overrides generic values when delivering via GCM/FCM. [See examples](https://www.ably.io/documentation/general/push/publish#payload-structure)", + "properties": { + "notification": { + "$ref": "#/components/schemas/Notification" + } + }, + "type": "object" + }, + "notification": { + "$ref": "#/components/schemas/Notification" + }, + "web": { + "description": "Extends and overrides generic values when delivering via web. [See examples](https://www.ably.io/documentation/general/push/publish#payload-structure)", + "properties": { + "notification": { + "$ref": "#/components/schemas/Notification" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "Recipient": { + "description": "Push recipient details for a device.", + "properties": { + "clientId": { + "description": "Client ID of recipient", + "type": "string", + "writeOnly": true + }, + "deviceId": { + "description": "Client ID of recipient", + "type": "string", + "writeOnly": true + }, + "deviceToken": { + "description": "when using APNs, specifies the required device token.", + "type": "string" + }, + "registrationToken": { + "description": "when using GCM or FCM, specifies the required registration token.", + "type": "string" + }, + "transportType": { + "description": "Defines which push platform is being used.", + "enum": [ + "apns", + "fcm", + "gcm" + ], + "type": "string" + } + }, + "type": "object" + }, + "SignedTokenRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/TokenRequest" + }, + { + "properties": { + "mac": { + "description": "A signature, generated as an HMAC of each of the above components, using the key secret value.", + "type": "string" + } + }, + "required": [ + "mac" + ], + "type": "object" + } + ] + }, + "TokenDetails": { + "properties": { + "capability": { + "description": "Regular expression representation of the capabilities of the token.", + "type": "string" + }, + "expires": { + "description": "Timestamp of token expiration.", + "type": "integer" + }, + "issued": { + "description": "Timestamp of token creation.", + "type": "integer" + }, + "keyName": { + "description": "Name of the key used to create the token", + "type": "string" + }, + "token": { + "description": "The Ably Token.", + "type": "string" + } + }, + "type": "object" + }, + "TokenRequest": { + "properties": { + "capability": { + "description": "The [capabilities](https://www.ably.io/documentation/core-features/authentication#capabilities-explained) (i.e. a set of channel names/namespaces and, for each, a set of operations) which should be a subset of the set of capabilities associated with the key specified in keyName.", + "example": { + "channel1": [ + "publish", + "subscribe" + ] + }, + "type": "object" + }, + "clientId": { + "description": "The [client ID](https://www.ably.io/documentation/core-features/authentication#identified-clients) to be assosciated with the token. Can be set to * to allow for any client ID to be used.", + "type": "string" + }, + "keyName": { + "description": "Name of the key used for the TokenRequest. The keyName comprises of the app ID and key ID on an API Key.", + "example": "xVLyHw.LMJZxw", + "type": "string" + }, + "nonce": { + "description": "An unquoted, un-escaped random string of at least 16 characters. Used to ensure the Ably TokenRequest cannot be reused.", + "type": "string" + }, + "timestamp": { + "description": "Time of creation of the Ably TokenRequest.", + "type": "integer" + } + }, + "required": [ + "keyName", + "capability", + "timestamp", + "nonce" + ], + "type": "object" + } + }, + "securitySchemes": { + "basicAuth": { + "description": "Basic Authentication using an [API key](https://www.ably.io/documentation/core-features/authentication#basic-authentication).", + "scheme": "basic", + "type": "http" + }, + "bearerAuth": { + "description": "Token Authentication using an [Ably Token](https://www.ably.io/documentation/core-features/authentication#basic-authentication), or optionally an [Ably JWT](https://www.ably.io/documentation/core-features/authentication#ably-jwt-process).", + "scheme": "bearer", + "type": "http" + } + } + } +} \ No newline at end of file diff --git a/Demos/Data/samples/authentiqio.json b/Demos/Data/samples/authentiqio.json new file mode 100644 index 0000000..316fcf3 --- /dev/null +++ b/Demos/Data/samples/authentiqio.json @@ -0,0 +1,1001 @@ +{ + "openapi": "3.0.0", + "servers": [ + { + "url": "https://6-dot-authentiqio.appspot.com" + } + ], + "info": { + "contact": { + "email": "hello@authentiq.com", + "name": "Authentiq team", + "url": "http://authentiq.io/support" + }, + "description": "Strong authentication, without the passwords.", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "termsOfService": "http://authentiq.com/terms/", + "title": "Authentiq API", + "version": "6", + "x-apisguru-categories": [ + "security" + ], + "x-logo": { + "backgroundColor": "#F26641", + "url": "https://api.apis.guru/v2/cache/logo/https_www.authentiq.com_theme_images_authentiq-logo-a-inverse.svg" + }, + "x-origin": [ + { + "format": "openapi", + "url": "https://raw.githubusercontent.com/AuthentiqID/authentiq-docs/master/docs/swagger/issuer.yaml", + "version": "3.0" + } + ], + "x-providerName": "6-dot-authentiqio.appspot.com" + }, + "paths": { + "/key": { + "delete": { + "description": "Revoke an Authentiq ID using email & phone.\n\nIf called with `email` and `phone` only, a verification code \nwill be sent by email. Do a second call adding `code` to \ncomplete the revocation.\n", + "operationId": "key_revoke_nosecret", + "parameters": [ + { + "description": "primary email associated to Key (ID)", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "primary phone number, international representation", + "in": "query", + "name": "phone", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "verification code sent by email", + "in": "query", + "name": "code", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "pending or done", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully deleted" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Authentication error `auth-error`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Confirm with code sent `confirm-first`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "delete" + ] + }, + "post": { + "description": "Register a new ID `JWT(sub, devtoken)`\n\nv5: `JWT(sub, pk, devtoken, ...)`\n\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "key_register", + "requestBody": { + "$ref": "#/components/requestBodies/AuthentiqID" + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "secret": { + "description": "revoke key", + "type": "string" + }, + "status": { + "description": "registered", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully registered" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Key already registered `duplicate-key`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "post" + ] + } + }, + "/key/{PK}": { + "delete": { + "description": "Revoke an Identity (Key) with a revocation secret", + "operationId": "key_revoke", + "parameters": [ + { + "$ref": "#/components/parameters/PK" + }, + { + "description": "revokation secret", + "in": "query", + "name": "secret", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "done", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Key not found / wrong code `auth-error`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "delete" + ] + }, + "get": { + "description": "Get public details of an Authentiq ID.\n", + "operationId": "key_retrieve", + "parameters": [ + { + "$ref": "#/components/parameters/PK" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "since": { + "format": "date-time", + "type": "string" + }, + "status": { + "type": "string" + }, + "sub": { + "description": "base64safe encoded public signing key", + "type": "string" + } + }, + "title": "JWT", + "type": "object" + } + } + }, + "description": "Successfully retrieved" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "410": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Key is revoked (gone). `revoked-key`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "get" + ] + }, + "head": { + "description": "HEAD info on Authentiq ID\n", + "parameters": [ + { + "$ref": "#/components/parameters/PK" + } + ], + "responses": { + "200": { + "description": "Key exists" + }, + "404": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "410": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Key is revoked `revoked-key`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "head" + ] + }, + "post": { + "description": "update properties of an Authentiq ID.\n(not operational in v4; use PUT for now)\n\nv5: POST issuer-signed email & phone scopes in\na self-signed JWT\n\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "key_update", + "parameters": [ + { + "$ref": "#/components/parameters/PK" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/AuthentiqID" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "confirmed", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully updated" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "post" + ] + }, + "put": { + "description": "Update Authentiq ID by replacing the object.\n\nv4: `JWT(sub,email,phone)` to bind email/phone hash; \n\nv5: POST issuer-signed email & phone scopes\nand PUT to update registration `JWT(sub, pk, devtoken, ...)`\n\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "key_bind", + "parameters": [ + { + "$ref": "#/components/parameters/PK" + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/AuthentiqID" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "confirmed", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully updated" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unknown key `unknown-key`" + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Already bound to another key `duplicate-hash`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "key", + "put" + ] + } + }, + "/login": { + "post": { + "description": "push sign-in request\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "push_login_request", + "parameters": [ + { + "description": "URI App will connect to", + "in": "query", + "name": "callback", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "description": "Push Token.", + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "sent", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Unauthorized for this callback audience `aud-error` or JWT should be self-signed `auth-error`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "login", + "post" + ] + } + }, + "/scope": { + "post": { + "description": "scope verification request\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "sign_request", + "parameters": [ + { + "description": "test only mode, using test issuer", + "in": "query", + "name": "test", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/Claims" + } + } + }, + "description": "Claims of scope", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "properties": { + "job": { + "description": "20-character ID", + "type": "string" + }, + "status": { + "description": "waiting", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successful response" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Too Many Requests on same address / number `rate-limit`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "post" + ] + } + }, + "/scope/{job}": { + "delete": { + "description": "delete a verification job", + "operationId": "sign_delete", + "parameters": [ + { + "$ref": "#/components/parameters/JobID" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "done", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully deleted" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not found `unknown-job`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "delete" + ] + }, + "get": { + "description": "get the status / current content of a verification job", + "operationId": "sign_retrieve", + "parameters": [ + { + "$ref": "#/components/parameters/JobID" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "exp": { + "type": "integer" + }, + "field": { + "type": "string" + }, + "sub": { + "description": "base64safe encoded public signing key", + "type": "string" + } + }, + "title": "JWT", + "type": "object" + } + }, + "application/jwt": { + "schema": { + "properties": { + "exp": { + "type": "integer" + }, + "field": { + "type": "string" + }, + "sub": { + "description": "base64safe encoded public signing key", + "type": "string" + } + }, + "title": "JWT", + "type": "object" + } + } + }, + "description": "Successful response (JWT)" + }, + "204": { + "description": "Confirmed, waiting for signing" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + }, + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not found `unknown-job`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "get" + ] + }, + "head": { + "description": "HEAD to get the status of a verification job", + "operationId": "sign_retrieve_head", + "parameters": [ + { + "$ref": "#/components/parameters/JobID" + } + ], + "responses": { + "200": { + "description": "Confirmed and signed" + }, + "204": { + "description": "Confirmed, waiting for signing" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not found `unknown-job`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "head" + ] + }, + "post": { + "description": "this is a scope confirmation", + "operationId": "sign_confirm", + "parameters": [ + { + "$ref": "#/components/parameters/JobID" + } + ], + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "confirmed", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully confirmed" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Confirmation error `auth-error`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not found `unknown-job`" + }, + "405": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "JWT POSTed to scope `not-supported`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "post" + ] + }, + "put": { + "description": "authority updates a JWT with its signature\nSee: https://github.com/skion/authentiq/wiki/JWT-Examples\n", + "operationId": "sign_update", + "parameters": [ + { + "$ref": "#/components/parameters/JobID" + } + ], + "responses": { + "200": { + "content": { + "application/jwt": { + "schema": { + "properties": { + "jwt": { + "description": "result is JWT or JSON??", + "type": "string" + }, + "status": { + "description": "ready", + "type": "string" + } + }, + "type": "object" + } + } + }, + "description": "Successfully updated" + }, + "404": { + "content": { + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not found `unknown-job`" + }, + "409": { + "content": { + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Job not confirmed yet `confirm-first`" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + }, + "tags": [ + "scope", + "put" + ] + } + } + }, + "components": { + "parameters": { + "JobID": { + "description": "Job ID (20 chars)", + "in": "path", + "name": "job", + "required": true, + "schema": { + "type": "string" + } + }, + "PK": { + "description": "Public Signing Key - Authentiq ID (43 chars)", + "in": "path", + "name": "PK", + "required": true, + "schema": { + "type": "string" + } + } + }, + "requestBodies": { + "AuthentiqID": { + "content": { + "application/jwt": { + "schema": { + "$ref": "#/components/schemas/AuthentiqID" + } + } + }, + "description": "Authentiq ID to register", + "required": true + } + }, + "responses": { + "ErrorResponse": { + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Error response" + } + }, + "schemas": { + "AuthentiqID": { + "description": "Authentiq ID in JWT format, self-signed.\n", + "properties": { + "devtoken": { + "description": "device token for push messages", + "type": "string" + }, + "sub": { + "description": "UUID and public signing key", + "type": "string" + } + }, + "required": [ + "sub" + ] + }, + "Claims": { + "description": "Claim in JWT format, self- or issuer-signed. \n", + "properties": { + "email": { + "description": "", + "type": "string" + }, + "phone": { + "description": "", + "type": "string" + }, + "scope": { + "description": "claim scope", + "type": "string" + }, + "sub": { + "description": "UUID", + "type": "string" + }, + "type": { + "description": "", + "type": "string" + } + }, + "required": [ + "sub", + "scope" + ] + }, + "Error": { + "properties": { + "detail": { + "type": "string" + }, + "error": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "type": { + "description": "unique uri for this error", + "type": "string" + } + }, + "required": [ + "error" + ] + }, + "PushToken": { + "description": "PushToken in JWT format, self-signed. \n", + "properties": { + "aud": { + "description": "audience (URI)", + "type": "string" + }, + "exp": { + "type": "integer" + }, + "iat": { + "type": "integer" + }, + "iss": { + "description": "issuer (URI)", + "type": "string" + }, + "nbf": { + "type": "integer" + }, + "sub": { + "description": "UUID and public signing key", + "type": "string" + } + }, + "required": [ + "sub", + "iss", + "aud" + ] + } + } + } +} \ No newline at end of file diff --git a/Demos/Data/samples/ipgeolocation.json b/Demos/Data/samples/ipgeolocation.json new file mode 100644 index 0000000..c635a48 --- /dev/null +++ b/Demos/Data/samples/ipgeolocation.json @@ -0,0 +1,233 @@ +{ + "openapi": "3.0.1", + "servers": [ + { + "url": "https://ipgeolocation.abstractapi.com" + } + ], + "info": { + "description": "Abstract IP geolocation API allows developers to retrieve the region, country and city behind any IP worldwide. The API covers the geolocation of IPv4 and IPv6 addresses in 180+ countries worldwide. Extra information can be retrieved like the currency, flag or language associated to an IP.", + "title": "IP geolocation API", + "version": "1.0.0", + "x-apisguru-categories": [ + "location" + ], + "x-logo": { + "url": "https://api.apis.guru/v2/cache/logo/https_global-uploads.webflow.com_5ebbd0a566a3996636e55959_5ec2ba29feeeb05d69160e7b_webclip.png" + }, + "x-origin": [ + { + "format": "openapi", + "url": "https://documentation.abstractapi.com/ip-geolocation-openapi.json", + "version": "3.0" + } + ], + "x-providerName": "abstractapi.com", + "x-serviceName": "geolocation" + }, + "externalDocs": { + "description": "API Documentation", + "url": "https://www.abstractapi.com/ip-geolocation-api#docs" + }, + "paths": { + "/v1/": { + "get": { + "description": "Retrieve the location of an IP address", + "parameters": [ + { + "explode": true, + "in": "query", + "name": "api_key", + "required": true, + "schema": { + "type": "string" + }, + "style": "form" + }, + { + "explode": true, + "in": "query", + "name": "ip_address", + "required": false, + "schema": { + "example": "195.154.25.40", + "type": "string" + }, + "style": "form" + }, + { + "explode": true, + "in": "query", + "name": "fields", + "required": false, + "schema": { + "example": "country,city,timezone", + "type": "string" + }, + "style": "form" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "examples": { + "0": { + "value": "{\"ip_address\":\"195.154.25.40\",\"city\":\"Paris\",\"city_geoname_id\":2988507,\"region\":\"Île-de-France\",\"region_iso_code\":\"IDF\",\"region_geoname_id\":3012874,\"postal_code\":\"75008\",\"country\":\"France\",\"country_code\":\"FR\",\"country_geoname_id\":3017382,\"country_is_eu\":true,\"continent\":\"Europe\",\"continent_code\":\"EU\",\"continent_geoname_id\":6255148,\"longitude\":2.4075,\"latitude\":48.8323,\"security\":{\"is_vpn\":false},\"timezone\":{\"name\":\"Europe/Paris\",\"abbreviation\":\"CEST\",\"gmt_offset\":2,\"current_time\":\"15:42:18\",\"is_dst\":true},\"flag\":{\"emoji\":\"<ë<÷\",\"unicode\":\"U+1F1EB U+1F1F7\",\"png\":\"https://static.abstractapi.com/country-flags/FR_flag.png\",\"svg\":\"https://static.abstractapi.com/country-flags/FR_flag.svg\"},\"currency\":{\"currency_name\":\"Euros\",\"currency_code\":\"EUR\"},\"connection\":{\"autonomous_system_number\":12876,\"autonomous_system_organization\":\"Online S.a.s.\",\"connection_type\":\"Corporate\",\"isp_name\":\"Online S.A.S.\",\"organization_name\":\"ONLINE\"}}" + } + }, + "schema": { + "$ref": "#/components/schemas/inline_response_200" + } + } + }, + "description": "Location of geolocated IP" + } + }, + "servers": [ + { + "url": "https://ipgeolocation.abstractapi.com" + } + ] + }, + "servers": [ + { + "url": "https://ipgeolocation.abstractapi.com" + } + ] + } + }, + "components": { + "schemas": { + "inline_response_200": { + "properties": { + "city": { + "type": "string" + }, + "city_geoname_id": { + "type": "integer" + }, + "connection": { + "properties": { + "autonomous_system_number": { + "type": "integer" + }, + "autonomous_system_organization": { + "type": "string" + }, + "connection_type": { + "type": "string" + }, + "isp_name": { + "type": "string" + }, + "organization_name": { + "type": "string" + } + }, + "type": "object" + }, + "continent": { + "type": "string" + }, + "continent_code": { + "type": "string" + }, + "continent_geoname_id": { + "type": "integer" + }, + "country": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "country_geoname_id": { + "type": "integer" + }, + "country_is_eu": { + "type": "boolean" + }, + "currency": { + "properties": { + "currency_code": { + "type": "string" + }, + "currency_name": { + "type": "string" + } + }, + "type": "object" + }, + "flag": { + "properties": { + "emoji": { + "type": "string" + }, + "png": { + "type": "string" + }, + "svg": { + "type": "string" + }, + "unicode": { + "type": "string" + } + }, + "type": "object" + }, + "ip_address": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "postal_code": { + "type": "string" + }, + "region": { + "type": "string" + }, + "region_geoname_id": { + "type": "integer" + }, + "region_iso_code": { + "type": "string" + }, + "security": { + "properties": { + "is_vpn": { + "type": "boolean" + } + }, + "type": "object" + }, + "timezone": { + "properties": { + "abbreviation": { + "type": "string" + }, + "current_time": { + "type": "string" + }, + "gmt_offset": { + "type": "integer" + }, + "is_dst": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/Demos/Data/samples/petstore.json b/Demos/Data/samples/petstore.json new file mode 100644 index 0000000..b52f370 --- /dev/null +++ b/Demos/Data/samples/petstore.json @@ -0,0 +1,175 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "An paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/Demos/OpenAPI/Demo.Form.Main.dfm b/Demos/OpenAPI/Demo.Form.Main.dfm index e903449..cc41f0e 100644 --- a/Demos/OpenAPI/Demo.Form.Main.dfm +++ b/Demos/OpenAPI/Demo.Form.Main.dfm @@ -12,7 +12,6 @@ object frmMain: TfrmMain Font.Style = [] OnCreate = FormCreate OnDestroy = FormDestroy - PixelsPerInch = 96 TextHeight = 13 object memoDocument: TMemo Left = 200 @@ -28,6 +27,7 @@ object frmMain: TfrmMain ParentFont = False ScrollBars = ssVertical TabOrder = 0 + ExplicitLeft = 203 end object catMenu: TCategoryPanelGroup Left = 0 @@ -40,16 +40,17 @@ object frmMain: TfrmMain HeaderFont.Name = 'Tahoma' HeaderFont.Style = [] TabOrder = 1 - object panGeneral: TCategoryPanel - Top = 0 - Height = 441 - Caption = 'OpenAPI Document' + object pnlSections: TCategoryPanel + Top = 192 + Height = 369 + Caption = 'Fill Document Sections' TabOrder = 0 + Visible = False object CategoryButtons1: TCategoryButtons Left = 0 Top = 0 Width = 194 - Height = 415 + Height = 343 Align = alClient ButtonFlow = cbfVertical ButtonOptions = [boFullSize, boGradientFill, boShowCaptions, boUsePlusMinus] @@ -97,25 +98,33 @@ object frmMain: TfrmMain end item Action = actAddSecurity - end + end> + end + item + Caption = 'Serialization' + Color = 16777194 + Collapsed = False + Items = < item + Action = actJSONGenerate end> end> RegularButtonColor = clWhite SelectedButtonColor = 15132390 TabOrder = 0 + ExplicitHeight = 327 end end - object CategoryPanel1: TCategoryPanel - Top = 441 - Height = 120 - Caption = 'JSON Generation' + object pnlDocument: TCategoryPanel + Top = 0 + Height = 192 + Caption = 'OpenAPI Document' TabOrder = 1 object CategoryButtons2: TCategoryButtons Left = 0 Top = 0 Width = 194 - Height = 94 + Height = 166 Align = alClient ButtonFlow = cbfVertical Categories = <> @@ -127,19 +136,34 @@ object frmMain: TfrmMain Left = 0 Top = 0 Width = 194 - Height = 94 + Height = 166 Align = alClient ButtonFlow = cbfVertical ButtonOptions = [boFullSize, boGradientFill, boShowCaptions, boUsePlusMinus] Categories = < item - Caption = 'Generation' + Caption = 'Document' Color = 15466474 Collapsed = False Items = < item - Action = actJSONGenerate + Action = actDocumentNew + end + item + Action = actDocumentClose + end + item + Action = actDocumentOpen end + item + Action = actDocumentSave + end> + end + item + Caption = 'General' + Color = 16771818 + Collapsed = False + Items = < item Action = actJSONReplace end> @@ -152,8 +176,8 @@ object frmMain: TfrmMain end object aclCommands: TActionList Images = imgCommands - Left = 64 - Top = 144 + Left = 176 + Top = 168 object actAddInfo: TAction Caption = 'Add Info Object' OnExecute = actAddInfoExecute @@ -171,7 +195,7 @@ object frmMain: TfrmMain OnExecute = actAddPathsExecute end object actAddSecurity: TAction - Caption = 'actAddSecurity' + Caption = 'Add Security' OnExecute = actAddSecurityExecute end object actCompAddSchemas: TAction @@ -194,16 +218,43 @@ object frmMain: TfrmMain Caption = 'Add RequestBodies' end object actJSONGenerate: TAction - Caption = 'Generate JSON' + Caption = 'Generate JSON Document' OnExecute = actJSONGenerateExecute end object actJSONReplace: TAction Caption = 'Replace Escaped Slash "/"' OnExecute = actJSONReplaceExecute end + object actDocumentOpen: TAction + Caption = 'Load Document (JSON)' + OnExecute = actDocumentOpenExecute + end + object actDocumentSave: TAction + Caption = 'Save Document (JSON)' + OnExecute = actDocumentSaveExecute + end + object actDocumentNew: TAction + Caption = 'New Document' + OnExecute = actDocumentNewExecute + end + object actDocumentClose: TAction + Caption = 'Close Document' + OnExecute = actDocumentCloseExecute + end end object imgCommands: TImageList - Left = 72 + Left = 184 Top = 216 end + object dlgOpenJSON: TOpenDialog + Filter = 'Schema Documents|*.json|All Files|*.*' + Left = 440 + Top = 296 + end + object dlgSaveDocument: TSaveDialog + DefaultExt = 'json' + Filter = 'OpenAPI Document|*.json' + Left = 440 + Top = 360 + end end diff --git a/Demos/OpenAPI/Demo.Form.Main.pas b/Demos/OpenAPI/Demo.Form.Main.pas index 81ef715..155d97f 100644 --- a/Demos/OpenAPI/Demo.Form.Main.pas +++ b/Demos/OpenAPI/Demo.Form.Main.pas @@ -29,9 +29,9 @@ TPerson = class TfrmMain = class(TForm) memoDocument: TMemo; catMenu: TCategoryPanelGroup; - panGeneral: TCategoryPanel; + pnlSections: TCategoryPanel; CategoryButtons1: TCategoryButtons; - CategoryPanel1: TCategoryPanel; + pnlDocument: TCategoryPanel; CategoryButtons2: TCategoryButtons; catJSON: TCategoryButtons; aclCommands: TActionList; @@ -48,6 +48,13 @@ TfrmMain = class(TForm) actCompAddParameters: TAction; actCompAddRequestBodies: TAction; actAddInfoExtensions: TAction; + actDocumentOpen: TAction; + actDocumentSave: TAction; + dlgOpenJSON: TOpenDialog; + actDocumentNew: TAction; + actDocumentClose: TAction; + dlgSaveDocument: TSaveDialog; + procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure actAddInfoExecute(Sender: TObject); procedure actAddInfoExtensionsExecute(Sender: TObject); @@ -58,10 +65,14 @@ TfrmMain = class(TForm) procedure actCompAddSecurityDefsExecute(Sender: TObject); procedure actAddSecurityExecute(Sender: TObject); procedure actCompAddParametersExecute(Sender: TObject); + procedure actDocumentCloseExecute(Sender: TObject); + procedure actDocumentNewExecute(Sender: TObject); procedure actJSONGenerateExecute(Sender: TObject); + procedure actDocumentOpenExecute(Sender: TObject); + procedure actDocumentSaveExecute(Sender: TObject); procedure actJSONReplaceExecute(Sender: TObject); - procedure FormCreate(Sender: TObject); private + FDocumentName: string; FDocument: TOpenAPIDocument; public { Public declarations } @@ -72,8 +83,16 @@ TfrmMain = class(TForm) implementation +uses + System.IOUtils; + {$R *.dfm} +procedure TfrmMain.FormCreate(Sender: TObject); +begin + dlgOpenJSON.InitialDir := TPath.GetDirectoryName(TPath.GetDirectoryName(Application.ExeName)) + '\Data\samples\'; +end; + procedure TfrmMain.FormDestroy(Sender: TObject); begin FDocument.Free; @@ -99,7 +118,7 @@ procedure TfrmMain.actAddInfoExtensionsExecute(Sender: TObject); LJSON.AddPair('backgroundColor', '#000000'); LJSON.AddPair('altText', 'WiRL Logo'); - FDocument.Info.Extensions.AddPair('x-logo', LJSON); + FDocument.Info.Extensions.Add('x-logo', LJSON); // Testing the Dictionary serialization for Extensions //FDocument.Info.Ext.Add('test', 'prova'); @@ -130,18 +149,19 @@ procedure TfrmMain.actAddPathsExecute(Sender: TObject); LParameter := LOperation.AddParameter('id', 'query'); LParameter.Description := 'Customer ID'; LParameter.Schema.Type_ := 'string'; - LParameter.Schema.Enum.ValueFrom>(['enum1', 'enum2']); LParameter := LOperation.AddParameter('country', 'query'); LParameter.Description := 'Country Code'; LParameter.Schema.Type_ := 'string'; - LParameter.Schema.Enum.ValueFrom>(['it', 'en', 'de', 'ch', 'fr']); + LParameter.Schema.AddEnum('it'); + LParameter.Schema.AddEnum('br'); + LParameter.Schema.AddEnum('us'); + LParameter.Schema.AddEnum('uk'); LParameter := LOperation.AddParameter('date', 'query'); LParameter.Description := 'Date'; LParameter.Schema.Type_ := 'string'; LParameter.Schema.Format := 'date-time'; - LParameter.Schema.Enum.ValueFrom>(['it', 'en', 'de', 'ch', 'fr']); // Uses a JSON schema already existing as a TJSONObject LParameter := LOperation.AddParameter('person', 'query'); @@ -151,7 +171,7 @@ procedure TfrmMain.actAddPathsExecute(Sender: TObject); // Uses #ref LParameter := LOperation.AddParameter('order', 'query'); LParameter.Description := 'Order Entity'; - LParameter.Schema.Reference.Ref := '#comp/deidjed/'; + LParameter.Schema.Reference.Ref := '#components/schemas/order'; end; procedure TfrmMain.actCompAddResponsesExecute(Sender: TObject); @@ -211,8 +231,21 @@ procedure TfrmMain.actCompAddParametersExecute(Sender: TObject); LParameter := FDocument.Components.AddParameter('idParam', 'id', 'query'); LParameter.Description := 'Customer ID'; LParameter.Schema.Type_ := 'string'; - LParameter.Schema.Enum.ValueFrom>(['enum1', 'enum2']); - LParameter.Schema.MaxLength := 123; + LParameter.Schema.MaxLength := 20; +end; + +procedure TfrmMain.actDocumentCloseExecute(Sender: TObject); +begin + FreeAndNil(FDocument); + memoDocument.Clear; + pnlSections.Visible := False; +end; + +procedure TfrmMain.actDocumentNewExecute(Sender: TObject); +begin + FDocument := TOpenAPIDocument.Create(TOpenAPIVersion.v303); + pnlSections.Visible := True; + FDocumentName := 'MyAPI'; end; procedure TfrmMain.actJSONGenerateExecute(Sender: TObject); @@ -221,15 +254,47 @@ procedure TfrmMain.actJSONGenerateExecute(Sender: TObject); TNeon.ObjectToJSONString(FDocument, TOpenAPISerializer.GetNeonConfig); end; -procedure TfrmMain.actJSONReplaceExecute(Sender: TObject); +procedure TfrmMain.actDocumentOpenExecute(Sender: TObject); +var + LDocument: TOpenAPIDocument; + LJSON: TJSONObject; + LConfig: INeonConfiguration; begin - memoDocument.Lines.Text := - StringReplace(memoDocument.Lines.Text, '\/', '/', [rfReplaceAll]); + if not dlgOpenJSON.Execute() then + Exit; + + LDocument := TOpenAPIDocument.Create(TOpenAPIVersion.v303); + try + LJSON := TJSONObject.ParseJSONValue(TFile.ReadAllText(dlgOpenJSON.FileName)) as TJSONObject; + try + LConfig := TOpenAPISerializer.GetNeonConfig; + + TNeon.JSONToObject(LDocument, LJSON, LConfig); + finally + LJSON.Free; + end; + + // Serialization in order to see the document loaded + memoDocument.Text := TNeon.ObjectToJSONString(LDocument, LConfig); + finally + LDocument.Free; + end; + FDocumentName := TPath.GetFileNameWithoutExtension(dlgOpenJSON.FileName); end; -procedure TfrmMain.FormCreate(Sender: TObject); +procedure TfrmMain.actDocumentSaveExecute(Sender: TObject); begin - FDocument := TOpenAPIDocument.Create(TOpenAPIVersion.v303); + dlgSaveDocument.FileName := FDocumentName + '.json'; + if not dlgSaveDocument.Execute then + Exit; + + memoDocument.Lines.SaveToFile(dlgSaveDocument.FileName, TEncoding.UTF8); +end; + +procedure TfrmMain.actJSONReplaceExecute(Sender: TObject); +begin + memoDocument.Lines.Text := + StringReplace(memoDocument.Lines.Text, '\/', '/', [rfReplaceAll]); end; end. diff --git a/Demos/OpenAPI/DemoOpenAPI.dproj b/Demos/OpenAPI/DemoOpenAPI.dproj index a775d7f..78782f6 100644 --- a/Demos/OpenAPI/DemoOpenAPI.dproj +++ b/Demos/OpenAPI/DemoOpenAPI.dproj @@ -1,7 +1,7 @@  {01D66204-181A-431C-9934-497B2123B2FA} - 19.3 + 19.5 VCL DemoOpenAPI.dpr True @@ -47,7 +47,7 @@ .\$(Platform)\$(Config) - .\$(Platform)\$(Config) + ..\Bin false false false @@ -85,7 +85,6 @@ false - true true 1033 PerMonitor @@ -139,17 +138,13 @@ DemoOpenAPI.dpr - Microsoft Office 2000 Sample Automation Server Wrapper Components - Microsoft Office XP Sample Automation Server Wrapper Components + Microsoft Office 2000 Sample Automation Server Wrapper Components + Microsoft Office XP Sample Automation Server Wrapper Components + File C:\WINDOWS\system32\dclSvComV8D28.bpl not found - - - - DemoOpenAPI.exe - true - - + + 1 @@ -172,16 +167,6 @@ 64 - - - classes - 1 - - - classes - 1 - - res\xml @@ -491,7 +476,7 @@ 1 .dylib - + 1 .dylib @@ -524,7 +509,7 @@ 1 .dylib - + 1 .dylib @@ -561,7 +546,7 @@ 0 - + 0 @@ -585,13 +570,17 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -601,71 +590,27 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - - - 1 - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - - - 1 - - - 1 - - - 1 - - ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 @@ -675,7 +620,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -685,7 +630,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -695,7 +640,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -705,7 +650,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -715,7 +660,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -725,60 +670,27 @@ ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - - - 1 - - - 1 - - - 1 - - ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - - - 1 - - - 1 - - - 1 - - - - - 1 - - - 1 - - - 1 - - ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 @@ -788,7 +700,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset 1 @@ -798,7 +710,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -808,7 +720,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -818,7 +730,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -828,7 +740,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -838,7 +750,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -848,7 +760,7 @@ ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 - + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset 1 @@ -870,12 +782,8 @@ ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 1 - - - - 1 - - + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 1 @@ -888,6 +796,10 @@ ..\ 1 + + ..\ + 1 + @@ -896,7 +808,7 @@ 1 - + 1 @@ -905,7 +817,7 @@ ..\$(PROJECTNAME).launchscreen 64 - + ..\$(PROJECTNAME).launchscreen 64 @@ -917,7 +829,7 @@ 1 - + 1 @@ -988,7 +900,7 @@ 1 - + 1 @@ -1044,17 +956,18 @@ 1 - - - - - - + - + + + + + + + True diff --git a/Libs/Neon b/Libs/Neon index c1bcf03..237394a 160000 --- a/Libs/Neon +++ b/Libs/Neon @@ -1 +1 @@ -Subproject commit c1bcf034f28f9677678dd46a2d647586c644f04e +Subproject commit 237394a66411c3b1fd556e6ec642b6246724548f diff --git a/README.md b/README.md index 8b1bbc6..bc29646 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenAPI for Delphi - OpenAPI 3.0 generator for Delphi +# OpenAPI for Delphi - OpenAPI 3.0 for Delphi
@@ -8,14 +8,18 @@ ## What is OpenAPI-Delphi -**OpenAPI-Delphi** is an OpenAPI 3.0 generator for [Delphi](https://www.embarcadero.com/products/delphi) that helps you to generate OpenAPI 3.0 documentation (in JSON) starting from plain Delphi classes. Delphi-OpenAPI uses the [Neon](https://github.com/paolo-rossi/delphi-neon) serialization library to transform the OpenAPI models from Delphi classes to JSON. Please take a look at the Demo to see OpenAPI-Delphi in action. +**OpenAPI-Delphi** is an OpenAPI 3.0 library for [Delphi](https://www.embarcadero.com/products/delphi) that helps you to generate (and load) OpenAPI 3.0 documentation (in JSON) starting from plain Delphi classes. Delphi-OpenAPI uses the [Neon](https://github.com/paolo-rossi/delphi-neon) serialization library to transform the OpenAPI models from Delphi classes to JSON and to load a OpenAPI document into a Delphi (OpenAPI) object. Please take a look at the Demo to see OpenAPI-Delphi in action. ## General Features -- Use plain Delphi classes to set the OpenAPI specification fields +- OpenAPI document generation (JSON) from a Delphi (OpenAPI) object +- OpenAPI loading and parsing into a Delphi (OpenAPI) object (:star2: new in 2.0) +- Use plain Delphi classes to set the OpenAPI specification sections & fields - Support for JSON Schema (the OpenAPI version) -- Use 1-line code (using the [Neon](https://github.com/paolo-rossi/delphi-neon) library) to transform into the JSON document -- Validation for the OpenAPI models (todo) +- Support for Schema field recursion (:star2: new in 2.0) +- Full Support for enum of any type (:star2: new in 2.0) +- Use 1-line code (using the [Neon](https://github.com/paolo-rossi/delphi-neon) library) to transform from and to JSON documents + +## Todo +- Full validation for the OpenAPI models -## TODO -- Better management of null objects (required by the OpenAPI specs) diff --git a/Source/OpenAPI.Model.Any.pas b/Source/OpenAPI.Model.Any.pas index 1029da2..16e2d8d 100644 --- a/Source/OpenAPI.Model.Any.pas +++ b/Source/OpenAPI.Model.Any.pas @@ -1,7 +1,7 @@ {******************************************************************************} { } { Delphi OpenAPI 3.0 Generator } -{ Copyright (c) 2018-2021 Paolo Rossi } +{ Copyright (c) 2018-2023 Paolo Rossi } { https://github.com/paolo-rossi/delphi-openapi } { } {******************************************************************************} @@ -27,7 +27,8 @@ interface System.Rtti, Neon.Core.Attributes, - Neon.Core.Nullables; + Neon.Core.Nullables, + OpenAPI.Model.Base; {$SCOPEDENUMS ON} @@ -179,6 +180,9 @@ TOpenAPIString = class(TOpenAPIAnyValue) constructor Create(const AValue: string); end; + /// + /// Class for representing... any value + /// TOpenAPIAny = class private FValue: TValue; @@ -189,6 +193,9 @@ TOpenAPIAny = class property Value: TValue read FValue write FValue; end; + TOpenAPIAnyValue = TValue; + TOpenAPIAnyValues = TOpenAPIList; + implementation { TOpenAPIAnyValue } diff --git a/Source/OpenAPI.Model.Base.pas b/Source/OpenAPI.Model.Base.pas index 9adfdec..6d7aa0d 100644 --- a/Source/OpenAPI.Model.Base.pas +++ b/Source/OpenAPI.Model.Base.pas @@ -50,15 +50,77 @@ TOpenAPIModel = class function CheckModel: Boolean; inline; end; + /// + /// Class for OpenAPI Extensions. Extensions are custom properties + /// and they can be used to describe extra functionality that is not + /// covered by the standard OpenAPI Specification. + /// + TOpenAPIExtensions = class(TOpenAPIModel) + private + FValues: TJSONObject; + procedure CheckName(const AName: string); + public + constructor Create; + + procedure Add(const AName: string; const AValue: NullString); overload; + procedure Add(const AName: string; const AValue: NullDouble); overload; + procedure Add(const AName: string; const AValue: NullInteger); overload; + procedure Add(const AName: string; const AValue: NullBoolean); overload; + procedure Add(const AName: string; const AValue: TJSONValue; AOWned: Boolean = True); overload; + public + [NeonUnwrapped] [NeonInclude(IncludeIf.NotEmpty)] + property Values: TJSONObject read FValues write FValues; + end; + + /// + /// Class for a property extension + /// + /// + /// Deprecated, use TOpenAPIExtensions + /// + TOpenAPIExtension = class(TObjectDictionary) + public + constructor Create; + end; + + /// + /// Base class for OpenAPI classes that are Extensible ( OpenAPIInfo, + /// OpenAPIPaths, OpenAPIPathItem, OpenAPIOperation, OpenAPIParameter, + /// OpenAPIResponses, OpenAPITag, OpenAPISecurityScheme) + /// + TOpenAPIExtensible = class(TOpenAPIModel) + protected + FExtensions: TOpenAPIExtensions; + public + constructor Create; + + /// + /// This object MAY be extended with Specification Extensions. + /// + [NeonUnwrapped] [NeonInclude(IncludeIf.NotEmpty)] + property Extensions: TOpenAPIExtensions read FExtensions write FExtensions; + end; + /// /// Class for the Reference Object. The Reference Object is defined /// by /// JSON Reference and follows the same structure, behavior and rules. /// - TOpenAPIModelReference = class(TOpenAPIModel) + TOpenAPIModelReference = class(TOpenAPIExtensible) protected + FUnresolvedReference: NullBoolean; FReference: TOpenAPIReference; public + constructor Create; + + function IsReference: Boolean; + + /// + /// Indicates object is a placeholder reference to an actual object and does not contain valid data. + /// + [NeonIgnore] + property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; + /// /// Reference object. /// @@ -66,51 +128,54 @@ TOpenAPIModelReference = class(TOpenAPIModel) property Reference: TOpenAPIReference read FReference write FReference; end; + /// - /// Class for a property extension + /// Base class for the value-based lists /// - /// - /// Deprecated, use TOpenAPIExtensions - /// - TOpenAPIExtension = class(TObjectDictionary) + TOpenAPIList = class(TList) public - constructor Create; + function IsEmpty: Boolean; end; /// - /// Class for OpenAPI Extensions. Extensions are custom properties - /// and they can be used to describe extra functionality that is not - /// covered by the standard OpenAPI Specification. + /// Base class for the OpenAPI lists /// - TOpenAPIExtensions = class(TOpenAPIModel) - private - FValues: TJSONObject; - procedure CheckName(const AName: string); + TOpenAPIModelList = class(TObjectList) public constructor Create; + function IsEmpty: Boolean; + end; - procedure AddValue(const AName: string; const AValue: NullString); overload; - procedure AddValue(const AName: string; const AValue: NullDouble); overload; - procedure AddValue(const AName: string; const AValue: NullInteger); overload; - procedure AddValue(const AName: string; const AValue: NullBoolean); overload; + /// + /// Value-owned Map + /// + TOpenAPIOwnedMap = class(TObjectDictionary) + public + constructor Create; + end; - procedure AddObject(const AName: string; AValue: TJSONObject); - procedure AddArray(const AName: string; AValue: TJSONArray); + /// + /// Base class for the OpenAPI Maps + /// + TOpenAPIModelMap = class(TOpenAPIOwnedMap) public - property Values: TJSONObject read FValues write FValues; + function IsEmpty: Boolean; end; /// - /// Base class for OpenAPI classes that are Extensible ( OpenAPIInfo, - /// OpenAPIPaths, OpenAPIPathItem, OpenAPIOperation, OpenAPIParameter, - /// OpenAPIResponses, OpenAPITag, OpenAPISecurityScheme) + /// Base class for the OpenAPI maps extensible with the Extension + /// Specifications /// - TOpenAPIExtensible = class(TOpenAPIModel) + TOpenAPIModelExtensibleMap = class(TOpenAPIModelMap) private FExtensions: TOpenAPIExtensions; public constructor Create; + destructor Destroy; override; + /// + /// This object MAY be extended with Specification Extensions. + /// [NeonUnwrapped] [NeonInclude(IncludeIf.NotEmpty)] property Extensions: TOpenAPIExtensions read FExtensions write FExtensions; end; @@ -159,39 +224,25 @@ constructor TOpenAPIExtension.Create; inherited Create(); end; -{ TOpenAPIExtensions } - -procedure TOpenAPIExtensions.AddArray(const AName: string; AValue: TJSONArray); -begin - CheckName(AName); - FValues.AddPair(AName, AValue.Clone as TJSONValue); -end; - -procedure TOpenAPIExtensions.AddObject(const AName: string; AValue: TJSONObject); -begin - CheckName(AName); - FValues.AddPair(AName, AValue.Clone as TJSONValue); -end; - -procedure TOpenAPIExtensions.AddValue(const AName: string; const AValue: NullString); +procedure TOpenAPIExtensions.Add(const AName: string; const AValue: NullString); begin CheckName(AName); FValues.AddPair(AName, AValue.Value); end; -procedure TOpenAPIExtensions.AddValue(const AName: string; const AValue: NullDouble); +procedure TOpenAPIExtensions.Add(const AName: string; const AValue: NullDouble); begin CheckName(AName); FValues.AddPair(AName, AValue.Value); end; -procedure TOpenAPIExtensions.AddValue(const AName: string; const AValue: NullInteger); +procedure TOpenAPIExtensions.Add(const AName: string; const AValue: NullInteger); begin CheckName(AName); FValues.AddPair(AName, AValue.Value); end; -procedure TOpenAPIExtensions.AddValue(const AName: string; const AValue: NullBoolean); +procedure TOpenAPIExtensions.Add(const AName: string; const AValue: NullBoolean); begin CheckName(AName); FValues.AddPair(AName, AValue.Value); @@ -209,6 +260,15 @@ constructor TOpenAPIExtensions.Create; FValues := CreateSubObject; end; +procedure TOpenAPIExtensions.Add(const AName: string; const AValue: TJSONValue; AOWned: Boolean); +begin + CheckName(AName); + if AOWned then + FValues.AddPair(AName, AValue) + else + FValues.AddPair(AName, AValue.Clone as TJSONValue); +end; + { TOpenAPIExtensible } constructor TOpenAPIExtensible.Create; @@ -217,4 +277,64 @@ constructor TOpenAPIExtensible.Create; FExtensions := CreateSubObject; end; +{ TOpenAPIModelReference } + +constructor TOpenAPIModelReference.Create; +begin + inherited Create; + FReference := CreateSubObject; +end; + +function TOpenAPIModelReference.IsReference: Boolean; +begin + Result := not FReference.Ref.IsEmpty; +end; + +{ TOpenAPIModelExtensibleMap } + +constructor TOpenAPIModelExtensibleMap.Create; +begin + inherited Create; + FExtensions := TOpenAPIExtensions.Create; +end; + +destructor TOpenAPIModelExtensibleMap.Destroy; +begin + FExtensions.Free; + inherited; +end; + +{ TOpenAPIModelList } + +constructor TOpenAPIModelList.Create; +begin + inherited Create(True); +end; + +function TOpenAPIModelList.IsEmpty: Boolean; +begin + Result := Count = 0; +end; + +{ TOpenAPIOwnedMap } + +constructor TOpenAPIOwnedMap.Create; +begin + inherited Create([doOwnsValues]); +end; + +{ TOpenAPIModelMap } + +function TOpenAPIModelMap.IsEmpty: Boolean; +begin + Result := Count = 0; +end; + +{ TOpenAPIList } + +function TOpenAPIList.IsEmpty: Boolean; +begin + Result := Count = 0; +end; + end. diff --git a/Source/OpenAPI.Model.Classes.pas b/Source/OpenAPI.Model.Classes.pas index d290db4..c79e9a8 100644 --- a/Source/OpenAPI.Model.Classes.pas +++ b/Source/OpenAPI.Model.Classes.pas @@ -26,8 +26,7 @@ interface {$SCOPEDENUMS ON} uses - System.SysUtils, System.Classes, System.Generics.Collections, System.JSON, - System.Rtti, + System.SysUtils, System.Classes, System.Generics.Collections, System.Rtti, Neon.Core.Types, Neon.Core.Nullables, @@ -201,7 +200,7 @@ TOperationTypeHelper = record helper for TOperationType /// /// Contact information for the exposed API /// - TOpenAPIContact = class(TOpenAPIModel) + TOpenAPIContact = class(TOpenAPIExtensible) private FName: NullString; FUrl: NullString; @@ -227,7 +226,7 @@ TOpenAPIContact = class(TOpenAPIModel) /// /// License information for the exposed API /// - TOpenAPILicense = class(TOpenAPIModel) + TOpenAPILicense = class(TOpenAPIExtensible) private FName: NullString; FUrl: NullString; @@ -246,7 +245,7 @@ TOpenAPILicense = class(TOpenAPIModel) /// /// ExternalDocs object /// - TOpenAPIExternalDocumentation = class(TOpenAPIModel) + TOpenAPIExternalDocumentation = class(TOpenAPIExtensible) private FDescription: NullString; FUrl: string; @@ -268,7 +267,7 @@ TOpenAPIMediaTypeMap = class; /// /// Parameter Object /// - TOpenAPIParameter = class(TOpenAPIModel) + TOpenAPIParameter = class(TOpenAPIExtensible) private FAllowEmptyValue: NullBoolean; FDeprecated_: NullBoolean; @@ -394,16 +393,10 @@ TOpenAPIParameter = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Content: TOpenApiMediaTypeMap read FContent write FContent; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: IOpenApiExtension; end; - TOpenAPIParameters = class(TObjectList) + TOpenAPIParameters = class(TOpenAPIModelList) public - constructor Create; function ParamExists(AParam: TOpenAPIParameter): Boolean; overload; function ParamExists(const AName, ALocation: string): Boolean; overload; function FindParam(const AName, ALocation: string): TOpenAPIParameter; @@ -412,10 +405,7 @@ TOpenAPIParameters = class(TObjectList) /// /// Used only in Components /// - TOpenAPIParameterMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIParameterMap = class(TOpenAPIModelMap); /// /// Example Object. @@ -425,8 +415,6 @@ TOpenAPIExample = class(TOpenAPIModelReference) FSummary: NullString; FDescription: NullString; FExternalValue: NullString; - FReference: TOpenAPIReference; - FUnresolvedReference: NullBoolean; FValue: TOpenAPIAny; public constructor Create; @@ -456,32 +444,18 @@ TOpenAPIExample = class(TOpenAPIModelReference) /// The value field and externalValue field are mutually exclusive. /// property ExternalValue: NullString read FExternalValue write FExternalValue; - - /// - /// Reference object. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; - - /// - /// Indicates object is a placeholder reference to an actual object and does not contain valid data. - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; end; - TOpenAPIExamples = class(TObjectList) - end; - - TOpenAPIExampleMap = class(TObjectDictionary) - end; + TOpenAPIExamples = class(TOpenAPIModelList); + TOpenAPIExampleMap = class(TOpenAPIModelMap); TOpenAPIEncoding = class; TOpenAPIEncodingMap = class; - + /// /// MediaType Object. /// - TOpenAPIMediaType = class(TOpenAPIModel) + TOpenAPIMediaType = class(TOpenAPIExtensible) private FSchema: TOpenAPISchema; FExamples: TOpenAPIExampleMap; @@ -517,28 +491,15 @@ TOpenAPIMediaType = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Encoding: TOpenAPIEncodingMap read FEncoding write FEncoding; - - /// - /// Serialize to Open Api v3.0. - /// - //property Extensions: TObjectDictionary; - end; - - TOpenAPIMediaTypes = class(TObjectList) end; - TOpenAPIMediaTypeMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIMediaTypeMap = class(TOpenAPIModelMap); /// /// Header Object. /// TOpenAPIHeader = class(TOpenAPIModelReference) private - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; FDescription: NullString; FRequired: NullBoolean; FDeprecated_: NullBoolean; @@ -553,16 +514,6 @@ TOpenAPIHeader = class(TOpenAPIModelReference) public constructor Create; public - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference pointer. - /// - property Reference: TOpenAPIReference read FReference write FReference; - /// /// A brief description of the header. /// @@ -608,6 +559,7 @@ TOpenAPIHeader = class(TOpenAPIModelReference) /// /// Example of the media type. /// + [NeonInclude(IncludeIf.NotEmpty)] property Example: TOpenAPIAny read FExample write FExample; /// @@ -621,22 +573,11 @@ TOpenAPIHeader = class(TOpenAPIModelReference) /// [NeonInclude(IncludeIf.NotEmpty)] property Content: TOpenAPIMediaTypeMap read FContent write FContent; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //Extensions: TObjectDictionary; end; - TOpenAPIHeaders = class(TObjectList) - end; - - TOpenAPIHeaderMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIHeaderMap = class(TOpenAPIModelMap); - TOpenAPIEncoding = class(TOpenAPIModel) + TOpenAPIEncoding = class(TOpenAPIExtensible) private FContentType: NullString; FHeaders: TOpenAPIHeaderMap; @@ -680,20 +621,11 @@ TOpenAPIEncoding = class(TOpenAPIModel) /// if the request body media type is not application/x-www-form-Urlencoded. /// property AllowReserved: NullBoolean read FAllowReserved write FAllowReserved; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; end; - TOpenAPIEncodings = class(TObjectList) - end; - - TOpenAPIEncodingMap = class(TObjectDictionary) - end; + TOpenAPIEncodingMap = class(TOpenAPIModelMap); - TOpenAPIExternalDocs = class(TOpenAPIModel) + TOpenAPIExternalDocs = class(TOpenAPIExtensible) private FDescription: NullString; FUrl: string; @@ -707,11 +639,6 @@ TOpenAPIExternalDocs = class(TOpenAPIModel) /// REQUIRED. The Url for the target documentation. Value MUST be in the format of a Url. /// property Url: string read FUrl write FUrl; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //Extensions: TObjectDictionary; end; TOpenAPITag = class(TOpenAPIModelReference) @@ -719,8 +646,6 @@ TOpenAPITag = class(TOpenAPIModelReference) FName: NullString; FDescription: NullString; FExternalDocs: TOpenAPIExternalDocs; - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; public constructor Create; public @@ -739,31 +664,12 @@ TOpenAPITag = class(TOpenAPIModelReference) /// [NeonInclude(IncludeIf.NotEmpty)] property ExternalDocs: TOpenAPIExternalDocs read FExternalDocs write FExternalDocs; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //Extensions: TObjectDictionary; - - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; end; - TOpenAPITags = class(TObjectList) - end; + TOpenAPITags = class(TOpenAPIModelList); TOpenAPIRequestBody = class(TOpenAPIModelReference) private - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; FDescription: NullString; FRequired: NullBoolean; FContent: TOpenAPIMediaTypeMap; @@ -771,17 +677,6 @@ TOpenAPIRequestBody = class(TOpenAPIModelReference) constructor Create; function AddMediaType(const AKeyName: string): TOpenAPIMediaType; public - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference object. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; - /// /// A brief description of the request body. This could contain examples of use. /// CommonMark syntax MAY be used for rich text representation. @@ -798,23 +693,14 @@ TOpenAPIRequestBody = class(TOpenAPIModelReference) /// For requests that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/* /// property Content: TOpenAPIMediaTypeMap read FContent write FContent; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; - end; - - TOpenAPIRequestBodyMap = class(TObjectDictionary) - public - constructor Create; end; + TOpenAPIRequestBodyMap = class(TOpenAPIModelMap); /// /// An object representing a Server Variable for server Url template substitution /// - TOpenAPIServerVariable = class(TOpenAPIModel) + TOpenAPIServerVariable = class(TOpenAPIExtensible) private FEnum: TArray; FDefault_: string; @@ -839,15 +725,12 @@ TOpenAPIServerVariable = class(TOpenAPIModel) property Enum: TArray read FEnum write FEnum; end; - TOpenAPIServerVariableMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIServerVariableMap = class(TOpenAPIModelMap); /// /// An object representing a Server /// - TOpenAPIServer = class(TOpenAPIModel) + TOpenAPIServer = class(TOpenAPIExtensible) private FDescription: NullString; FVariables: TOpenAPIServerVariableMap; @@ -875,20 +758,14 @@ TOpenAPIServer = class(TOpenAPIModel) property Variables: TOpenAPIServerVariableMap read FVariables write FVariables; end; - TOpenAPIServers = class(TObjectList) - public - constructor Create; - end; - - TOpenAPIServerMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIServers = class(TOpenAPIModelList); + TOpenAPIServerMap = class(TOpenAPIModelMap); /// - /// Link Object. + /// Link Object. The Link Object represents a possible design-time link for + /// a response /// - TOpenAPILink = class(TOpenAPIModel) + TOpenAPILink = class(TOpenAPIExtensible) private FOperationId: NullString; FOperationRef: NullString; @@ -934,18 +811,12 @@ TOpenAPILink = class(TOpenAPIModel) property Server: TOpenAPIServer read FServer write FServer; end; - TOpenAPILinks = class(TObjectList) - end; - - TOpenAPILinkMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPILinkMap = class(TOpenAPIModelMap); /// /// Response object. /// - TOpenAPIResponse = class(TOpenAPIModel) + TOpenAPIResponse = class(TOpenAPIModelReference) private FDescription: string; FLinks: TOpenAPILinkMap; @@ -985,18 +856,13 @@ TOpenAPIResponse = class(TOpenAPIModel) property Links: TOpenAPILinkMap read FLinks write FLinks; end; - TOpenAPIResponseMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIResponseMap = class(TOpenAPIModelExtensibleMap); TOpenAPIPathItem = class; TOpenAPICallback = class(TOpenAPIModelReference) private FPathItems: TObjectDictionary; - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; public constructor Create; public @@ -1004,33 +870,11 @@ TOpenAPICallback = class(TOpenAPIModelReference) /// A Path Item Object used to define a callback request and expected responses. /// property PathItems: TObjectDictionary read FPathItems write FPathItems; - - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference pointer. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; end; - TOpenAPICallbacks = class(TObjectList) - end; + TOpenAPICallbackMap = class(TOpenAPIModelMap); - TOpenAPICallbackMap = class(TObjectDictionary) - public - constructor Create; - end; - - TOpenAPIOAuthFlow = class(TOpenAPIModel) + TOpenAPIOAuthFlow = class(TOpenAPIExtensible) private FAuthorizationUrl: string; FTokenUrl: string; @@ -1061,14 +905,9 @@ TOpenAPIOAuthFlow = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Scopes: TDictionary read FScopes write FScopes; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; end; - TOpenAPIOAuthFlows = class(TOpenAPIModel) + TOpenAPIOAuthFlows = class(TOpenAPIExtensible) private FImplicit: TOpenAPIOAuthFlow; FPassword: TOpenAPIOAuthFlow; @@ -1116,8 +955,6 @@ TOpenAPISecurityScheme = class(TOpenAPIModelReference) FBearerFormat: NullString; FFlows: TOpenAPIOAuthFlows; FOpenIdConnectUrl: string; - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; public constructor Create; function ShouldInclude(const AContext: TNeonIgnoreIfContext): Boolean; @@ -1172,33 +1009,12 @@ TOpenAPISecurityScheme = class(TOpenAPIModelReference) /// [NeonInclude(IncludeIf.CustomFunction)] property OpenIdConnectUrl: string read FOpenIdConnectUrl write FOpenIdConnectUrl; - - /// - /// Specification Extensions. - /// - //property Extensions: TObjectDictionary; - - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference object. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; end; - TOpenAPISecuritySchemeMap = class(TObjectDictionary) - public - constructor Create; - end; - - TOpenAPISecurityRequirement = class(TDictionary>) - end; + TOpenAPISecuritySchemeMap = class(TOpenAPIModelMap); + TOpenAPISecurityRequirement = class(TDictionary>); - TOpenAPISecurityRequirements = class(TObjectList) + TOpenAPISecurityRequirements = class(TOpenAPIModelList) public procedure AddSecurityRequirement(ASecuritySchemes: TOpenAPISecuritySchemeMap; ASchemeName: string; AParams: TArray); @@ -1207,7 +1023,7 @@ TOpenAPISecurityRequirements = class(TObjectList) /// /// Operation Object /// - TOpenAPIOperation = class(TOpenAPIModel) + TOpenAPIOperation = class(TOpenAPIExtensible) private FTags: TArray; FSummary: NullString; @@ -1223,6 +1039,7 @@ TOpenAPIOperation = class(TOpenAPIModel) FResponses: TOpenAPIResponseMap; public constructor Create; + destructor Destroy; override; public procedure AddTag(const AName: string); function AddResponse(ACode: Integer): TOpenAPIResponse; overload; @@ -1277,6 +1094,7 @@ TOpenAPIOperation = class(TOpenAPIModel) /// In other cases where the HTTP spec is vague, requestBody SHALL be ignored by consumers. /// [NeonInclude(IncludeIf.NotEmpty)] + [NeonAutoCreate] property RequestBody: TOpenAPIRequestBody read FRequestBody write FRequestBody; /// @@ -1319,22 +1137,14 @@ TOpenAPIOperation = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Servers: TOpenAPIServers read FServers write FServers; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property TObjectDictionary Extensions { get; set; } = new Dictionary(); end; - TOpenAPIOperationMap = class (TObjectDictionary) - public - constructor Create; - end; + TOpenAPIOperationMap = class (TOpenAPIOwnedMap); /// /// The object provides metadata about the API /// - TOpenAPIInfo = class(TOpenAPIModel) + TOpenAPIInfo = class(TOpenAPIExtensible) private FContact: TOpenAPIContact; FDescription: NullString; @@ -1342,8 +1152,6 @@ TOpenAPIInfo = class(TOpenAPIModel) FTermsOfService: NullString; FTitle: string; FVersion: string; - FExtensions: TJSONObject; - //FExt: TOpenAPIExtension; public constructor Create; public @@ -1378,20 +1186,12 @@ TOpenAPIInfo = class(TOpenAPIModel) /// REQUIRED. The version of the OpenAPI document. /// property Version: string read FVersion write FVersion; - - /// - /// This object MAY be extended with Specification Extensions. - /// - [NeonUnwrapped] [NeonInclude(IncludeIf.NotEmpty)] - property Extensions: TJSONObject read FExtensions write FExtensions; - - //property Ext: TOpenAPIExtension read FExt write FExt; end; /// /// Component Object. /// - TOpenAPIComponents = class(TOpenAPIModel) + TOpenAPIComponents = class(TOpenAPIExtensible) private FSchemas: TOpenAPISchemaMap; FResponses: TOpenAPIResponseMap; @@ -1469,17 +1269,12 @@ TOpenAPIComponents = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Callbacks: TOpenAPICallbackMap read FCallbacks write FCallbacks; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; end; /// /// Path Item Object: to describe the operations available on a single path. /// - TOpenAPIPathItem = class(TOpenAPIModel) + TOpenAPIPathItem = class(TOpenAPIExtensible) private FSummary: NullString; FDescription: NullString; @@ -1570,18 +1365,9 @@ TOpenAPIPathItem = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property Parameters: TOpenAPIParameters read FParameters write FParameters; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //property Extensions: TObjectDictionary; - end; - - TOpenAPIPathMap = class(TObjectDictionary) - public - constructor Create; end; + TOpenAPIPathMap = class(TOpenAPIModelExtensibleMap); TOpenAPIVersion = (v303, v310); TOpenAPIVersionHelper = record helper for TOpenAPIVersion @@ -1593,7 +1379,7 @@ TOpenAPIVersionHelper = record helper for TOpenAPIVersion /// A document (or set of documents) that defines or describes an API. An Openapi /// definition uses and conforms to the Openapi Specification /// - TOpenAPIDocument = class(TOpenAPIModel) + TOpenAPIDocument = class(TOpenAPIExtensible) private FInfo: TOpenAPIInfo; FOpenapi: string; @@ -1603,7 +1389,6 @@ TOpenAPIDocument = class(TOpenAPIModel) FSecurity: TOpenAPISecurityRequirements; FTags: TOpenAPITags; FExternalDocs: TOpenAPIExternalDocs; - FExtensions: TJSONObject; public constructor Create(AVersion: TOpenAPIVersion); public @@ -1658,12 +1443,6 @@ TOpenAPIDocument = class(TOpenAPIModel) /// [NeonInclude(IncludeIf.NotEmpty)] property ExternalDocs: TOpenAPIExternalDocs read FExternalDocs write FExternalDocs; - - /// - /// This object MAY be extended with Specification Extensions. - /// - [NeonUnwrapped] [NeonInclude(IncludeIf.NotEmpty)] - property Extensions: TJSONObject read FExtensions write FExtensions; end; @@ -1832,13 +1611,6 @@ constructor TOpenAPILink.Create; FServer := CreateSubObject; end; -{ TOpenAPIParameterMap } - -constructor TOpenAPIParameterMap.Create; -begin - inherited Create([doOwnsValues]); -end; - { TOpenAPIDocument } function TOpenAPIDocument.AddPath(const AKeyName: string): TOpenAPIPathItem; @@ -1899,7 +1671,6 @@ constructor TOpenAPIDocument.Create(AVersion: TOpenAPIVersion); FSecurity := CreateSubObject; FTags := CreateSubObject; //FExternalDocs := CreateSubObject; - FExtensions := CreateSubObject; end; procedure TOpenAPIDocument.ReplaceInfo(AInfo: TOpenAPIInfo); @@ -1909,13 +1680,6 @@ procedure TOpenAPIDocument.ReplaceInfo(AInfo: TOpenAPIInfo); FInfo := AInfo; end; -{ TOpenAPIPathMap } - -constructor TOpenAPIPathMap.Create; -begin - inherited Create([doOwnsValues]); -end; - { TOpenAPIInfo } constructor TOpenAPIInfo.Create; @@ -1924,57 +1688,6 @@ constructor TOpenAPIInfo.Create; FContact := CreateSubObject; FLicense := CreateSubObject; - FExtensions := CreateSubObject; - //FExt := CreateSubObject; -end; - -{ TOpenAPIServers } - -constructor TOpenAPIServers.Create; -begin - inherited Create(True); -end; - -{ TOpenAPICallbackMap } - -constructor TOpenAPICallbackMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPIResponseMap } - -constructor TOpenAPIResponseMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPIServerMap } - -constructor TOpenAPIServerMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPILinkMap } - -constructor TOpenAPILinkMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPIHeaderMap } - -constructor TOpenAPIHeaderMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPIMediaTypeMap } - -constructor TOpenAPIMediaTypeMap.Create; -begin - inherited Create([doOwnsValues]); end; { TOpenAPIPathItem } @@ -1987,7 +1700,6 @@ function TOpenAPIPathItem.AddOperation(const AType: TOperationType): TOpenAPIOpe ASource := CreateSubObject; Result := ASource; end; - begin case AType of TOperationType.Get: Result := GetOrCreate(FGet); @@ -2031,13 +1743,6 @@ constructor TOpenAPIPathItem.Create; FParameters := CreateSubObject; end; -{ TOpenAPIOperationMap } - -constructor TOpenAPIOperationMap.Create; -begin - inherited Create([doOwnsValues]); -end; - { TOpenAPIOperation } function TOpenAPIOperation.AddParameter(const AName, ALocation: string): TOpenAPIParameter; @@ -2094,13 +1799,18 @@ constructor TOpenAPIOperation.Create; FResponses := CreateSubObject; end; +destructor TOpenAPIOperation.Destroy; +begin + FRequestBody.Free; + inherited; +end; + { TOpenAPIRequestBody } constructor TOpenAPIRequestBody.Create; begin inherited Create; - FReference := CreateSubObject; FContent := CreateSubObject; end; @@ -2120,7 +1830,6 @@ constructor TOpenAPITag.Create; inherited Create; //FExternalDocs: CreateSubObject; - FReference := CreateSubObject; end; { TOpenAPIExample } @@ -2129,7 +1838,6 @@ constructor TOpenAPIExample.Create; begin inherited Create; - FReference := CreateSubObject; FValue := CreateSubObject; end; @@ -2151,7 +1859,6 @@ constructor TOpenAPIHeader.Create; begin inherited Create; - FReference := CreateSubObject; FSchema := CreateSubObject; FExamples := CreateSubObject; FContent := CreateSubObject; @@ -2196,8 +1903,6 @@ function TOpenAPIParameter.GetHash: string; constructor TOpenAPICallback.Create; begin inherited Create; - - FReference := CreateSubObject; end; { TOpenAPIOAuthFlow } @@ -2213,6 +1918,8 @@ constructor TOpenAPIOAuthFlow.Create; constructor TOpenAPIOAuthFlows.Create; begin + inherited Create; + FImplicit := CreateSubObject; FPassword := CreateSubObject; FClientCredentials := CreateSubObject; @@ -2226,7 +1933,6 @@ constructor TOpenAPISecurityScheme.Create; inherited Create; FFlows := CreateSubObject; - FReference := CreateSubObject; end; function TOpenAPISecurityScheme.ShouldInclude(const AContext: TNeonIgnoreIfContext): Boolean; @@ -2265,20 +1971,6 @@ function TOpenAPISecurityScheme.ShouldInclude(const AContext: TNeonIgnoreIfConte end end; -{ TOpenAPIRequestBodyMap } - -constructor TOpenAPIRequestBodyMap.Create; -begin - inherited Create([doOwnsValues]); -end; - -{ TOpenAPISecuritySchemeMap } - -constructor TOpenAPISecuritySchemeMap.Create; -begin - inherited Create([doOwnsValues]); -end; - { TOperationTypeHelper } class function TOperationTypeHelper.FromString(const AValue: string): TOperationType; @@ -2346,11 +2038,6 @@ function TOpenAPIParameters.ParamExists(AParam: TOpenAPIParameter): Boolean; Exit(True); end; -constructor TOpenAPIParameters.Create; -begin - inherited Create(True); -end; - function TOpenAPIParameters.FindParam(const AName, ALocation: string): TOpenAPIParameter; var LParam: TOpenAPIParameter; @@ -2382,11 +2069,4 @@ function TOpenAPIVersionHelper.ToString: string; end; end; -{ TOpenAPIServerVariableMap } - -constructor TOpenAPIServerVariableMap.Create; -begin - inherited Create([doOwnsValues]); -end; - end. diff --git a/Source/OpenAPI.Model.Schema.pas b/Source/OpenAPI.Model.Schema.pas index ba16c8e..9b6697f 100644 --- a/Source/OpenAPI.Model.Schema.pas +++ b/Source/OpenAPI.Model.Schema.pas @@ -24,7 +24,7 @@ interface uses - System.Classes, System.Generics.Collections, System.JSON, System.Rtti, + System.SysUtils, System.Classes, System.Generics.Collections, System.JSON, System.Rtti, Neon.Core.Attributes, Neon.Core.Nullables, @@ -54,7 +54,6 @@ TOpenAPIDiscriminator = class /// [NeonInclude(IncludeIf.NotEmpty)] property Mapping: TDictionary read FMapping write FMapping; - end; TOpenAPISchemaBase = class @@ -90,17 +89,14 @@ TOpenAPISchemaBase = class property Description: NullString read FDescription write FDescription; end; - TOpenAPISchemas = class(TObjectList) - public - constructor Create; - end; + TOpenAPISchemas = class(TOpenAPIModelList); + TOpenAPISchemaMap = class(TOpenAPIModelMap); - TOpenAPISchemaMap = class(TObjectDictionary) - public - constructor Create; - end; + TOpenAPIEnum = class(TOpenAPIModelList); TOpenAPISchema = class(TOpenAPIModelReference) + private + FJSONObject: TJSONObject; private FFormat: NullString; FTitle: NullString; @@ -131,20 +127,19 @@ TOpenAPISchema = class(TOpenAPIModelReference) FAdditionalPropertiesAllowed: NullBoolean; FAdditionalProperties: TOpenAPISchema; FNullable: NullBoolean; - FUnresolvedReference: NullBoolean; - FReference: TOpenAPIReference; - //FDefault_: TOpenAPISchema; - FEnum: TOpenAPIAny; + FDefault_: TOpenAPIAny; + FEnum: TOpenAPIEnum; FDiscriminator: TOpenAPIDiscriminator; - FJSONObject: TJSONObject; public constructor Create; + destructor Destroy; override; public function AddProperty(const AKeyName: string): TOpenAPISchema; + function AddEnum(const AValue: TValue): TOpenAPIAny; + procedure SetJSONObject(AJSON: TJSONObject); procedure SetSchemaReference(const AReference: string); - - //procedure SetSchemaJSON(const AReference: string); overload; + function IsEmpty: Boolean; [NeonIgnore] property JSONObject: TJSONObject read FJSONObject write FJSONObject; @@ -221,9 +216,8 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level. /// For example, if type is string, then default can be "foo" but cannot be 1. /// - //[NeonProperty('default')] [NeonInclude(IncludeIf.NotEmpty)] - //property Default_: TOpenAPISchema read FDefault_ write FDefault_; - { TODO -opaolo -c : Il risultato è un valore (Any) 21/05/2019 14:47:36 } + [NeonProperty('default')] [NeonInclude(IncludeIf.NotEmpty)] + property Default_: TOpenAPIAny read FDefault_ write FDefault_; /// /// Relevant only for Schema "properties" definitions. Declares the property as "read only". @@ -274,6 +268,7 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// [NeonInclude(IncludeIf.NotNull)] [NeonProperty('not')] + [NeonAutoCreate] property Not_: TOpenAPISchema read FNot_ write FNot_; /// @@ -288,6 +283,7 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// and not a standard JSON Schema. items MUST be present if the type is array. /// [NeonInclude(IncludeIf.NotNull)] + [NeonAutoCreate] property Items: TOpenAPISchema read FItems write FItems; /// @@ -333,6 +329,7 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// MUST be of a Schema Object and not a standard JSON Schema. /// [NeonInclude(IncludeIf.NotNull)] + [NeonAutoCreate] property AdditionalProperties: TOpenAPISchema read FAdditionalProperties write FAdditionalProperties; /// @@ -347,13 +344,14 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// To represent examples that cannot be naturally represented in JSON or YAML, /// a string value can be used to contain the example with escaping where necessary. /// - //public IOpenApiAny Example + //property Example IOpenApiAny /// /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 /// [NeonInclude(IncludeIf.NotEmpty)] - property Enum: TOpenAPIAny read FEnum write FEnum; + //property Enum: TOpenAPIAny read FEnum write FEnum; + property Enum: TOpenAPIEnum read FEnum write FEnum; /// /// Allows sending a null value for the defined schema. Default value is false. @@ -364,22 +362,6 @@ TOpenAPISchema = class(TOpenAPIModelReference) /// Additional external documentation for this schema. /// //ExternalDocs: TOpenApiExternalDocs; - - /// - /// This object MAY be extended with Specification Extensions. - /// - //public IDictionary Extensions = new Dictionary(); - - /// - /// Indicates object is a placeholder reference to an actual object and does not contain valid data. - /// - property UnresolvedReference: NullBoolean read FUnresolvedReference write FUnresolvedReference; - - /// - /// Reference object. - /// - [NeonInclude(IncludeIf.NotEmpty)] - property Reference: TOpenAPIReference read FReference write FReference; end; implementation @@ -399,6 +381,13 @@ destructor TOpenAPIDiscriminator.Destroy; { TOpenAPISchema } +function TOpenAPISchema.AddEnum(const AValue: TValue): TOpenAPIAny; +begin + Result := TOpenAPIAny.Create; + Result.Value := AValue; + FEnum.Add(Result); +end; + function TOpenAPISchema.AddProperty(const AKeyName: string): TOpenAPISchema; begin Result := TOpenAPISchema.Create; @@ -410,7 +399,6 @@ constructor TOpenAPISchema.Create; begin inherited Create; - //FDefault_ := CreateSubObject; FAllOf := CreateSubObject; FOneOf := CreateSubObject; FAnyOf := CreateSubObject; @@ -419,34 +407,38 @@ constructor TOpenAPISchema.Create; FProperties := CreateSubObject; FDiscriminator := CreateSubObject; //FAdditionalProperties := CreateSubObject; - FReference := CreateSubObject; - FEnum := CreateSubObject; + FDefault_ := CreateSubObject; + FEnum := CreateSubObject; end; -procedure TOpenAPISchema.SetJSONObject(AJSON: TJSONObject); +destructor TOpenAPISchema.Destroy; begin - if Assigned(FJSONObject) then - FJSONObject.Free; - FJSONObject := AJSON; + // You need to destroy these in case Neon creates them + FNot_.Free; + FItems.Free; + FAdditionalProperties.Free; + + inherited; end; -procedure TOpenAPISchema.SetSchemaReference(const AReference: string); +function TOpenAPISchema.IsEmpty: Boolean; begin - Reference.Ref := '#/components/schemas/' + AReference; + Result := FType_.IsNull and FTitle.IsNull and FFormat.IsNull and + FAllOf.IsEmpty and FAnyOf.IsEmpty and FOneOf.IsEmpty and FProperties.IsEmpty and + not IsReference(); end; -{ TOpenAPISchemaMap } - -constructor TOpenAPISchemaMap.Create; +procedure TOpenAPISchema.SetJSONObject(AJSON: TJSONObject); begin - inherited Create([doOwnsValues]); + if Assigned(FJSONObject) then + FJSONObject.Free; + FJSONObject := AJSON; + AddSubObject(FJSONObject); end; -{ TOpenAPISchemas } - -constructor TOpenAPISchemas.Create; +procedure TOpenAPISchema.SetSchemaReference(const AReference: string); begin - inherited Create(True); + Reference.Ref := '#/components/schemas/' + AReference; end; end. diff --git a/Source/OpenAPI.Neon.Serializers.pas b/Source/OpenAPI.Neon.Serializers.pas index 4fad451..8e906f1 100644 --- a/Source/OpenAPI.Neon.Serializers.pas +++ b/Source/OpenAPI.Neon.Serializers.pas @@ -1,7 +1,7 @@ {******************************************************************************} { } { Delphi OpenAPI 3.0 Generator } -{ Copyright (c) 2018-2021 Paolo Rossi } +{ Copyright (c) 2018-2023 Paolo Rossi } { https://github.com/paolo-rossi/delphi-openapi } { } {******************************************************************************} @@ -35,6 +35,7 @@ interface OpenAPI.Model.Any, OpenAPI.Model.Base, OpenAPI.Model.Classes, + OpenAPI.Model.Reference, OpenAPI.Model.Schema; type @@ -114,6 +115,24 @@ TOpenAPIReferenceSerializer = class(TCustomSerializer) function Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; override; end; + TOpenAPIPathItemSerializer = class(TCustomSerializer) + protected + class function GetTargetInfo: PTypeInfo; override; + class function CanHandle(AType: PTypeInfo): Boolean; override; + public + function Serialize(const AValue: TValue; ANeonObject: TNeonRttiObject; AContext: ISerializerContext): TJSONValue; override; + function Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; override; + end; + + TOpenAPIExtensionsSerializer = class(TCustomSerializer) + protected + class function GetTargetInfo: PTypeInfo; override; + class function CanHandle(AType: PTypeInfo): Boolean; override; + public + function Serialize(const AValue: TValue; ANeonObject: TNeonRttiObject; AContext: ISerializerContext): TJSONValue; override; + function Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; override; + end; + TOpenAPISchemaSerializer = class(TCustomSerializer) protected class function GetTargetInfo: PTypeInfo; override; @@ -359,9 +378,11 @@ class function TOpenAPISerializer.GetNeonConfig: INeonConfiguration; .RegisterSerializer(TNullableDoubleSerializer) .RegisterSerializer(TNullableTDateTimeSerializer) // OpenAPI Models - //.RegisterSerializer(TOpenAPIAnySerializer) .RegisterSerializer(TOpenAPIReferenceSerializer) .RegisterSerializer(TOpenAPISchemaSerializer) + .RegisterSerializer(TOpenAPIAnySerializer) + .RegisterSerializer(TOpenAPIPathItemSerializer) + .RegisterSerializer(TOpenAPIExtensionsSerializer) ; end; @@ -377,8 +398,18 @@ class function TOpenAPIAnySerializer.CanHandle(AType: PTypeInfo): Boolean; function TOpenAPIAnySerializer.Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; +var + LAny: TOpenAPIAny; begin - Result := nil; + Result := AData; + LAny := AData.AsObject as TOpenAPIAny; + + if AValue is TJSONNumber then + LAny.ValueFrom((AValue as TJSONNumber).AsDouble) + else if AValue is TJSONString then + LAny.ValueFrom((AValue as TJSONString).Value) + else if AValue is TJSONBool then + LAny.ValueFrom((AValue as TJSONBool).AsBoolean); end; class function TOpenAPIAnySerializer.GetTargetInfo: PTypeInfo; @@ -402,7 +433,7 @@ function TOpenAPIAnySerializer.Serialize(const AValue: TValue; case ANeonObject.NeonInclude.Value of IncludeIf.NotEmpty, IncludeIf.NotDefault: begin - if (Result as TJSONObject).Count = 0 then + if not TJSONUtils.IsNotDefault(Result) then FreeAndNil(Result); end; end; @@ -417,8 +448,25 @@ class function TOpenAPIReferenceSerializer.CanHandle(AType: PTypeInfo): Boolean; function TOpenAPIReferenceSerializer.Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; +var + LType: TRttiType; + LRef: TOpenAPIModelReference; + LJSON: TJSONObject; begin - Result := nil; + Result := AData; + LRef := AData.AsObject as TOpenAPIModelReference; + LJSON := AValue as TJSONObject; + + if Assigned(LJSON.Values['$ref']) then + begin + LType := TRttiUtils.Context.GetType(TOpenAPIReference); + AContext.ReadDataMember(AValue, LType, LRef.Reference, False); + end + else + begin + LType := TRttiUtils.Context.GetType(AData.AsObject.ClassType); + AContext.ReadDataMember(AValue, LType, AData, False); + end; end; class function TOpenAPIReferenceSerializer.GetTargetInfo: PTypeInfo; @@ -437,8 +485,7 @@ function TOpenAPIReferenceSerializer.Serialize(const AValue: TValue; Exit(nil); if Assigned(LRefObj.Reference) and not (LRefObj.Reference.Ref.IsEmpty) then - Result := TJSONString.Create(LRefObj.Reference.Ref) - //AContext.WriteDataMember(LRefObj.Reference) + Exit(TJSONString.Create(LRefObj.Reference.Ref)) else begin LType := TRttiUtils.Context.GetType(AValue.TypeInfo); @@ -466,8 +513,27 @@ class function TOpenAPISchemaSerializer.CanHandle(AType: PTypeInfo): Boolean; function TOpenAPISchemaSerializer.Deserialize(AValue: TJSONValue; const AData: TValue; ANeonObject: TNeonRttiObject; AContext: IDeserializerContext): TValue; +var + LType: TRttiType; + LSchema: TOpenAPISchema; + LJSONSchema: TJSONObject; begin + Result := AData; + + LSchema := Result.AsObject as TOpenAPISchema; + LJSONSchema := AValue as TJSONObject; + if Assigned(LJSONSchema.Values['$ref']) then + begin + LType := TRttiUtils.Context.GetType(TOpenAPIReference); + AContext.ReadDataMember(AValue, LType, LSchema.Reference, False); + end + else + begin + LType := TRttiUtils.Context.GetType(TOpenAPISchema); + //AContext.ReadDataMember(AValue, LType, Result, True); + AContext.ReadMembers(LType, LSchema, LJSONSchema); + end; end; class function TOpenAPISchemaSerializer.GetTargetInfo: PTypeInfo; @@ -486,12 +552,12 @@ function TOpenAPISchemaSerializer.Serialize(const AValue: TValue; if LSchema = nil then Exit(nil); - // It's also a reference object - if Assigned(LSchema.Reference) and not (LSchema.Reference.Ref.IsEmpty) then - begin - Result := AContext.WriteDataMember(LSchema.Reference); - Exit; - end; + if LSchema.IsEmpty then + Exit(nil); + + // The Schema has a reference + if LSchema.IsReference then + Exit(AContext.WriteDataMember(LSchema.Reference, False)); if Assigned(LSchema.JSONObject) then Result := LSchema.JSONObject.Clone as TJSONObject @@ -511,11 +577,105 @@ function TOpenAPISchemaSerializer.Serialize(const AValue: TValue; end; end; +{ TOpenAPIPathItemSerializer } + +class function TOpenAPIPathItemSerializer.CanHandle(AType: PTypeInfo): Boolean; +begin + Result := TypeInfoIs(AType); +end; + +function TOpenAPIPathItemSerializer.Deserialize(AValue: TJSONValue; + const AData: TValue; ANeonObject: TNeonRttiObject; + AContext: IDeserializerContext): TValue; +var + LPath: TOpenAPIPathItem; + LOperation: TOpenAPIOperation; + LJSONPath: TJSONObject; + LOpType: TOperationType; + LType: TRttiType; + LJSONVal: TJSONValue; +begin + Result := AData; + LPath := AData.AsObject as TOpenAPIPathItem; + LJSONPath := AValue as TJSONObject; + LType := TRttiUtils.Context.GetType(TOpenAPIOperation); + + for LOpType := Low(TOperationType) to High(TOperationType) do + begin + LJSONVal := LJSONPath.Values[LOpType.ToString]; + if Assigned(LJSONVal) then + begin + LOperation := LPath.AddOperation(LOpType); + AContext.ReadMembers(LType, LOperation, LJSONVal as TJSONObject); + end; + end; +end; + +class function TOpenAPIPathItemSerializer.GetTargetInfo: PTypeInfo; +begin + Result := TOpenAPIPathItem.ClassInfo; +end; + +function TOpenAPIPathItemSerializer.Serialize(const AValue: TValue; + ANeonObject: TNeonRttiObject; AContext: ISerializerContext): TJSONValue; +begin + Result := AContext.WriteDataMember(AValue, False); +end; + +{ TOpenAPIExtensionsSerializer } + +class function TOpenAPIExtensionsSerializer.CanHandle(AType: PTypeInfo): Boolean; +begin + Result := TypeInfoIs(AType); +end; + +function TOpenAPIExtensionsSerializer.Deserialize(AValue: TJSONValue; + const AData: TValue; ANeonObject: TNeonRttiObject; + AContext: IDeserializerContext): TValue; +var + LExt: TOpenAPIExtensions; + LJSONValues: TJSONObject; + LPair: TJSONPair; +begin + Result := AData; + LExt := AData.AsObject as TOpenAPIExtensions; + LJSONValues := AValue as TJSONObject; + + for LPair in LJSONValues do + begin + if LPair.JsonString.Value.StartsWith('x-') then + LExt.Values.AddPair(LPair.Clone as TJSONPair); + end; +end; + +class function TOpenAPIExtensionsSerializer.GetTargetInfo: PTypeInfo; +begin + Result := TOpenAPIExtensions.ClassInfo; +end; + +function TOpenAPIExtensionsSerializer.Serialize(const AValue: TValue; + ANeonObject: TNeonRttiObject; AContext: ISerializerContext): TJSONValue; +var + LExt: TOpenAPIExtensions; +begin + LExt := AValue.AsType; + + if LExt = nil then + Exit(nil); + + if LExt.Values.Count = 0 then + Exit(nil); + + Result := AContext.WriteDataMember(LExt.Values, True); +end; + procedure RegisterOpenAPISerializers(ARegistry: TNeonSerializerRegistry); begin //Neon Serializers ARegistry.RegisterSerializer(TJSONValueSerializer); + //ARegistry.RegisterSerializer(TTValueSerializer); + //Nullable Serializers ARegistry.RegisterSerializer(TNullableStringSerializer); ARegistry.RegisterSerializer(TNullableBooleanSerializer); ARegistry.RegisterSerializer(TNullableIntegerSerializer); @@ -523,8 +683,12 @@ procedure RegisterOpenAPISerializers(ARegistry: TNeonSerializerRegistry); ARegistry.RegisterSerializer(TNullableDoubleSerializer); ARegistry.RegisterSerializer(TNullableTDateTimeSerializer); + //OpenAPI Serializers + ARegistry.RegisterSerializer(TOpenAPIAnySerializer); + ARegistry.RegisterSerializer(TOpenAPIPathItemSerializer); ARegistry.RegisterSerializer(TOpenAPIReferenceSerializer); ARegistry.RegisterSerializer(TOpenAPISchemaSerializer); + ARegistry.RegisterSerializer(TOpenAPIExtensionsSerializer); end; end.