Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/diamirio/endpoints
Type-Safe Swift Networking
https://github.com/diamirio/endpoints
generics ios macos networking parsing swift swift-5 tvos watchos
Last synced: 2 months ago
JSON representation
Type-Safe Swift Networking
- Host: GitHub
- URL: https://github.com/diamirio/endpoints
- Owner: diamirio
- License: mit
- Created: 2017-02-22T20:32:16.000Z (almost 8 years ago)
- Default Branch: develop
- Last Pushed: 2024-04-12T07:51:37.000Z (9 months ago)
- Last Synced: 2024-11-12T14:18:51.457Z (2 months ago)
- Topics: generics, ios, macos, networking, parsing, swift, swift-5, tvos, watchos
- Language: Swift
- Homepage:
- Size: 1.07 MB
- Stars: 46
- Watchers: 7
- Forks: 7
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Endpoints
[![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)
Endpoints makes it easy to write a type-safe network abstraction layer for any Web-API.
It requires Swift 5, makes heavy use of generics (and generalized existentials) and protocols (and protocol extensions). It also encourages a clean separation of concerns and the use of value types (i.e. structs).
## Usage
### Basics
Here's how to load a random image from Giphy.
```swift
// A client is responsible for encoding and parsing all calls for a given Web-API.
let client = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!)// A call encapsulates the request that is sent to the server and the type that is expected in the response.
let call = AnyCall(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))// A session wraps `URLSession` and allows you to start the request for the call and get the parsed response object (or an error) in a completion block.
let session = Session(with: client)// enable debug-mode to log network traffic
session.debug = true// start call
let (body, httpResponse) = try await session.dataTask(for: call)
```### Response Parsing
A call is supposed to know exactly what response to expect from its request. It delegates the parsing of the response to a `ResponseParser`.
Some built-in types already adopt the `ResponseParser` protocol (using protocol extensions), so you can for example turn any response into a JSON array or dictionary:
```swift
// Replace `DataResponseParser` with any `ResponseParser` implementation
let call = AnyCall>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))...
// body is now a JSON dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)
```````swift
let call = AnyCall>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))...
// body is now a `GiphyGif` dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)
```#### Provided `ResponseParser`s
Look up the documentation in the code for further explanations of the types.
* `DataResponseParser`
* `DictionaryParser`
* `JSONParser`
* `NoContentParser`
* `StringConvertibleParser`
* `StringParser`#### JSON Codable Integration
`Endpoints` has a built in JSON Codable support.
##### Decoding
The `ResponseParser` responsible for handling decodable types is the `JSONParser`.
The `JSONParser` uses the default `JSONDecoder()`, however, the `JSONParser` can be subclassed, and the `jsonDecoder` can be overwritten with your configured `JSONDecoder`.```swift
// Decode a type using the default decoder
struct GiphyCall: Call {
typealias Parser = JSONParser
...
}// custom decoder
struct GiphyParser: JSONParser {
override public var jsonDecoder: JSONDecoder {
let decoder = JSONDecoder()
// configure...
return decoder
}
}struct GiphyCall: Call {
typealias Parser = GiphyParser
...
}
```##### Encoding
Every encodable is able to provide a `JSONEncoder()` to encode itself via the `toJSON()` method.
### Dedicated Calls
`AnyCall` is the default implementation of the `Call` protocol, which you can use as-is. But if you want to make your networking layer really type-safe you'll want to create a dedicated `Call` type for each operation of your Web-API:
```swift
struct GetRandomImage: Call {
typealias Parser = DictionaryParser
var tag: String
var request: URLRequestEncodable {
return Request(.get, "gifs/random", query: [ "tag": tag, "api_key": "dc6zaTOxFJmzC" ])
}
}// `GetRandomImage` is much safer and easier to use than `AnyCall`
let call = GetRandomImage(tag: "cat")
```### Dedicated Clients
A client is responsible for handling things that are common for all operations of a given Web-API. Typically this includes appending API tokens or authentication tokens to a request or validating responses and handling errors.
`AnyClient` is the default implementation of the `Client` protocol and can be used as-is or as a starting point for your own dedicated client.
You'll usually need to create your own dedicated client that either subclasses `AnyClient` or delegates the encoding of requests and parsing of responses to an `AnyClient` instance, as done here:
```swift
class GiphyClient: Client {
private let anyClient = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!)
var apiKey = "dc6zaTOxFJmzC"
override func encode(call: C) async throws -> URLRequest {
var request = anyClient.encode(call: call)
// Append the API key to every request
request.append(query: ["api_key": apiKey])
return request
}
override func parse(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType
where C: Call {
do {
// Use `AnyClient` to parse the response
// If this fails, try to read error details from response body
return try await anyClient.parse(sessionTaskResult: result, for: call)
} catch {
// See if the backend sent detailed error information
guard
let response,
let data,
let errorDict = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
let meta = errorDict?["meta"] as? [String: Any],
let errorCode = meta["error_code"] as? String
else {
// no error info from backend -> rethrow default error
throw error
}
// Propagate error that contains errorCode as reason from backend
throw StatusCodeError.unacceptable(code: response.statusCode, reason: errorCode)
}
}
}
```### Dedicated Response Types
You usually want your networking layer to provide a dedicated response type for every supported call. In our example this could look like this:
```swift
struct RandomImage: Decodable {
struct Data: Decodable {
let url: URL
private enum CodingKeys: String, CodingKey {
case url = "image_url"
}
}
let data: Data
}struct GetRandomImage: Call {
typealias Parser = JSONParser
...
}
```### Type-Safety
With all the parts in place, users of your networking layer can now perform type-safe requests and get a type-safe response with a few lines of code:
```swift
let client = GiphyClient()
let call = GetRandomImage(tag: "cat")
let session = Session(with: client)let (body, response) = try await session.dataTask(for: call)
print("image url: \(body.data.url)")
```## Installation
**Swift Package Manager:**
```bash
.package(url: "https://github.com/tailoredmedia/Endpoints.git", .upToNextMajor(from: "3.0.0"))
```## Example
Example implementation can be found [here](./EndpointsTestbed).
## Requirements
* Swift 5
* iOS 13
* tvOS 12
* macOS 10.15
* watchOS 6