A simple tool to organize yours rest apis connection and use then.
This code is implemented to make use of either async/await or combine framework.
This framework will be usable by iOS 13 and above or MacOS 10.15 and above.
For versions bellow iOS 15 or MacOS 12 it will only be possible to use the combine methods.
LNSimpleNetwork is available via SwiftPackage.
Add the following to you Package.swift file's dependencies:
.package(url: "https://github.com/sciasxp/LNSimpleNetwork.git", from: "0.1.0"),
LNSimpleNetwork consists of two protocols that you should implement.
This one will be responsible for the network part connecting to your API.
struct Client: LNAPIClient {}
You can customize this struct in two ways:
struct Client: LNAPIClient {
private(set) var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
}
You can implement more de one client for different decodign needs.
struct Client: LNAPIClient {
private(set) var session: URLSession {
return URLSession(configuration: .default, delegate: nil, delegateQueue: nil)
}
}
This one is responsible for organize the route api paths and parameters.
enum Endpoint: LNEndpoint {
case test
var baseURL: String { "https://dummyjson.com" }
var path: String { "/products/1" }
var httpMethod: LNHTTPMethod { .get }
var urlParameters: [URLQueryItem] { [] }
var body: Data? { nil }
var token: String? { nil }
}
For each route you have you should implement different case for the enum.
For the above exemple we have only one route, case test, and it's configurations are described by de following computed variables:
baseURL: String This one will return the domain for your API. Usually it will have only one value but you can use diffent domains depending on your enviroment.
var baseURL: String {
if GlobalConstants.isDebug {
return "https://qa.dummyjson.com"
} else {
return "https://dummyjson.com"
}
}
path: String This one will discribe the path for each of the cases you defined. In our exemple we have only um route in this API so you can return only one value but if you have two or more case you can use a switch-case to respond to the path.
var path: String {
switch self {
case .products: return "/products"
case .charts: return "/charts"
}
}
Now, what if you have different paths for your products? /products/1 /products/2
Those are neither parameters nor body of you request so it should be encoded in your url. For this you should pass the variable in yours case statement and the use it in the switch-case as shown below:
enum Endpoint: LNEndpoint {
case product(code: Int)
var baseURL: String { "https://dummyjson.com" }
var path: String {
switch self {
case .product(let code): return "/products/\(code)"
}
}
var httpMethod: LNHTTPMethod { .get }
var urlParameters: [URLQueryItem] { [] }
var body: Data? { nil }
var token: String? { nil }
}
The same logic applies to the other properties.
httpMethod: LNHTTPMethod This will be responsible to inform which method your request should be using. Those methods are defined in the LNHTTPMethod enum:
public enum LNHTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
urlParameters: [URLQueryItem] If your route have to pass some parameters, normally on a get request, you will need return those parameters in the forma of an array of URLQueryItem.
var urlParameters: [URLQueryItem] {
switch self {
case .info(let version):
return [
URLQueryItem(name: "version", value: version)
]
default:
return []
}
}
In the above exemple only info route have to pass a parameter. The others routes don't have a need to do so, for these routes we pass an empty array at the switch default value.
body: Data? Here you will pass any configurations encoded to the resquest's body. This one should retourn an JSON enconded body.
var body: Data? = {
switch self {
case .login(let username, let password):
let bodyDictionary: [String: Any?] = [
"username": username,
"password": password
]
guard JSONSerialization.isValidJSONObject(bodyDictionary) else { return nil }
do {
let postData = try JSONSerialization.data(withJSONObject: bodyDictionary, options: [])
return postData
} catch(let error) {
debugPrint(error.localizedDescription)
return nil
}
default: return nil
}
}
In this case you can simplify the json conversion with the following snipt:
extension Dictionary {
/**
Returns an JSON object.
- returns: If the dictionary conforms to a JSON returns a JSON object.
In case the dictionary iso not a vlid JSON representation, returns nil.
*/
func json() -> Data? {
guard JSONSerialization.isValidJSONObject(self) else { return nil }
do {
let postData = try JSONSerialization.data(withJSONObject: self, options: [])
return postData
} catch(let error) {
debugPrint(error.localizedDescription)
return nil
}
}
}
With this, our previous exemple should become more simpler:
var body: Data? = {
switch self {
case .login(let username, let password):
let bodyDictionary: [String: Any?] = [
"username": username,
"password": password
]
return bodyDictionary.json()
default: return nil
}
}
token: String? This will only be used in the case of athenticated route. For security reasons I recomend to pass the token as a property of its route enum and neve hard code it.
enum Endpoint: LNEndpoint {
case product(token: String)
var baseURL: String { "https://dummyjson.com" }
var path: String { "/products/1" }
var httpMethod: LNHTTPMethod { .get }
var urlParameters: [URLQueryItem] { [] }
var body: Data? { nil }
var token: String? {
switch self {
case .product(let token): return token
}
}
}
struct Client: LNAPIClient {
private (set) var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()
}
enum Endpoint: LNEndpoint {
case test
case testAuthed(token: String)
case login(userName: String, password: String)
var baseURL: String { "https://dummyjson.com" }
var path: String {
switch self {
case .test: return "/products/1"
case .testAuthed(_): return "/products/1"
case .login(_, _): return "/auth/login"
}
}
var httpMethod: LNHTTPMethod {
switch self {
case .test: return .get
case .testAuthed(_): return .get
case .login(_, _): return .post
}
}
var urlParameters: [URLQueryItem] { [] }
var body: Data? {
switch self {
case .test: return nil
case .testAuthed(_): return nil
case .login(let userName, let password):
let parameters: [String: Any?] = [
"username" : userName,
"password" : password
]
guard JSONSerialization.isValidJSONObject(parameters) else { return nil }
do {
let postData = try JSONSerialization.data(withJSONObject: parameters, options: [])
return postData
} catch(let error) {
debugPrint(error.localizedDescription)
return nil
}
}
}
var token: String? {
switch self {
case .testAuthed(let token): return token
default: return nil
}
}
}
Now that we have ours client and endpoint implemented the usage become fairly simple.
There is two ways to use it:
The Combine Way
struct User: Decodable {
let id: Int
let token: String
}
var cancellables: Set<AnyCancellable> = []
var user: User?
func test() {
let endpoint = Endpoint.login(userName: "teste", password: "teste")
let client = Client()
client.connection(with: endpoint)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let returnedError):
error = returnedError
}
} receiveValue: { (value: User) in
user = value
}
.store(in: &cancellables)
}
The Async/Await Way
struct User: Decodable {
let id: Int
let token: String
}
func test() async throws {
let loginEndpoint = Endpoint.login(userName: "teste", password: "teste")
let user: User = try await Client().connect(with: loginEndpoint)
}
Included extension to Dictionary to better handle JSON transformations.
This extensions can be used with body from your requests.
let body: [String: Any] = [
"src": src,
"page": page,
"status": "ALL",
"sortby": "latest"
]
let data = body.json()
It's also possible to get a json string version from you dictionary.
let body: [String: Any] = [
"src": src,
"page": page,
"status": "ALL",
"sortby": "latest"
]
let string = body.jsonString()
You are most welcome in contributing to this project with either new features (maybe one mentioned from the future work or anything else), refactoring and improving the code. Also, feel free to give suggestions and feedbacks.
Created with ❤️ by Luciano Nunes.
Get in touch on [Email](https://raw.github.com/sciasxp/LNSimpleNetwork/main/mailto: [email protected]) Visit: LinkdIn
link |
Stars: 0 |
Last commit: 1 year ago |
First public release of this project
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics