Skip to content

Commit 9a4336a

Browse files
authored
Documentation Improvements (#8)
* Updated README. Renamed confusing associatedtype to HeaderValues * README typo fixes * Updated docs location * Documentation tweaks * Updated workflow file * Did slightly more organization of documentation
1 parent b25ed51 commit 9a4336a

File tree

7 files changed

+582
-16
lines changed

7 files changed

+582
-16
lines changed

.github/workflows/documentation.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ jobs:
2020
with:
2121
inputs: "Sources"
2222
module-name: Endpoints
23-
output: "Documentation"
23+
base-url: "https://github.com/velos/Endpoints/wiki"
24+
output: "docs"
25+
2426
- name: Upload Documentation to Wiki
2527
uses: SwiftDocOrg/github-wiki-publish-action@v1
2628
with:
27-
path: "Documentation"
29+
path: "docs"
2830
env:
2931
GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}

README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,54 @@
22

33
![CI](https://github.com/velos/Endpoints/workflows/CI/badge.svg) ![Documentation](https://github.com/velos/Endpoints/workflows/Documentation/badge.svg)
44

5-
A Swift package for creating staticly and strongly-typed definitions of endpoint with paths, methods, inputs and outputs.
5+
Endpoints is a small library for creating statically and strongly-typed definitions of endpoint with paths, methods, inputs and outputs.
6+
7+
## Purpose
8+
9+
The purpose of Endpoints is to, in a type-safe way, define how to create a `URLRequest` from typed properties and, additionally, define how a response for the request should be handled. The library not only includes the ability to create these requests in a type-safe way, but also includes helpers to perform the requests using `URLSession`. Endpoints does not try to wrap the URL loading system to provide features on top of it like Alamofire. Instead, Endpoints focuses on defining requests and converting those requests into `URLRequest` objects to be plugged into vanilla `URLSession`s. However, this library could be used in conjunction with Alamofire if desired.
10+
11+
## Getting Started
12+
13+
The basic process for defining an Endpoint starts with defining a value conforming to `RequestType`. With the `RequestType` protocol, you are encapsulating all the properties that are needed for making a request and the types for parsing the response. Within the `RequestType`, the `endpoint` static var serves as an immutable definition of the server's endpoint and how the variable pieces of the `RequestType` should fit together when making the full request.
14+
15+
To get started, first create a type (struct or class) conforming to `RequestType`. There are only two required elements to conform: defining the `Response` and creating the `Endpoint`.
16+
17+
Requests and Endpoints do not contain base URLs so that these requests can be used on different environments. Environments are defined as conforming to the `EnvironmentType` and implement a `baseURL` as well as an optional `requestProcessor` which has a final hook before `URLRequest` creation to modify the `URLRequest` to attach authentication or signatures.
18+
19+
To find out more about the pieces of the `RequestType`, check out [Defining a ResponseType](https://github.com/velos/Endpoints/wiki/DefiningResponseType) on the wiki.
20+
21+
## Examples
22+
23+
The most basic example of defining an Endpoint is creating a simple GET request. This means defining a type that conforms to `RequestType` such as:
24+
25+
```Swift
26+
struct MyRequest: RequestType {
27+
static let endpoint: Endpoint<MyRequest> = Endpoint(
28+
method: .get,
29+
path: "path/to/resource"
30+
)
31+
32+
struct Response: Decodable {
33+
let resourceId: String
34+
let resourceName: String
35+
}
36+
}
37+
```
38+
39+
This includes a `Response` associated type (can be typealiased to a more complex existing type) which defines how the response will come back from the request.
40+
41+
Then usage can employ the `URLSession` extensions:
42+
43+
#### Usage
44+
```Swift
45+
URLSession.shared.endpointPublisher(in: .production, with: MyRequest())
46+
.sink { completion in
47+
guard case .failure(let error) = completion else { return }
48+
// handle error
49+
} receiveValue: { (response: MyRequest.Response) in
50+
// handle MyRequest.Response
51+
}
52+
.store(in: &cancellables)
53+
```
54+
55+
To browse more complex examples, make sure to check out the [Examples](https://github.com/velos/Endpoints/wiki/Examples) wiki page.

Sources/Endpoints/Endpoint.swift

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,19 @@ public protocol DecoderType {
4343

4444
extension JSONDecoder: DecoderType { }
4545

46+
/// The `Response` is an associated type which defines the response from the server. Note that this is just type information which helpers, such as the built-in `URLSession` extensions, can use to know how to handle particular types. For instance, if this type conforms to `Decodable`, then a JSON decoder is used on the data coming from the server. If it's typealiased to `Void`, then the extension can know to ignore the response. If it's `Data`, then it can deliver the response data unmodified.
47+
/// An `ErrorResponse` type can be associated to define what value conforming to `Decodable` to use when parsing an error response from the server. This can be useful if your server returns a different JSON structure when there's an error versus a success. Often in a project, this can be defined globally and `typealias` can be used to associate this global type on all `RequestType`s.
48+
///
49+
///
50+
///
4651
public protocol RequestType {
4752
associatedtype Response
4853
associatedtype ErrorResponse: Decodable = EmptyResponse
4954

5055
associatedtype Body: Encodable = EmptyResponse
5156
associatedtype PathComponents = Void
5257
associatedtype Parameters = Void
53-
associatedtype Headers = Void
58+
associatedtype HeaderValues = Void
5459

5560
associatedtype BodyEncoder: EncoderType = JSONEncoder
5661
associatedtype ErrorDecoder: DecoderType = JSONDecoder
@@ -67,7 +72,7 @@ public protocol RequestType {
6772
var parameters: Parameters { get }
6873

6974
/// The instance of the associated `Headers` type. Used for filling in request data into the headers of the endpoint.
70-
var headers: Headers { get }
75+
var headerValues: HeaderValues { get }
7176

7277
/// The decoder instance to use when decoding the associated `Body` type
7378
static var bodyEncoder: BodyEncoder { get }
@@ -162,7 +167,7 @@ extension RequestType {
162167

163168
switch field.value {
164169
case .field(let valuePath):
165-
value = headers[keyPath: valuePath]
170+
value = headerValues[keyPath: valuePath]
166171
case .fieldValue(let fieldValue):
167172
value = fieldValue
168173
}
@@ -211,8 +216,8 @@ public extension RequestType where Parameters == Void {
211216
var parameters: Parameters { return () }
212217
}
213218

214-
public extension RequestType where Headers == Void {
215-
var headers: Headers { return () }
219+
public extension RequestType where HeaderValues == Void {
220+
var headerValues: HeaderValues { return () }
216221
}
217222

218223
/// The HTTP Method
@@ -279,15 +284,15 @@ public struct Endpoint<T: RequestType> {
279284
/// The parameters (form and query) that are included in the Endpoint
280285
public let parameters: [Parameter<T.Parameters>]
281286
/// The headers that are included in the Endpoint
282-
public let headers: [Headers: HeaderField<T.Headers>]
287+
public let headers: [Headers: HeaderField<T.HeaderValues>]
283288

284289
/// Initializes an Endpoint with the given properties, defining all dynamic pieces as type-safe parameters.
285290
/// - Parameters:
286291
/// - method: The HTTP method to use when fetching this Endpoint
287292
/// - path: The path template representing the path and all path-related parameters
288293
/// - parameters: The parameters passed to the endpoint. Either through query or form body.
289-
/// - headers: The headers associated with this request
290-
public init(method: Method, path: PathTemplate<T.PathComponents>, parameters: [Parameter<T.Parameters>] = [], headers: [Headers: HeaderField<T.Headers>] = [:]) {
294+
/// - headerValues: The headers associated with this request
295+
public init(method: Method, path: PathTemplate<T.PathComponents>, parameters: [Parameter<T.Parameters>] = [], headers: [Headers: HeaderField<T.HeaderValues>] = [:]) {
291296
self.method = method
292297
self.path = path
293298
self.parameters = parameters

Sources/Endpoints/Headers.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ extension Headers {
4848

4949
// Request Headers
5050
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.3
51+
52+
/// See [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) header documentation.
5153
public static let accept = Headers(name: "Accept", category: .request)
5254
public static let acceptCharset = Headers(name: "Accept-Charset", category: .request)
5355
public static let acceptEncoding = Headers(name: "Accept-Encoding", category: .request)

Tests/EndpointsTests/EndpointsTests.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ struct JSONProviderRequest: RequestType {
6464
}()
6565
}
6666

67-
6867
struct UserRequest: RequestType {
6968
static var endpoint: Endpoint<UserRequest> = Endpoint(
7069
method: .get,
@@ -86,7 +85,7 @@ struct UserRequest: RequestType {
8685
.queryValue("hard_coded_query", value: "true")
8786
],
8887
headers: [
89-
"HEADER_TYPE": .field(path: \UserRequest.Headers.headerValue),
88+
"HEADER_TYPE": .field(path: \UserRequest.HeaderValues.headerValue),
9089
"HARD_CODED_HEADER": .fieldValue(value: "test2"),
9190
.keepAlive: .fieldValue(value: "timeout=5, max=1000")
9291
]
@@ -111,13 +110,13 @@ struct UserRequest: RequestType {
111110
let optionalDate: String?
112111
}
113112

114-
struct Headers {
113+
struct HeaderValues {
115114
let headerValue: String
116115
}
117116

118117
let pathComponents: PathComponents
119118
let parameters: Parameters
120-
let headers: Headers
119+
let headerValues: HeaderValues
121120
}
122121

123122
struct PostRequest1: RequestType {
@@ -232,7 +231,7 @@ class EndpointsTests: XCTestCase {
232231
optionalString: nil,
233232
optionalDate: nil
234233
),
235-
headers: .init(headerValue: "test")
234+
headerValues: .init(headerValue: "test")
236235
).urlRequest(in: Environment.test)
237236

238237
XCTAssertEqual(request.httpMethod, "GET")

docs/DefiningResponseType.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
## Defining a ResponseType
2+
3+
### `Response` (associatedtype, required)
4+
5+
The `Response` is an associated type which defines the response from the server. Note that this is just type information which helpers, such as the built-in `URLSession` extensions, can use to know how to handle particular types. For instance, if this type conforms to `Decodable`, then a JSON decoder is used on the data coming from the server. If it's typealiased to `Void`, then the extension can know to ignore the response. If it's `Data`, then it can deliver the response data unmodified.
6+
7+
### `ErrorResponse` (associatedtype, optional, defaults to `EmptyResponse`)
8+
9+
An `ErrorResponse` type can be associated to define what value conforming to `Decodable` to use when parsing an error response from the server. This can be useful if your server returns a different JSON structure when there's an error versus a success. Often in a project, this can be defined globally and `typealias` can be used to associate this global type on all `RequestType`s.
10+
11+
### `Body` (associatedtype, optional, defaults to `EmptyResponse`)
12+
13+
When POST-ing JSON to your server, a `Body` conforming to `Encodable` can be associated. This value will be encoded as JSON into the body of the HTTP request.
14+
15+
### `PathComponents` (associatedtype, defaults to `Void`)
16+
17+
If a `PathComponents` type is associated, properties of that type can be utilized in the `path` of the `Endpoint` using a path string interpolation syntax:
18+
19+
```Swift
20+
struct DeleteRequest: RequestType {
21+
static let endpoint: Endpoint<DeleteRequest> = Endpoint(
22+
method: .delete,
23+
path: "calendar/v3/calendars/\(path: \.calendarId)/events\(path: \.eventId)"
24+
)
25+
26+
typealias Response = Void
27+
28+
struct PathComponents {
29+
let calendarId: String
30+
let eventId: String
31+
}
32+
33+
let pathComponents: PathComponents
34+
}
35+
```
36+
37+
### `Parameters` (associatedtype, defaults to `Void`)
38+
39+
A `Parameters` type, in a similar way to `PathComponents`, holds properties that can be referenced in the `Endpoint` as `Parameter<Parameters>` in order to define form parameters used in the body or query parameters attached to the URL. The enum type is defined as:
40+
41+
```Swift
42+
public enum Parameter<T> {
43+
case form(String, path: PartialKeyPath<T>)
44+
case formValue(String, value: PathRepresentable)
45+
case query(String, path: PartialKeyPath<T>)
46+
case queryValue(String, value: PathRepresentable)
47+
}
48+
```
49+
50+
With this enum, either hard-coded values can be injected into the `Endpoint` (with `.formValue(_:value:)` or `.queryValue(_:value:)`) or key paths can define which reference properties in the `Parameters` associated type to define a form or query parameter that is needed at the time of the request.
51+
52+
### `HeaderValues` (associatedtype, defaults to `Void`)
53+
54+
Custom headers can be included in your `Endpoint` definition by associating a type with `HeaderValues` in your `RequestType`. These properties can be referenced by key paths in the `Endpoint` definition:
55+
56+
```Swift
57+
static let endpoint: Endpoint<UserRequest> = Endpoint(
58+
method: .get,
59+
path: "/request",
60+
headers: [
61+
"X-TYPE": HeaderField.field(path: \UserRequest.HeaderValues.type),
62+
"X-VALUE": .fieldValue(value: "value"),
63+
.keepAlive: .fieldValue(value: "timeout=5, max=1000")
64+
]
65+
)
66+
```
67+
68+
Custom keys in the headers dictionary can be defined ad-hoc using a String, or by extending an encapsulating type `Headers`. Basic named headers, such as `.keepAlive`, `.accept`, etc., are already defined as part of the library.
69+
70+
### `BodyEncoder` (associatedtype, defaults to `JSONEncoder`)
71+
72+
This, coupled with the `bodyEncoder` property, can define custom encoders for the associated `Body` type when turning it into `Data` attached to the request. For instance, this can be customizations of the date encoding strategy or even completely different encoders for XML or other data formats.
73+
74+
### `ResponseDecoder` (associatedtype, defaults to `JSONEncoder`)
75+
76+
Similar to custom body encoding, the `ResponseDecoder` with the `responseDecoder` property can customize the decoder used for parsing responses from the server.
77+
78+
### `ErrorDecoder` (associatedtype, defaults to `JSONDecoder`)
79+
80+
Similar to `ResponseDecoder`, this allows customization of the decoder used when errors are encountered and parsed using the `ErrorResponse` type.
81+
82+
### `endpoint` (static var, required)
83+
84+
The `Endpoint` static var defines how all the pieces defined in the `RequestType` go together. When creating a `RequestType`, it's usually the last step, since it requires all the properties of the `RequestType` defined in order to put them together.
85+
86+
An `Endpoint` is generic type with the type parameter conforming to `RequestType`, or equivalently `Self` since the static let is defined as part of the `RequestType` protocol.

0 commit comments

Comments
 (0)