I would like to create a way to construct endpoint this way:
let endpoint: String = APIRequest.Playlist.getById(idValue).param1(param1value).param2(param2value)
I have a class APIRequest with baseUrl and I was trying to use enums.
The params are url encoded.
How can I reach my goal ?
If you are using Alamofire, You can create endpoints like below,
Create a protocol like below and extend it for default implementation.
import Foundation
import Alamofire
public protocol Endpoint {
var baseURL: String { get } // https://example.com
var path: String { get } // /users/
var fullURL: String { get } // This will automatically be set. https://example.com/users/
var method: HTTPMethod { get } // .get
var encoding: ParameterEncoding { get } // URLEncoding.default
var body: Parameters { get } // ["foo" : "bar"]
var headers: HTTPHeaders { get } // ["Authorization" : "Bearer SOME_TOKEN"]
}
public extension Endpoint {
var encoding: ParameterEncoding {
return method == .get ? URLEncoding.default : JSONEncoding.default
}
var fullURL: String {
return baseURL + path
}
var body: Parameters {
return Parameters()
}
var headers : HTTPHeaders{
return [:]
}
var baseURL : String{
return Config.baseUrl
}
}
Create an enum for APIEndpoints and add your cases there. Extend APIEndponts for your custom cases.
enum APIEnpoints {
case login(apiModel:APILogInRequest)
case home
}
extension APIEnpoints : Endpoint {
var path: String {
switch self{
case .login(_): return "/api/v1/user/get_auth_token/"
case .home: return "/api/v1/home/"
}
}
var method: HTTPMethod {
switch self{
case .login(_): return .post
case .home: return .get
}
}
var headers : HTTPHeaders{
return ["Authorization":"JWT " + authtoken]
}
var body: Parameters {
var body: Parameters = Parameters()
switch self {
case .login(let model):
body = model.toDictionary
break
case .home:
break
}
return body
}
}
And finally you can use your endpoints like below
let model = APILogInRequest.init("email","password")
//1
APIEnpoints.login(apiModel: model)
//2
APIEnpoints.home
Related
I am following along with this tutorial in order to create an async generic network layer. I got the network manager working correctly.
https://betterprogramming.pub/async-await-generic-network-layer-with-swift-5-5-2bdd51224ea9
As I try to implement more APIs, that I can use with the networking layer, some of the APIs require different tokens, different content in the body, or header etc, that I have to get at runtime.
In the snippet of code below from the tutorial, I get that we are building up the Movie endpoint based on .self, and then return the specific values we need. But the issue is, some of the data in this, for example, the access token, has to be hard coded here. I am looking for a way, that I can 'inject' the accessToken, and then it will be created with this new token. Again, the reason for this, is that in other APIs, the access token might not always be known.
protocol Endpoint {
var scheme: String { get }
var host: String { get }
var version: String? { get }
var path: String { get }
var method: RequestMethod { get }
var queryItems: [String: String]? { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
}
extension MoviesEndpoint: Endpoint {
var path: String {
switch self {
case .topRated:
return "/3/movie/top_rated"
case .movieDetail(let id):
return "/3/movie/\(id)"
}
}
var method: RequestMethod {
switch self {
case .topRated, .movieDetail:
return .get
}
}
var header: [String: String]? {
// Access Token to use in Bearer header
let accessToken = "insert your access token here -> https://www.themoviedb.org/settings/api"
switch self {
case .topRated, .movieDetail:
return [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8"
]
}
}
var body: [String: String]? {
switch self {
case .topRated, .movieDetail:
return nil
}
}
For an example, I tried converting the var body to a function, so I could do
func body(_ bodyDict: [String, String]?) -> [String:String]? {
switch self{
case .test:
return bodyDict
}
The idea of above, was that I changed it to a function, so I could pass in a dict, and then return that dict in the api call, but that did not work. The MoviesEnpoint adheres to the extension Endpoint, which then gives the compiler error 'Protocol Methods must not have bodies'.
Is there a way to dependency inject runtime parameters into this Extension/Protocol method?
Change the declaration of MoviesEndpoint so that it stores the access token:
struct MoviesEndpoint {
var accessToken: String
var detail: Detail
enum Detail {
case topRated
case movieDetail(id: Int)
}
}
You'll need to change all the switch self statements to switch detail.
However, I think the solution in the article (four protocols) is overwrought.
Instead of a pile of protocols, make one struct with a single function property:
struct MovieDatabaseClient {
var getRaw: (MovieEndpoint) async throws -> (Data, URLResponse)
}
Extend it with a generic method to handle the response parsing and decoding:
extension MovieDatabaseClient {
func get<T: Decodable>(
endpoint: MovieEndpoint,
as responseType: T.Type = T.self
) async throws -> T {
let (data, response) = try await getRaw(endpoint)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
switch response.statusCode {
case 200...299:
break
case 401:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(responseType, from: data)
}
}
Provide a “live“ implementation that actually sends network requests:
extension MovieDatabaseClient {
static func live(host: String, accessToken: String) -> Self {
return .init { endpoint in
let request = try liveURLRequest(
host: host,
accessToken: accessToken,
endpoint: endpoint
)
return try await URLSession.shared.data(for: request)
}
}
// Factored out in case you want to write unit tests for it:
static func liveURLRequest(
host: String,
accessToken: String,
endpoint: MovieEndpoint
) throws -> URLRequest {
var components = URLComponents()
components.scheme = "https"
components.host = host
components.path = endpoint.urlPath
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8",
]
return request
}
}
extension MovieEndpoint {
var urlPath: String {
switch self {
case .topRated: return "/3/movie/top_rated"
case .movieDetail(id: let id): return "/3/movie/\(id)"
}
}
}
To use it in your app:
// At app startup...
let myAccessToken = "loaded from UserDefaults or something"
let client = MovieDatabaseClient.live(
host: "api.themoviedb.org",
accessToken: myAccessToken
)
// Using it:
let topRated: TopRated = try await client.get(endpoint: .topRated)
let movieDetail: MovieDetail = try await client.get(endpoint: .movieDetail(id: 123))
For testing, you can create a mock client by providing a single closure that fakes the network request/response. Simple examples:
extension MovieDatabaseClient {
static func mockSuccess<T: Encodable>(_ body: T) -> Self {
return .init { _ in
let data = try JSONEncoder().encode(body)
let response = HTTPURLResponse(
url: URL(string: "test")!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (data, response)
}
}
static func mockFailure(_ error: Error) -> Self {
return .init { _ in
throw error
}
}
}
So a test can create a mock client that always responds with a TopRated response like this:
let mockTopRatedClient = MovieDatabaseClient.mockSuccess(TopRated(...))
If you want to learn more about this style of dependency management and mocking, Point-Free has a good (but subscription required) series of episodes: Designing Dependencies.
Recently I'm developing API parts using GraphQL.
When I call API, I need to generate a query from structure like this.
// from this model
struct ModelA {
let id: String
let title: String
....
}
// to this query
query {
id
title
}
If I have an instance of ModelA, I can reflect properties from instance using Mirror.
But I don't want to make the instance in this case and I don't want to make the properties to variables because I need to use this model for response.
Additionally class_copyPropertyList is a good solution if the model is NSObject class. However in this case this is a structure in swift.
Is this possible? I appreciate your help in advance.
try this
extension Encodable {
func query() -> String? {
guard let encodeData: Data = try? JSONEncoder().encode(self) else { return nil }
guard let jsonRepresentation: [String: Any] = try? JSONSerialization.jsonObject(with: encodeData, options: []) as? [String: Any] else { return nil }
let keys: String = jsonRepresentation.map{ $0.key }.joined(separator: " ")
return "query { " + keys + " }"
}
}
struct ModelA: Encodable {
let id: String
let title: String
}
let model = ModelA(id: "abc123", title: "Model title")
if let query = model.query() {
print(query)
}
EDIT
// query function in Encodable extension stays the same
protocol Querable: Encodable {
static var dummy: Encodable { get }
}
extension Querable {
static var query: String? {
return self.dummy.query()
}
}
struct ModelA: Querable {
let id: String
let title: String
static var dummy: Encodable {
return ModelA(id: "", title: "")
}
}
if let query = ModelA.query {
print(query)
}
Unfortunately you're not going to be able to do that with structs. You can, however, do something like this.
protocol Queryable {
static var queryableProperties: [String] { get }
}
extension Queryable {
static func makeQuery() -> String {
return "query {\n"
+ queryableProperties
.map { " \($0)" }
.joined(separator: "\n")
+ "\n}"
}
}
struct Dog {
let name: String
let age: Int
}
extension Dog: Queryable {
static var queryableProperties: [String] {
return ["name", "age"]
}
}
struct Person {
let firstName: String
let lastName: String
}
extension Person: Queryable {
static var queryableProperties: [String] {
return ["firstName", "lastName"]
}
}
print(Person.makeQuery())
print(Dog.makeQuery())
Which prints:
query {
firstName
lastName
}
query {
name
age
}
My code is like this:
enum API {
case login(phone:String, password:String, deviceID:String)
}
extension API:TargetType {
var task: Task {
switch self {
case let .login(phone, password, deviceID):
///How to get the parameter name here?
///For example:"phone", "password", "deviceID"
///Can this be generated automatically?
let parameters =
["phone":phone,
"password:":password,
"deviceID":deviceID]
return .requestParameters(parameters, encoding: JSONEncoding.default);
}
}
}
How to get the parameter name in Switch case?
For example:"phone", "password", "deviceID"
Can this be generated automatically?
How to avoid writing "phone" and the other dictionary keys literally, and make the compiler generate them from the associated value labels.
Maybe after the completion is like this
func parameters(_ api:API) -> [String, Any] {
}
switch self {
case .login:
return .requestParameters(parameters(self), encoding: JSONEncoding.default);
}
It seems that it is impossible to complete temporarily.
Who is the hero?
You can assign all associated values of the enum case to a single variable and then access the separate values using their labels.
enum API {
case login(phone:String, password:String, deviceID:String)
}
extension API:TargetType {
var task: Task {
switch self {
case let .token(params)
let parameters =
["phone":params.phone,
"password:":params.password,
"deviceID":params.deviceID]
return .requestParameters(parameters, encoding: JSONEncoding.default);
}
}
}
Btw shouldn't that .token be .login? There's no .token case in your API enum defined.
If you want to generate the Dictionary keys to match the String representation of the associated value labels, that cannot be done automatically, but as a workaround, you can define another enum with a String raw value and use that for the Dictionary keys.
enum API {
case login(phone:String, password:String, deviceID:String)
enum ParameterNames: String {
case phone, password, deviceID
}
}
extension API:TargetType {
var task: Task {
switch self {
case let .token(params)
let parameters =
["\(API.ParameterNames.phone)" : params.phone,
"\(API.ParameterNames.phone)" : params.password,
"\(API.ParameterNames.deviceID)" : params.deviceID]
return .requestParameters(parameters, encoding: JSONEncoding.default);
}
}
}
There is code where you could get the label plus all the value(s) of an enum.
public extension Enum {
public var associated: (label: String, value: Any?, values: Dictionary<String,Any>?) {
get {
let mirror = Mirror(reflecting: self)
if mirror.displayStyle == .enum {
if let associated = mirror.children.first {
let values = Mirror(reflecting: associated.value).children
var dict = Dictionary<String,Any>()
for i in values {
dict[i.label ?? ""] = i.value
}
return (associated.label!, associated.value, dict)
}
print("WARNING: Enum option of \(self) does not have an associated value")
return ("\(self)", nil, nil)
}
print("WARNING: You can only extend an enum with the EnumExtension")
return ("\(self)", nil, nil)
}
}
}
You will then be able to get the .associated.label and .associated.value of your enum. In your case your .value will be a tupple. Then you would need to use the .associated.values. Unfortunately you won't get the field names for these values. Because it's a tupple you will get field names like .0, .1 and .2. As far as I know there is no way to get the actual field names.
So in your case your code will be something like this:
enum API {
case login(phone:String, password:String, deviceID:String)
}
extension API:TargetType {
var task: Task {
return .requestParameters(self.associated.values, encoding: JSONEncoding.default);
}
}
But then you still need some functionality for going from the self.associated.values where the keys are .0, .1 and .2 to the names you like. I think the only option is for you to do this mapping yourself. You could extend your enum with a function for that.
If you want to see some more enum helpers, then have a look at Stuff/Enum
Your switch should look like this:
switch self {
case .login(let phone, let password, let deviceID)
let parameters =
["phone":phone,
"password:":password,
"deviceID":deviceID]
return .requestParameters(parameters, encoding: JSONEncoding.default);
}
Swift automatically generates the declared variables for you
You can take a look about Reflection in Swift
And you can make it automatic generate parameter like this:
class ParameterAble {
func getParameters() -> [String: Any] {
var param = [String: Any]()
let childen = Mirror(reflecting: self).children
for item in childen {
guard let key = item.label else {
continue
}
param[key] = item.value
}
return param
}
}
class LoginData: ParameterAble {
var phone: String
var password: String
var deviceID: String
init(phone: String, password: String, deviceID: String) {
self.phone = phone
self.password = password
self.deviceID = deviceID
}
}
enum API {
case login(data: LoginData)
}
extension API {
var task: [String: Any] {
switch self {
case let .login(data):
return data.getParameters()
}
}
}
let loginData = LoginData(phone: "fooPhone", password: "fooPass", deviceID:
"fooId")
let login = API.login(data: loginData)
print(login.task)
This is output: ["phone": "fooPhone", "deviceID": "fooId", "password": "fooPass"]
You can try it in Playground
I would like to post binary data through RxAlamofire, Alamofire or even without any library but after some days of research and tries, I'm not able to do it.
Here you can find the POSTMAN example of the request that I am trying to reproduce is:
Is a post method with the Authorization and Content-Type headers and the binary data attached.
I have tried to find some example or something related but I couldn't find a solution. I could just find multipart form data examples but with multipart form data the server doesn't work (is a external API)
If someone could guide me or show me some example code.
Here the code used for login as example and to show you something that I want to achieve:
public class APIClient: DataSource {
public static var shared: APIClient = APIClient()
private init(){}
public func login(email:String, password:String) -> Observable<LoginResponse> {
return RxAlamofire.requestJSON(APIRouter.login(email:email, password:password))
.subscribeOn(MainScheduler.asyncInstance)
.debug()
.mapObject(type: LoginResponse.self)
}
}
Here the LoginResponse object:
public struct LoginResponse: Mappable {
var tokenId: String?
var userId: String?
public init?(map: Map) {}
public mutating func mapping(map: Map) {
tokenId <- map["id"]
userId <- map["userId"]
}
}
And finally the APIRouter extending URLRequestConvertible:
enum APIRouter: URLRequestConvertible {
case login(email: String, password: String)
private var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
private var path: String {
switch self {
case .login:
return "users/login"
}
}
private var parameters: Parameters? {
switch self {
case .login(let email, let password):
return [APIConstants.LoginParameterKey.email: email, APIConstants.LoginParameterKey.password: password]
}
}
private var query: [URLQueryItem]? {
var queryItems = [URLQueryItem]()
switch self {
case .login:
return nil
}
}
func asURLRequest() throws -> URLRequest {
var urlComponents = URLComponents(string: APIConstants.ProductionServer.baseURL)!
if let query = query {
urlComponents.queryItems = query
}
var urlRequest = URLRequest(url: urlComponents.url!.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}
Thank you in advance!
EDIT To convert into RxAlamofire
With the code below I could solve the problem and convert it into RxSwift but I would like to use RxAlamofire to obtain the same result:
public func upload(media: Data) -> Observable<ContentUri> {
let headers = [
"content-type": "image/png",
"authorization": "token header"
]
return Observable<ContentUri>.create({observer in
Alamofire.upload(media, to: "\(endPoint)/api/media/upload", headers: headers)
.validate()
.responseJSON { response in
print(response)
}
return Disposables.create();
})
}
Alamofire.upload() (which returns an UploadRequest) might do what you want:
let headers = [
"Content-Type":"image/jpeg",
"Authorization":"sometoken",
]
let yourData = ... // Data of your image you want to upload
let endPoint = ...
Alamofire.upload(yourData, to: "\(endPoint)/api/media/upload", headers: headers)
.validate(statusCode: 200..<300)
.responseJSON { response in
// handle response
}
This example does not include RxAlamofire - but I am pretty sure it has a similar upload function. I hope it helps!
For my networking module, I have this protocol that I adopt for accessing different parts of the API:
protocol Router: URLRequestConvertible {
var baseUrl: URL { get }
var route: Route { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var encoding: ParameterEncoding? { get }
var responseResultType: Decodable.Type? { get }
}
I'm adopting this with enums that look like this:
enum TestRouter: Router {
case getTestData(byId: Int)
case updateTestData(byId: Int)
var route: Route {
switch self {
case .getTestData(let id): return Route(path: "/testData/\(id)")
case .updateTestData(let id): return Route(path: "/testDataOtherPath/\(id)")
}
}
var method: HTTPMethod {
switch self {
case .getTestData: return .get
case .updateTestData: return .put
}
}
var headers: [String : String]? {
return [:]
}
var encoding: ParameterEncoding? {
return URLEncoding.default
}
var responseResultType: Decodable.Type? {
switch self {
case .getTestData: return TestData.self
case .updateTestData: return ValidationResponse.self
}
}
}
I want to use Codable for decoding nested Api responses. Every response consists of a token and a result which content is depending on the request route.
For making the request I want to use the type specified in the responseResultType property in the enum above.
struct ApiResponse<Result: Decodable>: Decodable {
let token: String
let result: Result
}
extension Router {
func asURLRequest() throws -> URLRequest {
// Construct URL
var completeUrl = baseUrl.appendingPathComponent(route.path, isDirectory: false)
completeUrl = URL(string: completeUrl.absoluteString.removingPercentEncoding ?? "")!
// Create URL Request...
var urlRequest = URLRequest(url: completeUrl)
// ... with Method
urlRequest.httpMethod = method.rawValue
// Add headers
headers?.forEach { urlRequest.addValue($0.value, forHTTPHeaderField: $0.key) }
// Encode URL Request with the parameters
if encoding != nil {
return try encoding!.encode(urlRequest, with: route.parameters)
} else {
return urlRequest
}
}
func requestAndDecode(completion: #escaping (Result?) -> Void) {
NetworkAdapter.sessionManager.request(urlRequest).validate().responseData { response in
let responseObject = try? JSONDecoder().decode(ApiResponse<self.responseResultType!>, from: response.data!)
completion(responseObject.result)
}
}
}
But in my requestAndDecode method It throws an compiler error (Cannot invoke 'decode' with an argument list of type '(Any.Type, from: Data)'). I can't use ApiResponse<self.responseResultType!> like that.
I could make this function generic and call it like this:
TestRouter.getTestData(byId: 123).requestAndDecode(TestData.self, completion:)
but then I'd have to pass the response type everytime I want to use this endpoint.
What I want to achieve is that the extension function requestAndDecode takes it response type information from itself, the responseResultType property.
Is this possible?
Ignoring the actual error report you have a fundamental problem with requestAndDecode: it is a generic function whose type parameters are determined at the call site which is declared to return a value of type Result yet it attempts to return a value of type self.responseResultType whose value is an unknown type.
If Swift's type system supported this it would require runtime type checking, potential failure, and your code would have to handle that. E.g. you could pass TestData to requestAndDecode while responseResultType might be ValidationResponse...
Change the JSON call to:
JSONDecoder().decode(ApiResponse<Result>.self ...
and the types statically match (even though the actual type that Result is is unknown).
You need to rethink your design. HTH
Create a Generic function with Combine and AlomFire. You can use it for all method(get, post, put, delete)
func fatchData<T: Codable>(requestType: String, url: String, params: [String : Any]?, myType: T.Type, completion: #escaping (Result<T, Error>) -> Void) {
var method = HTTPMethod.get
switch requestType {
case "Get":
method = HTTPMethod.get
case "Post":
method = HTTPMethod.post
print("requestType \(requestType) \(method) ")
case "Put":
method = HTTPMethod.put
default:
method = HTTPMethod.delete
}
print("url \(url) \(method) \(AppConstant.headers) ")
task = AF.request(url, method: method, parameters: params, encoding: JSONEncoding.default, headers: AppConstant.headers)
.publishDecodable(type: myType.self)
.sink(receiveCompletion: { (completion) in
switch completion{
case .finished:
()
case .failure(let error):
// completion(.failure(error))
print("error \(error)")
}
}, receiveValue: {
[weak self ](response) in
print("response \(response)")
switch response.result{
case .success(let model):
completion(.success(model))
print("error success")
case .failure(let error):
completion(.failure(error))
print("error failure \(error.localizedDescription)")
}
}
)
}