Skip to content

Commit 6e29cbc

Browse files
committed
Merge branch release/1.0.1
2 parents 04eb3d5 + c029663 commit 6e29cbc

36 files changed

+749
-125
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//: [Previous](@previous)
2+
3+
import Foundation
4+
5+
import APIWrapper
6+
7+
/*:
8+
The `RaAPIWrapper` is extremely extensible.
9+
You can work with the `userInfo` property to customize the api parameters you need.
10+
11+
The `RaAPIWrapper/AF` module then takes advantage of this feature and supports the `ParameterEncoding` field of `Alamofire`.
12+
13+
The following code demonstrates how to add a custom parameter to `API`:
14+
*/
15+
16+
// will be used later as a custom parameter of the api.
17+
enum VerificationType: Hashable {
18+
case normal
19+
case special
20+
}
21+
22+
extension API {
23+
/// You can extend the `API` structure to add your custom parameters to the property wrapper
24+
/// by adding custom initialization methods, while keeping the types as you wish.
25+
///
26+
/// **Note**: The first parameter `wrappedValue` cannot be omitted!
27+
convenience init(
28+
wrappedValue: ParameterBuilder? = nil,
29+
_ path: String,
30+
verification: VerificationType? = nil
31+
) {
32+
self.init(wrappedValue: wrappedValue, path, userInfo: ["verification": verification])
33+
}
34+
}
35+
36+
enum AdvancedAPI {
37+
/// Finally, the new initialization method declared above is called on
38+
/// the property wrapper to complete the interface definition.
39+
40+
@GET("/api", verification: .normal)
41+
static var testAPI: APIParameterBuilder<()>? = nil
42+
}
43+
44+
45+
//: [Next](@next)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Foundation
2+
3+
import APIWrapper
4+
5+
/*:
6+
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.
7+
8+
The return value of this api depends on the parameters and will return the parameters, headers and other data as is.
9+
*/
10+
11+
//: To begin by showing some of the most basic uses, look at how the api is defined.
12+
13+
enum BasicAPI {
14+
/// This is an api for requests using the **GET** method.
15+
///
16+
/// The full api address is: [](https://postman-echo.com/get?foo1=bar1&foo2=bar2) .
17+
/// The api does not require the caller to pass in any parameters.
18+
@GET("/get?foo1=bar1&foo2=bar2")
19+
static var get: APIParameterBuilder<()>? = nil
20+
}
21+
22+
//: After defining the api, try to execute the request:
23+
24+
do {
25+
// Requests the api and parses the return value of the interface. Note the use of the `$` character.
26+
let response = try await BasicAPI.$get.request(to: PostManResponse<Arg>.self)
27+
28+
// You can also ignore the return value and focus only on the act of requesting the api itself.
29+
try await BasicAPI.$get.request()
30+
31+
} catch {
32+
print("❌ get request failure: \(error)")
33+
}
34+
35+
//: The api with parameters is a little more complicated to define:
36+
37+
extension BasicAPI {
38+
/// This is an api for requests using the **POST** method.
39+
///
40+
/// The full api address is: [](https://postman-echo.com/post) .
41+
/// The api is entered as a **tuple** type and requires two parameters, where the second parameter can be `nil`.
42+
@POST("/post")
43+
static var postWithTuple: APIParameterBuilder<(foo1: String, foo2: Int?)>? = {
44+
[
45+
"foo1": $0.foo1,
46+
"foo2": $0.foo2,
47+
]
48+
49+
// Eliminate the warning by explicitly converting to `[String: Any?]`.
50+
// Also ensure that `nil` parameters can be filtered.
51+
as [String: Any?]
52+
}
53+
54+
/// This is an api for requests using the **POST** method.
55+
///
56+
/// The full api address is: [](https://postman-echo.com/post) .
57+
/// This api is referenced with the `Arg` type.
58+
@POST("/post")
59+
static var postWithModel: APIParameterBuilder<Arg>? = {
60+
// You can have your model follow the `APIParameterConvertible` protocol.
61+
// or use `AnyAPIHashableParameter` to wrap your model in an outer layer.
62+
AnyAPIHashableParameter($0)
63+
}
64+
}
65+
66+
do {
67+
// Request the api and parse the return value.
68+
let tupleAPIResponse = try await BasicAPI.$postWithTuple.request(with: (foo1: "foo1", foo2: nil), to: PostManResponse<Arg>.self)
69+
70+
/**
71+
* If you look at the return value, you will see that `foo2` is not passed to the server.
72+
* This is because `RaAPIWrapper` filters out all parameters with the value `nil`.
73+
*/
74+
75+
// Try using model as a parameter and you will get the same result.
76+
let modelAPIResponse = try await BasicAPI.$postWithModel.request(with: .init(foo2: "foo2"), to: PostManResponse<Arg>.self)
77+
78+
} catch {
79+
print("❌ post request failure: \(error)")
80+
}
81+
82+
//: [Next](@next)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//: [Previous](@previous)
2+
3+
import Foundation
4+
5+
import Combine
6+
import ObjectiveC
7+
8+
import APIWrapper
9+
10+
/*:
11+
The design goal of `RaAPIWrapper` is to better encapsulate requests and simplify the request process rather than execute them.
12+
13+
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.
14+
15+
Here are 2 request wrappers for `Combine`, which are roughly written for reference only:
16+
*/
17+
18+
// For subsequent examples
19+
enum CombineAPI {
20+
@POST("/post")
21+
static var post: APIParameterBuilder<String>? = { $0 }
22+
}
23+
24+
// MARK: - AnyPublisher
25+
26+
//: The first way: deliver an `AnyPublisher<T, Error>` object externally and subscribe to it to trigger requests.
27+
28+
extension API {
29+
func requestPublisher(with params: Parameter) -> AnyPublisher<Data, URLError> {
30+
let info = createRequestInfo(params)
31+
32+
// To simplify the demo process, here is a forced unpacking
33+
let url = URL(string: "https://postman-echo.com" + info.path)!
34+
35+
var request = URLRequest(url: url)
36+
request.httpMethod = info.httpMethod.rawValue
37+
38+
if let parameters = info.parameters {
39+
do {
40+
request.httpBody = try JSONEncoder().encode(parameters)
41+
} catch {
42+
fatalError("")
43+
}
44+
45+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
46+
}
47+
48+
return URLSession.shared
49+
.dataTaskPublisher(for: request)
50+
.map { (data, _) in return data }
51+
.mapError { $0 }
52+
.eraseToAnyPublisher()
53+
}
54+
}
55+
56+
var cancellable = Set<AnyCancellable>()
57+
let publisher = CombineAPI.$post.requestPublisher(with: "123")
58+
publisher.sink(receiveCompletion: {
59+
print($0)
60+
61+
}, receiveValue: {
62+
print(String(data: $0, encoding: .utf8) as Any)
63+
64+
}).store(in: &cancellable)
65+
66+
// MARK: - PassthroughSubject
67+
68+
/*:
69+
The second one is to provide a `PassthroughSubject` object to the outside world,
70+
send parameters when requesting the api, subscribe to the object at other places,
71+
accept the parameters and send the request.
72+
*/
73+
74+
private var kParamSubjectKey: String = "kParamSubjectKey"
75+
76+
public extension API {
77+
@available(iOS 13.0, *)
78+
var paramSubject: PassthroughSubject<Parameter, URLError>? {
79+
get {
80+
if let value = objc_getAssociatedObject(self, &kParamSubjectKey) as? PassthroughSubject<Parameter, URLError> {
81+
return value
82+
}
83+
let paramSubject = PassthroughSubject<Parameter, URLError>()
84+
objc_setAssociatedObject(self, &kParamSubjectKey, paramSubject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
85+
return paramSubject
86+
}
87+
set { objc_setAssociatedObject(self, &kParamSubjectKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
88+
}
89+
90+
@available(iOS 13.0, *)
91+
func requestPublisher() -> AnyPublisher<Data, URLError>? {
92+
return paramSubject?.flatMap { self.requestPublisher(with: $0) }.eraseToAnyPublisher()
93+
}
94+
}
95+
96+
let api = CombineAPI.$post
97+
98+
api.requestPublisher()?.sink(receiveCompletion: {
99+
print($0)
100+
101+
}, receiveValue: {
102+
print(String(data: $0, encoding: .utf8) as Any)
103+
104+
}).store(in: &cancellable)
105+
106+
api.paramSubject?.send("233")
107+
api.paramSubject?.send("433")
108+
api.paramSubject?.send(completion: .finished)
109+
110+
//: [Next](@next)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Foundation
2+
3+
import APIWrapper
4+
5+
/// Before formally defining the api, we need to encapsulate a method for requesting the api.
6+
///
7+
/// The role of `RaAPIWrapper` is to encapsulate the parameters needed to request the api,
8+
/// so we don't add any logic for requesting the api.
9+
///
10+
/// This part of the logic needs to be implemented by you in your own project,
11+
/// for now we provide a simple implementation:
12+
13+
public extension API {
14+
/// Request an api **with parameters** and resolve the api return value to a `T` type.
15+
///
16+
/// - Parameters:
17+
/// - params: api parameters.
18+
/// - type: the type of the api return value.
19+
/// - Returns: The result of the parsing.
20+
func request<T: Decodable>(with params: Parameter, to type: T.Type) async throws -> T {
21+
let data = try await _request(with: params)
22+
return try JSONDecoder().decode(type, from: data)
23+
}
24+
25+
/// Request an api **without** parameters.
26+
///
27+
/// This method means: the requesting party does not need the parameters returned by the api,
28+
/// so no return value is provided.
29+
///
30+
/// - Parameter params: api parameters.
31+
func request(with params: Parameter) async throws {
32+
_ = try await _request(with: params)
33+
}
34+
}
35+
36+
/// For some api that do not require parameters,
37+
/// we can also provide the following methods to make the request process even simpler.
38+
39+
public extension API where Parameter == Void {
40+
/// Request an api **without** parameters and resolve the api return value to a `T` type.
41+
///
42+
/// - Parameter type: The type of the api's return value.
43+
/// - Returns: The result of the parsing.
44+
func request<T: Decodable>(to type: T.Type) async throws -> T {
45+
return try await request(with: (), to: type)
46+
}
47+
48+
/// Request an api **without** parameters.
49+
///
50+
/// This method means: the requesting party does not need the parameters returned by the api,
51+
/// so no return value is provided.
52+
func request() async throws {
53+
try await request(with: ())
54+
}
55+
}
56+
57+
// MARK: - Tools
58+
59+
private extension API {
60+
func _request(with params: Parameter) async throws -> Data {
61+
let info = createRequestInfo(params)
62+
63+
// To simplify the demo process, here is a forced unpacking
64+
let url = URL(string: "https://postman-echo.com" + info.path)!
65+
print("▶️ Requests will begin soon: \(url.absoluteString)")
66+
67+
var request = URLRequest(url: url)
68+
request.httpMethod = info.httpMethod.rawValue
69+
70+
if let parameters = info.parameters {
71+
print("🚧 parameters: \(parameters)")
72+
request.httpBody = try JSONEncoder().encode(parameters)
73+
74+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
75+
}
76+
77+
let (data, response) = try await URLSession.shared.data(for: request)
78+
print("\(String(describing: response.url?.absoluteString)) End of request")
79+
80+
return data
81+
}
82+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
3+
public typealias DemoResponse = Codable & Hashable
4+
5+
public struct PostManResponse<T: DemoResponse>: DemoResponse {
6+
public let args: T?
7+
8+
public let data: T?
9+
10+
public let url: String
11+
12+
public let headers: [String: String]
13+
}
14+
15+
public struct Arg: DemoResponse {
16+
let foo1: String?
17+
18+
let foo2: String?
19+
20+
private enum CodingKeys: String, CodingKey {
21+
case foo1 = "foo1"
22+
case foo2 = "foo2"
23+
}
24+
25+
public init(foo1: String? = nil, foo2: String? = nil) {
26+
self.foo1 = foo1
27+
self.foo2 = foo2
28+
}
29+
30+
public init(from decoder: Decoder) throws {
31+
let c = try decoder.container(keyedBy: CodingKeys.self)
32+
33+
self.foo1 = try c.decodeIfPresent(String.self, forKey: .foo1)
34+
self.foo2 = try c.decodeIfPresent(String.self, forKey: .foo2)
35+
}
36+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<playground version='6.0' target-platform='ios' display-mode='raw' buildActiveScheme='true' importAppTypes='true'>
3+
<pages>
4+
<page name='Basic'/>
5+
<page name='Advanced'/>
6+
<page name='Combine'/>
7+
</pages>
8+
</playground>

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)