Skip to content

Commit 24e2827

Browse files
authored
Merge pull request #15 from bow-swift/tomas/consuming-generated-code
Documentation for Consuming generated code
2 parents ec4be70 + f2bf805 commit 24e2827

File tree

12 files changed

+355
-2
lines changed

12 files changed

+355
-2
lines changed

Documentation.app/Contents/MacOS/Consuming generated code.playground/Pages/Adding the module to your project.xcplaygroundpage/Contents.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,17 @@
77
/*:
88
# Adding the module to your project
99

10+
After generating a network client using Bow OpenAPI, you will get a folder that contains all files together with the Swift Package manifest that describes the provided artifacts. Adding it to your Xcode Project is as easy as dragging the folder to the left panel in Xcode, and Xcode will automatically trigger the Swift Package Manager to download the dependencies.
11+
12+
![](/assets/project-tree.png)
13+
14+
Then, you need to add the dependency to the target where you want to use the generated code:
15+
16+
![](/assets/add-frameworks.png)
17+
18+
Finally, you can import it in your Swift files as:
19+
20+
```swift
21+
import SampleAPI
22+
```
1023
*/

Documentation.app/Contents/MacOS/Consuming generated code.playground/Pages/Customizing the configuration.xcplaygroundpage/Contents.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,92 @@
44
title: Customizing the configuration
55
*/
66
// nef:end
7+
// nef:begin:hidden
8+
import Bow
9+
import BowEffects
10+
import Foundation
11+
12+
public enum DecodingError: Error {}
13+
14+
public protocol ResponseDecoder {
15+
func safeDecode<T: Decodable>(_ type: T.Type, from: Data) -> IO<DecodingError, T>
16+
}
17+
18+
extension JSONDecoder: ResponseDecoder {
19+
func safeDecode<T: Decodable>(_ type: T.Type, from data: Data) -> IO<DecodingError, T> {
20+
IO.invoke {
21+
try! self.decode(type, from: data)
22+
}
23+
}
24+
}
25+
26+
enum API {
27+
struct Config: Equatable {
28+
public let basePath: String
29+
public let headers: [String: String]
30+
public let session: URLSession
31+
public let decoder: ResponseDecoder
32+
33+
init (basePath: String, session: URLSession = .shared, decoder: ResponseDecoder = JSONDecoder()) {
34+
self.init(basePath: basePath, headers: [:], session: session, decoder: decoder)
35+
}
36+
37+
private init (basePath: String, headers: [String: String], session: URLSession, decoder: ResponseDecoder) {
38+
self.basePath = basePath
39+
self.headers = headers
40+
self.session = session
41+
self.decoder = decoder
42+
}
43+
}
44+
}
45+
46+
extension API.Config {
47+
func appending(headers: [String: String]) -> API.Config { self }
48+
func appending(contentType: API.ContentType) -> API.Config { self }
49+
func appendingHeader(value: String, forKey key: String) -> API.Config { self }
50+
func appendingHeader(token: String) -> API.Config { self }
51+
}
52+
// nef:end
753
/*:
854
# Customizing the configuration
955

56+
Before running any network request, we must provide an `API.Config` object. The minimum setup for this configuration includes a base path for all network requests:
57+
*/
58+
let baseConfig = API.Config(basePath: "https://url-to-my-server.com")
59+
/*:
60+
However, this can be customized in different ways to achieve our needs.
61+
62+
## Adding headers
63+
64+
In some cases we may need to add headers to our requests. They can be added to a base configuration. Note that the original configuration will not be mutated, but a copy will be created with the new appended headers. We can use the following methods:
65+
*/
66+
let configWithHeaders1 = baseConfig.appending(headers: ["Accept": "application/json"])
67+
let configWithHeaders2 = baseConfig.appendingHeader(value: "application/json", forKey: "Accept")
68+
let configWithHeaders3 = baseConfig.appending(contentType: .json)
69+
/*:
70+
Besides this, all methods in our specification that require a header parameter will add an extension method to `API.Config`, where, if we provide a value, it will add it to the headers with the right key. For instance, if our methods require an authentication token, we may have a method like:
71+
*/
72+
let authConfig = baseConfig.appendingHeader(token: "my-secure-token")
73+
/*:
74+
## Customizing URLSession
75+
76+
By default, the configuration object uses `URLSession.shared`. However, you can create a configuration that uses your custom `URLSession` by passing an instance to the initializer:
77+
*/
78+
let customSessionConfig = API.Config(basePath: "https://url-to-my-server.com",
79+
session: URLSession())
80+
/*:
81+
## Custom decoding
82+
83+
The generated code uses `JSONDecoder` as a default decoder for the received responses. You can provide your own decoder if your server is sending responses in other formats. In such case, your decoder must implement a protocol named `ResponseDecoder` provided in the generated code.
84+
85+
You may also need to provide a custom `JSONDecoder` depending on the date format that your backend is using. In such case, you can create the decoder:
86+
*/
87+
let dateFormatter = DateFormatter()
88+
dateFormatter.timeStyle = .medium
89+
let decoder = JSONDecoder()
90+
decoder.dateDecodingStrategy = .formatted(dateFormatter)
91+
/*:
92+
And then pass it in the creation of the configuration object:
1093
*/
94+
let customDecodingConfig = API.Config(basePath: "https://url-to-my-server.com",
95+
decoder: decoder)

