Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
Flinesoft/Microya
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 as cases
with the request parameters/data specified as parameters.
For example, when writing a client for the Microsoft Translator API:
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 }
}
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
}
}
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)
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
:
class RequestResponseLoggerPlugin<EndpointType: Endpoint>: Plugin<EndpointType> {
override func willPerformRequest(_ request: URLRequest, endpoint: EndpointType) {
print("Endpoint: \(endpoint), Request: \(request)")
}
override func didPerformRequest<ResultType: Decodable>(
urlSessionResult: ApiProvider<EndpointType>.URLSessionResult,
typedResult: ApiProvider<EndpointType>.TypedResult<ResultType>,
endpoint: EndpointType
) {
print("Endpoint: \(endpoint), URLSession result: \(urlSessionResult), Typed result: \(typedResult)")
}
}
Shortcuts
Endpoint
provides default implementations for most of its required methods, namely:
public var decoder: JSONDecoder { JSONDecoder() }
public var encoder: JSONEncoder { JSONEncoder() }
public var plugins: [Plugin<Self>] { [] }
public var headers: [String: String] {
[
"Content-Type": "application/json",
"Accept": "application/json",
"Accept-Language": Locale.current.languageCode ?? "en"
]
}
public var queryParameters: [String: QueryParameterValue] { [:] }
So technically, the Endpoint
type only requires you to specify the following 4 things:
protocol Endpoint {
associatedtype ClientErrorType: Decodable
var baseUrl: URL { get }
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.
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.
Github
link |
Stars: 20 |
Last commit: 1 week ago |
You may find interesting
Releases
- 2020-11-21T15:05:46
Added
- Microya now supports Combine publishers, just call
publisher(on:decodeBodyTo:)
orpublisher(on:)
instead ofperformRequest(on:decodeBodyTo:)
orperformRequest(on:)
and you'll get 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.
Changed
- The
queryParameters
is no longer of type[String: String]
, but[String: QueryParameterValue]
now. Existing code like["search": searchTerm]
will need to be updated to["search": .string(searchTerm)]
. Apart from.string
this now also allows specifying an array of strings like so:["tags": .array(userSelectedTags)]
. String & array literals are supported directly, e.g.["sort": "createdAt"]
or["sort": ["createdAt", "id"]]
.