Skip to content

Instantly share code, notes, and snippets.

@mattpolzin
Created January 27, 2020 07:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattpolzin/57dbfbc33e9e97b50f63b3e280866bca to your computer and use it in GitHub Desktop.
Save mattpolzin/57dbfbc33e9e97b50f63b3e280866bca to your computer and use it in GitHub Desktop.
Example OpenAPI tooling script in Swift
#!/usr/bin/swift sh
import Foundation
import ConsoleKit // vapor/console-kit ~> 4.0.0-beta.2.1
import TransformEncoder // @mattpolzin ~> 0.2.0
import OpenAPIKit // @mattpolzin ~> 0.16.0
import Yams // @jpsim ~> 2.0.0
// MARK: - Helpers
extension OpenAPI.HttpVerb: LosslessStringConvertible, CustomStringConvertible {
public init?(_ operation: String) {
guard let value = Self(rawValue: operation) else {
return nil
}
self = value
}
public var description: String { rawValue }
}
// MARK: - Commands
final class NewDocumentationCommand: Command {
struct Signature: CommandSignature {
init() {}
}
var help: String {
"Create a new ./openapi.yml documentation file."
}
func run(using context: CommandContext, signature: Signature) throws {
let templateDoc = OpenAPI.Document(
info: .init(
title: "New API",
version: "1.0"
),
servers: [],
paths: [:],
components: .noComponents
)
let output = try YAMLEncoder().encode(templateDoc)
try output.data(using: .utf8)!.write(to: URL(fileURLWithPath: "./openapi.yml"), options: .withoutOverwriting)
console.success("API documentation created 🎉")
}
}
final class AddEndpointCommand: Command {
struct Signature: CommandSignature {
@Argument(name: "path", help: "Path for new endpoint.")
var path: String
@Argument(name: "operation", help: "The operation to use. One of [\(OpenAPI.HttpVerb.allCases.map { $0.rawValue }.joined(separator: ", "))].")
var operation: OpenAPI.HttpVerb
init() {}
}
var help: String {
"Add a new endpoint to the OpenAPI document at ./openapi.yml"
}
func run(using context: CommandContext, signature: Signature) throws {
let pathComponents = OpenAPI.PathComponents(rawValue: signature.path)
let templateOperation = OpenAPI.PathItem.Operation(
parameters: [
.parameter(
name: "Content-Type",
parameterLocation: .header(required: false),
schema: .string(
allowedValues: [
.init(OpenAPI.ContentType.json.rawValue),
.init(OpenAPI.ContentType.html.rawValue)
]
)
)
],
responses: [
200: .response(
description: "Successful Retrieval",
content: [
.json: .init(
schema: .object(
properties: [
"hello": .string
]
),
example: #"{ "hello": "world" }"#
)
]
)
]
)
let document = try YAMLDecoder().decode(OpenAPI.Document.self, from: String(contentsOf: URL(fileURLWithPath: "./openapi.yml")))
let transformer = TransformEncoder()
// validate that the endpoint does not already exist
transformer.validate { (paths: OpenAPI.PathItem.Map, codingPath) in
let existingOperation = paths[pathComponents]?[OpenAPI.PathItem.self]?[signature.operation]
if existingOperation != nil, existingOperation != templateOperation {
throw ValidationError(
reason: "The \(signature.operation) endpoint for \(pathComponents.rawValue) already exists.",
at: codingPath
)
}
}
// add the new endpoint
transformer.transformOnce { (paths: OpenAPI.PathItem.Map, _) -> OpenAPI.PathItem.Map in
var paths = paths
var currentPathItem = paths[pathComponents]?[OpenAPI.PathItem.self] ?? OpenAPI.PathItem()
currentPathItem[signature.operation] = templateOperation
paths[pathComponents] = .init(currentPathItem)
return paths
}
do {
let transformedDoc = try transformer.encode(document)
let output = try YAMLEncoder().encode(transformedDoc)
try output.data(using: .utf8)!.write(to: URL(fileURLWithPath: "./openapi.yml"))
console.success("Endpoint added 🎉")
} catch let error as EncodingError {
switch error {
case .invalidValue(_, let errorContext):
if let underlyingError = errorContext.underlyingError {
context.console.error(String(describing: underlyingError))
break
}
throw error
}
}
}
}
// MARK: - Create Console, configure, run
let console: Console = Terminal()
var input = CommandInput(arguments: CommandLine.arguments)
var context = CommandContext(console: console, input: input)
var commands = Commands()
commands.use(AddEndpointCommand(), as: "new-endpoint", isDefault: false)
commands.use(NewDocumentationCommand(), as: "new", isDefault: false)
do {
let group = commands
.group(help: "OpenAPI Tooling")
try console.run(group, input: input)
} catch let error {
console.error(error.localizedDescription)
exit(1)
}
@mattpolzin
Copy link
Author

