Is there a better way to build many different URLs? - swift

I am working on building a framework to connect to a specific API and will need to build a lot of different paths. My current setup is using an Enum for returning a URL, which works pretty well for the most part. My only problem I have with this approach is there will be a lot of different cases (~30 total) by the time I'm done. I was wondering if anyone has a better solution?
enum API {
var baseURL: URL {
return URL(string: "https://api.example.com")!
}
case user
case emails
case posts
case post(id: String)
// etc . . .
}
extension API: Path {
func appendPathComponent(_ string: String) -> URL {
return baseURL.appendingPathComponent(string)
}
var url: URL {
switch self {
case .user: return baseURL
case .emails: return appendPathComponent("email")
case .posts: return appendPathComponent("posts")
case .post(let id): return appendPathComponent(id)
// etc
}
}
}
// call site
let url = API.emails.url

I would turn this around the same way that Notification.Name does. Make these extensions on URL:
extension URL {
static let apiBase = URL(string: "https://api.example.com")!
static let apiUser = apiBase
static let apiEmails = apiBase.appendingPathComponent("email")
static let apiPosts = apiBase.appendingPathComponent("posts")
static func apiPost(id: String) -> URL { return apiBase.appendingPathComponent(id) }
}
Then calling it is just:
let x = URL.apiEmails
And in cases where URL is known, you don't even have to include that:
let task = URLSession.shared.dataTask(with: .apiEmails)
or
let task = URLSession.shared.dataTask(with: .apiPost(id: "123"))

Related

Dependency Injection in Protocol/Extension

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.

By Moya in Swift,how can I send a request with absolute url when already having baseURL?