Documentation.app/Contents/MacOS/Consuming generated code.playground/Pages/Running a network request.xcplaygroundpage/Contents.swift

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,81 @@
44
title: Running a network request
55
*/
66
// nef:end
7+
// nef:begin:hidden
8+
import Bow
9+
import BowEffects
10+
11+
enum API {
12+
struct Config {
13+
let basePath: String
14+
}
15+
16+
enum HTTPError: Error {}
17+
}
18+
19+
struct Customer {
20+
let identifier: Int
21+
let name: String
22+
}
23+
24+
typealias Customers = [Customer]
25+
// nef:end
726
/*:
8-
# Running a network request
27+
# Running a network request
28+
29+
Bow OpenAPI groups network calls in small, concise protocols that can be accessed through the `API` object. Each generated method returns an `EnvIO<API.Config, API.HTTPError, A>` value, where `A` is the type of the returned value from the network call.
30+
31+
This type represents a suspended computation that still needs a configuration object before running, and when provided so, it describes a computation that either produces an `HTTPError`, or a value of type `A`.
932

33+
For instance, considering the generated code provides a `CustomerAPI`:
34+
*/
35+
protocol CustomerAPI {
36+
func getCustomers() -> EnvIO<API.Config, API.HTTPError, Customers>
37+
}
38+
// nef:begin:hidden
39+
struct CustomerAPIClient: CustomerAPI {
40+
func getCustomers() -> EnvIO<API.Config, API.HTTPError, Customers> {
41+
EnvIO { _ in
42+
IO.invoke {
43+
[ Customer(id: 1, name: "Tomás") ]
44+
}
45+
}
46+
}
47+
}
48+
49+
extension API {
50+
var customer: CustomerAPI {
51+
CustomerAPIClient()
52+
}
53+
}
54+
// nef:end
55+
/*:
56+
We can invoke this method as:
57+
*/
58+
let customersRequest = API.customer.getCustomers()
59+
/*:
60+
However, this will not run the request. Here, `customersRequest` is just a description of the request that we can manipulate; we can handle potential errors, transform the output type, chain it with other requests and even run them in parallel. We recommend you to read the section for [Effects](https://bow-swift.io/next/docs/effects/overview/) in the documentation for Bow, for further information.
61+
62+
We need to provide an API configuration to this request. To do so, we need a base path that will be used to append the paths to the requests described in the OpenAPI specification.
63+
*/
64+
let config = API.Config(basePath: "https://url-to-my-server.com")
65+
customersRequest.provide(config)
66+
/*:
67+
Finally, we can decide to run the request synchronously or asynchronously, and even change the dispatch queue where it will be executed:
1068
*/
69+
70+
// Synchronous run
71+
let customers: Customers? =
72+
try? customersRequest.provide(config).unsafeRunSync()
73+
74+
let either: Either<API.HTTPError, Customers> =
75+
customersRequest.provide(config).unsafeRunSyncEither()
76+
77+
// Asynchronous run
78+
customersRequest.provide(config).unsafeRunAsync { either in
79+
either.fold({ httpError in /* ... */ },
80+
{ customers in /* ... */ })
81+
}
82+
83+
// Changing queue
84+
let customers: Customers? = try? customersRequest.provide(config).unsafeRunSync(on: .background)

