From 879faa843487eede72b9a2c48d1565d8aea6fc36 Mon Sep 17 00:00:00 2001 From: Marcin Czachursk Date: Sat, 26 Jun 2021 10:04:01 +0200 Subject: [PATCH] Support for dictionaries in HTTP body (request/response). --- Sources/Swiftgger/APIModel/APIBodyType.swift | 17 +++++++ Sources/Swiftgger/APIModel/APIRequest.swift | 4 +- Sources/Swiftgger/APIModel/APIResponse.swift | 4 +- .../Swiftgger/APIModel/APIResponseType.swift | 11 ---- .../Builder/OpenAPIMediaTypeBuilder.swift | 23 ++++++--- Sources/Swiftgger/Common/APIDataType.swift | 31 +++++++++++ Sources/SwiftggerTestApp/Program.swift | 18 +++++++ .../OpenAPIPathsBuilderTests.swift | 51 +++++++++++++++++++ 8 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 Sources/Swiftgger/APIModel/APIBodyType.swift delete mode 100644 Sources/Swiftgger/APIModel/APIResponseType.swift diff --git a/Sources/Swiftgger/APIModel/APIBodyType.swift b/Sources/Swiftgger/APIModel/APIBodyType.swift new file mode 100644 index 0000000..16ef206 --- /dev/null +++ b/Sources/Swiftgger/APIModel/APIBodyType.swift @@ -0,0 +1,17 @@ +// +// https://mczachurski.dev +// Copyright © 2021 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +/// Possible response types. +public enum APIBodyType { + /// HTTP body is a dictionary. Dictionary key is always String. Parameter is a type which is a value in dictionary. + case dictionary(Any.Type) + + /// HTTP body is a object (or array of objects). Here we have to specify object defined in `APIObject` collection. + case object(Any.Type, asCollection: Bool = false) + + /// HPPT body is a simple type (string, integer etc.) or array of simple types. + case value(Any) +} diff --git a/Sources/Swiftgger/APIModel/APIRequest.swift b/Sources/Swiftgger/APIModel/APIRequest.swift index 793d513..56c2453 100644 --- a/Sources/Swiftgger/APIModel/APIRequest.swift +++ b/Sources/Swiftgger/APIModel/APIRequest.swift @@ -8,11 +8,11 @@ import Foundation /// Information about HTTP request. public class APIRequest { - var type: APIResponseType? + var type: APIBodyType? var description: String? var contentType: String? - public init(type: APIResponseType? = nil, description: String? = nil, contentType: String? = nil) { + public init(type: APIBodyType? = nil, description: String? = nil, contentType: String? = nil) { self.type = type self.description = description self.contentType = contentType diff --git a/Sources/Swiftgger/APIModel/APIResponse.swift b/Sources/Swiftgger/APIModel/APIResponse.swift index 3e9ec05..e2bd943 100644 --- a/Sources/Swiftgger/APIModel/APIResponse.swift +++ b/Sources/Swiftgger/APIModel/APIResponse.swift @@ -10,7 +10,7 @@ import Foundation public class APIResponse { var code: String var description: String - var type: APIResponseType? + var type: APIBodyType? var contentType: String? public init(code: String, description: String) { @@ -18,7 +18,7 @@ public class APIResponse { self.description = description } - public init(code: String, description: String, type: APIResponseType?, contentType: String? = nil) { + public init(code: String, description: String, type: APIBodyType?, contentType: String? = nil) { self.code = code self.description = description self.type = type diff --git a/Sources/Swiftgger/APIModel/APIResponseType.swift b/Sources/Swiftgger/APIModel/APIResponseType.swift deleted file mode 100644 index 7f79518..0000000 --- a/Sources/Swiftgger/APIModel/APIResponseType.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// https://mczachurski.dev -// Copyright © 2021 Marcin Czachurski and the repository contributors. -// Licensed under the MIT License. -// - -/// Possible response types. -public enum APIResponseType { - case object(Any.Type, asCollection: Bool = false) - case value(Any) -} diff --git a/Sources/Swiftgger/Builder/OpenAPIMediaTypeBuilder.swift b/Sources/Swiftgger/Builder/OpenAPIMediaTypeBuilder.swift index 3bc3379..310499a 100644 --- a/Sources/Swiftgger/Builder/OpenAPIMediaTypeBuilder.swift +++ b/Sources/Swiftgger/Builder/OpenAPIMediaTypeBuilder.swift @@ -10,9 +10,9 @@ import AnyCodable /// Builder of `paths` part of OpenAPI. class OpenAPIMediaTypeBuilder { let objects: [APIObjectProtocol] - let type: APIResponseType + let type: APIBodyType - init(objects: [APIObjectProtocol], for type: APIResponseType) { + init(objects: [APIObjectProtocol], for type: APIBodyType) { self.objects = objects self.type = type } @@ -21,6 +21,17 @@ class OpenAPIMediaTypeBuilder { var openAPISchema: OpenAPISchema? switch type { + case .dictionary(let type): + if let dataType = APIDataType(fromSwiftType: type) { + let additionalProperties = OpenAPISchema(type: dataType.type, format: dataType.format) + openAPISchema = OpenAPISchema(type: "object", additionalProperties: additionalProperties) + } else { + let objectTypeReference = self.createObjectReference(for: type) + let additionalProperties = OpenAPISchema(ref: objectTypeReference) + openAPISchema = OpenAPISchema(type: "object", additionalProperties: additionalProperties) + } + + break case .object(let type, let isCollection): let objectTypeReference = self.createObjectReference(for: type) @@ -32,16 +43,16 @@ class OpenAPIMediaTypeBuilder { } break - case .value(let type): - let example = AnyCodable(type) + case .value(let value): + let example = AnyCodable(value) - if let items = type as? Array, let first = items.first { + if let items = value as? Array, let first = items.first { let dataType = APIDataType(fromSwiftValue: first) let objectInArraySchema = OpenAPISchema(type: dataType?.type, format: dataType?.format) openAPISchema = OpenAPISchema(type: APIDataType.array.type, items: objectInArraySchema, example: example) } else { - let dataType = APIDataType(fromSwiftValue: type) + let dataType = APIDataType(fromSwiftValue: value) openAPISchema = OpenAPISchema(type: dataType?.type, format: dataType?.format, example: example) } diff --git a/Sources/Swiftgger/Common/APIDataType.swift b/Sources/Swiftgger/Common/APIDataType.swift index c047fb1..5fad961 100644 --- a/Sources/Swiftgger/Common/APIDataType.swift +++ b/Sources/Swiftgger/Common/APIDataType.swift @@ -47,6 +47,37 @@ extension APIDataType { return nil } } + + init?(fromSwiftType type: Any.Type) { + switch type { + case is Int32.Type: + self.type = "integer" + self.format = "int32" + case is Int.Type: + self.type = "integer" + self.format = "int64" + case is Float.Type: + self.type = "number" + self.format = "float" + case is Double.Type: + self.type = "number" + self.format = "double" + case is Bool.Type: + self.type = "boolean" + self.format = nil + case is Date.Type: + self.type = "string" + self.format = "date" + case is String.Type: + self.type = "string" + self.format = nil + case is UUID.Type: + self.type = "string" + self.format = "uuid" + default: + return nil + } + } } extension APIDataType { diff --git a/Sources/SwiftggerTestApp/Program.swift b/Sources/SwiftggerTestApp/Program.swift index e83f8fc..bc5665c 100644 --- a/Sources/SwiftggerTestApp/Program.swift +++ b/Sources/SwiftggerTestApp/Program.swift @@ -124,6 +124,24 @@ class Program { ], authorization: true ), + APIAction(method: .get, + route: "/vehicles/tags", + summary: "Get vehicle associated tags", + description: "GET action for downloading vehicle associated tags.", + responses: [ + APIResponse(code: "200", description: "Vehicle tags", type: .dictionary(String.self)), + APIResponse(code: "401", description: "Unauthorized") + ] + ), + APIAction(method: .get, + route: "/vehicles/fuels", + summary: "Get vehicle associated fuels", + description: "GET action for downloading vehicle associated fuels.", + responses: [ + APIResponse(code: "200", description: "Vehicle fuels", type: .dictionary(Fuel.self)), + APIResponse(code: "401", description: "Unauthorized") + ] + ), APIAction(method: .get, route: "/echo", summary: "Send text", diff --git a/Tests/SwiftggerTests/OpenAPIPathsBuilderTests.swift b/Tests/SwiftggerTests/OpenAPIPathsBuilderTests.swift index 51652b9..4f742d3 100644 --- a/Tests/SwiftggerTests/OpenAPIPathsBuilderTests.swift +++ b/Tests/SwiftggerTests/OpenAPIPathsBuilderTests.swift @@ -628,6 +628,57 @@ class OpenAPIPathsBuilderTests: XCTestCase { // Assert. XCTAssertEqual("#/components/schemas/CustomAnimal", openAPIDocument.paths["/animals"]?.get?.responses?["200"]?.content?["application/json"]?.schema?.ref) } + + func testActionObjectResponseDictionaryStringShouldBeAddedToOpenAPIDocument() { + + // Arrange. + let openAPIBuilder = OpenAPIBuilder( + title: "Title", + version: "1.0.0", + description: "Description" + ) + .add(APIController(name: "ControllerName", description: "ControllerDescription", actions: [ + APIAction(method: .get, route: "/tags", summary: "Action summary", + description: "Action description", responses: [ + APIResponse(code: "200", description: "Response description", type: .dictionary(String.self)) + ] + ) + ])) + + // Act. + let openAPIDocument = openAPIBuilder.built() + + // Assert. + XCTAssertEqual("object", openAPIDocument.paths["/tags"]?.get?.responses?["200"]?.content?["application/json"]?.schema?.type) + XCTAssertEqual("string", openAPIDocument.paths["/tags"]?.get?.responses?["200"]?.content?["application/json"]?.schema?.additionalProperties?.type) + } + + func testActionObjectResponseDictionaryObjectShouldBeAddedToOpenAPIDocument() { + + // Arrange. + let openAPIBuilder = OpenAPIBuilder( + title: "Title", + version: "1.0.0", + description: "Description" + ) + .add(APIController(name: "ControllerName", description: "ControllerDescription", actions: [ + APIAction(method: .get, route: "/tags", summary: "Action summary", + description: "Action description", responses: [ + APIResponse(code: "200", description: "Response description", type: .dictionary(Animal.self)) + ] + ) + ])) + .add([ + APIObject(object: Animal(name: "Dog", age: 21)) + ]) + + // Act. + let openAPIDocument = openAPIBuilder.built() + + // Assert. + XCTAssertEqual("object", openAPIDocument.paths["/tags"]?.get?.responses?["200"]?.content?["application/json"]?.schema?.type) + XCTAssertEqual("#/components/schemas/Animal", openAPIDocument.paths["/tags"]?.get?.responses?["200"]?.content?["application/json"]?.schema?.additionalProperties?.ref) + } func testActionStringValueTypeResponseShouldBeAddedToOpenAPIDocument() { // Arrange.