Skip to content

Commit

Permalink
Merge branch release/1.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
rakuyoMo committed Dec 21, 2022
2 parents 04eb3d5 + c029663 commit 6e29cbc
Show file tree
Hide file tree
Showing 36 changed files with 749 additions and 125 deletions.
45 changes: 45 additions & 0 deletions Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift
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 Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift
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 Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift
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)
82 changes: 82 additions & 0 deletions Demo.playground/Sources/API+Request.swift
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
}
}
36 changes: 36 additions & 0 deletions Demo.playground/Sources/PostManResponse.swift
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)
}
}
8 changes: 8 additions & 0 deletions Demo.playground/contents.xcplayground
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>
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire.git",
"state" : {
"revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
"version" : "5.6.2"
"revision" : "78424be314842833c04bc3bef5b72e85fff99204",
"version" : "5.6.4"
}
}
],
Expand Down
Loading

0 comments on commit 6e29cbc

Please sign in to comment.