Syntax highlighting:

#!/usr/bin/swift sh

import Foundation
import ConsoleKit // vapor/console-kit ~> 4.0.0-beta.2.1
import TransformEncoder // @mattpolzin ~> 0.2.0
import OpenAPIKit // @mattpolzin ~> 0.16.0
import Yams // @jpsim ~> 2.0.0

// MARK: - Helpers
extension OpenAPI.HttpVerb: LosslessStringConvertible, CustomStringConvertible {
    public init?(_ operation: String) {
        guard let value = Self(rawValue: operation) else {
            return nil
        }
        self = value
    }

    public var description: String { rawValue }
}

// MARK: - Commands
final class NewDocumentationCommand: Command {
    struct Signature: CommandSignature {
        init() {}
    }

    var help: String {
        "Create a new ./openapi.yml documentation file."
    }

    func run(using context: CommandContext, signature: Signature) throws {

        let templateDoc = OpenAPI.Document(
            info: .init(
              title: "New API",
              version: "1.0"
            ),
            servers: [],
            paths: [:],
            components: .noComponents
        )

        let output = try YAMLEncoder().encode(templateDoc)

        try output.data(using: .utf8)!.write(to: URL(fileURLWithPath: "./openapi.yml"), options: .withoutOverwriting)

        console.success("API documentation created 🎉")
    }
}

final class AddEndpointCommand: Command {
    struct Signature: CommandSignature {
        @Argument(name: "path", help: "Path for new endpoint.")
        var path: String

        @Argument(name: "operation", help: "The operation to use. One of [\(OpenAPI.HttpVerb.allCases.map { $0.rawValue }.joined(separator: ", "))].")
        var operation: OpenAPI.HttpVerb

        init() {}
    }

    var help: String {
        "Add a new endpoint to the OpenAPI document at ./openapi.yml"
    }

    func run(using context: CommandContext, signature: Signature) throws {
        let pathComponents = OpenAPI.PathComponents(rawValue: signature.path)
        let templateOperation = OpenAPI.PathItem.Operation(
            parameters: [
                .parameter(
                    name: "Content-Type",
                    parameterLocation: .header(required: false),
                    schema: .string(
                        allowedValues: [
                            .init(OpenAPI.ContentType.json.rawValue),
                            .init(OpenAPI.ContentType.html.rawValue)
                        ]
                    )
                )
            ],
            responses: [
                200: .response(
                    description: "Successful Retrieval",
                    content: [
                        .json: .init(
                            schema: .object(
                                properties: [
                                    "hello": .string
                                ]
                            ),
                            example: #"{ "hello": "world" }"#
                        )
                    ]
                )
            ]
        )

        let document = try YAMLDecoder().decode(OpenAPI.Document.self, from: String(contentsOf: URL(fileURLWithPath: "./openapi.yml")))

        let transformer = TransformEncoder()
        // validate that the endpoint does not already exist
        transformer.validate { (paths: OpenAPI.PathItem.Map, codingPath) in
            let existingOperation = paths[pathComponents]?[OpenAPI.PathItem.self]?[signature.operation]
            if existingOperation != nil, existingOperation != templateOperation {
                throw ValidationError(
                    reason: "The \(signature.operation) endpoint for \(pathComponents.rawValue) already exists.",
                    at: codingPath
                )
            }
        }

        // add the new endpoint
        transformer.transformOnce { (paths: OpenAPI.PathItem.Map, _) -> OpenAPI.PathItem.Map in
            var paths = paths

            var currentPathItem = paths[pathComponents]?[OpenAPI.PathItem.self] ?? OpenAPI.PathItem()

            currentPathItem[signature.operation] = templateOperation

            paths[pathComponents] = .init(currentPathItem)

            return paths
        }

        do {
            let transformedDoc = try transformer.encode(document)

            let output = try YAMLEncoder().encode(transformedDoc)

            try output.data(using: .utf8)!.write(to: URL(fileURLWithPath: "./openapi.yml"))

            console.success("Endpoint added 🎉")
        } catch let error as EncodingError {
            switch error {
            case .invalidValue(_, let errorContext):
                if let underlyingError = errorContext.underlyingError {
                    context.console.error(String(describing: underlyingError))
                    break
                }
                throw error
            }
        }
    }
}

// MARK: - Create Console, configure, run
let console: Console = Terminal()
var input = CommandInput(arguments: CommandLine.arguments)
var context = CommandContext(console: console, input: input)

var commands = Commands()
commands.use(AddEndpointCommand(), as: "new-endpoint", isDefault: false)
commands.use(NewDocumentationCommand(), as: "new", isDefault: false)

do {
    let group = commands
        .group(help: "OpenAPI Tooling")
    try console.run(group, input: input)
} catch let error {
    console.error(error.localizedDescription)
    exit(1)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment