Generates a package from a swagger spec file that can be imported by a Vapor 4 project. The package will contain a routes.swift, protocols to implement the APIs and data models.
This package was originally generated by the Swagger Codegen tool.
This project was designed so that you start building your API using Vapor 4 from a swagger spec file. Then using this tool generate the Vapor Server interface as a package. Your Vapor project will depend on the new interface package and use the auto-generated routes.swift
, API Interfaces and data models. See instructions below on how to consume the package's interfaces.
I hope that you find this project to be useful. I would like to see server side swift adopted more commonly as I love the swift language for its performance and other conveniences. Vapor is a low memory, low cpu web server and hope to see it become well established in the future. Now if you get an API swagger definition file, you can get starte fairly quickly using this project. And if you don't have a swagger spec file to start from, I would recommend starting to write an API by defining the swagger spec file first. This project should reduce the overhead of going from API design to compiling working code as you make changes to your APIs.
Let me know if you run into issues with this project or its documentation. Help is welcome!
Known Gaps were identified by the tests included in this project.
I manage a docker image to simplify the codegen process.
Docker image is expected to mount the current working directry to the container's /output
directory. You can run the codegen using the following:
docker run --rm -v "$(pwd):/output" thecheatah/vapor-server-codegen:4 -i api.yaml -o ./output --additional-properties projectName=MyProjectAPI
You can structure your directory like this:
|- MyProjectAPI
|-- build.sh
|-- api.yaml
build.sh
can contain the following:
#!/bin/sh
docker run --rm -v "$(pwd):/output" thecheatah/vapor-server-codegen:4 -i api.yaml -o . --additional-properties projectName=MyProjectAPI
Running build.sh
will create a swiftpm compatible package within the MyProjectAPI
directory. You can commit this package and reference it from you Vapor Server using swiftpm.
|- MyProjectAPI
|-- build.sh
|-- api.yaml
|-- Package.swift
|-- Sources
|--- MyProjectAPI
|---- APIs
|----- etc.
|---- Models
|----- etc.
|---- routes.swift
As of now, you must generate your own jar file. Check out or download the complete source.
git clone https://github.com/thecheatah/vapor-server-codegen.git
cd vapor-server-codegen
./build.sh
The ./build.sh
command will run mvn -Dmaven.test.skip=true package
to build the java package without running test and produce a docker image synology:5350/swagger-codegen/swift/vapor-server-codegen
. This project was created on MacOS with Swift 5.1.3 installed. The project also contains ./src/test/resources/AllTest/VaporTestServer/run_linux_test
and ./src/test/resources/WithoutAuthTest/VaporTestServer/run_linux_test
to run the tests in a docker container.
Once the package has been created, you will find vapor-server-codegen-4.0.0.jar
in the target
directory.
You can now generate the Vapor Server Interface Package from a swagger file.
java -cp vapor-server-codegen-4.0.0.jar:swagger-codegen-cli-3.0.7.jar io.swagger.codegen.v3.cli.SwaggerCodegen generate -l SwiftVapor4 -i ./codegen_test.yml -o ./output/MyApiVaporInterface --additional-properties projectName=MyApiVaporInterface
The swagger-codegen-cli-3.0.7.jar
can be built from the swagger-codegen package. Personally, I use the one from maven .m2/repository/io/swagger/codegen/v3/swagger-codegen-cli/3.0.7/swagger-codegen-cli-3.0.7.jar
This project depends on it and will be pulled in to your maven cache.
The auto-generated package will contain 3 key directories:
|- Sources
|-- {Package Name}
|--- routes.swift
|--- APIs
|---- {Swagger Tag 1}ApiDelegate.swift
|---- {Swagger Tag 2}ApiDelegate.swift
|--- Models
|---- {Swagger Model 1}.swift
|---- {Swagger Model 2}.swift
routes.swift
will contain a public function where the Router
and controllers implementing the protocols defined in the APIs directory need to be passed in. The protocols defined in the APIs directory provide the key interface that your code needs to interact with. The interface provided is designed to exchange simple data types or models created from the swagger. The Models
directory contains all of the models generated from the swagger spec.
In order to use the auto-generated package, your Vapor project needs to define a dependency on the package. The swift project included to run test cases has an example Package.swift
that defines such a dependency.
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "VaporTestServer",
platforms: [
.macOS(.v10_14)
],
products: [
.executable(name: "Run", targets: ["Run"]),
.library(name: "App", targets: ["App"]),
],
dependencies: [
// đź’§ A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta.2.1"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-beta.2"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0-beta.2"),
.package(path: "../VaporTestInterface/")
],
targets: [
.target(name: "App", dependencies: ["Fluent", "FluentSQLiteDriver", "Vapor", "VaporTestInterface"]),
.target(name: "Run", dependencies: ["App"]),
.testTarget(name: "AppTests", dependencies: ["App", "XCTVapor"])
]
)
In the example above the generated package is VaporTestInterface
and is located next to the VaporTestServer
vapor project.
Next, you will need to create controller classes for each of the APIs defined within the swagger. The swagger-codegen defaults to generating an API for each tag used in swagger spec. Each controller class will need to implement the specific API's interface as defined within the generated package.
import Vapor
import VaporTestInterface
class DataModelController: DataModelApiDelegate {
typealias AuthType = SampleAuthType
func referencedObject(with req: Request, body: SimpleObject) throws -> EventLoopFuture<referencedObjectResponse> {
return req.eventLoop.makeSucceededFuture(.http200(body))
}
}
In the example above the DataModelController
class is in a file within your project. DataModelApiDelegate
is from the auto-generated package VaporTestInterface
.
Required flags are respected and determines if a field is optional or not.
The generated interface is designed to handle HTTP response codes by building an enum for each possible response code. The enum is parameterized to take in the payload to be returned for each response code.
public enum multipleResponseCodesResponse: ResponseEncodable {
case http200
case http201(SimpleObject)
case http401
case http500
public func encodeResponse(for request: Request) -> EventLoopFuture<Response> {
switch self {
case .http200:
let response = Response()
response.status = HTTPStatus(statusCode: 200)
return request.eventLoop.makeSucceededFuture(response)
case .http201(let content):
return content.encodeResponse(for: request).map { (response: Response) -> (Response) in
response.status = HTTPStatus(statusCode: 201)
return response
}
case .http401:
let response = Response()
response.status = HTTPStatus(statusCode: 401)
return request.eventLoop.makeSucceededFuture(response)
case .http500:
let response = Response()
response.status = HTTPStatus(statusCode: 500)
return request.eventLoop.makeSucceededFuture(response)
}
}
}
Here are examples of returning various response codes:
func multipleResponseCodes(with req: Request, body: MultipleResponseCodeRequest) throws -> EventLoopFuture<multipleResponseCodesResponse> {
switch body.responseCode {
case MultipleResponseCodeRequest.ResponseCode._200:
return req.eventLoop.makeSucceededFuture(.http200)
case MultipleResponseCodeRequest.ResponseCode._201:
return req.eventLoop.makeSucceededFuture(.http201(SimpleObject(simpleString: "Simple String", simpleNumber: 44.22, simpleInteger: 44, simpleDate: Date(), simpleEnumString: ._1, simpleBoolean: false, simpleArray: ["Hi!"])))
case MultipleResponseCodeRequest.ResponseCode._401:
return req.eventLoop.makeSucceededFuture(.http401)
case MultipleResponseCodeRequest.ResponseCode._500:
return req.eventLoop.makeSucceededFuture(.http500)
}
}
A POST/PATCH/etc. request and a response body will map to an API interface as such:
func referencedObject(with req: Request, body: SimpleObject) throws -> EventLoopFuture<referencedObjectResponse> {
return req.eventLoop.makeSucceededFuture(.http200(body))
}
This library is designed to only handle json responses. The swagger spec supports multiple response types and the "enum" approach could be extended to handle multiple response types. It's not something that is currently supported.
GET parameters map to function parameters within a controller. In the example below param1 and param2 are get parameters like ?param1=string¶m2=44
func queryParameters(with req: Request, param1: String, param2: Int?) throws -> EventLoopFuture<queryParametersResponse> {
return req.eventLoop.makeSucceededFuture(.http200(QueryParametersResponse(param1: param1, param2: param2)))
}
Parameters within a path are always handled as strings. The /path/{param1}/and/{param2} path parameters will generate the following:
func multipleParameter(with req: Request, param1: String, param2: String) throws -> EventLoopFuture<multipleParameterResponse> {
return req.eventLoop.makeSucceededFuture(.http200(MultipleParameterResponse(param1: param1, param2: param2)))
}
Headers from requests and responses will generate an interface like this:
class HeadersController: HeadersApiDelegate {
typealias AuthType = SampleAuthType
func responseHeaders(with req: Request) throws -> EventLoopFuture<responseHeadersResponse> {
return req.eventLoop.makeSucceededFuture(.http303(location: "https://chckt.com/login"))
}
func requestHeaders(with req: Request, xExampleRequiredHeader: String, xExampleArrayHeader: [String]) throws -> EventLoopFuture<requestHeadersResponse> {
return req.eventLoop.makeSucceededFuture(.http200(RequestHeadersResponse(requiredHeader: xExampleRequiredHeader, arrayHeader: xExampleArrayHeader)))
}
}
The swagger-codegen library flattens the first level of the data model representing the form parameters.
SimpleObject:
type: object
required: [simpleString, simpleNumber, simpleInteger, simpleDate, simpleEnumString, simpleBoolean, simpleArray]
properties:
simpleString:
$ref: '#/components/schemas/SimpleString'
simpleNumber:
$ref: '#/components/schemas/SimpleNumber'
simpleInteger:
$ref: '#/components/schemas/SimpleInteger'
simpleDate:
$ref: '#/components/schemas/SimpleDate'
simpleEnumString:
$ref: '#/components/schemas/SimpleEnumString'
simpleBoolean:
$ref: '#/components/schemas/SimpleBoolean'
simpleArray:
type: array
items:
$ref: '#/components/schemas/SimpleString'
Would map to:
func formRequest(with req: Request, simpleString: SimpleString, simpleNumber: SimpleNumber, simpleInteger: SimpleInteger, simpleDate: SimpleDate, simpleEnumString: SimpleEnumString, simpleBoolean: SimpleBoolean, simpleArray: [SimpleString]) throws -> EventLoopFuture<formRequestResponse> {
return req.eventLoop.makeSucceededFuture(.http200(SimpleObject(simpleString: simpleString, simpleNumber: simpleNumber, simpleInteger: simpleInteger, simpleDate: simpleDate, simpleEnumString: simpleEnumString, simpleBoolean: simpleBoolean, simpleArray: simpleArray)))
}
Due to the limitation of how the codegen represents swagger and how I wanted to generate the interface, this generator can only handle one authentication mechanism per endpoint.
If multiple authentication mechanisms are used, a single endpoint can only handle one authentication mechanism, but different endpoints can handle different authentication mechanisms. The "AuthType" object type set from the multiple authentication mechanisms must be the same. This library uses generics to enforce that.
Here is an example interface from the test suite. The user
parameter will contain the object authenticated by the authenticator.
class AuthenticationController: AuthenticationApiDelegate {
typealias AuthType = SampleAuthType
func securityProtectedEndpoint(with req: Request, asAuthenticated user: SampleAuthType) throws -> EventLoopFuture<securityProtectedEndpointResponse> {
return req.eventLoop.makeSucceededFuture(.http200(SecurityProtectedEndpointResponse(secret: user.secret, securedBy: user.securedBy)))
}
func anotherSecurityProtectedEndpoint(with req: Request, asAuthenticated user: SampleAuthType) throws -> EventLoopFuture<anotherSecurityProtectedEndpointResponse> {
return req.eventLoop.makeSucceededFuture(.http200(SecurityProtectedEndpointResponse(secret: user.secret, securedBy: user.securedBy)))
}
}
The authenticator is a Vapor Middleware, but needs to extend AuthenticationMiddleware from the generated library. The interface for AuthenticationMiddleware is as follows:
public protocol AuthenticationMiddleware: Middleware {
associatedtype AuthType: Authenticatable
func authType() -> AuthType.Type
}
Here is an example authentication middleware:
import Vapor
import VaporTestInterface
struct SampleAuthType: Authenticatable {
let securedBy: SecurityProtectedEndpointResponse.SecuredBy
let secret: String
}
class SecurityMiddleware: AuthenticationMiddleware {
typealias AuthType = SampleAuthType
func authType() -> SampleAuthType.Type {
return SampleAuthType.self
}
func respond(to request: Request, chainingTo next: Responder) -> EventLoopFuture<Response> {
guard let bearer = request.headers.bearerAuthorization else {
return request.eventLoop.makeFailedFuture(Abort(.unauthorized))
}
if bearer.token != "Secret" {
return request.eventLoop.makeFailedFuture(Abort(.unauthorized))
}
request.auth.login(SampleAuthType(securedBy: .security1, secret: bearer.token))
return next.respond(to: request)
}
}
In the example above, note that the middleware sets try request.authenticate(SampleAuthType(secret: bearer.token))
where the authenticated object is the same type SampleAuthType
as typealias AuthType = SampleAuthType
.
The authentication middleware needs to be passed in the generated interface's routes
method as a parameter.
This is the generated routes interface from the test suite. You don't need to understand what it does, but know that it uses generics to map the AuthType
returned from the authenticator to the AuthType
.
public func routes<authForSecurity1: AuthenticationMiddleware, authForSecurity2: AuthenticationMiddleware, anotherAuthentication: AnotherAuthenticationApiDelegate, authentication: AuthenticationApiDelegate, dataModel: DataModelApiDelegate, formData: FormDataApiDelegate, headers: HeadersApiDelegate, multipleResponseCodes: MultipleResponseCodesApiDelegate, pathParsing: PathParsingApiDelegate, queryParameters: QueryParametersApiDelegate>
(_ app: Application, anotherAuthentication: anotherAuthentication, authentication: authentication, dataModel: dataModel, formData: formData, headers: headers, multipleResponseCodes: multipleResponseCodes, pathParsing: pathParsing, queryParameters: queryParameters, authForSecurity1: authForSecurity1, authForSecurity2: authForSecurity2)
throws
where authForSecurity1.AuthType == anotherAuthentication.AuthType, authForSecurity1.AuthType == authentication.AuthType, authForSecurity1.AuthType == dataModel.AuthType, authForSecurity1.AuthType == formData.AuthType, authForSecurity1.AuthType == headers.AuthType, authForSecurity1.AuthType == multipleResponseCodes.AuthType, authForSecurity1.AuthType == pathParsing.AuthType, authForSecurity1.AuthType == queryParameters.AuthType, authForSecurity2.AuthType == anotherAuthentication.AuthType, authForSecurity2.AuthType == authentication.AuthType, authForSecurity2.AuthType == dataModel.AuthType, authForSecurity2.AuthType == formData.AuthType, authForSecurity2.AuthType == headers.AuthType, authForSecurity2.AuthType == multipleResponseCodes.AuthType, authForSecurity2.AuthType == pathParsing.AuthType, authForSecurity2.AuthType == queryParameters.AuthType
The first parameter in the routes
will be the Router
from the Vapor library, the next set of parameters will be the Controllers implementing the delegates followed by the authenticator.
When generating the API delegate, the swagger library does not pass in the authentication schemes used by the API's operations and the mustache template engine does not allow one to aggregate the authentication schemes used within an API's operations. Thus, this library, regardless if authentication is used within the swagger or not, will generate a associatedtype AuthType
in the generated API protocol. A default DummyAuthType
is provided that needs to be set on a controller whose operations do not use an authentication.
Here is what the DummyAuthType looks like:
//Used when auth is not used
public class DummyAuthType: Authenticatable {}
Here is an example of a Controller setting the dummy auth type:
class DataModelController: DataModelApiDelegate {
typealias AuthType = DummyAuthType.Type
func referencedObject(with req: Request, body: SimpleObject) throws -> EventLoopFuture<referencedObjectResponse> {
return req.eventLoop.makeSucceededFuture(.http200(body))
}
}
If any of the controllers uses an authentication mechanism, all other controllers must set the AuthType
to the same. The authentication mechanism must also set the same auth type object.
The generated package will contain a routes.swift
. This file will contain a public function public func routes(_ router: Router, ...
. You can call this function from the routes.swift
from your vapor project and pass in the Router
as well as the controllers implementing the API interfaces.
Once this step is done, you can now run the vapor server (vapor run
) from your project and try out the APIs.
When I work on this project, I work on a Mac and use xcode. I have included linux build script using docker that runs the swift test cases in a docker image. If you want to do development on Linux or Windows, there is nothing theoretically stopping you.
To get into the grove of making changes, running tests, adding new functionality and adding test cases, you should configure the project locally as follows:
This project uses docker and maven to build. The root contains a build.sh
that results in a docker image being built. The default docker image is configured using environment variables located in .env
. You can run build.sh
after checking out the project and use the resulting docker image synology:5350/swagger-codegen/swift/vapor-server-codegen
. The swift tests below make use of this image as well. So if you change the .env
file you will need to update the run_codegen.sh
files as well.
Once you can see that the java project works, you should configure the swift project as well for development
There are two swift projects AllTest
and WithoutAuthTest
that test the codegen to ensure that the output compiles and does what's expected. The projects are located in src/test/resources/
.
Each project directory has a test yaml codegen_test.yml
, a build script run_codegen.sh
and a test project VaporTestServer
. Running the build script will output a VaporTestInterface
folder. You should be able to open the VaporTestServer
project and run the tests.
Now you are ready for the edit, build, test cycle.
- You can edit the java code as well as the mustache templates in the java project
- Run
build.sh
to build the java package and generate a docker image - Run codegen
src/test/resources/AllTest/run_codegen.sh
orsrc/test/resources/WithoutAuthTest/run_codegen.sh
- Run the tests in swift using xcode or the commandline
I mostly edited the mustache templates in this project. run_codegen.sh
pipes the output by default to a file in the same directory called codegen.out
. It's configured to dump the json payloads that are fed into the mustache template engine. Each generated swift file has a "Template Input" line like this: Template Input: /APIs.FormData
. In the example template input line you can search for /APIs.FormData
in the codegen.out
file and find the json payload. I usually copy that subset of the json payload into Chrome/Safari/Firefox's developer console. For example I will do var json = command+v
and then press up and type in json
and hit enter. The browser's developer console will let you browse the json tree easily.
The instructions below were generated automatically by the swagger-codegen build script. I have kept them here to assist users new to the swagger-codegen tool. (It's Awesome!)
At this point, you've likely generated a client setup. It will include something along these lines:
.
|- README.md // this file
|- pom.xml // build script
|-- src
|--- main
|---- java
|----- com.ravneetsingh.swagger.swift.vapor4.Swiftvapor4Generator.java // generator file
|---- resources
|----- SwiftVapor4 // template files
|----- META-INF
|------ services
|------- io.swagger.codegen.CodegenConfig
You will need to make changes in at least the following:
Swiftvapor4Generator.java
Templates in this folder:
src/main/resources/SwiftVapor4
Once modified, you can run this:
mvn package
In your generator project. A single jar file will be produced in target
. You can now use that with codegen:
java -cp /path/to/swagger-codegen-cli.jar:/path/to/your.jar io.swagger.codegen.Codegen -l SwiftVapor4 -i /path/to/swagger.yaml -o ./test
Now your templates are available to the client generator and you can write output values
The Swiftvapor4Generator.java
has comments in it--lots of comments. There is no good substitute
for reading the code more, though. See how the Swiftvapor4Generator
implements CodegenConfig
.
That class has the signature of all values that can be overridden.
For the templates themselves, you have a number of values available to you for generation.
You can execute the java
command from above while passing different debug flags to show
the object you have available during client generation:
# The following additional debug options are available for all codegen targets:
# -DdebugSwagger prints the OpenAPI Specification as interpreted by the codegen
# -DdebugModels prints models passed to the template engine
# -DdebugOperations prints operations passed to the template engine
# -DdebugSupportingFiles prints additional data passed to the template engine
java -DdebugOperations -cp /path/to/swagger-codegen-cli.jar:/path/to/your.jar io.swagger.codegen.Codegen -l SwiftVapor4 -i /path/to/swagger.yaml -o ./test
Will, for example, output the debug info for operations. You can use this info
in the api.mustache
file.