Skip to content

Commit

Permalink
#10 Improvments in json schema (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski authored Jan 24, 2021
1 parent 2c05148 commit 61b9cf0
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 29 deletions.
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ let package = Package(
name: "Swiftgger",
products: [
.library(name: "Swiftgger",targets: ["Swiftgger"]),
.executable(name: "swiftggerapp", targets: ["SwiftggerApp"])
],
dependencies: [
.package(url: "https://github.com/Flight-School/AnyCodable", from: "0.4.0")
],
targets: [
.target(name: "Swiftgger", dependencies: []),
.target(name: "Swiftgger", dependencies: ["AnyCodable"]),
.target(name: "SwiftggerApp", dependencies: ["Swiftgger"]),
.testTarget(name: "SwiftggerTests",
dependencies: ["Swiftgger"],
resources: [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/)
[![Platforms macOS | Linux](https://img.shields.io/badge/Platforms-macOS%20%7C%20Linux%20-lightgray.svg?style=flat)](https://developer.apple.com/swift/)

Swiftgger is simple library which generate output compatible with [OpenAPI version 3.0.1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securitySchemeObject). Library is generating objects tree which you can serialize to JSON and return by your API endpoint. URL to that endpoint can be used in [Swagger UI](https://swagger.io/swagger-ui/). Thanks to this you have GUI which shows you exactly how your API looks like and you can use that GUI to execute actions (requests). It's especially helpful during API testing.
Swiftgger is simple library which generate output compatible with [OpenAPI version 3.0.1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.mdt). Library is generating objects tree which you can serialize to JSON and return by your API endpoint. URL to that endpoint can be used in [Swagger UI](https://swagger.io/swagger-ui/). Thanks to this you have GUI which shows you exactly how your API looks like and you can use that GUI to execute actions (requests). It's especially helpful during API testing.

![swagger](Images/screen-02.png)

Expand Down
119 changes: 99 additions & 20 deletions Sources/Swiftgger/Builder/OpenAPISchemasBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import AnyCodable

/// Builder for object information stored in `components/schemas` part of OpenAPI.
class OpenAPISchemasBuilder {
Expand Down Expand Up @@ -48,15 +49,34 @@ class OpenAPISchemasBuilder {
var array: [(name: String, type: OpenAPIObjectProperty)] = []
for property in properties {

// Simple value type (also unwrapped optionals).
let unwrapped = unwrap(property.value)
if let dataType = makeAPIDataType(fromSwiftValue: unwrapped) {
self.appendProperties(property: property, unwrapped: unwrapped, dataType: dataType, array: &array)
} else {
if let items = property.value as? Array<Any> {
self.appendItems(property: property, items: items, array: &array)
} else {
self.appendReference(property: property, array: &array)
}
self.append(dataType: dataType, property: property, unwrapped: unwrapped, array: &array)
continue
}

// Non optional or initialized array (array which contains any data).
if let items = property.value as? Array<Any> {
self.append(items: items, property: property, array: &array)
continue
}

// Non optional or initialized object.
if self.isInitialized(object: unwrapped) {
self.append(reference: property, array: &array)
continue
}

// Optional and not initialized object.
if let typeName = self.getTypeName(from: unwrapped) {
self.append(typeName: typeName, property: property, array: &array)
continue
}

// Optional and not initialized arrays.
if let typeName = self.getArrayTypeName(from: unwrapped) {
self.append(arrayName: typeName, property: property, array: &array)
}
}

Expand Down Expand Up @@ -89,19 +109,20 @@ class OpenAPISchemasBuilder {
}
}

private func appendProperties(property: Mirror.Child,
unwrapped: Any,
dataType: APIDataType,
array: inout [(name: String, type: OpenAPIObjectProperty)]) {
let example = String(describing: unwrapped)
private func append(dataType: APIDataType,
property: Mirror.Child,
unwrapped: Any,
array: inout [(name: String, type: OpenAPIObjectProperty)]
) {
let example = AnyCodable(unwrapped)
let objectProperty = OpenAPIObjectProperty(type: dataType.type, format: dataType.format, example: example)
array.append((name: property.label ?? "", type: objectProperty))
}

private func appendItems(property: Mirror.Child,
items: Array<Any>,
array: inout [(name: String, type: OpenAPIObjectProperty)]) {

private func append(items: Array<Any>,
property: Mirror.Child,
array: inout [(name: String, type: OpenAPIObjectProperty)]
) {
guard let item = items.first else {
return
}
Expand All @@ -114,15 +135,34 @@ class OpenAPISchemasBuilder {
self.nestedObjects.append(item)
}

private func appendReference(property: Mirror.Child,
array: inout [(name: String, type: OpenAPIObjectProperty)]) {
let unwrapped = unwrap(property.value)
private func append(reference: Mirror.Child,
array: inout [(name: String, type: OpenAPIObjectProperty)]
) {
let unwrapped = unwrap(reference.value)
let typeName = String(describing: type(of: unwrapped))
let objectProperty = OpenAPIObjectProperty(ref: "#/components/schemas/\(typeName)")
array.append((name: property.label ?? "", type: objectProperty))
array.append((name: reference.label ?? "", type: objectProperty))

self.nestedObjects.append(unwrapped)
}

private func append(typeName: String,
property: Mirror.Child,
array: inout [(name: String, type: OpenAPIObjectProperty)]
) {

let objectProperty = OpenAPIObjectProperty(ref: "#/components/schemas/\(typeName)")
array.append((name: property.label ?? "", type: objectProperty))
}

private func append(arrayName: String,
property: Mirror.Child,
array: inout [(name: String, type: OpenAPIObjectProperty)]
) {
let openApiSchema = OpenAPISchema(ref: "#/components/schemas/\(arrayName)")
let objectProperty = OpenAPIObjectProperty(items: openApiSchema)
array.append((name: property.label ?? "", type: objectProperty))
}

private func getRequiredProperties(properties: Mirror.Children) -> [String] {
var array: [String] = []
Expand All @@ -142,11 +182,50 @@ class OpenAPISchemasBuilder {
guard mirror.displayStyle == .optional, let first = mirror.children.first else {
return any
}

return first.value
}

private func isOptional<T>(_ any: T) -> Bool {
let mirror = Mirror(reflecting: any)
return mirror.displayStyle == .optional
}

private func isInitialized<T>(object any: T) -> Bool {
let mirror = Mirror(reflecting: any)

return mirror.displayStyle == .struct
|| mirror.displayStyle == .class
|| mirror.displayStyle == .enum
}

private func getTypeName(from any: Any) -> String? {
let typeName = String(describing: type(of: any))

let pattern = "^Optional<(?<type>\\w+)>$"
return self.match(pattern: pattern, in: typeName)
}

private func getArrayTypeName(from any: Any) -> String? {
let typeName = String(describing: type(of: any))

let pattern = "^Optional<Array<(?<type>\\w+)>>$"
return self.match(pattern: pattern, in: typeName)
}

private func match(pattern: String, in text: String) -> String? {
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)

if let match = regex?.firstMatch(in: text, options: [], range: NSRange(location: 0, length: text.utf8.count)) {
guard match.numberOfRanges == 2 else {
return nil
}

if let typeRange = Range(match.range(at: 1), in: text) {
return String(text[typeRange])
}
}

return nil
}
}
5 changes: 3 additions & 2 deletions Sources/Swiftgger/OpenAPIModel/OpenAPIObjectProperty.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
//

import Foundation
import AnyCodable

/// Information about property which exists in schema (input/output) data.
public class OpenAPIObjectProperty: Codable {

public private(set) var ref: String?
public private(set) var type: String?
public private(set) var format: String?
public private(set) var example: String?
public private(set) var example: AnyCodable?
public private(set) var items: OpenAPISchema?

init(ref: String) {
Expand All @@ -24,7 +25,7 @@ public class OpenAPIObjectProperty: Codable {
self.items = items
}

init(type: String, format: String?, example: String?) {
init(type: String, format: String?, example: AnyCodable?) {
self.type = type
self.format = format
self.example = example
Expand Down
19 changes: 19 additions & 0 deletions Sources/SwiftggerApp/Models/Fuel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Fuel.swift
// SwiftggerApp
//
// Created by Marcin Czachurski on 21/10/2021.
// Copyright © 2021 Marcin Czachurski. All rights reserved.
//

import Foundation

struct Fuel {
var level: Int
var type: String

init(level: Int, type: String) {
self.level = level
self.type = type
}
}
21 changes: 21 additions & 0 deletions Sources/SwiftggerApp/Models/Vehicle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Vehicle.swift
// SwiftggerApp
//
// Created by Marcin Czachurski on 21/10/2021.
// Copyright © 2021 Marcin Czachurski. All rights reserved.
//

import Foundation

class Vehicle {
var name: String
var age: Int?
var fuels: [Fuel]?

init(name: String, age: Int, fuels: [Fuel]? = nil) {
self.name = name
self.age = age
self.fuels = fuels
}
}
115 changes: 115 additions & 0 deletions Sources/SwiftggerApp/Program.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// Program.swift
// SwiftggerApp
//
// Created by Marcin Czachurski on 21/10/2021.
// Copyright © 2021 Marcin Czachurski. All rights reserved.
//

import Foundation
import Swiftgger

class Program {
func run() -> Bool {
let openAPIBuilder = OpenAPIBuilder(
title: "Swiftgger OpenAPI document",
version: "1.0.0",
description: "OpenAPI documentation for test structure purposes",
authorizations: [
.basic(description: "Basic authorization"),
.apiKey(description: "Api key authorization"),
.jwt(description: "JWT authorization"),
.oauth2(description: "OAuth authorization", flows: [
.implicit(APIAuthorizationFlow(authorizationUrl: "https://oauth2.com", tokenUrl: "https://oauth2.com/token", scopes: [:]))
]),
.openId(description: "OpenIdConnect authorization", openIdConnectUrl: "https//opeind.com")
]
)
.add([
APIObject(object: Vehicle(name: "Ford", age: 21)),
APIObject(object: Fuel(level: 90, type: "GAS"))
])
.add(APIController(name: "VehiclesController", description: "Contoller for vehicles", actions: [
APIAction(method: .get,
route: "/vehicles",
summary: "GET action for downloading list of vehicles.",
description: "List of vehicles",
responses: [
APIResponse(code: "200", description: "List of vehicles", array: Vehicle.self, contentType: "application/json"),
APIResponse(code: "401", description: "Unauthorized")
]
),
APIAction(method: .get,
route: "/vehicles/{id}",
summary: "GET action for downloading specific vehicle.",
description: "Single vehicle",
parameters: [
APIParameter(name: "id")
],
responses: [
APIResponse(code: "200", description: "List of vehicles", object: Vehicle.self, contentType: "application/json"),
APIResponse(code: "401", description: "Unauthorized"),
APIResponse(code: "403", description: "Forbidden"),
APIResponse(code: "404", description: "NotFound")
]
),
APIAction(method: .post,
route: "/vehicles",
summary: "POST action for creating new vehicle.",
description: "New vehicle",
request: APIRequest(object: Vehicle.self, description: "New vehicle", contentType: "application/json"),
responses: [
APIResponse(code: "201", description: "Created vehicles", object: Vehicle.self, contentType: "application/json"),
APIResponse(code: "401", description: "Unauthorized"),
APIResponse(code: "403", description: "Forbidden")
]
),
APIAction(method: .put,
route: "/vehicles/{id}",
summary: "PUT action for updating existing vehicle.",
description: "Update vehicle",
parameters: [
APIParameter(name: "id")
],
request: APIRequest(object: Vehicle.self, description: "New vehicle", contentType: "application/json"),
responses: [
APIResponse(code: "200", description: "Updated vehicles", object: Vehicle.self, contentType: "application/json"),
APIResponse(code: "401", description: "Unauthorized"),
APIResponse(code: "403", description: "Forbidden"),
APIResponse(code: "404", description: "NotFound")
]
),
APIAction(method: .delete,
route: "/vehicles/{id}",
summary: "DELETE action for deleting specific vehicle.",
description: "Delete vehicle",
parameters: [
APIParameter(name: "id")
],
responses: [
APIResponse(code: "200", description: "Success"),
APIResponse(code: "401", description: "Unauthorized"),
APIResponse(code: "403", description: "Forbidden"),
APIResponse(code: "404", description: "NotFound")
],
authorization: true
)
]))

let openAPIDocument = openAPIBuilder.built()

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let jsonData = try! encoder.encode(openAPIDocument)
let jsonOptionalString = String(bytes: jsonData, encoding: .utf8)

guard let jsonString = jsonOptionalString else {
print("Error during converting object into string")
return false
}

print(jsonString)
return true
}
}
15 changes: 15 additions & 0 deletions Sources/SwiftggerApp/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// main.swift
// SwiftggerApp
//
// Created by Marcin Czachurski on 21/10/2021.
// Copyright © 2021 Marcin Czachurski. All rights reserved.
//

import Foundation
import Swiftgger

let program = Program()
let result = program.run()

exit(result ? EXIT_SUCCESS : EXIT_FAILURE)
Loading

0 comments on commit 61b9cf0

Please sign in to comment.