-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'Swaggen Templates' into v1.0.0
- Loading branch information
Showing
11 changed files
with
574 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{% if description %} | ||
/** {{ description }} */ | ||
{% endif %} | ||
public enum {{ enumName }}: {{ type }}, Codable, Equatable, CaseIterable { | ||
{% for enumCase in enums %} | ||
case {{ enumCase.name }} = {% if type == "String" %}"{% endif %}{{enumCase.value}}{% if type == "String" %}"{% endif %} | ||
{% endfor %} | ||
{% if options.enumUndecodableCase %} | ||
case undecodable | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.singleValueContainer() | ||
let rawValue = try container.decode({{ type }}.self) | ||
self = {{ enumName }}(rawValue: rawValue) ?? .undecodable | ||
} | ||
{% endif %} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// | ||
// Generated by SwagGen | ||
// https://github.com/yonaskolb/SwagGen | ||
// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
{% if description %} | ||
/** {{ description }} */ | ||
{% endif %} | ||
{% if enum %} | ||
{% include "Includes/Enum.stencil" enum %} | ||
{% elif aliasType %} | ||
public typealias {{ type }} = {{ aliasType }} | ||
{% elif additionalPropertiesType and allProperties.count == 0 %} | ||
public typealias {{ type }} = [String: {{ additionalPropertiesType }}] | ||
{% elif discriminatorType %} | ||
public enum {{ type }}: Codable, Equatable, Content { | ||
{% for subType in discriminatorType.subTypes %} | ||
case {{ subType.name}}({{ subType.type }}) | ||
{% endfor %} | ||
{% if options.enumUndecodableCase %} | ||
case undecodable | ||
{% endif %} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let container = try decoder.container(keyedBy: StringCodingKey.self) | ||
let discriminator: String = try container.decode("{{ discriminatorType.discriminatorProperty }}") | ||
switch discriminator { | ||
{% for name, subType in discriminatorType.mapping %} | ||
case "{{ name }}": | ||
self = .{{ subType.name}}(try {{ subType.type }}(from: decoder)) | ||
{% endfor %} | ||
default: | ||
{% if options.enumUndecodableCase %} | ||
self = .undecodable | ||
{% else %} | ||
throw DecodingError.dataCorrupted(DecodingError.Context.init(codingPath: decoder.codingPath, debugDescription: "Couldn't find type to decode with discriminator \(discriminator)")) | ||
{% endif %} | ||
} | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.singleValueContainer() | ||
switch self { | ||
{% for subType in discriminatorType.subTypes %} | ||
case .{{ subType.name}}(let content): | ||
try container.encode(content) | ||
{% endfor %} | ||
{% if options.enumUndecodableCase %} | ||
case .undecodable: | ||
try container.encode("undecodable") | ||
{% endif %} | ||
} | ||
} | ||
} | ||
{% else %} | ||
public {{ options.modelType }} {{ type }}: {% if parent %}{{ parent.type }}{% else %}{% if options.modelProtocol %}{{ options.modelProtocol }}{% else %}Codable, Equatable, Content{% endif %}{% endif %} { | ||
{% for enum in enums %} | ||
{% if not enum.isGlobal %} | ||
|
||
{% filter indent:4 %}{% include "Includes/Enum.stencil" enum %}{% endfilter %} | ||
{% endif %} | ||
{% endfor %} | ||
{% for property in properties %} | ||
|
||
{% if property.description %} | ||
/** {{ property.description }} */ | ||
{% endif %} | ||
public {% if options.mutableModels %}var{% else %}let{% endif %} {{ property.name }}: {{ property.optionalType }} | ||
{% endfor %} | ||
{% if additionalPropertiesType %} | ||
|
||
public {% if options.mutableModels %}var{% else %}let{% endif %} additionalProperties: [String: {{ additionalPropertiesType }}] = [:] | ||
{% endif %} | ||
{% for schema in schemas %} | ||
|
||
{% filter indent:4 %}{% include "Includes/Model.stencil" schema %}{% endfilter %} | ||
{% endfor %} | ||
|
||
public {% if parent %}{% if properties.count == 0 %}override {% endif %}{% endif %}init({% for property in allProperties %}{{ property.name }}: {{ property.optionalType }}{% ifnot property.required %} = nil{% endif %}{% ifnot forloop.last %}, {% endif %}{% endfor %}) { | ||
{% for property in properties %} | ||
self.{{ property.name }} = {{ property.name }} | ||
{% endfor %} | ||
{% if parent %} | ||
super.init({% for property in parent.allProperties %}{{ property.name }}: {{ property.name }}{% ifnot forloop.last %}, {% endif %}{% endfor %}) | ||
{% endif %} | ||
} | ||
|
||
{% if additionalPropertiesType %} | ||
|
||
public subscript(key: String) -> {{ additionalPropertiesType }}? { | ||
get { | ||
return additionalProperties[key] | ||
} | ||
set { | ||
additionalProperties[key] = newValue | ||
} | ||
} | ||
{% endif %} | ||
|
||
public enum CodingKeys: String, CodingKey { | ||
{% for property in properties %} | ||
case {{ property.name }} = "{{ property.value }}" | ||
{% endfor %} | ||
} | ||
} | ||
{% endif %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// swift-tools-version:5.7 | ||
// swiftlint:disable explicit_top_level_acl | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "{{ options.name }}", | ||
platforms: [ | ||
.macOS(.v13), | ||
.iOS(.v16), | ||
.tvOS(.v16), | ||
.watchOS(.v9) | ||
], | ||
products: [ | ||
.library(name: "{{ options.name }}", targets: ["{{ options.name }}"]) | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/brightdigit/Prch.git", from: "1.0.0-alpha.1"), | ||
{% for dependency in options.dependencies %} | ||
.package(url: "https://github.com/{{ dependency.github }}.git", from: "{{ dependency.version }}"), | ||
{% endfor %} | ||
], | ||
targets: [ | ||
.target(name: "{{ options.name }}", dependencies: [ | ||
.product(name: "PrchModel", package: "Prch") | ||
{% for dependency in options.dependencies %} | ||
"{{ dependency.name }}", | ||
{% endfor %} | ||
]) | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# {{ options.name }} | ||
|
||
This is an api generated from a OpenAPI 3.0 spec with [SwagGen](https://github.com/yonaskolb/SwagGen) | ||
|
||
## Operation | ||
|
||
Each operation lives under the `{{ options.name }}` namespace and within an optional tag: `{{ options.name }}(.tagName).operationId`. If an operation doesn't have an operationId one will be generated from the path and method. | ||
|
||
Each operation has a nested `Request` and a `Response`, as well as a static `service` property | ||
|
||
#### Service | ||
|
||
This is the struct that contains the static information about an operation including it's id, tag, method, pre-modified path, and authorization requirements. It has a generic `ResponseType` type which maps to the `Response` type. | ||
You shouldn't really need to interact with this service type. | ||
|
||
#### Request | ||
|
||
Each request is a subclass of `APIRequest` and has an `init` with a body param if it has a body, and a `options` struct for other url and path parameters. There is also a convenience init for passing parameters directly. | ||
The `options` and `body` structs are both mutable so they can be modified before actually sending the request. | ||
|
||
#### Response | ||
|
||
The response is an enum of all the possible responses the request can return. it also contains getters for the `statusCode`, whether it was `successful`, and the actual decoded optional `success` response. If the operation only has one type of failure type there is also an optional `failure` type. | ||
|
||
## Model | ||
Models that are sent and returned from the API are {% if options.mutableModels %}mutable{% else %}immutable{% endif %} classes. Each model is `Equatable` and `Codable`. | ||
|
||
`Required` properties are non optional and non-required are optional | ||
|
||
All properties can be passed into the initializer, with `required` properties being mandatory. | ||
|
||
If a model has `additionalProperties` it will have a subscript to access these by string | ||
|
||
## APIClient | ||
The `APIClient` is used to encode, authorize, send, monitor, and decode the requests. There is a `APIClient.default` that uses the default `baseURL` otherwise a custom one can be initialized: | ||
|
||
```swift | ||
public init(baseURL: String, sessionManager: SessionManager = .default, defaultHeaders: [String: String] = [:], behaviours: [RequestBehaviour] = []) | ||
``` | ||
|
||
#### APIClient properties | ||
|
||
- `baseURL`: The base url that every request `path` will be appended to | ||
- `behaviours`: A list of [Request Behaviours](#requestbehaviour) to add to every request | ||
- `sessionManager`: An `Alamofire.SessionManager` that can be customized | ||
- `defaultHeaders`: Headers that will be applied to every request | ||
- `decodingQueue`: The `DispatchQueue` to decode responses on | ||
|
||
#### Making a request | ||
To make a request first initialize a [Request](#request) and then pass it to `makeRequest`. The `complete` closure will be called with an `APIResponse` | ||
|
||
```swift | ||
func makeRequest<T>(_ request: APIRequest<T>, behaviours: [RequestBehaviour] = [], queue: DispatchQueue = DispatchQueue.main, complete: @escaping (APIResponse<T>) -> Void) -> Request? { | ||
``` | ||
|
||
Example request (that is not neccessarily in this api): | ||
|
||
```swift | ||
|
||
let getUserRequest = {{ options.name }}.User.GetUser.Request(id: 123) | ||
let apiClient = APIClient.default | ||
|
||
apiClient.makeRequest(getUserRequest) { apiResponse in | ||
switch apiResponse { | ||
case .result(let apiResponseValue): | ||
if let user = apiResponseValue.success { | ||
print("GetUser returned user \(user)") | ||
} else { | ||
print("GetUser returned \(apiResponseValue)") | ||
} | ||
case .error(let apiError): | ||
print("GetUser failed with \(apiError)") | ||
} | ||
} | ||
``` | ||
|
||
Each [Request](#request) also has a `makeRequest` convenience function that uses `{{ options.name }}.default`. | ||
|
||
#### APIResponse | ||
The `APIResponse` that gets passed to the completion closure contains the following properties: | ||
|
||
- `request`: The original request | ||
- `result`: A `Result` type either containing an `APIClientError` or the [Response](#response) of the request | ||
- `urlRequest`: The `URLRequest` used to send the request | ||
- `urlResponse`: The `HTTPURLResponse` that was returned by the request | ||
- `data`: The `Data` returned by the request. | ||
- `timeline`: The `Alamofire.Timeline` of the request which contains timing information. | ||
|
||
#### Encoding and Decoding | ||
Only JSON requests and responses are supported. These are encoded and decoded by `JSONEncoder` and `JSONDecoder` respectively, using Swift's `Codable` apis. | ||
There are some options to control how invalid JSON is handled when decoding and these are available as static properties on `{{ options.name }}`: | ||
|
||
- `safeOptionalDecoding`: Whether to discard any errors when decoding optional properties. Defaults to `true`. | ||
- `safeArrayDecoding`: Whether to remove invalid elements instead of throwing when decoding arrays. Defaults to `true`. | ||
|
||
Dates are encoded and decoded differently according to the swagger date format. They use different `DateFormatter`'s that you can set. | ||
- `date-time` | ||
- `DateTime.dateEncodingFormatter`: defaults to `yyyy-MM-dd'T'HH:mm:ss.Z` | ||
- `DateTime.dateDecodingFormatters`: an array of date formatters. The first one to decode successfully will be used | ||
- `date` | ||
- `DateDay.dateFormatter`: defaults to `yyyy-MM-dd` | ||
|
||
#### APIClientError | ||
This is error enum that `APIResponse.result` may contain: | ||
|
||
```swift | ||
public enum APIClientError: Error { | ||
case unexpectedStatusCode(statusCode: Int, data: Data) | ||
case decodingError(DecodingError) | ||
case requestEncodingError(String) | ||
case validationError(String) | ||
case networkError(Error) | ||
case unknownError(Error) | ||
} | ||
``` | ||
|
||
#### RequestBehaviour | ||
Request behaviours are used to modify, authorize, monitor or respond to requests. They can be added to the `APIClient.behaviours` for all requests, or they can passed into `makeRequest` for just that single request. | ||
|
||
`RequestBehaviour` is a protocol you can conform to with each function being optional. As the behaviours must work across multiple different request types, they only have access to a typed erased `AnyRequest`. | ||
|
||
```swift | ||
public protocol RequestBehaviour { | ||
|
||
/// runs first and allows the requests to be modified. If modifying asynchronously use validate | ||
func modifyRequest(request: AnyRequest, urlRequest: URLRequest) -> URLRequest | ||
|
||
/// validates and modifies the request. complete must be called with either .success or .fail | ||
func validate(request: AnyRequest, urlRequest: URLRequest, complete: @escaping (RequestValidationResult) -> Void) | ||
|
||
/// called before request is sent | ||
func beforeSend(request: AnyRequest) | ||
|
||
/// called when request successfuly returns a 200 range response | ||
func onSuccess(request: AnyRequest, result: Any) | ||
|
||
/// called when request fails with an error. This will not be called if the request returns a known response even if the a status code is out of the 200 range | ||
func onFailure(request: AnyRequest, error: APIClientError) | ||
|
||
/// called if the request recieves a network response. This is not called if request fails validation or encoding | ||
func onResponse(request: AnyRequest, response: AnyResponse) | ||
} | ||
``` | ||
|
||
### Authorization | ||
Each request has an optional `securityRequirement`. You can create a `RequestBehaviour` that checks this requirement and adds some form of authorization (usually via headers) in `validate` or `modifyRequest`. An alternative way is to set the `APIClient.defaultHeaders` which applies to all requests. | ||
|
||
#### Reactive and Promises | ||
To add support for a specific asynchronous library, just add an extension on `APIClient` and add a function that wraps the `makeRequest` function and converts from a closure based syntax to returning the object of choice (stream, future...ect) | ||
|
||
## Models | ||
|
||
{% for model in schemas %} | ||
- **{{ model.type }}** | ||
{% endfor %} | ||
|
||
## Requests | ||
|
||
{% for operationTag in operationsByTag %} | ||
{% if operationTag.name != "" %} | ||
- **{{ options.name }}.{{ options.tagPrefix }}{{ operationTag.name|upperCamelCase }}{{ options.tagSuffix }}** | ||
{% for operation in operationTag.operations %} | ||
- **{{ operation.type }}**: {{ operation.method }} `{{ operation.path | lowercase }}` | ||
{% endfor %} | ||
{% else %} | ||
{% for operation in operationTag.operations %} | ||
- **{{ options.name }}.{{ operation.type }}**: {{ operation.method }} `{{ operation.path | lowercase }}` | ||
{% endfor %} | ||
{% endif %} | ||
{% endfor %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
{% include "Includes/Header.stencil" %} | ||
|
||
import Foundation | ||
import PrchModel | ||
|
||
{% if options.modelProtocol %} | ||
public protocol {{ options.modelProtocol }}: Codable, Equatable {} | ||
{% endif %} | ||
|
||
{% for type, typealias in options.typeAliases %} | ||
public typealias {{ type }} = {{ typealias }} | ||
{% endfor %} | ||
|
||
{% for tag in tags %} | ||
public enum {{ options.tagPrefix }}{{ tag | upperCamelCase }} {{ options.tagSuffix }} {} | ||
{% endfor %} | ||
|
||
public class {{ options.name }}API: API { | ||
public init(baseURLComponents: URLComponents) { | ||
self.baseURLComponents = baseURLComponents | ||
} | ||
|
||
public let baseURLComponents: URLComponents | ||
|
||
public var headers: [String: String] = [:] | ||
|
||
public var encoder: any Encoder<Data> { | ||
Defaults.encoder | ||
} | ||
|
||
public var decoder: any Decoder<Data> { | ||
Defaults.decoder | ||
} | ||
|
||
public typealias RequestDataType = Data | ||
|
||
public typealias ResponseDataType = Data | ||
|
||
|
||
{% if servers %} | ||
|
||
public enum Server { | ||
{% for server in servers %} | ||
|
||
{% if server.description %} | ||
/** {{ server.description }} **/ | ||
{% endif %} | ||
{% if server.variables %} | ||
public static func {{ server.name }}({% for variable in server.variables %}{{ variable.name }}: String = "{{ variable.defaultValue }}"{% ifnot forloop.last %}, {% endif %}{% endfor %}) -> String { | ||
var url = "{{ server.url }}" | ||
{% for variable in server.variables %} | ||
url = url.replacingOccurrences(of: {{'"{'}}{{variable.name}}{{'}"'}}, with: {{variable.name}}) | ||
{% endfor %} | ||
return url | ||
} | ||
{% else %} | ||
public static let {{ server.name }} = "{{ server.url }}" | ||
{% endif %} | ||
{% endfor %} | ||
} | ||
{% else %} | ||
|
||
// No servers defined in swagger. Documentation for adding them: https://swagger.io/specification/#schema | ||
{% endif %} | ||
|
||
} |
Oops, something went wrong.