Skip to content

Commit bbd96d2

Browse files
authored
Merge pull request #14 from bow-swift/tomas/generation-docs
Documentation for Generation examples
2 parents 4175293 + 24e2827 commit bbd96d2

File tree

49 files changed

+812
-64
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+812
-64
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'/>

0 commit comments

Comments
 (0)