Skip to content

Commit

Permalink
Support for dictionaries in HTTP body (request/response).
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski committed Jun 26, 2021
1 parent 7e6d60c commit 879faa8
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 21 deletions.
17 changes: 17 additions & 0 deletions Sources/Swiftgger/APIModel/APIBodyType.swift
Original file line number Diff line number Diff line change
@@ -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)
}
4 changes: 2 additions & 2 deletions Sources/Swiftgger/APIModel/APIRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Sources/Swiftgger/APIModel/APIResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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) {
self.code = code
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
Expand Down
11 changes: 0 additions & 11 deletions Sources/Swiftgger/APIModel/APIResponseType.swift

This file was deleted.

23 changes: 17 additions & 6 deletions Sources/Swiftgger/Builder/OpenAPIMediaTypeBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)

Expand All @@ -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<Any>, let first = items.first {
if let items = value as? Array<Any>, 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)
}

Expand Down
31 changes: 31 additions & 0 deletions Sources/Swiftgger/Common/APIDataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions Sources/SwiftggerTestApp/Program.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
51 changes: 51 additions & 0 deletions Tests/SwiftggerTests/OpenAPIPathsBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 879faa8

Please sign in to comment.