Documentation.app/Contents/MacOS/Consuming generated code.playground/Pages/Testing your network calls.xcplaygroundpage/Contents.swift

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,150 @@
44
title: Testing your network calls
55
*/
66
// nef:end
7+
// nef:begin:hidden
8+
import Bow
9+
import BowEffects
10+
11+
enum API {
12+
struct Config {
13+
let basePath: String
14+
15+
func stub(json: String, code: Int = 200, endpoint: String = "") -> API.Config {
16+
self
17+
}
18+
19+
func stub(error: Error, code: Int = 400, endpoint: String = "") -> API.Config {
20+
self
21+
}
22+
}
23+
24+
enum HTTPError {}
25+
}
26+
27+
struct Customer: Codable, Equatable {
28+
let identifier: Int
29+
let name: String
30+
}
31+
32+
typealias Customers = [Customer]
33+
34+
protocol CustomerAPI {
35+
func getCustomers() -> EnvIO<API.Config, API.HTTPError, Customers>
36+
}
37+
38+
struct CustomerAPIClient: CustomerAPI {
39+
func getCustomers() -> EnvIO<API.Config, API.HTTPError, Customers> {
40+
EnvIO.pure([])^
41+
}
42+
}
43+
44+
extension API {
45+
var customer: CustomerAPI {
46+
CustomerAPIClient()
47+
}
48+
}
49+
50+
func assert<T: Codable & Equatable>(_ envIO: EnvIO<API.Config, API.HTTPError, T>,
51+
withConfig: API.Config,
52+
succeeds: T,
53+
_ info: String) {}
54+
55+
// nef:end
756
/*:
857
# Testing your network calls
958

59+
An important part of software development is testing. However, testing networking operations is usually cumbersome. It involves mocking dependencies in order to control the results of the network calls, and often we feel uncertain if we are actually testing our code or the mocks.
60+
61+
Bow OpenAPI generates testing tools as part of its output, so that you can easily test your network calls behave as you expect, without actually going to the network. The tool produces a Swift Package that contains two libraries: one that contains the actual APIs described in the specification, and one with the same name and the suffix `Test` that provides some test utilities. For instance, assuming you invoke the tool as:
62+
63+
```bash
64+
$> bow-openapi --name SampleAPI --schema swagger.yaml --output ./Folder
65+
```
66+
67+
It will create a Swift Package with two modules: `SampleAPI` and `SampleAPITest`. In order to use the testing utilities, you would only need to import them:
68+
69+
```swift
70+
import SampleAPI
71+
import SampleAPITest
72+
```
73+
74+
In the following sections we will describe the utilities you have for testing.
75+
76+
## Stubbing responses
77+
78+
In order to test our code, we need to control the scenarios that we want to test. When it comes to network calls, we usually need to stub the content we want in response of out requests.
79+
80+
All this is done through the `API.Config` object. That means we do not need to modify our production code, but only provide a special configuration for testing. We can actually pass the same configuration that we use for production code, but with a small modification.
81+
82+
Importing the testing module generated by Bow OpenAPI will add some extension methods on `API.Config` to let us stub content. The provided methods are:
83+
84+
- `stub(data: Data)`: stubs a `Data` object in response to any network call.
85+
- `stub(error: Error)`: stubs an `Error` in response to any network call.
86+
- `stub(json: String)`: stubs a JSON formatted String in response to any network call.
87+
- `stub(contentsOfFile: URL)`: stubs the content of a file at a given `URL` in response to any network call.
88+
89+
All these methods allow us to provide content in response to any network call, regardless of the endpoint that is being called. They also have an optional parameter `code`, where we can provide the HTTP response code that we want to receive (e.g. 404, 500, etc.).
90+
91+
For instance, we can stub the response of some JSON content with code 200:
92+
*/
93+
let json = """
94+
{
95+
"identifier": 1234,
96+
"name": "Tomás"
97+
}
98+
"""
99+
let successConfig = API.Config(basePath: "https://url-to-my-server.com")
100+
.stub(json: json, code: 200)
101+
102+
/*:
103+
Or we can stub an error with a 404 code:
104+
*/
105+
enum CustomerError: Error {
106+
case notFound
107+
}
108+
109+
let failureConfig = API.Config(basePath: "https://url-to-my-server.com")
110+
.stub(error: CustomerError.notFound, code: 404)
111+
/*:
112+
Then, depending on the scenario you want to test, you only need use the right configuration:
113+
*/
114+
API.customer.getCustomers().provide(successConfig) // Tests the happy path
115+
API.customer.getCustomers().provide(failureConfig) // Tests the unhappy path
116+
/*:
117+
## Stacking responses
118+
119+
Stubs can be stacked in the `API.Config`, and they are removed as they are consumed. You can call several times to `stub` and those responses will be returned sequentially.
120+
121+
If your code requires more responses than you have stubbed, you will get an error. Similarly, if you stub more content than you consume in the test, you should call `config.reset()` in order to clear the extra stubbed content.
122+
123+
## Routing responses
124+
125+
If you are doing integration or end-to-end testing, your tests may perform several network calls in an order that you may not know beforehand. The testing tools provided in the module also let you stub content for a specific endpoint:
126+
*/
127+
let routingConfig = API.Config(basePath: "https://url-to-my-server.com")
128+
.stub(json: json, code: 200, endpoint: "/customers")
129+
.stub(json: "[]", code: 200, endpoint: "/products")
130+
/*:
131+
## Custom assertions
132+
133+
Finally, the testing module provides custom assertions to test the success and failure scenarios. They are available as extension methods in `XCTest`. Therefore, you could write a test like:
134+
*/
135+
func testHappyPath() {
136+
let json = """
137+
{
138+
"identifier": 1234,
139+
"name": "Tomás"
140+
}
141+
"""
142+
let successConfig = API.Config(basePath: "https://url-to-my-server.com")
143+
.stub(json: json, code: 200)
144+
let expected = [Customer(identifier: 1234, name: "Tomás")]
145+
146+
assert(API.customer.getCustomers(),
147+
withConfig: successConfig,
148+
succeeds: expected,
149+
"Customer API did not return the expected result")
150+
}
151+
/*:
152+
Notice that Bow OpenAPI does not add conformance to `Equatable` in the generated code. Therefore, is up to the user to define when two value objects are equal by adding the corresponding extension.
10153
*/

