Skip to content

Commit

Permalink
Merge pull request #83 from futuredapp/housekeep/inline-documentation…
Browse files Browse the repository at this point in the history
…-1-2-3-rev2

Inline documentation
  • Loading branch information
mikolasstuchlik authored Sep 20, 2021
2 parents db527a2 + 3db581b commit 544e732
Show file tree
Hide file tree
Showing 15 changed files with 247 additions and 28 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/macos-latest.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: Test – macOS

on:
- pull_request
push:
branches:
- main
pull_request:
branches:
- '*'

jobs:
test:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ubuntu-latest.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
name: Test – Ubuntu

on:
- pull_request
push:
branches:
- main
pull_request:
branches:
- '*'

jobs:
ubuntu-swift-latest:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
![Cocoapods platforms](https://img.shields.io/cocoapods/p/FTAPIKit)
![License](https://img.shields.io/cocoapods/l/FTAPIKit)

![macOS](https://github.com/futuredapp/FTAPIKit/actions/workflows/macos-latest.yml/badge.svg?branch=main)
![Ubuntu](https://github.com/futuredapp/FTAPIKit/actions/workflows/ubuntu-latest.yml/badge.svg?branch=main)

Declarative and generic REST API framework using Codable.
With standard implementation using URLSesssion and JSON encoder/decoder.
Easily extensible for your asynchronous framework or networking stack.
Expand Down
18 changes: 18 additions & 0 deletions Sources/FTAPIKit/APIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ import Foundation
import FoundationNetworking
#endif

/// Error protocol used in types conforming to `URLServer` protocol. Default implementation called `APIErrorStandard`
/// is provided. A type conforming to `APIError` protocol can be provided to `URLServer`
/// to use custom error handling.
///
/// - Note: Since this type is specific to the standard implementation, it works with Foundation `URLSession`
/// network API.
public protocol APIError: Error {
/// Standard implementation of `APIError`
typealias Standard = APIErrorStandard

/// Creates instance if arguments do not represent a valid server response.
///
/// - Parameters:
/// - data: The data returned from the server
/// - response: The URL response returned from the server
/// - error: Error returned by `URLSession` task execution
/// - decoding: The decoder associated with this server, in case the `data` parameter is encoded
///
/// - Warning: Initializer can't return an instance if arguments contain a valid server response. The
/// response would be discarded if it does, and the API call would be treated as a failure.
init?(data: Data?, response: URLResponse?, error: Error?, decoding: Decoding)

/// If the initializer fails but the server response is not valid, this property is used as a fallback.
static var unhandled: Self { get }
}
23 changes: 21 additions & 2 deletions Sources/FTAPIKit/Coding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,37 @@ import Foundation
import FoundationNetworking
#endif

/// `Encoding` represents Swift encoders and provides network-specific features, such as configuring
/// the request with correct headers.
///
/// - Note: A standard implementation is provided in the form of `JSONEncoding`
public protocol Encoding {

/// Encodes the argument
func encode<T: Encodable>(_ object: T) throws -> Data

/// Configures the request with proper headers etc., such as `Content-Type`.
func configure(request: inout URLRequest) throws
}

/// Protocol which enables use of any decoder using type-erasure.
public protocol Decoding {
func decode<T: Decodable>(data: Data) throws -> T
}

/// Type-erased JSON encoder for use with types conforming to `Server` protocol.
public struct JSONEncoding: Encoding {
private let encoder: JSONEncoder

public init(encoder: JSONEncoder = .init()) {
self.encoder = encoder
}

public init(configure: (JSONEncoder) -> Void) {
/// Creates new encoder with ability to configure `JSONEncoder` in a compact manner
/// using a closure.
///
/// - Parameter configure: Function with custom configuration of the encoder
public init(configure: (_ encoder: JSONEncoder) -> Void) {
let encoder = JSONEncoder()
configure(encoder)
self.encoder = encoder
Expand All @@ -35,14 +49,19 @@ public struct JSONEncoding: Encoding {
}
}

/// Type-erased JSON decoder for use with types conforming to `Server` protocol.
public struct JSONDecoding: Decoding {
private let decoder: JSONDecoder

public init(decoder: JSONDecoder = .init()) {
self.decoder = decoder
}

public init(configure: (JSONDecoder) -> Void) {
/// Creates new decoder with ability to configure `JSONDecoder` in a compact manner
/// using a closure.
///
/// - Parameter configure: Function with custom configuration of the decoder
public init(configure: (_ decoder: JSONDecoder) -> Void) {
let decoder = JSONDecoder()
configure(decoder)
self.decoder = decoder
Expand Down
20 changes: 20 additions & 0 deletions Sources/FTAPIKit/Combine/URLServer+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@ import Combine

@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public extension URLServer {

/// Performs call to endpoint which does not return any data in the HTTP response.
/// - Note: Canceling this chain will result in the abortion of the URLSessionTask.
/// - Note: This call maps `func call(endpoint: Endpoint, completion: @escaping (Result<Void, ErrorType>) -> Void) -> URLSessionTask?` to the Combine API
/// - Parameters:
/// - endpoint: The endpoint
/// - Returns: On success void, otherwise error.
func publisher(endpoint: Endpoint) -> AnyPublisher<Void, ErrorType> {
Publishers.Endpoint { completion in
self.call(endpoint: endpoint, completion: completion)
}
.eraseToAnyPublisher()
}

/// Performs call to endpoint which returns an arbitrary data in the HTTP response, that won't be parsed by the decoder of the
/// server.
/// - Note: Canceling this chain will result in the abortion of the URLSessionTask.
/// - Note: This call maps `func call(data endpoint: Endpoint, completion: @escaping (Result<Data, ErrorType>) -> Void) -> URLSessionTask?` to the Combine API
/// - Parameters:
/// - endpoint: The endpoint
/// - Returns: On success plain data, otherwise error.
func publisher(data endpoint: Endpoint) -> AnyPublisher<Data, ErrorType> {
Publishers.Endpoint { completion in
self.call(data: endpoint, completion: completion)
}
.eraseToAnyPublisher()
}

/// Performs call to endpoint which returns data which will be parsed by the server decoder.
/// - Note: Canceling this chain will result in the abortion of the URLSessionTask.
/// - Note: This call maps `func call<EP: ResponseEndpoint>(response endpoint: EP, completion: @escaping (Result<EP.Response, ErrorType>) -> Void) -> URLSessionTask?` to the Combine API
/// - Parameters:
/// - endpoint: The endpoint
/// - Returns: On success instance of the required type, otherwise error.
func publisher<EP: ResponseEndpoint>(response endpoint: EP) -> AnyPublisher<EP.Response, ErrorType> {
Publishers.Endpoint { completion in
self.call(response: endpoint, completion: completion)
Expand Down
57 changes: 41 additions & 16 deletions Sources/FTAPIKit/Endpoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ public protocol Endpoint {
/// URL path component without base URI.
var path: String { get }

/// HTTP headers.
/// - Note: Provided default implementation.
var headers: [String: String] { get }

/// Query of the request, expressible as a dictionary literal with non-unique keys.
/// - Note: Provided default implementation.
var query: URLQuery { get }

/// HTTP method/verb describing the action.
/// - Note: Provided default implementation.
var method: HTTPMethod { get }
}

Expand All @@ -29,25 +34,49 @@ public extension Endpoint {
var method: HTTPMethod { .get }
}

/// `DataEndpoint` transmits data provided in the `body` property without any further encoding.
public protocol DataEndpoint: Endpoint {
var body: Data { get }
}

#if !os(Linux)
/// `UploadEndpoint` will send the provided file to the API.
///
/// - Note: If the standard implementation is used, `URLSession.uploadTask( ... )` will be used.
public protocol UploadEndpoint: Endpoint {

/// File which shell be sent.
var file: URL { get }
}

/// Endpoint which will be sent as a multipart HTTP request.
///
/// - Note: If the standard implementation is used, the body parts will be merged into a temporary file, which will
/// then be transformed to an input stream and passed to the request as a httpBodyStream.
public protocol MultipartEndpoint: Endpoint {

/// List of individual body parts.
var parts: [MultipartBodyPart] { get }
}
#endif

/// The body of the endpoint with the URL query format.
public protocol URLEncodedEndpoint: Endpoint {
var body: URLQuery { get }
}

/// Endpoint protocol extending `Endpoint` having decodable associated type, which is used
/// An abstract representation of endpoint, body of which is represented by Swift encodable type. It serves as an
/// abstraction between the `Server` protocol and more specific `Endpoint` conforming protocols.
/// Do not use this protocol to represent an encodable endpoint, use `RequestEndpoint` instead.
public protocol EncodableEndpoint: Endpoint {

/// Returns `data` which will be sent as the body of the endpoint. Note that only the encoder is passed to
/// the function. The origin of the encodable data is not specified by this protocol.
/// - Parameter encoding: Server provided encoder, which will also configure headers.
func body(encoding: Encoding) throws -> Data
}

/// Protocol extending `Endpoint` with decodable associated type, which is used
/// for automatic deserialization.
public protocol ResponseEndpoint: Endpoint {
/// Associated type describing the return type conforming to `Decodable`
Expand All @@ -56,30 +85,26 @@ public protocol ResponseEndpoint: Endpoint {
associatedtype Response: Decodable
}

/// Endpoint protocol extending `Endpoint` encapsulating and improving sending JSON models to API.
/// Protocol extending `Endpoint`, which supports sending `Encodable` data to the server.
///
/// - Note: Provides default implementation for `func body(encoding: Encoding) throws -> Data`
/// and `var method: HTTPMethod`.
public protocol RequestEndpoint: EncodableEndpoint {
/// Associated type describing the encodable request model for
/// JSON serialization. The associated type is derived from
/// the body property.
/// Associated type describing the encodable request model for serialization. The associated type is derived
/// from the body property.
associatedtype Request: Encodable
/// Generic encodable model, which will be sent as JSON body.
/// Generic encodable model, which will be sent in the body of the request.
var request: Request { get }
}

public extension RequestEndpoint {
var method: HTTPMethod { .post }

func body(encoding: Encoding) throws -> Data {
try encoding.encode(request)
}
}

public protocol EncodableEndpoint: Endpoint {
func body(encoding: Encoding) throws -> Data
}

public extension RequestEndpoint {
var method: HTTPMethod { .post }
}

/// Typealias combining request and response API endpoint. For describing JSON
/// request which both sends and expects JSON model from the server.
/// Typealias combining request and response API endpoint. For describing codable
/// request which both sends and expects serialized model from the server.
public typealias RequestResponseEndpoint = RequestEndpoint & ResponseEndpoint
8 changes: 0 additions & 8 deletions Sources/FTAPIKit/HTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
//
// APIAdapter+Types.swift
// FTAPIKit
//
// Created by Matěj Kašpar Jirásek on 08/02/2018.
// Copyright © 2018 FUNTASTY Digital s.r.o. All rights reserved.
//

/// HTTP method enum with all commonly used verbs.
public enum HTTPMethod: String, CustomStringConvertible {
/// `OPTIONS` HTTP method
Expand Down
22 changes: 22 additions & 0 deletions Sources/FTAPIKit/Server.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
/// `Server` is an abstraction rather than a protocol-bound requirement.
///
/// The expectation of a `Server` conforming type is, that it provides a gateway to an API over HTTP. Conforming
/// type should also have the ability to encode/decode data into requests and responses using the `Codable`
/// conformances and strongly typed coding of the Swift language.
///
/// Conforming type must specify the type representing a request like `Foundation.URLRequest` or
/// `Alamofire.Request`. However, conforming type is expected to have the ability to execute the request too.
///
/// The `FTAPIKit` provides a standard implementation tailored for `Foundation.URLSession` and
/// `Foundation` JSON coders. The standard implementation is represented by `protocol URLServer`.
public protocol Server {
/// The type representing a `Request` of the network library, like `Foundation.URLRequest` or
/// `Alamofire.Request`.
associatedtype Request

/// The instance providing strongly typed decoding.
var decoding: Decoding { get }

/// The instance providing strongly typed encoding.
var encoding: Encoding { get }

/// Takes a Swift description of an endpoint call and transforms it into a valid request. The reason why
/// the function returns the request to the user is so the user is able to modify the request before executing.
/// This is useful in cases when the API uses OAuth or some other token-based authorization, where
/// the request may be delayed before the valid tokens are received.
/// - Parameter endpoint: An instance of an endpoint representing a call.
/// - Returns: A valid request.
func buildRequest(endpoint: Endpoint) throws -> Request
}
19 changes: 19 additions & 0 deletions Sources/FTAPIKit/URLQuery.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import Foundation

/// `URLQuery` is a helper type, that provides a bridge between Swift and URL queries. It provides a nice, dictionary
/// based Swift API and returns a correct URL query items.
///
/// Notice, that the elements of the dictionary literal are not internally stored as a dictionary. Therefore, arrays are
/// expressible (example follows). However, only type acceptable as `Key` and `Value` is `String`.
///
/// ```
/// let query: URLQuery = [
/// "name" : "John",
/// "child[]" : "Eve",
/// "child[]" : "John jr."
/// "child[]" : "Maggie"
/// ]
/// ```
public struct URLQuery: ExpressibleByDictionaryLiteral {
/// Array of query items.
public let items: [URLQueryItem]

init() {
Expand All @@ -11,16 +26,20 @@ public struct URLQuery: ExpressibleByDictionaryLiteral {
self.items = items
}

/// Dictionary literals may not be unique, same keys are allowed and can't be overridden.
public init(dictionaryLiteral elements: (String, String)...) {
self.init(items: elements.map(URLQueryItem.init))
}

/// Returns the query item, which is percent encoded version of provided item. If an item is already percent
/// encoded, it **will** be encoded again.
private func encode(item: URLQueryItem) -> URLQueryItem {
let encodedName = item.name.addingPercentEncoding(withAllowedCharacters: .urlQueryNameValueAllowed) ?? item.name
let encodedValue = item.value?.addingPercentEncoding(withAllowedCharacters: .urlQueryNameValueAllowed)
return URLQueryItem(name: encodedName, value: encodedValue)
}

/// String of all query items, encoded by percent encoding and divided by `&` delimiter.
public var percentEncoded: String? {
guard !items.isEmpty else {
return nil
Expand Down
5 changes: 5 additions & 0 deletions Sources/FTAPIKit/URLRequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public extension URLServer {
}
}

/// Use this structure to translate an instance of `Endpoint` to a valid `URLRequest`. This type is part
/// of the standard implementation.
struct URLRequestBuilder<S: URLServer> {
let server: S
let endpoint: Endpoint
Expand All @@ -19,6 +21,9 @@ struct URLRequestBuilder<S: URLServer> {
self.endpoint = endpoint
}

/// Creates an instance of `URLRequest` corresponding to provided endpoint executed on provided server.
/// It is safe to execute this method multiple times per instance lifetime.
/// - Returns: A valid `URLRequest`.
func build() throws -> URLRequest {
let url = server.baseUri
.appendingPathComponent(endpoint.path)
Expand Down
Loading

0 comments on commit 544e732

Please sign in to comment.