Moya version: master integrated by GitHub
part of code sample:
enum Api {
case initialize(deviceName: String, upushDeviceToken: String)
case loadImage(_ url: String)
}
extension Api: TargetType {
var baseURL: URL {
URL(string: ApiKit.baseUrl)!
}
var path: String {
switch self {
case .initialize:
return "init"
case .loadImage(let url):
return "\(url)"
}
}
the loadImage request url is an absolute url like https://some-site/storage/image.png
When I tried loadImage, it combined basic url and the url, therefore server side return error 404
I guess it might have a solution of creating another TargetType to handle these absolute url requests, but I still want to know if there's a way to solve this in one TargetType.
Hope this helps.
enum Api {
case initialize(deviceName: String, upushDeviceToken: String)
case loadImage(_ url: String)
}
extension Api: TargetType {
var baseURL: URL {
URL(string: "https://some-website.com")!
}
var path: String {
switch self {
case .initialize:
return "init"
case .loadImage(let url):
// url should have /storage/image.png
return "\(url)"
}
}
}
BaseURL + path is the final URL = https://some-website.com/storage/image.png

Enum int with string value in swift

I have enum of API endpoints which i use during the base url creation.
one of the API which has pagination is like pagination = "/api/pagination_test/%#?"
Now during pagination i want to pass value to enum initalizer and create an enum which will be accepted by the base-url creation function.
enum APIEndPoints{
case registerUser = "/register"
case login = "/login"
case getData = "/data?"
case pagination = "/api/pagination_test/%#?"
}
func callPaginationAPI(pagenumber: Int){
//make enum with pagenumber, i am stuck in this line.
//let enum =
//call main service method,pass enum as argument.
mainService(endPoint: .pagination)
// this expect an enum of pagination with proper pagenumber
}
func mainService(endpoint: APIEndpoints){
//create base url here
let url = requestUrl()
//do nsurlsession with prepared url
}
func requestUrl( endPoint: APIEndPoints) -> String {
let baseURL = self.requestBaseUrl()
return baseURL + endPoint.rawValue
}
How can i create a pagination enum with value one - am expecting enum as /api/pagination_test/1? , /api/pagination_test/2?
First of all the enum should be update so we can use String(format:)
enum APIEndPoints: String {
case registerUser = "/register"
case login = "/login"
case getData = "/data?"
case pagination = "/api/pagination_test/%d?"
}
To avoid having to pass the page number as a parameter through all methods it is probably better to wrap the enum inside a struct together with the (optional) page number and have a computed property that gets the endpoint as a string
struct EndPoint {
let apiEndPoint: APIEndPoints
let page: Int?
var endPointValue: String {
switch apiEndPoint {
case .pagination:
return String(format: apiEndPoint.rawValue, page ?? 1)
default:
return apiEndPoint.rawValue
}
}
init(_ endPoint: APIEndPoints, page: Int? = nil) {
apiEndPoint = endPoint
self.page = page
}
}
And then pass an instance of this struct instead
func callPaginationAPI(pagenumber: Int){
mainService(endpoint: EndPoint(.pagination, page: pagenumber))
}
And use the computed property when creating the url
func requestUrl(endPoint: EndPoint) -> String {
let baseURL = self.requestBaseUrl()
return baseURL + endPoint.endPointValue
}
And an example to use the struct without a page number
func callLoginAPI() {
mainService(endpoint: EndPoint(.login))
}

Is it possible to have lazy behaviour in enum's function?

Swift provides a very handy lazy var
However, I was wondering, can we achieve similar lazy functionality for Enum's function?
For instance,
class Utils {
static let userDataDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
}
enum UserDataDirectory : String {
case Extract = "extract"
case Camera = "camera"
case Mic = "mic"
case Attachment = "attachment"
case Recording = "recording"
case RestoreAttachment = "restore_attachment"
case RestoreRecording = "restore_recording"
func get() -> URL {
return Utils.userDataDirectory.appendingPathComponent(self.rawValue)
}
}
Is it ever possible, to turn enum UserDataDirectory's get function to have lazy evaluation behaviour.
Or, is there a way to avoid evaluation of appendingPathComponent every-time, since Utils.userDataDirectory and self.rawValue is constant?
You just mean that you want a lazily-evaluated static value. That's straightforward; add a static cache:
enum UserDataDirectory : String {
// ...
// Define storage
private static var urls: [Self: URL] = [:]
func url() -> URL {
// Check the cache
if let url = Self.urls[self] {
return url
} else {
// Compute and cache
let url = Utils.userDataDirectory.appendingPathComponent(self.rawValue)
Self.urls[self] = url
return url
}
}
}

Singleton pattern and proper use of Alamofire's URLRequestConvertible

This is a 2 part question the first is similar to this question here: Proper usage of the Alamofire's URLRequestConvertible. But I need a little more help!
1) Do I create an enum router which implements URLRequestConvertible for each model in my model layer?
The alamofire github page provides an example of a router which i've copied here:
enum Router: URLRequestConvertible {
static let baseURLString = "http://example.com"
static var OAuthToken: String?
case CreateUser([String: AnyObject])
case ReadUser(String)
case UpdateUser(String, [String: AnyObject])
case DestroyUser(String)
var method: Alamofire.Method {
switch self {
case .CreateUser:
return .POST
case .ReadUser:
return .GET
case .UpdateUser:
return .PUT
case .DestroyUser:
return .DELETE
}
}
var path: String {
switch self {
case .CreateUser:
return "/users"
case .ReadUser(let username):
return "/users/\(username)"
case .UpdateUser(let username, _):
return "/users/\(username)"
case .DestroyUser(let username):
return "/users/\(username)"
}
}
// MARK: URLRequestConvertible
var URLRequest: NSURLRequest {
let URL = NSURL(string: Router.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = Router.OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateUser(let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateUser(_, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
When I look at this (i'm new at swift so please bear with me >_<) I see operations on a user object; they are creating a user, updating a user etc... So, if I had model objects person, company, location in my model layer, would I create a router for each model object?
2) When interacting heavily with an API, I'm used to creating a "network manager" singleton to abstract away the network layer and to hold headers and the baseurl for that API. The alamofire has a "Manager" described here:
Top-level convenience methods like Alamofire.request use a shared instance of Alamofire.Manager, which is configured with the default NSURLSessionConfiguration. As such, the following two statements are equivalent:
Alamofire.request(.GET, "http://httpbin.org/get")
let manager = Alamofire.Manager.sharedInstance
manager.request(NSURLRequest(URL: NSURL(string: "http://httpbin.org/get")))
is this manager what I should be using as my singleton? If so, how do I set the baseurl on the manager? Also, if I use this manager does / can this work together with the router construct shown above (with each model object setting it's baseurl and NSURLRquest)? If so can you provide a simple example?
I'm new to the Alamofire library and swift. So, I know there are a lot of holes in my understanding but I'm just trying to understand the best that I can! Any info helps. Thanks.
These are some really good questions. Let me attempt to answer each one in turn.
Do I create an enum router which implements URLRequestConvertible for each model in my model layer?
This is a great question and unfortunately there's no one perfect answer. There are certainly some ways that you could extend the Router pattern to accommodate multiple object types. The first option would be to add more cases to support another object type. However, this gets hairy pretty quickly when you get more than 6 or 7 cases. Your switch statements just start to get out-of-control. Therefore, I wouldn't recommend this approach.
Another way to approach the problem is by introducing generics into the Router.
RouterObject Protocol
protocol RouterObject {
func createObjectPath() -> String
func readObjectPath(identifier: String) -> String
func updateObjectPath(identifier: String) -> String
func destroyObjectPath(identifier: String) -> String
}
Model Objects
struct User: RouterObject {
let rootPath = "/users"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Company: RouterObject {
let rootPath = "/companies"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
struct Location: RouterObject {
let rootPath = "/locations"
func createObjectPath() -> String { return rootPath }
func readObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func updateObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
func destroyObjectPath(identifier: String) -> String { return "\(rootPath)/\(identifier)" }
}
Router
let baseURLString = "http://example.com"
var OAuthToken: String?
enum Router<T where T: RouterObject>: URLRequestConvertible {
case CreateObject(T, [String: AnyObject])
case ReadObject(T, String)
case UpdateObject(T, String, [String: AnyObject])
case DestroyObject(T, String)
var method: Alamofire.Method {
switch self {
case .CreateObject:
return .POST
case .ReadObject:
return .GET
case .UpdateObject:
return .PUT
case .DestroyObject:
return .DELETE
}
}
var path: String {
switch self {
case .CreateObject(let object, _):
return object.createObjectPath()
case .ReadObject(let object, let identifier):
return object.readObjectPath(identifier)
case .UpdateObject(let object, let identifier, _):
return object.updateObjectPath(identifier)
case .DestroyObject(let object, let identifier):
return object.destroyObjectPath(identifier)
}
}
// MARK: URLRequestConvertible
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
}
Example Usage
func exampleUsage() {
let URLRequest = Router.CreateObject(Location(), ["address": "1234 Road of Awesomeness"]).URLRequest
Alamofire.request(URLRequest)
.response { request, response, data, error in
print(request)
print(response)
print(data)
print(error)
}
}
Now there are certainly a few tradeoffs that you have to make here. First off, your model objects need to conform to the RouterObject protocol. Otherwise the Router has no idea what to use for the path. Also, you'll need to make sure all your paths can be constructed with the a single identifier. If they cannot, this design might not work. The last issue is that you cannot store the baseURL or the OAuthToken directly inside the Router enum. Unfortunately, static and stored properties are not yet supported in generic enumerations.
Regardless, this would certainly be a valid way to avoid having to create a Router for every model object.
Should the Alamofire.Manager.sharedInstance be used as my singleton NetworkManager instance?
It certainly could be used in that fashion. It really depends upon your use case and how you have designed your network access. It also depends on how many different types of sessions you need. If you need background sessions and default sessions, then you probably still need the concept of a NetworkManager that contains each custom Manager instance. However, if you are just hitting the network with a default session, then the sharedInstance would probably be sufficient.
How could the baseURL of the Alamofire singleton be used in conjunction with the Router pattern?
Good question...the code below is one example of how it could be done.
Alamofire Manager extension
extension Manager {
static let baseURLString = "http://example.com"
static var OAuthToken: String?
}
Router URLRequestConvertible Updates
var URLRequest: NSMutableURLRequest {
let URL = NSURL(string: Alamofire.Manager.baseURLString)!
let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
mutableURLRequest.HTTPMethod = method.rawValue
if let token = Alamofire.Manager.OAuthToken {
mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
switch self {
case .CreateObject(_, let parameters):
return Alamofire.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
case .UpdateObject(_, _, let parameters):
return Alamofire.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
default:
return mutableURLRequest
}
}
Hopefully that helps shed some light. Best of luck!