All Projects → kula-app → Postie

kula-app / Postie

Licence: MIT license
Structured HTTP Client using Combine

Programming Languages

swift
15916 projects
ruby
36898 projects - #4 most used programming language

Projects that are alternatives of or similar to Postie

JobHunter
一個練習 Swift Combine framework 的小專案,抓取知名平台們的職缺資料,方便使用者快速瀏覽尋找,並可連回該平台閱讀詳細資料。
Stars: ✭ 15 (-46.43%)
Mutual labels:  combine
Weather
A simple SwiftUI weather app using MVVM.
Stars: ✭ 23 (-17.86%)
Mutual labels:  combine
SwiftReactor
A protocol which should help structure your data flow in SwiftUI (and UIKit).
Stars: ✭ 57 (+103.57%)
Mutual labels:  combine
GITGET
GitHub의 Contributions를 iOS의 Widget으로 보여주는 App
Stars: ✭ 101 (+260.71%)
Mutual labels:  combine
NetworkAgent
This package is meant to make http request of an easy way inspiren in the architecture of Moya package. This package is 100% free of dependencies and works with Combine api + Codable
Stars: ✭ 16 (-42.86%)
Mutual labels:  combine
Opencombine
Open source implementation of Apple's Combine framework for processing values over time.
Stars: ✭ 2,040 (+7185.71%)
Mutual labels:  combine
UrlCombine
C# util for combining Url paths. Works similarly to Path.Combine.
Stars: ✭ 23 (-17.86%)
Mutual labels:  combine
grunt-angular-combine
Grunt task for combining AngularJS partials into a single HTML file.
Stars: ✭ 16 (-42.86%)
Mutual labels:  combine
E-Rezept-App-iOS
https://gematik.github.io/E-Rezept-App-iOS/
Stars: ✭ 76 (+171.43%)
Mutual labels:  combine
clouds
🌦 A weather app for iOS, written in SwiftUI.
Stars: ✭ 26 (-7.14%)
Mutual labels:  combine
CancelBag
A DisposeBag for Combine
Stars: ✭ 53 (+89.29%)
Mutual labels:  combine
Notflix
📱Netflix like application using SwiftUI and Combine
Stars: ✭ 76 (+171.43%)
Mutual labels:  combine
Swiftui Tutorials
A code example and translation project of SwiftUI. / 一个 SwiftUI 的示例、翻译的教程项目。
Stars: ✭ 1,992 (+7014.29%)
Mutual labels:  combine
mocka
Mocka — A Mock Server Made for Developers by Developers, made in Swift ❤️
Stars: ✭ 56 (+100%)
Mutual labels:  combine
99StocksSwiftUI
SwiftUI app that fetches a list of companies, sort them by their share price and can show its details on a separate view
Stars: ✭ 34 (+21.43%)
Mutual labels:  combine
Luna
Tracking the moon phase using SwiftUI and Combine
Stars: ✭ 19 (-32.14%)
Mutual labels:  combine
Pdfsam
PDFsam, a desktop application to extract pages, split, merge, mix and rotate PDF files
Stars: ✭ 1,829 (+6432.14%)
Mutual labels:  combine
LittleBlueTooth
A simple library that helps you in connecting with BLE devices
Stars: ✭ 68 (+142.86%)
Mutual labels:  combine
iOS-App
🕹️ iOS application of HardcoreTap game
Stars: ✭ 17 (-39.29%)
Mutual labels:  combine
Carekit
CareKit is an open source software framework for creating apps that help people better understand and manage their health.
Stars: ✭ 2,142 (+7550%)
Mutual labels:  combine

Postie

Postie - The Next-Level HTTP API Client using Combine

Created and maintained by Philip Niedertscheider at kula.app and all the amazing contributors.

Postie is a pure Swift library for building URLRequests using property wrappers.

Example

Checkout this full example starting at defining the request and the expected response, up to creating a client and sending it to the remote endpoint.

import Foundation
import Postie

// Request contains body data encoded as a JSON
struct MyRequest: JSONRequest {

    // The request body is strongly typed defined
    struct RequestBody: Encodable {
        var someNumberValue: Int
    }

    // Define the response directly inside the request, so every
    // Request-Response are isolated.
    // Also directly define, that the response body shall be decoded
    // from Form-URL-Encoding
    struct Response: FormURLEncodedDecodable {

        // The expected response body structure
        struct Body: Decodable {
            var someNumberValue: Int
        }

        // The expected response body structure, in case we did something wrong
        struct ErrorBody: Decodable {
            var message: String
        }

        // Property wrappers define the purpose
        @ResponseBody<Body> var body
        @ResponseErrorBody<ErrorBody> var errorBody

