-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
36 changed files
with
749 additions
and
125 deletions.
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
//: [Previous](@previous) | ||
|
||
import Foundation | ||
|
||
import APIWrapper | ||
|
||
/*: | ||
The `RaAPIWrapper` is extremely extensible. | ||
You can work with the `userInfo` property to customize the api parameters you need. | ||
The `RaAPIWrapper/AF` module then takes advantage of this feature and supports the `ParameterEncoding` field of `Alamofire`. | ||
The following code demonstrates how to add a custom parameter to `API`: | ||
*/ | ||
|
||
// will be used later as a custom parameter of the api. | ||
enum VerificationType: Hashable { | ||
case normal | ||
case special | ||
} | ||
|
||
extension API { | ||
/// You can extend the `API` structure to add your custom parameters to the property wrapper | ||
/// by adding custom initialization methods, while keeping the types as you wish. | ||
/// | ||
/// **Note**: The first parameter `wrappedValue` cannot be omitted! | ||
convenience init( | ||
wrappedValue: ParameterBuilder? = nil, | ||
_ path: String, | ||
verification: VerificationType? = nil | ||
) { | ||
self.init(wrappedValue: wrappedValue, path, userInfo: ["verification": verification]) | ||
} | ||
} | ||
|
||
enum AdvancedAPI { | ||
/// Finally, the new initialization method declared above is called on | ||
/// the property wrapper to complete the interface definition. | ||
|
||
@GET("/api", verification: .normal) | ||
static var testAPI: APIParameterBuilder<()>? = nil | ||
} | ||
|
||
|
||
//: [Next](@next) |
82 changes: 82 additions & 0 deletions
82
Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import Foundation | ||
|
||
import APIWrapper | ||
|
||
/*: | ||
This example uses [Postman Echo](https://www.postman.com/postman/workspace/published-postman-templates/documentation/631643-f695cab7-6878-eb55-7943-ad88e1ccfd65?ctx=documentation) as the sample api. | ||
The return value of this api depends on the parameters and will return the parameters, headers and other data as is. | ||
*/ | ||
|
||
//: To begin by showing some of the most basic uses, look at how the api is defined. | ||
|
||
enum BasicAPI { | ||
/// This is an api for requests using the **GET** method. | ||
/// | ||
/// The full api address is: [](https://postman-echo.com/get?foo1=bar1&foo2=bar2) . | ||
/// The api does not require the caller to pass in any parameters. | ||
@GET("/get?foo1=bar1&foo2=bar2") | ||
static var get: APIParameterBuilder<()>? = nil | ||
} | ||
|
||
//: After defining the api, try to execute the request: | ||
|
||
do { | ||
// Requests the api and parses the return value of the interface. Note the use of the `$` character. | ||
let response = try await BasicAPI.$get.request(to: PostManResponse<Arg>.self) | ||
|
||
// You can also ignore the return value and focus only on the act of requesting the api itself. | ||
try await BasicAPI.$get.request() | ||
|
||
} catch { | ||
print("❌ get request failure: \(error)") | ||
} | ||
|
||
//: The api with parameters is a little more complicated to define: | ||
|
||
extension BasicAPI { | ||
/// This is an api for requests using the **POST** method. | ||
/// | ||
/// The full api address is: [](https://postman-echo.com/post) . | ||
/// The api is entered as a **tuple** type and requires two parameters, where the second parameter can be `nil`. | ||
@POST("/post") | ||
static var postWithTuple: APIParameterBuilder<(foo1: String, foo2: Int?)>? = { | ||
[ | ||
"foo1": $0.foo1, | ||
"foo2": $0.foo2, | ||
] | ||
|
||
// Eliminate the warning by explicitly converting to `[String: Any?]`. | ||
// Also ensure that `nil` parameters can be filtered. | ||
as [String: Any?] | ||
} | ||
|
||
/// This is an api for requests using the **POST** method. | ||
/// | ||
/// The full api address is: [](https://postman-echo.com/post) . | ||
/// This api is referenced with the `Arg` type. | ||
@POST("/post") | ||
static var postWithModel: APIParameterBuilder<Arg>? = { | ||
// You can have your model follow the `APIParameterConvertible` protocol. | ||
// or use `AnyAPIHashableParameter` to wrap your model in an outer layer. | ||
AnyAPIHashableParameter($0) | ||
} | ||
} | ||
|
||
do { | ||
// Request the api and parse the return value. | ||
let tupleAPIResponse = try await BasicAPI.$postWithTuple.request(with: (foo1: "foo1", foo2: nil), to: PostManResponse<Arg>.self) | ||
|
||
/** | ||
* If you look at the return value, you will see that `foo2` is not passed to the server. | ||
* This is because `RaAPIWrapper` filters out all parameters with the value `nil`. | ||
*/ | ||
|
||
// Try using model as a parameter and you will get the same result. | ||
let modelAPIResponse = try await BasicAPI.$postWithModel.request(with: .init(foo2: "foo2"), to: PostManResponse<Arg>.self) | ||
|
||
} catch { | ||
print("❌ post request failure: \(error)") | ||
} | ||
|
||
//: [Next](@next) |
110 changes: 110 additions & 0 deletions
110
Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
//: [Previous](@previous) | ||
|
||
import Foundation | ||
|
||
import Combine | ||
import ObjectiveC | ||
|
||
import APIWrapper | ||
|
||
/*: | ||
The design goal of `RaAPIWrapper` is to better encapsulate requests and simplify the request process rather than execute them. | ||
So we don't provide any methods for request api. You can define your own request methods by referring to the code in the `Demo/Sources/API+Request.swift` file. | ||
Here are 2 request wrappers for `Combine`, which are roughly written for reference only: | ||
*/ | ||
|
||
// For subsequent examples | ||
enum CombineAPI { | ||
@POST("/post") | ||
static var post: APIParameterBuilder<String>? = { $0 } | ||
} | ||
|
||
// MARK: - AnyPublisher | ||
|
||
//: The first way: deliver an `AnyPublisher<T, Error>` object externally and subscribe to it to trigger requests. | ||
|
||
extension API { | ||
func requestPublisher(with params: Parameter) -> AnyPublisher<Data, URLError> { | ||
let info = createRequestInfo(params) | ||
|
||
// To simplify the demo process, here is a forced unpacking | ||
let url = URL(string: "https://postman-echo.com" + info.path)! | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = info.httpMethod.rawValue | ||
|
||
if let parameters = info.parameters { | ||
do { | ||
request.httpBody = try JSONEncoder().encode(parameters) | ||
} catch { | ||
fatalError("") | ||
} | ||
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
} | ||
|
||
return URLSession.shared | ||
.dataTaskPublisher(for: request) | ||
.map { (data, _) in return data } | ||
.mapError { $0 } | ||
.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
var cancellable = Set<AnyCancellable>() | ||
let publisher = CombineAPI.$post.requestPublisher(with: "123") | ||
publisher.sink(receiveCompletion: { | ||
print($0) | ||
|
||
}, receiveValue: { | ||
print(String(data: $0, encoding: .utf8) as Any) | ||
|
||
}).store(in: &cancellable) | ||
|
||
// MARK: - PassthroughSubject | ||
|
||
/*: | ||
The second one is to provide a `PassthroughSubject` object to the outside world, | ||
send parameters when requesting the api, subscribe to the object at other places, | ||
accept the parameters and send the request. | ||
*/ | ||
|
||
private var kParamSubjectKey: String = "kParamSubjectKey" | ||
|
||
public extension API { | ||
@available(iOS 13.0, *) | ||
var paramSubject: PassthroughSubject<Parameter, URLError>? { | ||
get { | ||
if let value = objc_getAssociatedObject(self, &kParamSubjectKey) as? PassthroughSubject<Parameter, URLError> { | ||
return value | ||
} | ||
let paramSubject = PassthroughSubject<Parameter, URLError>() | ||
objc_setAssociatedObject(self, &kParamSubjectKey, paramSubject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | ||
return paramSubject | ||
} | ||
set { objc_setAssociatedObject(self, &kParamSubjectKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } | ||
} | ||
|
||
@available(iOS 13.0, *) | ||
func requestPublisher() -> AnyPublisher<Data, URLError>? { | ||
return paramSubject?.flatMap { self.requestPublisher(with: $0) }.eraseToAnyPublisher() | ||
} | ||
} | ||
|
||
let api = CombineAPI.$post | ||
|
||
api.requestPublisher()?.sink(receiveCompletion: { | ||
print($0) | ||
|
||
}, receiveValue: { | ||
print(String(data: $0, encoding: .utf8) as Any) | ||
|
||
}).store(in: &cancellable) | ||
|
||
api.paramSubject?.send("233") | ||
api.paramSubject?.send("433") | ||
api.paramSubject?.send(completion: .finished) | ||
|
||
//: [Next](@next) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import Foundation | ||
|
||
import APIWrapper | ||
|
||
/// Before formally defining the api, we need to encapsulate a method for requesting the api. | ||
/// | ||
/// The role of `RaAPIWrapper` is to encapsulate the parameters needed to request the api, | ||
/// so we don't add any logic for requesting the api. | ||
/// | ||
/// This part of the logic needs to be implemented by you in your own project, | ||
/// for now we provide a simple implementation: | ||
|
||
public extension API { | ||
/// Request an api **with parameters** and resolve the api return value to a `T` type. | ||
/// | ||
/// - Parameters: | ||
/// - params: api parameters. | ||
/// - type: the type of the api return value. | ||
/// - Returns: The result of the parsing. | ||
func request<T: Decodable>(with params: Parameter, to type: T.Type) async throws -> T { | ||
let data = try await _request(with: params) | ||
return try JSONDecoder().decode(type, from: data) | ||
} | ||
|
||
/// Request an api **without** parameters. | ||
/// | ||
/// This method means: the requesting party does not need the parameters returned by the api, | ||
/// so no return value is provided. | ||
/// | ||
/// - Parameter params: api parameters. | ||
func request(with params: Parameter) async throws { | ||
_ = try await _request(with: params) | ||
} | ||
} | ||
|
||
/// For some api that do not require parameters, | ||
/// we can also provide the following methods to make the request process even simpler. | ||
|
||
public extension API where Parameter == Void { | ||
/// Request an api **without** parameters and resolve the api return value to a `T` type. | ||
/// | ||
/// - Parameter type: The type of the api's return value. | ||
/// - Returns: The result of the parsing. | ||
func request<T: Decodable>(to type: T.Type) async throws -> T { | ||
return try await request(with: (), to: type) | ||
} | ||
|
||
/// Request an api **without** parameters. | ||
/// | ||
/// This method means: the requesting party does not need the parameters returned by the api, | ||
/// so no return value is provided. | ||
func request() async throws { | ||
try await request(with: ()) | ||
} | ||
} | ||
|
||
// MARK: - Tools | ||
|
||
private extension API { | ||
func _request(with params: Parameter) async throws -> Data { | ||
let info = createRequestInfo(params) | ||
|
||
// To simplify the demo process, here is a forced unpacking | ||
let url = URL(string: "https://postman-echo.com" + info.path)! | ||
print("▶️ Requests will begin soon: \(url.absoluteString)") | ||
|
||
var request = URLRequest(url: url) | ||
request.httpMethod = info.httpMethod.rawValue | ||
|
||
if let parameters = info.parameters { | ||
print("🚧 parameters: \(parameters)") | ||
request.httpBody = try JSONEncoder().encode(parameters) | ||
|
||
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
} | ||
|
||
let (data, response) = try await URLSession.shared.data(for: request) | ||
print("✅ \(String(describing: response.url?.absoluteString)) End of request") | ||
|
||
return data | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import Foundation | ||
|
||
public typealias DemoResponse = Codable & Hashable | ||
|
||
public struct PostManResponse<T: DemoResponse>: DemoResponse { | ||
public let args: T? | ||
|
||
public let data: T? | ||
|
||
public let url: String | ||
|
||
public let headers: [String: String] | ||
} | ||
|
||
public struct Arg: DemoResponse { | ||
let foo1: String? | ||
|
||
let foo2: String? | ||
|
||
private enum CodingKeys: String, CodingKey { | ||
case foo1 = "foo1" | ||
case foo2 = "foo2" | ||
} | ||
|
||
public init(foo1: String? = nil, foo2: String? = nil) { | ||
self.foo1 = foo1 | ||
self.foo2 = foo2 | ||
} | ||
|
||
public init(from decoder: Decoder) throws { | ||
let c = try decoder.container(keyedBy: CodingKeys.self) | ||
|
||
self.foo1 = try c.decodeIfPresent(String.self, forKey: .foo1) | ||
self.foo2 = try c.decodeIfPresent(String.self, forKey: .foo2) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | ||
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true' importAppTypes='true'> | ||
<pages> | ||
<page name='Basic'/> | ||
<page name='Advanced'/> | ||
<page name='Combine'/> | ||
</pages> | ||
</playground> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.