Documentation.app/Contents/MacOS/Consuming generated code.playground/contents.xcplayground

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<playground version='6.0' target-platform='ios'>
2+
<playground version='6.0' target-platform='ios' display-mode='raw'>
33
<pages>
44
<page name='Adding the module to your project'/>
55
<page name='Running a network request'/>

Documentation.app/Contents/MacOS/Generation examples.playground/Pages/Limitations.xcplaygroundpage/Contents.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,19 @@
77
/*:
88
# Limitations
99

10+
Bow OpenAPI is very powerful and generates a ready to use network client in Swift that is as good as the specification you provide. If the specification is poor or flaky, the generated code will have the same problems. Generating code according to this specification is a way of honoring it, but it is important that this contract is also honored by the backend side.
11+
12+
Nevertheless, Bow OpenAPI has some other limitations.
13+
14+
## Inline data types
15+
16+
In order to have a proper generation, you need to define data models in the `components` section in OpenAPI, or in the `definitions` section in Swagger. Defining your data models inline can result in generated code that is not properly named and therefore difficult to use. Nested data type definition is not supported either; you will need to extract those types to the root of the definition of your models.
17+
18+
## Error data
19+
20+
If you are specifying data models as part of the error response of an endpoint, Bow OpenAPI will not parse that into a value object. However, you will be able to access such information as it is carried in the `API.HTTPError` value that you will get.
21+
22+
## Body parameters encoding
23+
24+
Currently, the only supported encoding for body parameters is `application/json`.
1025
*/

Documentation.app/Contents/MacOS/Quick start.playground/Pages/Integration in Xcode.xcplaygroundpage/Contents.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,27 @@
77
/*:
88
# Integration in Xcode
99

10+
Bow OpenAPI can be integrated in Xcode easily to regenerate your network client whenever the specification changes. Assuming you have already installed Bow OpenAPI in your computer, you can follow these steps.
11+
12+
## Add the specification file
13+
14+
Add the OpenAPI/Swagger specification file to the root of your project, as depicted in the image.
15+
16+
![](/assets/spec-file.png)
17+
18+
## Create an Aggregate
19+
20+
Add a new target to your project and select Aggregate, giving it the name you prefer.
21+
22+
![](/assets/aggregate.png)
23+
24+
## Run script
25+
26+
Select your recently created Aggregate and go to its Build Phases tab. Add a New Run Script Phase. There, you can invoke the Bow OpenAPI command, passing the values you need for your project.
27+
28+
![](/assets/build-phase.png)
29+
30+
## Build
31+
32+
From now on, if you run the scheme corresponding to this Aggregate, it will regenerate the network client based on the specification file. Drag the folder containing the generated code and drop it onto your project, and Swift Package Manager will start fetching your dependencies.
1033
*/

assets/add-frameworks.png

9.37 KB
Loading

assets/aggregate.png

116 KB
Loading

assets/build-phase.png

38.9 KB
Loading

0 commit comments

Comments
 (0)