├── Sources ├── App │ ├── Controllers │ │ ├── .gitkeep │ │ ├── HelloWorldController.swift │ │ └── APIDocsController.swift │ ├── configure.swift │ ├── routes.swift │ └── Models │ │ └── HelloWorld.swift └── Run │ └── main.swift ├── .dockerignore ├── Screen Shot 2019-12-28 at 7.18.48 PM.png ├── .gitignore ├── Tests └── AppTests │ └── AppTests.swift ├── web.Dockerfile ├── Package.swift ├── README.md └── Package.resolved /Sources/App/Controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | .swiftpm/ 3 | -------------------------------------------------------------------------------- /Screen Shot 2019-12-28 at 7.18.48 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattpolzin/VaporOpenAPIExample/HEAD/Screen Shot 2019-12-28 at 7.18.48 PM.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Packages 2 | .build 3 | xcuserdata 4 | *.xcodeproj 5 | DerivedData/ 6 | .DS_Store 7 | db.sqlite 8 | .swiftpm 9 | harmony.json 10 | 11 | -------------------------------------------------------------------------------- /Sources/Run/main.swift: -------------------------------------------------------------------------------- 1 | import App 2 | import Vapor 3 | 4 | var env = try Environment.detect() 5 | try LoggingSystem.bootstrap(from: &env) 6 | let app = Application(env) 7 | defer { app.shutdown() } 8 | try configure(app) 9 | try app.run() 10 | -------------------------------------------------------------------------------- /Sources/App/configure.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | // configures your application 4 | public func configure(_ app: Application) throws { 5 | // uncomment to serve files from /Public folder 6 | // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) 7 | 8 | // register routes 9 | try routes(app) 10 | } -------------------------------------------------------------------------------- /Tests/AppTests/AppTests.swift: -------------------------------------------------------------------------------- 1 | @testable import App 2 | import XCTVapor 3 | 4 | final class AppTests: XCTestCase { 5 | // func testHelloWorld() throws { 6 | // let app = Application(.testing) 7 | // defer { app.shutdown() } 8 | // try configure(app) 9 | // 10 | // try app.test(.GET, "hello") { res in 11 | // XCTAssertEqual(res.status, .ok) 12 | // XCTAssertEqual(res.body.string, "Hello, world!") 13 | // } 14 | // } 15 | } 16 | -------------------------------------------------------------------------------- /web.Dockerfile: -------------------------------------------------------------------------------- 1 | # ================================ 2 | # Build image 3 | # ================================ 4 | FROM swift:5.9 as build 5 | WORKDIR /build 6 | 7 | # Copy entire repo into container 8 | COPY . . 9 | 10 | # Compile with optimizations 11 | RUN swift build \ 12 | --enable-test-discovery \ 13 | -c release 14 | 15 | # ================================ 16 | # Run image 17 | # ================================ 18 | FROM swift:5.9 19 | WORKDIR /run 20 | 21 | # Copy build artifacts 22 | COPY --from=build /build/.build/release /run 23 | # Copy Swift runtime libraries 24 | COPY --from=build /usr/lib/swift/ /usr/lib/swift/ 25 | 26 | ENTRYPOINT ["./Run", "serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "80"] 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "openapi-example", 7 | platforms: [ 8 | .macOS(.v12) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/vapor/vapor.git", from: "4.86.0"), 12 | .package(url: "https://github.com/jpsim/Yams.git", from: "6.0.0"), 13 | .package(url: "https://github.com/mattpolzin/VaporOpenAPI.git", .upToNextMinor(from: "0.8.0")) 14 | ], 15 | targets: [ 16 | .target(name: "App", dependencies: [ 17 | .product(name:"Vapor", package: "vapor"), 18 | "VaporOpenAPI", 19 | "Yams" 20 | ]), 21 | .executableTarget(name: "Run", dependencies: ["App"]), 22 | .testTarget(name: "AppTests", dependencies: [ 23 | "App", 24 | .product(name: "XCTVapor", package: "vapor") 25 | ]) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /Sources/App/routes.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | func routes(_ app: Application) throws { 4 | let apiDocsController = APIDocsController(app: app) 5 | 6 | app.get("docs", use: apiDocsController.view) 7 | .summary("View API Documentation") 8 | .description("API Documentation is served using the Redoc web app.") 9 | .tags("Documentation") 10 | 11 | app.get("docs", "openapi.yml", use: apiDocsController.show) 12 | .summary("Download API Documentation") 13 | .description("Retrieve the OpenAPI documentation as a YAML file.") 14 | .tags("Documentation") 15 | 16 | 17 | app.get("hello", use: HelloWorldController.show) 18 | .summary("View a greeting") 19 | .description("Say hello in one of the supported languages!") 20 | .tags("Greetings") 21 | 22 | app.post("hello", use: HelloWorldController.create) 23 | .summary("Create a greeting") 24 | .description("The endpoint is not actually implemented. It is just mocked up.") 25 | .tags("Greetings") 26 | 27 | app.delete("hello", use: HelloWorldController.delete) 28 | .summary("Delete a greeting") 29 | .tags("Greetings") 30 | } 31 | -------------------------------------------------------------------------------- /Sources/App/Models/HelloWorld.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelloWorld.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 12/28/19. 6 | // 7 | 8 | import Foundation 9 | import Vapor 10 | import Sampleable 11 | import OpenAPIKit 12 | import OpenAPIReflection 13 | 14 | struct HelloWorld: Codable { 15 | 16 | let language: Language 17 | let greeting: String 18 | 19 | init(language: Language) { 20 | self.language = language 21 | switch language { 22 | case .english: 23 | greeting = "Hello World!" 24 | case .spanish: 25 | greeting = "¡Hola Mundo!" 26 | } 27 | } 28 | 29 | enum Language: String, CaseIterable, AnyJSONCaseIterable, Codable { 30 | case english 31 | case spanish 32 | 33 | static var allCasesString: String { 34 | Self.allCases.map { $0.rawValue }.joined(separator: ", ") 35 | } 36 | } 37 | } 38 | 39 | extension HelloWorld: ResponseEncodable { 40 | func encodeResponse(for request: Request) -> EventLoopFuture { 41 | return request.eventLoop 42 | .makeSucceededFuture(()) 43 | .flatMapThrowing { 44 | try Response(body: .init(data: JSONEncoder().encode(self))) 45 | } 46 | } 47 | } 48 | 49 | extension HelloWorld: AsyncResponseEncodable { 50 | func encodeResponse(for request: Request) async throws -> Response { 51 | try Response(body: .init(data: JSONEncoder().encode(self))) 52 | } 53 | } 54 | 55 | extension HelloWorld: Sampleable { 56 | static var sample: HelloWorld { 57 | .init(language: .english) 58 | } 59 | } 60 | 61 | extension HelloWorld: OpenAPIEncodedSchemaType { 62 | static func openAPISchema(using encoder: JSONEncoder) throws -> JSONSchema { 63 | return try genericOpenAPISchemaGuess(using: encoder) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Sources/App/Controllers/HelloWorldController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HelloWorldController.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 12/28/19. 6 | // 7 | 8 | import Vapor 9 | import VaporOpenAPI 10 | 11 | final class HelloWorldController { 12 | static func show(_ req: TypedRequest) -> EventLoopFuture { 13 | let requestedLanguage = req.query.language 14 | .flatMap { HelloWorld.Language(rawValue: $0) } 15 | 16 | guard let language = requestedLanguage else { 17 | return req.response.unavailableLanguage 18 | } 19 | 20 | return req 21 | .response 22 | .success 23 | .encode(.init(language: language)) 24 | } 25 | 26 | static func create(_ req: TypedRequest) -> EventLoopFuture { 27 | // does not actually do anything, sorry to say. 28 | 29 | return req 30 | .response 31 | .success 32 | .encode(.init(language: .english)) 33 | } 34 | 35 | static func delete(_ req: TypedRequest) -> EventLoopFuture { 36 | // also does not actually perform a DELETE 37 | 38 | return req 39 | .response 40 | .success 41 | .encodeEmptyResponse() 42 | } 43 | } 44 | 45 | // MARK: - Contexts 46 | extension HelloWorldController { 47 | struct ShowContext: RouteContext { 48 | typealias RequestBodyType = EmptyRequestBody 49 | 50 | static let defaultContentType: HTTPMediaType? = nil 51 | static let shared = Self() 52 | 53 | let language: StringQueryParam = .init( 54 | name: "language", 55 | defaultValue: HelloWorld.Language.english.rawValue, 56 | allowedValues: HelloWorld.Language.allCases.map { $0.rawValue } 57 | ) 58 | 59 | let success: ResponseContext = .init { response in 60 | response.headers.contentType = .json 61 | response.status = .ok 62 | } 63 | 64 | let unavailableLanguage: CannedResponse = .init( 65 | response: Response( 66 | status: .badRequest, 67 | headers: ["Content-Type": "text/plain"], 68 | body: .init( 69 | string: "The only available languages are \(HelloWorld.Language.allCasesString)") 70 | ) 71 | ) 72 | } 73 | 74 | struct CreateContext: RouteContext { 75 | typealias RequestBodyType = HelloWorld 76 | 77 | static let defaultContentType: HTTPMediaType? = nil 78 | static let shared = Self() 79 | 80 | let success: ResponseContext = .init { response in 81 | response.headers.contentType = .json 82 | response.status = .created 83 | } 84 | } 85 | 86 | struct DeleteContext: RouteContext { 87 | typealias RequestBodyType = EmptyRequestBody 88 | 89 | static let defaultContentType: HTTPMediaType? = nil 90 | static let shared = Self() 91 | 92 | let success: ResponseContext = .init { response in 93 | response.status = .noContent 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/App/Controllers/APIDocsController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIDocsController.swift 3 | // 4 | // 5 | // Created by Mathew Polzin on 12/28/19. 6 | // 7 | 8 | import Vapor 9 | import VaporOpenAPI 10 | import Foundation 11 | import Yams 12 | 13 | final class APIDocsController { 14 | 15 | let app: Application 16 | 17 | init(app: Application) { 18 | self.app = app 19 | } 20 | 21 | func view(_ req: TypedRequest) -> EventLoopFuture { 22 | let html = 23 | """ 24 | 25 | 26 | 27 | ReDoc 28 | 29 | 30 | 31 | 32 | 33 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | """ 49 | 50 | return req.response.success.encode(html) 51 | } 52 | 53 | func show(_ req: TypedRequest) throws -> EventLoopFuture { 54 | 55 | // TODO: Add support for ContentEncoder to JSONAPIOpenAPI 56 | let jsonEncoder = JSONEncoder() 57 | if #available(macOS 10.12, *) { 58 | jsonEncoder.dateEncodingStrategy = .iso8601 59 | jsonEncoder.outputFormatting = .sortedKeys 60 | } 61 | #if os(Linux) 62 | jsonEncoder.dateEncodingStrategy = .iso8601 63 | jsonEncoder.outputFormatting = .sortedKeys 64 | #endif 65 | 66 | let info = OpenAPI.Document.Info( 67 | title: "Vapor OpenAPI Example API", 68 | description: 69 | ###""" 70 | ## Descriptive Text 71 | This text supports _markdown_! 72 | """###, 73 | version: "1.0" 74 | ) 75 | 76 | let servers = [ 77 | OpenAPI.Server(url: URL(string: "https://\(app.http.server.configuration.hostname)")!) 78 | ] 79 | 80 | let paths = try app.routes.openAPIPathItems(using: jsonEncoder) 81 | 82 | let document = OpenAPI.Document( 83 | info: info, 84 | servers: servers, 85 | paths: paths, 86 | components: .noComponents, 87 | security: [] 88 | ) 89 | 90 | return req 91 | .response 92 | .success 93 | .encode(try YAMLEncoder().encode(document)) 94 | } 95 | } 96 | 97 | // MARK: - Contexts 98 | extension APIDocsController { 99 | struct ShowContext: RouteContext { 100 | typealias RequestBodyType = EmptyRequestBody 101 | 102 | static let defaultContentType: HTTPMediaType? = nil 103 | static let shared = Self() 104 | 105 | let success: ResponseContext = .init { response in 106 | response.headers.contentType = .init(type: "application", subType: "x-yaml") 107 | response.status = .ok 108 | } 109 | } 110 | 111 | struct ViewContext: RouteContext { 112 | typealias RequestBodyType = EmptyRequestBody 113 | 114 | static let defaultContentType: HTTPMediaType? = nil 115 | static let shared = Self() 116 | 117 | let success: ResponseContext = .init { response in 118 | response.headers.contentType = .html 119 | response.status = .ok 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VaporOpenAPIExample 2 | 3 | This example shows off an early stage project to both add type information to Vapor requests/responses and also take advantage of that information to generate OpenAPI documentation. 4 | 5 | Note that this app and the libraries it showcases are built off of Vapor 4. 6 | 7 | The example app serves up OpenAPI documentation on itself using the beautiful Redoc viewer. 8 | 9 | The OpenAPI document produced is compatible with the OpenAPI specification v3.1.x. 10 | 11 | ![Documentation served by example app](./Screen%20Shot%202019-12-28%20at%207.18.48%20PM.png) 12 | 13 | The OpenAPI it produces contains (among other things) routes, path and query parameters, and success and error responses including response body schemas. 14 | 15 | ```yaml 16 | openapi: 3.1.1 17 | info: 18 | title: Vapor OpenAPI Example API 19 | description: '## Descriptive Text 20 | 21 | This text supports _markdown_!' 22 | version: '1.0' 23 | servers: 24 | - url: https://127.0.0.1 25 | paths: 26 | /docs: 27 | get: 28 | tags: 29 | - Documentation 30 | summary: View API Documentation 31 | description: API Documentation is served using the Redoc web app. 32 | responses: 33 | 200: 34 | description: OK 35 | content: 36 | text/html: 37 | schema: 38 | type: string 39 | /docs/openapi.yml: 40 | get: 41 | tags: 42 | - Documentation 43 | summary: Download API Documentation 44 | description: Retrieve the OpenAPI documentation as a YAML file. 45 | responses: 46 | 200: 47 | description: OK 48 | content: 49 | application/x-yaml: 50 | schema: 51 | type: string 52 | /hello: 53 | get: 54 | tags: 55 | - Greetings 56 | summary: View a greeting 57 | description: Say hello in one of the supported languages! 58 | parameters: 59 | - name: language 60 | in: query 61 | schema: 62 | type: string 63 | enum: 64 | - english 65 | - spanish 66 | responses: 67 | 200: 68 | description: OK 69 | content: 70 | application/json: 71 | schema: 72 | type: object 73 | properties: 74 | language: 75 | type: string 76 | enum: 77 | - english 78 | - spanish 79 | greeting: 80 | type: string 81 | required: 82 | - language 83 | - greeting 84 | 400: 85 | description: Bad Request 86 | content: 87 | text/plain: 88 | schema: 89 | type: string 90 | post: 91 | tags: 92 | - Greetings 93 | summary: Create a greeting 94 | description: The endpoint is not actually implemented. It is just mocked up. 95 | requestBody: 96 | content: 97 | application/json: 98 | schema: 99 | type: object 100 | properties: 101 | language: 102 | type: string 103 | enum: 104 | - english 105 | - spanish 106 | greeting: 107 | type: string 108 | required: 109 | - language 110 | - greeting 111 | responses: 112 | 201: 113 | description: Created 114 | content: 115 | application/json: 116 | schema: 117 | type: object 118 | properties: 119 | language: 120 | type: string 121 | enum: 122 | - english 123 | - spanish 124 | greeting: 125 | type: string 126 | required: 127 | - language 128 | - greeting 129 | delete: 130 | tags: 131 | - Greetings 132 | summary: Delete a greeting 133 | responses: 134 | 204: 135 | description: No Content 136 | ``` 137 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "async-http-client", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server/async-http-client.git", 7 | "state" : { 8 | "revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf", 9 | "version" : "1.19.0" 10 | } 11 | }, 12 | { 13 | "identity" : "async-kit", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/vapor/async-kit.git", 16 | "state" : { 17 | "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", 18 | "version" : "1.19.0" 19 | } 20 | }, 21 | { 22 | "identity" : "console-kit", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/vapor/console-kit.git", 25 | "state" : { 26 | "revision" : "f4ef965dadd999f7e4687053153c97b8b320819c", 27 | "version" : "4.10.1" 28 | } 29 | }, 30 | { 31 | "identity" : "multipart-kit", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/vapor/multipart-kit.git", 34 | "state" : { 35 | "revision" : "1adfd69df2da08f7931d4281b257475e32c96734", 36 | "version" : "4.5.4" 37 | } 38 | }, 39 | { 40 | "identity" : "openapikit", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/mattpolzin/OpenAPIKit.git", 43 | "state" : { 44 | "revision" : "6a4bcdcd4335176c99da68bc2d49ef25ecac8b7c", 45 | "version" : "4.2.0" 46 | } 47 | }, 48 | { 49 | "identity" : "openapireflection", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/mattpolzin/OpenAPIReflection.git", 52 | "state" : { 53 | "revision" : "d5981296867215f62363d6c88cf05c8d71cf8d48", 54 | "version" : "3.0.0" 55 | } 56 | }, 57 | { 58 | "identity" : "routing-kit", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/vapor/routing-kit.git", 61 | "state" : { 62 | "revision" : "17a7a3facce8285fd257aa7c72d5e480351e7698", 63 | "version" : "4.8.2" 64 | } 65 | }, 66 | { 67 | "identity" : "sampleable", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/mattpolzin/Sampleable.git", 70 | "state" : { 71 | "revision" : "df44bf1a860481109dcf455e3c6daf0a0f1bc259", 72 | "version" : "2.1.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-algorithms", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/apple/swift-algorithms.git", 79 | "state" : { 80 | "revision" : "bcd4f369ac962bc3e5244c9df778739f8f5bdbf1", 81 | "version" : "1.1.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swift-atomics", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/apple/swift-atomics.git", 88 | "state" : { 89 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 90 | "version" : "1.2.0" 91 | } 92 | }, 93 | { 94 | "identity" : "swift-collections", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/apple/swift-collections.git", 97 | "state" : { 98 | "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", 99 | "version" : "1.0.5" 100 | } 101 | }, 102 | { 103 | "identity" : "swift-crypto", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/apple/swift-crypto.git", 106 | "state" : { 107 | "revision" : "b51f1d6845b353a2121de1c6a670738ec33561a6", 108 | "version" : "3.1.0" 109 | } 110 | }, 111 | { 112 | "identity" : "swift-http-types", 113 | "kind" : "remoteSourceControl", 114 | "location" : "https://github.com/apple/swift-http-types", 115 | "state" : { 116 | "revision" : "99d066e29effa8845e4761dd3f2f831edfdf8925", 117 | "version" : "1.0.0" 118 | } 119 | }, 120 | { 121 | "identity" : "swift-log", 122 | "kind" : "remoteSourceControl", 123 | "location" : "https://github.com/apple/swift-log.git", 124 | "state" : { 125 | "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", 126 | "version" : "1.5.3" 127 | } 128 | }, 129 | { 130 | "identity" : "swift-metrics", 131 | "kind" : "remoteSourceControl", 132 | "location" : "https://github.com/apple/swift-metrics.git", 133 | "state" : { 134 | "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", 135 | "version" : "2.4.1" 136 | } 137 | }, 138 | { 139 | "identity" : "swift-nio", 140 | "kind" : "remoteSourceControl", 141 | "location" : "https://github.com/apple/swift-nio.git", 142 | "state" : { 143 | "revision" : "853522d90871b4b63262843196685795b5008c46", 144 | "version" : "2.61.1" 145 | } 146 | }, 147 | { 148 | "identity" : "swift-nio-extras", 149 | "kind" : "remoteSourceControl", 150 | "location" : "https://github.com/apple/swift-nio-extras.git", 151 | "state" : { 152 | "revision" : "798c962495593a23fdea0c0c63fd55571d8dff51", 153 | "version" : "1.20.0" 154 | } 155 | }, 156 | { 157 | "identity" : "swift-nio-http2", 158 | "kind" : "remoteSourceControl", 159 | "location" : "https://github.com/apple/swift-nio-http2.git", 160 | "state" : { 161 | "revision" : "3bd9004b9d685ed6b629760fc84903e48efec806", 162 | "version" : "1.29.0" 163 | } 164 | }, 165 | { 166 | "identity" : "swift-nio-ssl", 167 | "kind" : "remoteSourceControl", 168 | "location" : "https://github.com/apple/swift-nio-ssl.git", 169 | "state" : { 170 | "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", 171 | "version" : "2.25.0" 172 | } 173 | }, 174 | { 175 | "identity" : "swift-nio-transport-services", 176 | "kind" : "remoteSourceControl", 177 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 178 | "state" : { 179 | "revision" : "ebf8b9c365a6ce043bf6e6326a04b15589bd285e", 180 | "version" : "1.20.0" 181 | } 182 | }, 183 | { 184 | "identity" : "swift-numerics", 185 | "kind" : "remoteSourceControl", 186 | "location" : "https://github.com/apple/swift-numerics", 187 | "state" : { 188 | "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", 189 | "version" : "1.0.2" 190 | } 191 | }, 192 | { 193 | "identity" : "vapor", 194 | "kind" : "remoteSourceControl", 195 | "location" : "https://github.com/vapor/vapor.git", 196 | "state" : { 197 | "revision" : "3744d4200c9293584603d650f8ef70ae7e6896e7", 198 | "version" : "4.86.0" 199 | } 200 | }, 201 | { 202 | "identity" : "vaporopenapi", 203 | "kind" : "remoteSourceControl", 204 | "location" : "https://github.com/mattpolzin/VaporOpenAPI.git", 205 | "state" : { 206 | "revision" : "7535ecbab7f13ccd581621acee70b36e09101ad2", 207 | "version" : "0.8.0" 208 | } 209 | }, 210 | { 211 | "identity" : "vaportypedroutes", 212 | "kind" : "remoteSourceControl", 213 | "location" : "https://github.com/mattpolzin/VaporTypedRoutes.git", 214 | "state" : { 215 | "revision" : "f28103ae5a8124dfb8b2386460e7ff8b0dd9aef7", 216 | "version" : "0.10.0" 217 | } 218 | }, 219 | { 220 | "identity" : "websocket-kit", 221 | "kind" : "remoteSourceControl", 222 | "location" : "https://github.com/vapor/websocket-kit.git", 223 | "state" : { 224 | "revision" : "53fe0639a98903858d0196b699720decb42aee7b", 225 | "version" : "2.14.0" 226 | } 227 | }, 228 | { 229 | "identity" : "yams", 230 | "kind" : "remoteSourceControl", 231 | "location" : "https://github.com/jpsim/Yams.git", 232 | "state" : { 233 | "revision" : "d41ba4e7164c0838c6d48351f7575f7f762151fe", 234 | "version" : "6.1.0" 235 | } 236 | } 237 | ], 238 | "version" : 2 239 | } 240 | --------------------------------------------------------------------------------