        // Access specific response headers
        @ResponseHeader<DefaultStrategy> var contentType: String

        // Status codes also have convenience utilities
        @ResponseStatusCode var statusCode
    }
    
    // The `keyEncodingStrategy` determines how to encode a type’s coding keys as JSON keys.
    // The default value return `.convertToSnakeCase` but you can optionally choose to return `.useDefaultKeys` by implementing JSONRequest's protocol requirement as follow:
    // var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy {
    //     .useDefaultKeys
    // }


    // This property holds the data which will be encoded
    var body: RequestBody

    // Location of our resource with template string
    @RequestPath var path = "/profile/{user_id}"

    // Parameter to replace in the template string
    @RequestPathParameter(name: "userId") var userId: String

    // HTTP method that shall be used
    @RequestHTTPMethod var method = .post

    // Set request headers using the property naming
    @RequestHeader var authorization: String?
}

// Create a request
var request = MyRequest(body: MyRequest.RequestBody(someNumberValue: 42),
                        userId: "my-user-id")
request.authorization = "Bearer my-oauth-token"

// Create a client
let client = HTTPAPIClient(url: URL(string: "https://example.org")!)

// Send the request
client.send(request)
    .sink { result in
        switch result {
        case .failure(let error):
            print("Oh no something went wrong :(")
            print(error)
        case .finished:
            print("Everything worked fine :)")
        }
    } receiveValue: { response in
        // The single response object contains all the interesting data
        print(response.statusCode)
        print(response.body)
        print(response.errorBody)
        print(response.contentType)
    }

Core Concept

The networking layer of Foundation (and with Combine) is already quite advanced. Using URLRequest you can set many different configuration values, e.g. the HTTP Method or Headers.

Unfortunately you still need to manually serialize your payload into Foundation.Data and set it as the request body. Additionally you also have to set Content-Type header, or otherwise the remote won't be able to understand the content.

Also the response needs to be decoded, and even if a few decoders are included, e.g. JSONDecoder, reading and parsing the URLResponse is not intuitive.

Even worse when the response structure differs in case of an error, e.g. instead of

{ 
    "some": "data"
}

an error object is returned:

{
    "error":  {
        "message": "Something went wrong!"
    }
}

This would require to create combined types such as this one:

struct Response: Decodable {
    struct ErrorResponse: Decodable {
        var message: String
    }

    var some: String?
    var error: ErrorResponse?
}

and you would have to use nil-checking (probably in combination with the HTTP Status Code) to see which data is present.

Postie simplifies these use cases. The main idea is defining slim struct types to build the requests, and serialize the associated responses. Configuration of the request is done using property wrappers, e.g. @QueryItem.

Usage

Defining the request

Postie includes a couple of types to build your requests. As a first step, create your Request type, with an associated Response:

import Postie

struct FooRequest: Request  {
    typealias Response = EmptyResponse
}

The default Request type is used for URL requests without any body data. If you want to include payload data, use one of the following ones:

  • PlainRequest
  • JSONRequest
  • FormURLEncodedRequest
  • XMLRequest

All of these expect a body instance variable. For JSONRequest, FormURLEncodedRequest and XMLRequest the type of body is generic but needs to implement the Encodable protocol.

Example:

struct Foo: JSONRequest {

    struct Body: Encodable {}
    typealias Response = EmptyResponse

    var body: Body
    
}

struct Bar: FormURLEncodedRequest {

    struct Body: Encodable {}
    typealias Response = EmptyResponse

    var body: Body
    
}

struct Bar: XMLRequest {

    struct Body: Encodable {}
    typealias Response = EmptyResponse

    var body: Body
    
}

For the PlainRequest the body expects a plain String content. Optionally you can also overwrite the encoding variable with a custom encoding (default is utf8).

Example:

struct Foo: PlainRequest {

    typealias Response = EmptyResponse

    var body: String
    var encoding: String.Encoding = .utf16 // default: .utf8
    
}

Setting the request HTTP Method

The default HTTP method is GET, but it can be overwritten by adding an instance property with the property wrapper @RequestHTTPMethod:

Example:

struct Request: Encodable {

    typealias Response = EmptyResponse

    @RequestHTTPMethod var method

}

// Usage
var request = Request()
request.method = .post

Note:

As the property name is ignored, it is possible to have multiple properties with this property wrapper, but only the last one will be used.

Setting the request URL path

The default path /, but it can be overwritten by adding an instance property with the property wrapper @RequestPath:

Example:

struct Request: Encodable {

    typealias Response = EmptyResponse

    @RequestPath var path

}

// Usage
let request = Request(path: "/some-detail-path")

