If you need to support platform where the Combine framework is not available (< iOS/tvOS 13, < macOS 10.15), please use the support/without-combine branch instead.
Usage
Step 1: Defining your Endpoints
Create an Api enum with all supported endpoints as cases with the request parameters/data specified as parameters.
enum MicrosoftTranslatorApi {
case languages
case translate(texts: [String], from: Language, to: [Language])
}
Step 2: Making your Api Endpoint compliant
Add an extension for your Api enum that makes it Endpoint compliant, which means you need to add implementations for the following protocol:
public protocol Endpoint {
associatedtype ClientErrorType: Decodable
var decoder: JSONDecoder { get }
var encoder: JSONEncoder { get }
var baseUrl: URL { get }
var headers: [String: String] { get }
var subpath: String { get }
var method: HttpMethod { get }
var queryParameters: [String: QueryParameterValue] { get }
var mockedResponse: MockedResponse? { get }
}
Use switch statements over self to differentiate between the cases (if needed) and to provide the appropriate data the protocol asks for (using Value Bindings).
Toggle me to see an example
extension MicrosoftTranslatorEndpoint: Endpoint {
typealias ClientErrorType = EmptyResponseType
var decoder: JSONDecoder {
return JSONDecoder()
}
var encoder: JSONEncoder {
return JSONEncoder()
}
var baseUrl: URL {
return URL(string: "https://api.cognitive.microsofttranslator.com")!
}
var headers: [String: String] {
switch self {
case .languages:
return [:]
case .translate:
return [
"Ocp-Apim-Subscription-Key": "<SECRET>",
"Content-Type": "application/json"
]
}
}
var subpath: String {
switch self {
case .languages:
return "/languages"
case .translate:
return "/translate"
}
}
var method: HttpMethod {
switch self {
case .languages:
return .get
case let .translate(texts, _, _, _):
return .post(try! encoder.encode(texts))
}
}
var queryParameters: [String: QueryParameterValue] {
var queryParameters: [String: QueryParameterValue] = ["api-version": "3.0"]
switch self {
case .languages:
break
case let .translate(_, sourceLanguage, targetLanguages):
queryParameters["from"] = .string(sourceLanguage.rawValue)
queryParameters["to"] = .array(targetLanguages.map { $0.rawValue })
}
return queryParameters
}
var mockedResponse: MockedResponse? {
switch self {
case .languages:
return mock(status: .ok, bodyJson: #"{ "languages: ["de", "en", "fr", "ja"] }"#)
case let .translate(texts, _, _):
let pseudoTranslationsJson = texts.map { $0.reversed() }.joined(separator: ",")
return mock(status: .ok, bodyJson: "[\(pseudoTranslationsJson)]")
}
}
}
Step 3: Calling your API endpoint with the Result type
Call an API endpoint providing a Decodable type of the expected result (if any) by using one of the methods pre-implemented in the ApiProvider type:
/// Performs the asynchornous request for the chosen endpoint and calls the completion closure with the result.
performRequest<ResultType: Decodable>(
on endpoint: EndpointType,
decodeBodyTo: ResultType.Type,
completion: @escaping (Result<ResultType, ApiError<ClientErrorType>>) -> Void
)
/// Performs the request for the chosen endpoint synchronously (waits for the result) and returns the result.
public func performRequestAndWait<ResultType: Decodable>(
on endpoint: EndpointType,
decodeBodyTo bodyType: ResultType.Type
)
There’s also extra methods for endpoints where you don’t expect a response body:
/// Performs the asynchronous request for the chosen write-only endpoint and calls the completion closure with the result.
performRequest(on endpoint: EndpointType, completion: @escaping (Result<EmptyBodyResponse, ApiError<ClientErrorType>>) -> Void)
/// Performs the request for the chosen write-only endpoint synchronously (waits for the result).
performRequestAndWait(on endpoint: EndpointType) -> Result<EmptyBodyResponse, ApiError<ClientErrorType>>
The EmptyBodyResponse returned here is just an empty type, so you can just ignore it.
Here’s a full example of a call you could make with Mircoya:
let provider = ApiProvider<MicrosoftTranslatorEndpoint>()
let endpoint = MicrosoftTranslatorEndpoint.translate(texts: ["Test"], from: .english, to: [.german, .japanese, .turkish])
provider.performRequest(on: endpoint, decodeBodyTo: [String: String].self) { result in
switch result {
case let .success(translationsByLanguage):
// use the already decoded `[String: String]` result
case let .failure(apiError):
// error handling
}
}
// OR, if you prefer a synchronous call, use the `AndWait` variant
switch provider.performRequestAndWait(on: endpoint, decodeBodyTo: [String: String].self) {
case let .success(translationsByLanguage):
// use the already decoded `[String: String]` result
case let .failure(apiError):
// error handling
}
Note that you can also use the throwing get() function of Swift 5’s Result type instead of using a switch:
provider.performRequest(on: endpoint, decodeBodyTo: [String: String].self) { result in
let translationsByLanguage = try result.get()
// use the already decoded `[String: String]` result
}
// OR, if you prefer a synchronous call, use the `AndWait` variant
let translationsByLanguage = try provider.performRequestAndWait(on: endpoint, decodeBodyTo: [String: String].self).get()
// use the already decoded `[String: String]` result
There’s even useful functional methods defined on the Result type like map(), flatMap() or mapError() and flatMapError(). See the “Transforming Result” section in this article for more information.
Combine Support
If you are using Combine in your project (e.g. because you’re using SwiftUI), you might want to replace the calls to performRequest(on:decodeBodyTo:) or performRequest(on:) with the Combine calls publisher(on:decodeBodyTo:) or publisher(on:). This will give you an AnyPublisher request stream to subscribe to. In success cases you will receive the decoded typed object, in error cases an ApiError object exactly like within the performRequest completion closure. But instead of a Result type you can use sink or catch from the Combine framework.
For example, the usage with Combine might look something like this:
var cancellables: Set<AnyCancellable> = []
provider.publisher(on: endpoint, decodeBodyTo: TranslationsResponse.self)
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in }
receiveValue: { (translationsResponse: TranslationsResponse) in
// do something with the success response object
}
)
.catch { apiError in
switch apiError {
case let .clientError(statusCode, clientError):
// show an alert to customer with status code & data from clientError body
default:
logger.handleApiError(apiError)
}
}
.store(in: &cancellables)
Concurrency Support
If you are using Swift 5.5 in your project and your minimum target is iOS/tvOS 15+, macOS 12+ or watchOS 8+, you might want to use the async method response instead. For example, the usage might look something like this:
let result = await provider.response(on: endpoint, decodeBodyTo: TranslationsResponse.self)
switch result {
case let .success(translationsByLanguage):
// use the already decoded `[String: String]` result
case let .failure(apiError):
// error handling
}
Plugins
The initializer of ApiProvider accepts an array of Plugin objects. You can implement your own plugins or use one of the existing ones in the Plugins directory. Here’s are the callbacks a custom Plugin subclass can override:
/// Called to modify a request before sending.
modifyRequest(_ request: inout URLRequest, endpoint: EndpointType)
/// Called immediately before a request is sent.
willPerformRequest(_ request: URLRequest, endpoint: EndpointType)
/// Called after a response has been received & decoded, but before calling the completion handler.
didPerformRequest<ResultType: Decodable>(
urlSessionResult: (data: Data?, response: URLResponse?, error: Error?),
typedResult: Result<ResultType, ApiError<EndpointType.ClientErrorType>>,
endpoint: EndpointType
)
Toggle me to see a full custom plugin example
Here’s a possible implementation of a RequestResponseLoggerPlugin that logs using print:
Endpoint provides default implementations for most of its required methods, namely:
public var decoder: JSONDecoder { JSONDecoder() }
public var encoder: JSONEncoder { JSONEncoder() }
public var headers: [String: String] {
[
"Content-Type": "application/json",
"Accept": "application/json",
"Accept-Language": Locale.current.languageCode ?? "en"
]
}
public var queryParameters: [String: QueryParameterValue] { [:] }
public var mockedResponse: MockedResponse? { nil }
So technically, the Endpoint type only requires you to specify the following 4 things:
protocol Endpoint {
associatedtype ClientErrorType: Decodable
var subpath: String { get }
var method: HttpMethod { get }
}
This can be a time (/ code) saver for simple APIs you want to access.
You can also use EmptyBodyResponse type for ClientErrorType to ignore the client error body structure.
Testing
Microya supports mocking responses in your tests.
To do that, just initialize a different ApiProvider in your tests and specify with a given delay and scheduler as the mockingBehavior parameter.
Now, instead of making actual calls, Microya will respond with the provided mockedResponse computed property in your Endpoint type.
Note that the .delay mocking behavior is designed for use with Combine schedulers. Use DispatchQueue.test from the combine-schedulers library (which is included with Microya) to control time in your tests so you don’t need to actually wait for the requests when using .delay.
For example, you might want to add an extension in your tests to provide a .mocked property to use whenever you need an ApiProvider like so:
Now, in your tests you can just call testScheduler.advance(by: .milliseconds(300)) fast-forward the time so your tests stay fast.
Donation
Microya was brought to you by Cihat Gündüz in his free time. If you want to thank me and support the development of this project, please make a small donation on PayPal. In case you also like my other open source contributions and articles, please consider motivating me by becoming a sponsor on GitHub or a patron on Patreon.
Thank you very much for any donation, it really helps out a lot! 💯
Installation • Usage • Donation • Issues • Contributing • License
Microya
A micro version of the Moya network abstraction layer written in Swift.
Installation
Installation is only supported via SwiftPM.
Usage
Step 1: Defining your Endpoints
Create an Api
enum
with all supported endpoints ascases
with the request parameters/data specified as parameters.For example, when writing a client for the Microsoft Translator API:
Step 2: Making your Api
Endpoint
compliantAdd an extension for your Api
enum
that makes itEndpoint
compliant, which means you need to add implementations for the following protocol:Use
switch
statements overself
to differentiate between the cases (if needed) and to provide the appropriate data the protocol asks for (using Value Bindings).Toggle me to see an example
Step 3: Calling your API endpoint with the Result type
Call an API endpoint providing a
Decodable
type of the expected result (if any) by using one of the methods pre-implemented in theApiProvider
type:There’s also extra methods for endpoints where you don’t expect a response body:
The
EmptyBodyResponse
returned here is just an empty type, so you can just ignore it.Here’s a full example of a call you could make with Mircoya:
Note that you can also use the throwing
get()
function of Swift 5’sResult
type instead of using aswitch
:There’s even useful functional methods defined on the
Result
type likemap()
,flatMap()
ormapError()
andflatMapError()
. See the “Transforming Result” section in this article for more information.Combine Support
If you are using Combine in your project (e.g. because you’re using SwiftUI), you might want to replace the calls to
performRequest(on:decodeBodyTo:)
orperformRequest(on:)
with the Combine callspublisher(on:decodeBodyTo:)
orpublisher(on:)
. This will give you anAnyPublisher
request stream to subscribe to. In success cases you will receive the decoded typed object, in error cases anApiError
object exactly like within theperformRequest
completion closure. But instead of aResult
type you can usesink
orcatch
from the Combine framework.For example, the usage with Combine might look something like this:
Concurrency Support
If you are using Swift 5.5 in your project and your minimum target is iOS/tvOS 15+, macOS 12+ or watchOS 8+, you might want to use the
async
methodresponse
instead. For example, the usage might look something like this:Plugins
The initializer of
ApiProvider
accepts an array ofPlugin
objects. You can implement your own plugins or use one of the existing ones in the Plugins directory. Here’s are the callbacks a customPlugin
subclass can override:Toggle me to see a full custom plugin example
Here’s a possible implementation of a
RequestResponseLoggerPlugin
that logs usingprint
:Shortcuts
Endpoint
provides default implementations for most of its required methods, namely:So technically, the
Endpoint
type only requires you to specify the following 4 things:This can be a time (/ code) saver for simple APIs you want to access. You can also use
EmptyBodyResponse
type forClientErrorType
to ignore the client error body structure.Testing
Microya supports mocking responses in your tests. To do that, just initialize a different
ApiProvider
in your tests and specify with a givendelay
andscheduler
as themockingBehavior
parameter.Now, instead of making actual calls, Microya will respond with the provided
mockedResponse
computed property in yourEndpoint
type.Note that the
.delay
mocking behavior is designed for use with Combine schedulers. UseDispatchQueue.test
from thecombine-schedulers
library (which is included with Microya) to control time in your tests so you don’t need to actually wait for the requests when using.delay
.For example, you might want to add an extension in your tests to provide a
.mocked
property to use whenever you need anApiProvider
like so:Now, in your tests you can just call
testScheduler.advance(by: .milliseconds(300))
fast-forward the time so your tests stay fast.Donation
Microya was brought to you by Cihat Gündüz in his free time. If you want to thank me and support the development of this project, please make a small donation on PayPal. In case you also like my other open source contributions and articles, please consider motivating me by becoming a sponsor on GitHub or a patron on Patreon.
Thank you very much for any donation, it really helps out a lot! 💯
Contributing
See the file CONTRIBUTING.md.
License
This library is released under the MIT License. See LICENSE for details.