Additionally the request path can contain variables using the mustache syntax, e.g. /path/with/{variable_name}/inside.

To set the variable value, add a new instance property using the @RequestPathParameter property wrapper. By default the encoder uses the variable name for encoding, but you can also define a custom name:

struct Request: Encodable {

    typealias Response = EmptyResponse

    @RequestPath var path = "/app/{id}/contacts/{cid}"
    @RequestPathParameter var id: Int
    @RequestPathParameter(name: "cid") var contactId: String

}

// Usage
var request = Request(id: 123)
request.contactId = "ABC456"

// Result: 
https://postie.local/app/123/contacts/ABC456

Note:

As the property name is ignored, it is possible to have multiple properties with this property wrapper, but only the last one will be used. Also you need to require a leading forward slash (/) in the path.

Adding query items to the URL

Multiple query items can be added by adding them as properties using the property wrapper @QueryItem.

Example:

struct Request: Encodable {

    typealias Response = EmptyResponse

    @QueryItem
    var text: String

    @QueryItem(name: "other_text")
    var anotherQuery: String

    @QueryItem
    var optionalText: String?

}

// Usage
var request = Request(text: "foo")
request.anotherQuery = "bar"

// Result query in URL:
?text=foo&other_text=bar

If no custom name is set, the variable name is used. If the query item is optional, and not set (therefore nil), it won't be added to the list.

Supported query value types can be found in QueryItemValue.swift.

Note:

When using an Array as the query item type, every value in the array is appended using the same name. The remote server is then responsible to collect all query items with the same name and merge them into an array.

Example: [1, 2, 3] with name values becomes ?values=1&values=2&values=3

As multiple query items can use the same custom name, they will all be appended to the query. This does not apply to synthesized names, as a Swift type can not have more than one property with the exact same name.

Adding Headers to the request

Multiple headers can be set by adding them as properties using the property wrapper @RequestHeader.

Example:

struct Request: Encodable {

    typealias Response = EmptyResponse

    @RequestHeader
    var text: String

    @RequestHeader(name: "other_text")
    var anotherQuery: String

    @RequestHeader
    var optionalText: String?

}

// Usage
var request = Request(text: "foo")
request.anotherQuery = "bar"

// Result query in URL:
?text=foo&other_text=bar

If no custom name is set, the variable name is used. If the header is optional, and not set (therefore nil), it won't be added to the list.

Supported header values types can be found in RequestHeaderValue.swift.

Note:

As multiple query items can use the same custom name, the last one will be used. This does not apply to synthesized names, as a Swift type can not have more than one property with the exact same name.

Defining the response

Every struct implementing Request expects to have an associated Response type implementing the Decodable protocol. In the examples above the EmptyResponse convenience type (which is an empty, decodable type) has been used.

The response structure will be populated with data from either the response body data or metadata.

Parsing the response body

To parse the response data into a Decodable type, add a property with the property wrapper @ResponseBody<BodyType> where BodyType is the response body type.

Example:

struct Request: Postie.Request {
    struct Response: Decodable {
        struct Body: Decodable {
            var value: String
        }

        @ResponseBody<Body> var body
    }
}

To indicate the decoding system which response data format should be expected, conform your response type to one of the following protocols:

  • PlainDecodable
  • JSONDecodable
  • XMLDecodable
  • FormURLEncodedDecodable

For JSONDecodable, FormURLEncodedDecodable and XMLDecodable the type of body is generic but needs to implement the Decodable protocol.

Example:

struct Request: Postie.Request {
    struct Response: Decodable {
        struct Body: JSONDecodable {
            var value: String
        }

        @ResponseBody<Body> var body
    }
}

struct Request: Postie.Request {
    struct Response: Decodable {
        struct Body: FormURLEncodedDecodable {
            var value: String
        }

        @ResponseBody<Body> var body
    }
}

struct Request: Postie.Request {
    struct Response: Decodable {
        struct Body: XMLDecodable {
            var value: String
        }

        @ResponseBody<Body> var body
    }
}

For the type PlainDecodable, use it directly, as it is an alias for String.

Example:

struct Request: Postie.Request {
    struct Response: Decodable {
        @ResponseBody<PlainDecodable> var body
    }
}

Response body on error

As mentioned in Core Concept Postie allows defining a body response type when receiving an invalid status code (>=400).

It's usage is exactly the same as with @ResponseBody, but instead you need to use the property wrapper @ResponseErrorBody. Either the @ResponseBody or the @ResponseErrorBody is set, never both at the same time.

The error response body gets set if the response status code is neither a 2XX nor a 3XX status code.

Example:

struct Request: Postie.Request {
    struct Response: Decodable {
        struct ErrorBody: JSONDecodable {
            var message: String
        }
        @ResponseErrorBody<ErrorBody> var errorBody
    }
}

Response headers

Use the property wrapper @ResponseHeader<Strategy> inside the response type.

In the moment, the following decoding strategies are implemented:

  • DefaultHeaderStrategy

Converts the property name into camel-case format (e.g. Content-Type becomes contentType) and compares case-insensitive (e.g. Authorization equals authorization) This strategy expects the response header to be set, otherwise an error will be thrown.

Response from URL requests are always of type String and no casting will be performed. Therefore the only valid property type is String.

  • DefaultHeaderOptionalStrategy

Same as DefaultHeaderStrategy but won't fail if the header can not be found.

Example:

struct Response: Decodable {

    @ResponseHeader<DefaultHeaderStrategy>
    var authorization: String

    @ResponseHeader<DefaultHeaderStrategy>
    var contentType: String

    @ResponseHeader<DefaultHeaderStrategyOptional>
    var optionalValue: String?

}

Response Status

The default HTTP method is GET, but it can be overwritten by adding an instance property with the property wrapper @RequestHTTPMethod:

Example:

struct Response: Decodable {

    @ResponseStatusCode var statusCode

}

Note:

Multiple properties can be declared with this property wrapper. All of them will have the value set.

Nested Responses

To support inheritance, which can be especially useful for pagination, use the property wrapper @NestedResponse to add nested responses.

While decoding the flat HTTP response will be applied recursively to all nested responses, therefore it is possible, that different nested responses access different values of the original HTTP response.

Example:

struct PaginatedResponse<NestedRequest: Request>: Decodable {

    /// Header which indicates how many more elements are available
    @ResponseHeader<DefaultHeaderStrategy> var totalElements

    @NestedResponse var nested: NestedRequest
}

struct ListRequest: Request {

    typealias Response = PaginatedResponse<ListResponse>

    struct ListResponse: Decodable {
        // see other examples
    }
}

HTTP API Client

The easiest way of sending Postie requests, is using the HTTPAPIClient which takes care of encoding requests, and decoding responses.

All it takes to create a client, is the URL which is used as a base for all requests. Afterwards you can just send the requests, using the Combine publishers.

Additionally the HTTPAPIClient provides the option of setting a session provider, which encapsulates the default URLSession by a protocol. This allows to create networking clients which can be mocked (perfect for unit testing).

Example:

let url: URL = ...
let client = HTTPAPIClient(baseURL: url)

// ... create request ...

client.send(request)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            // handle error
            break
        case .finished:
            break
        }
    }, receiveValue: { response in
        // process response
        print(response)
    })
    .store(in: &cancellables)
//

Encoding & Decoding

The RequestEncoder is responsible to turn an encodable Request into an URLRequest. It requires an URL in the initializer, as Postie requests are relative requests.

Example:

// A request as explained above
let request: Request = ...

// Create a request encoder
let url = URL(string: "http://techprimate.com")
let encoder = RequestEncoder(baseURL: url)

// Encode request
let urlRequest: URLRequest
do {
    let urlRequest = try encoder.encode(request)
    // continue with url request
    ...
} catch {
    // Handle error
    ...
}

As its contrarity component, the RequestDecoder is responsible to turn a tuple of (data: Data, response: HTTPURLResponse) into a given type Response.

Example:

// Data received from the URL session task
let response: HTTPURLResponse = ...
let data: Data = ...

// Create decoder
let decoder = ResponseDecoder()
do {
    let decoded = try decoder.decode(Response.self, from: (data, response))) 
    // continue with decoded response
    ...
} catch{
    // Handle error
    ...
}

Combine Support

RequestEncoder conforms to TopLevelEncoder and RequestDecoder conforms to TopLevelDecoder. This means both encoders can be used in a Combine pipeline.

Example:

let request = Request()
let session = URLSession.shared

let url = URL(string: "https://techprimate.com")!
let encodedRequest = try RequestEncoder(baseURL: url).encode(request)

// Send request using the given URL session provider
return session
    .dataTaskPublisher(for: encodedRequest)
    .tryMap { (data: Data, response: URLResponse) in
        guard let response = response as? HTTPURLResponse else {
            fatalError("handle non HTTP url responses")
        }
        return (data: data, response: response)
    }
    .decode(type: Request.Response.self, decoder: ResponseDecoder())
    .sink(receiveCompletion: { result in
        // handle result
    }, receiveValue: { decoded in
        // do something with decoded response
    })

Articles & Stories

Here is a list of relevant articles and stories regarding Postie 🥳

(Please let us know if you found more.)

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].