diff --git a/Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift b/Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..5e6ae6a --- /dev/null +++ b/Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift @@ -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) diff --git a/Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift b/Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..0a03fa5 --- /dev/null +++ b/Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift @@ -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.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? = { + // 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.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.self) + +} catch { + print("❌ post request failure: \(error)") +} + +//: [Next](@next) diff --git a/Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift b/Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..be75468 --- /dev/null +++ b/Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift @@ -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? = { $0 } +} + +// MARK: - AnyPublisher + +//: The first way: deliver an `AnyPublisher` object externally and subscribe to it to trigger requests. + +extension API { + func requestPublisher(with params: Parameter) -> AnyPublisher { + 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() +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? { + get { + if let value = objc_getAssociatedObject(self, &kParamSubjectKey) as? PassthroughSubject { + return value + } + let paramSubject = PassthroughSubject() + 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? { + 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) diff --git a/Demo.playground/Sources/API+Request.swift b/Demo.playground/Sources/API+Request.swift new file mode 100644 index 0000000..aa66f6b --- /dev/null +++ b/Demo.playground/Sources/API+Request.swift @@ -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(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(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 + } +} diff --git a/Demo.playground/Sources/PostManResponse.swift b/Demo.playground/Sources/PostManResponse.swift new file mode 100644 index 0000000..57c5fc1 --- /dev/null +++ b/Demo.playground/Sources/PostManResponse.swift @@ -0,0 +1,36 @@ +import Foundation + +public typealias DemoResponse = Codable & Hashable + +public struct PostManResponse: 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) + } +} diff --git a/Demo.playground/contents.xcplayground b/Demo.playground/contents.xcplayground new file mode 100644 index 0000000..ef206ed --- /dev/null +++ b/Demo.playground/contents.xcplayground @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 85333c0..152a654 100644 --- a/Package.resolved +++ b/Package.resolved @@ -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" } } ], diff --git a/Package.swift b/Package.swift index 34cddd7..a62759b 100644 --- a/Package.swift +++ b/Package.swift @@ -5,11 +5,15 @@ import PackageDescription let package = Package( name: "APIWrapper", - platforms: [.iOS(.v10)], + platforms: [ + .macOS(.v10_13), + .iOS(.v11), + .tvOS(.v11), + .watchOS(.v4) + ], products: [ - .library( - name: "APIWrapper", - targets: ["APIWrapper"]), + .library(name: "APIWrapper", targets: ["APIWrapper"]), + .library(name: "AFAPIWrapper", targets: ["AFAPIWrapper"]), ], dependencies: [ .package( @@ -21,11 +25,21 @@ let package = Package( .target( name: "APIWrapper", dependencies: ["Alamofire"], - path: "Sources"), + path: "Sources/Core" + ), + .target( + name: "AFAPIWrapper", + dependencies: [ + "APIWrapper", + .product(name: "Alamofire", package: "Alamofire") + ], + path: "Sources/Alamofire" + ), .testTarget( name: "APIWrapperTests", dependencies: ["APIWrapper"], - path: "Tests"), + path: "Tests" + ), ], swiftLanguageVersions: [.v5] ) diff --git a/README.md b/README.md index c6bcf46..fead957 100644 --- a/README.md +++ b/README.md @@ -1 +1,83 @@ -# RaAPIWrapper \ No newline at end of file +# RaAPIWrapper + +

+ + + +

+ +> [中文](https://github.com/rakuyoMo/RaAPIWrapper/blob/main/README_CN.md) + +`RaAPIWrapper` uses `@propertyWrapper` to achieve a similar effect to that of defining network requests in the Android `Retrofit` library. + +When you have a large number of network request apis in the same file `RaAPIWrapper` can help you define each request in a more aggregated form, so you don't have to jump back and forth within the file. + +## Say it before + +**Special Note!** : `RaAPIWrapper` is just a syntactic sugar for **defining** web requests. You need to use `Alamofire`, `Moya`, other third-party web framework or call `URLSession` directly to initiate web requests on this basis. + +The good thing is that you can easily integrate `RaAPIWrapper` into your existing project with few or no code changes, and `RaAPIWrapper` can coexist very well with the existing web framework in your project. + +## Prerequisites + +- **iOS 11**、**macOS 10.13**、**watchOS 4.0**、**tvOS 11** or later. +- **Xcode 14** or later required. +- **Swift 5.7** or later required. + +## Example + +```swift +@GET("/api/v1/no_param") +static var noParamAPI: APIParameterBuilder<()>? = nil + +@POST("/api/v1/tuple_param") +static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = { + // Eliminate the warning by explicitly converting to `[String: Any?]`. + // Also ensure that `nil` parameters can be filtered. + ["id": $0.id, "name": $0.name] as [String: Any?] +} + +@POST("/post") +static var postWithModel: APIParameterBuilder? = { + // You can have your model follow the `APIParameterConvertible` protocol, + // or use `AnyAPIHashableParameter` to wrap your model in an outer layer. + AnyAPIHashableParameter($0) +} +``` + +## Install + +### CocoaPods + +```ruby +pod 'RaAPIWrapper' +``` + +If your project relies on `Alamofire`, then you may also consider relying on `RaAPIWrapper/AF`. This module provides a wrapper for `ParameterEncoding`. + +### Swift Package Manager + +- File > Swift Packages > Add Package Dependency +- Add https://github.com/rakuyoMo/RaAPIWrapper.git +- SSelect "Up to Next Major" and fill in the corresponding version number + +Or add the following to your `Package.swift` file: + +```swift +dependencies: [ + .package( + url: "https://github.com/rakuyoMo/RaAPIWrapper.git", + .upToNextMajor(from: "1.0.0") + ) +] +``` + +## Usage + +Please refer to the example in `Demo.playground`. + +> Since playground depends on `RaAPIWrapper` in the form of Swift Package Manager, please open the project via `Package.swift` first, then select `Demo.playground` from the left navigation bar and run the content. + +## License + +`RaAPIWrapper` is available under the **MIT** license. For more information, see [LICENSE](LICENSE). diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..fbc9b0a --- /dev/null +++ b/README_CN.md @@ -0,0 +1,79 @@ +# RaAPIWrapper + +

+ + + +

+ +`RaAPIWrapper` 利用 `@propertyWrapper` 来实现类似于 Android `Retrofit` 库中定义网络请求的效果。 + +在同一个文件中包含大量的网络请求接口时,`RaAPIWrapper` 可以帮助您以更聚合的形式定义每一个请求,让您不用在文件内来回跳转。 + +## 说在前面 + +**特别注意!**:`RaAPIWrapper` 仅仅是一个**定义**网络请求的语法糖。您需要在此基础上借助 `Alamofire`、`Moya` 、其他第三方网络框架或者直接调用 `URLSession` 来发起网络请求。 + +好的一点是,您只需要修改少量的代码,甚至无需修改代码,就可以很简单地将 `RaAPIWrapper` 集成进您已有的项目,`RaAPIWrapper` 可以很好的和您项目中现有的网络框架共存。 + +## 基本要求 + +- 运行 **iOS 11**、**macOS 10.13**、**watchOS 4.0**、**tvOS 11** 及以上版本的设备。 +- 使用 **Xcode 14** 及以上版本编译运行。 +- **Swift 5.7** 及以上版本。 + +## 示例 + +```swift +@GET("/api/v1/no_param") +static var noParamAPI: APIParameterBuilder<()>? = nil + +@POST("/api/v1/tuple_param") +static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = { + // 通过显式转换为 `[String: Any?]` 来消除警告,同时确保为 `nil` 的参数能够被过滤。 + ["id": $0.id, "name": $0.name] as [String: Any?] +} + +@POST("/post") +static var postWithModel: APIParameterBuilder? = { + // 您可以让您的模型遵循 `APIParameterConvertible` 协议,或者使用 `AnyAPIHashableParameter` 在外面包裹一层。 + AnyAPIHashableParameter($0) +} +``` + +## 安装 + +### CocoaPods + +```ruby +pod 'RaAPIWrapper' +``` + +如果您的项目依赖了 `Alamofire`,那么您还可以考虑依赖 `RaAPIWrapper/AF`。该模块提供了针对 `ParameterEncoding` 的封装。 + +### Swift Package Manager + +- 依次选择 File > Swift Packages > Add Package Dependency +- 输入 https://github.com/rakuyoMo/RaAPIWrapper.git +- 选择 "Up to Next Major" 并填入对应的版本号 + +或者将下面的内容添加到 `Package.swift` 文件中: + +```swift +dependencies: [ + .package( + url: "https://github.com/rakuyoMo/RaAPIWrapper.git", + .upToNextMajor(from: "1.0.0") + ) +] +``` + +## 使用 + +请参考 `Demo.playground` 中的示例。 + +> 因为 playground 以 Swift Package Manager 的形式依赖 `RaAPIWrapper`,所以请先通过 `Package.swift` 打开项目,再从左侧的导航栏中选择 `Demo.playground`,运行相关内容。 + +## License + +`RaAPIWrapper` 在 **MIT** 许可下可用。 有关更多信息,请参见 [LICENSE](LICENSE) 文件。 diff --git a/RaAPIWrapper.podspec b/RaAPIWrapper.podspec index 6a7340f..98b8e0b 100755 --- a/RaAPIWrapper.podspec +++ b/RaAPIWrapper.podspec @@ -3,32 +3,44 @@ Pod::Spec.new do |s| - s.name = 'RaAPIWrapper' + s.name = 'RaAPIWrapper' - s.version = '0.9.0' + s.version = '1.0.1' - s.summary = 'Wrappers for requesting api.' + s.summary = 'Makes it easier to define a network request.' - s.description = 'Provide the necessary data for the requesting api in a more aggregated form.' + s.description = 'Use `@propertyWrapper to provide the necessary data for network requests in a more aggregated form.' - s.homepage = 'https://github.com/rakuyoMo/RaAPIWrapper' + s.homepage = 'https://github.com/rakuyoMo/RaAPIWrapper' - s.license = 'MIT' + s.license = 'MIT' - s.author = { 'Rakuyo' => 'rakuyo.mo@gmail.com' } + s.author = { 'Rakuyo' => 'rakuyo.mo@gmail.com' } - s.source = { :git => 'https://github.com/rakuyoMo/RaAPIWrapper.git', :tag => s.version.to_s } + s.source = { :git => 'https://github.com/rakuyoMo/RaAPIWrapper.git', :tag => s.version.to_s } - s.requires_arc = true - - s.platform = :ios, '10.0' + s.requires_arc = true - s.swift_version = '5.0' + s.ios.deployment_target = '11.0' + s.osx.deployment_target = '10.13' + s.tvos.deployment_target = '11.0' + s.watchos.deployment_target = '4.0' - s.module_name = 'APIWrapper' + s.swift_versions = ['5'] - s.source_files = 'Sources/*/**/*' - - s.dependency 'Alamofire' + s.module_name = 'APIWrapper' + + s.default_subspec = "Core" + + s.subspec "Core" do |cs| + cs.source_files = 'Sources/Core/**/*' + end + + s.subspec "AF" do |cs| + cs.source_files = 'Sources/Alamofire/*' + + cs.dependency "RaAPIWrapper/Core" + cs.dependency "Alamofire", "~> 5.0" + end end diff --git a/Sources/Alamofire/API+AF.swift b/Sources/Alamofire/API+AF.swift new file mode 100644 index 0000000..0c752e3 --- /dev/null +++ b/Sources/Alamofire/API+AF.swift @@ -0,0 +1,42 @@ +// +// API+AF.swift +// RaAPIWrapper +// +// Created by Rakuyo on 2022/12/19. +// Copyright © 2022 Rakuyo. All rights reserved. +// + +import Foundation + +#if !COCOAPODS +import APIWrapper +#endif + +let parameterEncodingKey = "af_parameter_encoding" + +public extension API { + convenience init( + wrappedValue: ParameterBuilder?, + _ path: String, + specialBaseURL: URL? = nil, + header: HeaderBuilder? = nil, + parameterEncoding: AnyAPIParameterEncoding, + userInfo: APIRequestUserInfo = [:] + ) { + var _userInfo = userInfo + _userInfo[parameterEncodingKey] = parameterEncoding + + self.init( + wrappedValue: wrappedValue, + path, + specialBaseURL: specialBaseURL, + header: header, + userInfo: _userInfo + ) + } + + /// Encoding of `Parameters`. + var parameterEncoding: AnyAPIParameterEncoding? { + userInfo[parameterEncodingKey] as? AnyAPIParameterEncoding + } +} diff --git a/Sources/Alamofire/APIRequestInfo+AF.swift b/Sources/Alamofire/APIRequestInfo+AF.swift new file mode 100644 index 0000000..9c09268 --- /dev/null +++ b/Sources/Alamofire/APIRequestInfo+AF.swift @@ -0,0 +1,20 @@ +// +// APIRequestInfo+AF +// RaAPIWrapper +// +// Created by Rakuyo on 2022/12/19. +// Copyright © 2022 Rakuyo. All rights reserved. +// + +import Foundation + +#if !COCOAPODS +import APIWrapper +#endif + +public extension APIRequestInfo { + /// Encoding of `Parameters`. + var parameterEncoding: AnyAPIParameterEncoding? { + userInfo[parameterEncodingKey] as? AnyAPIParameterEncoding + } +} diff --git a/Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameterEncoding.swift b/Sources/Alamofire/AnyAPIHashableParameterEncoding.swift similarity index 82% rename from Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameterEncoding.swift rename to Sources/Alamofire/AnyAPIHashableParameterEncoding.swift index 767e4a2..d50b4b6 100644 --- a/Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameterEncoding.swift +++ b/Sources/Alamofire/AnyAPIHashableParameterEncoding.swift @@ -10,6 +10,13 @@ import Foundation import Alamofire +#if !COCOAPODS +import APIWrapper +#endif + +/// Represents an arbitrary api parameter. +public typealias AnyAPIParameterEncoding = AnyAPIHashableParameterEncoding + /// Make `AlamofireParameterEncoding` follow `Hashable` protocol. public struct AnyAPIHashableParameterEncoding: AnyAPIHashable { public typealias Value = ParameterEncoding @@ -31,7 +38,7 @@ public struct AnyAPIHashableParameterEncoding: AnyAPIHashable { // MARK: - AlamofireParameterEncoding -extension AnyAPIHashableParameterEncoding: ParameterEncoding { +extension AnyAPIParameterEncoding: ParameterEncoding { public func encode( _ urlRequest: Alamofire.URLRequestConvertible, with parameters: Alamofire.Parameters? diff --git a/Sources/RequestInfo/APIHTTPMethod.swift b/Sources/Core/RequestInfo/APIHTTPMethod.swift similarity index 100% rename from Sources/RequestInfo/APIHTTPMethod.swift rename to Sources/Core/RequestInfo/APIHTTPMethod.swift diff --git a/Sources/RequestInfo/APIHeaders.swift b/Sources/Core/RequestInfo/APIHeaders.swift similarity index 100% rename from Sources/RequestInfo/APIHeaders.swift rename to Sources/Core/RequestInfo/APIHeaders.swift diff --git a/Sources/Core/RequestInfo/APIParameterConvertible.swift b/Sources/Core/RequestInfo/APIParameterConvertible.swift new file mode 100644 index 0000000..8f7102b --- /dev/null +++ b/Sources/Core/RequestInfo/APIParameterConvertible.swift @@ -0,0 +1,90 @@ +// +// APIParameters.swift +// RaAPIWrapper +// +// Created by Rakuyo on 2022/8/25. +// Copyright © 2022 Rakuyo. All rights reserved. +// + +import Foundation + +/// Used to constrain what types can be used as api parameters. +public typealias APIParametrizable = AnyAPIParameter.Input + +/// Means that the type can be converted to an interface parameter for requesting an api. +public protocol APIParameterConvertible { + /// Converts the target to an Encodable-compliant type. + var toParameters: AnyAPIParameter { get } +} + +// MARK: - Default + +extension APIParameterConvertible where Self: APIParametrizable { + public var toParameters: AnyAPIParameter { .init(self) } +} + +// MARK: - AnyAPIParameter + +extension AnyAPIParameter: APIParameterConvertible { } + +// MARK: - String + +extension String: APIParameterConvertible { } + +// MARK: - Number + +extension Int: APIParameterConvertible { } +extension Float: APIParameterConvertible { } +extension Double: APIParameterConvertible { } + +// MARK: - Bool + +extension Bool: APIParameterConvertible { } + +// MARK: - Array + +extension Array: APIParameterConvertible { + public var toParameters: AnyAPIParameter { + let result: [AnyAPIParameter] = (self as [Any?]) + .compactMap { + if let value = $0 as? (any APIParametrizable) { return .init(value) } + if let value = $0 as? APIParameterConvertible { return value.toParameters } + return mapAnyObjectToEncodable($0 as? AnyObject) + } + + return .init(result) + } +} + +// MARK: - Dictionary + +extension Dictionary: APIParameterConvertible where Key == String { + public var toParameters: AnyAPIParameter { + let result: [String: AnyAPIParameter] = (self as [String: Any?]) + .compactMapValues { + if let value = $0 as? (any APIParametrizable) { return .init(value) } + if let value = $0 as? APIParameterConvertible { return value.toParameters } + return mapAnyObjectToEncodable($0 as? AnyObject) + } + + return .init(result) + } +} + +// MARK: - Tools + +fileprivate extension APIParameterConvertible { + func mapAnyObjectToEncodable(_ value: AnyObject?) -> AnyAPIParameter? { + guard let _value = value else { return nil } + + if let result = _value as? String { return .init(result) } + if let result = _value as? Int { return .init(result) } + if let result = _value as? Double { return .init(result) } + if let result = _value as? Bool { return .init(result) } + if let result = _value as? Data { return .init(result) } + if let result = _value as? [String: Any] { return result.toParameters } + if let result = _value as? [Any] { return result.toParameters } + + return nil + } +} diff --git a/Sources/RequestInfo/APIRequestInfo.swift b/Sources/Core/RequestInfo/APIRequestInfo.swift similarity index 77% rename from Sources/RequestInfo/APIRequestInfo.swift rename to Sources/Core/RequestInfo/APIRequestInfo.swift index edb6d93..acd0ce9 100644 --- a/Sources/RequestInfo/APIRequestInfo.swift +++ b/Sources/Core/RequestInfo/APIRequestInfo.swift @@ -25,12 +25,10 @@ public struct APIRequestInfo { public let header: APIHeaders? /// Parameters of the requested api - public let parameters: AnyAPIHashableParameter? + public let parameters: AnyAPIParameter? - /// Encoding of `parameters` - public let parameterEncoding: AnyAPIHashableParameterEncoding? - - /// + /// An additional storage space. + /// You can use this property to store some custom data. public let userInfo: APIRequestUserInfo public init( @@ -38,8 +36,7 @@ public struct APIRequestInfo { specialBaseURL: URL? = nil, httpMethod: APIHTTPMethod, header: APIHeaders? = nil, - parameters: AnyAPIHashableParameter? = nil, - parameterEncoding: AnyAPIHashableParameterEncoding? = nil, + parameters: AnyAPIParameter? = nil, userInfo: APIRequestUserInfo = [:] ) { self.path = path @@ -47,7 +44,6 @@ public struct APIRequestInfo { self.httpMethod = httpMethod self.header = header self.parameters = parameters - self.parameterEncoding = parameterEncoding self.userInfo = userInfo } } diff --git a/Sources/RequestInfo/APIRequestUserInfo.swift b/Sources/Core/RequestInfo/APIRequestUserInfo.swift similarity index 100% rename from Sources/RequestInfo/APIRequestUserInfo.swift rename to Sources/Core/RequestInfo/APIRequestUserInfo.swift diff --git a/Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashable.swift b/Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashable.swift similarity index 100% rename from Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashable.swift rename to Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashable.swift diff --git a/Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameter.swift b/Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashableParameter.swift similarity index 89% rename from Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameter.swift rename to Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashableParameter.swift index 47936f3..3aed0f0 100644 --- a/Sources/RequestInfo/AnyAPIHashableWrapper /AnyAPIHashableParameter.swift +++ b/Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashableParameter.swift @@ -8,8 +8,8 @@ import Foundation -/// API parameters. -public typealias APIHashableParameter = AnyAPIHashableParameter.Input +/// Represents an arbitrary api parameter. +public typealias AnyAPIParameter = AnyAPIHashableParameter /// Make `Encodable` follow `Hashable` protocol. public struct AnyAPIHashableParameter: AnyAPIHashable { diff --git a/Sources/Wrapper/API.swift b/Sources/Core/Wrapper/API.swift similarity index 87% rename from Sources/Wrapper/API.swift rename to Sources/Core/Wrapper/API.swift index 051f25d..0eb38a7 100644 --- a/Sources/Wrapper/API.swift +++ b/Sources/Core/Wrapper/API.swift @@ -9,7 +9,7 @@ import Foundation /// Parameter constructor for the api. Supports passing one parameter. -public typealias APIParameterBuilder = (ParamType) -> APIParameter +public typealias APIParameterBuilder = (ParamType) -> APIParameterConvertible /// Used to encapsulate the `APIHTTPMethod` object provided to the `API`. public protocol APIHTTPMethodWrapper { @@ -18,7 +18,7 @@ public protocol APIHTTPMethodWrapper { /// API wrapper. Used to wrap the data needed to request an api. @propertyWrapper -public struct API { +public class API { public typealias HeaderBuilder = (Parameter) -> APIHeaders public typealias ParameterBuilder = APIParameterBuilder @@ -40,9 +40,6 @@ public struct API { /// Used to construct the api request header. public let headerBuilder: HeaderBuilder? - /// Encoding of `Parameters`. - public let parameterEncoding: AnyAPIHashableParameterEncoding? - /// An additional storage space. /// You can use this property to store some custom data. public let userInfo: APIRequestUserInfo @@ -52,14 +49,12 @@ public struct API { _ path: String, specialBaseURL: URL? = nil, header: HeaderBuilder? = nil, - parameterEncoding: AnyAPIHashableParameterEncoding? = nil, userInfo: APIRequestUserInfo = [:] ) { self.wrappedValue = wrappedValue self.path = path self.specialBaseURL = specialBaseURL self.headerBuilder = header - self.parameterEncoding = parameterEncoding self.userInfo = userInfo } } @@ -81,7 +76,6 @@ public extension API { httpMethod: Self.httpMethod.httpMethod, header: headerBuilder?(parameter), parameters: wrappedValue?(parameter).toParameters, - parameterEncoding: parameterEncoding, userInfo: userInfo ) } diff --git a/Sources/Wrapper/HTTPMethod/CONNECT.swift b/Sources/Core/Wrapper/HTTPMethod/CONNECT.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/CONNECT.swift rename to Sources/Core/Wrapper/HTTPMethod/CONNECT.swift diff --git a/Sources/Wrapper/HTTPMethod/DELETE.swift b/Sources/Core/Wrapper/HTTPMethod/DELETE.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/DELETE.swift rename to Sources/Core/Wrapper/HTTPMethod/DELETE.swift diff --git a/Sources/Wrapper/HTTPMethod/GET.swift b/Sources/Core/Wrapper/HTTPMethod/GET.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/GET.swift rename to Sources/Core/Wrapper/HTTPMethod/GET.swift diff --git a/Sources/Wrapper/HTTPMethod/HEAD.swift b/Sources/Core/Wrapper/HTTPMethod/HEAD.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/HEAD.swift rename to Sources/Core/Wrapper/HTTPMethod/HEAD.swift diff --git a/Sources/Wrapper/HTTPMethod/OPTIONS.swift b/Sources/Core/Wrapper/HTTPMethod/OPTIONS.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/OPTIONS.swift rename to Sources/Core/Wrapper/HTTPMethod/OPTIONS.swift diff --git a/Sources/Wrapper/HTTPMethod/PATCH.swift b/Sources/Core/Wrapper/HTTPMethod/PATCH.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/PATCH.swift rename to Sources/Core/Wrapper/HTTPMethod/PATCH.swift diff --git a/Sources/Wrapper/HTTPMethod/POST.swift b/Sources/Core/Wrapper/HTTPMethod/POST.swift similarity index 84% rename from Sources/Wrapper/HTTPMethod/POST.swift rename to Sources/Core/Wrapper/HTTPMethod/POST.swift index 722ad9d..f15fb0f 100644 --- a/Sources/Wrapper/HTTPMethod/POST.swift +++ b/Sources/Core/Wrapper/HTTPMethod/POST.swift @@ -9,7 +9,7 @@ import Foundation public enum PostHTTPMethod: APIHTTPMethodWrapper { - public static var httpMethod: APIHTTPMethod { "GET" } + public static var httpMethod: APIHTTPMethod { "POST" } } /// Encapsulates the data needed to request the `POST` api. diff --git a/Sources/Wrapper/HTTPMethod/PUT.swift b/Sources/Core/Wrapper/HTTPMethod/PUT.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/PUT.swift rename to Sources/Core/Wrapper/HTTPMethod/PUT.swift diff --git a/Sources/Wrapper/HTTPMethod/QUERY.swift b/Sources/Core/Wrapper/HTTPMethod/QUERY.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/QUERY.swift rename to Sources/Core/Wrapper/HTTPMethod/QUERY.swift diff --git a/Sources/Wrapper/HTTPMethod/TRACE.swift b/Sources/Core/Wrapper/HTTPMethod/TRACE.swift similarity index 100% rename from Sources/Wrapper/HTTPMethod/TRACE.swift rename to Sources/Core/Wrapper/HTTPMethod/TRACE.swift diff --git a/Sources/RequestInfo/APIParameter.swift b/Sources/RequestInfo/APIParameter.swift deleted file mode 100644 index 66d5911..0000000 --- a/Sources/RequestInfo/APIParameter.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// APIParameters.swift -// RaAPIWrapper -// -// Created by Rakuyo on 2022/8/25. -// Copyright © 2022 Rakuyo. All rights reserved. -// - -import Foundation - -public protocol APIParameter { - /// Converts the target to an Encodable-compliant type. - var toParameters: AnyAPIHashableParameter { get } -} - -// MARK: - Default - -extension APIParameter where Self: APIHashableParameter { - public var toParameters: AnyAPIHashableParameter { .init(self) } -} - -// MARK: - Array - -extension Array: APIParameter { - public var toParameters: AnyAPIHashableParameter { - let result: [AnyAPIHashableParameter] = (self as [Any?]) - .compactMap { - if let value = $0 as? (any APIHashableParameter) { return .init(value) } - if let value = $0 as? APIParameter { return value.toParameters } - return mapAnyObjectToEncodable($0 as? AnyObject) - } - - return .init(result) - } -} - -// MARK: - Dictionary - -extension Dictionary: APIParameter where Key == String { - public var toParameters: AnyAPIHashableParameter { - let result: [String: AnyAPIHashableParameter] = (self as [String: Any?]) - .compactMapValues { - if let value = $0 as? (any APIHashableParameter) { return .init(value) } - if let value = $0 as? APIParameter { return value.toParameters } - return mapAnyObjectToEncodable($0 as? AnyObject) - } - - return .init(result) - } -} - -// MARK: - Tools - -fileprivate extension APIParameter { - func mapAnyObjectToEncodable(_ value: AnyObject?) -> AnyAPIHashableParameter? { - guard let _value = value else { return nil } - - if let result = _value as? String { return .init(result) } - if let result = _value as? Int { return .init(result) } - if let result = _value as? Double { return .init(result) } - if let result = _value as? Bool { return .init(result) } - if let result = _value as? Data { return .init(result) } - if let result = _value as? [String: Any] { return result.toParameters } - if let result = _value as? [Any] { return result.toParameters } - - return nil - } -} diff --git a/Tests/AvailabilityTests.swift b/Tests/AvailabilityTests.swift index d7e0812..1459b01 100644 --- a/Tests/AvailabilityTests.swift +++ b/Tests/AvailabilityTests.swift @@ -20,11 +20,10 @@ final class AvailabilityTests: XCTestCase { XCTAssertEqual(info.httpMethod, PostHTTPMethod.httpMethod) XCTAssertEqual(info.path, TestAPI.path) XCTAssertNil(info.specialBaseURL) - XCTAssertNil(info.parameterEncoding) } - private func packToParameters(_ value: [String: Optional]) -> AnyAPIHashableParameter { - return .init(value.mapValues { AnyAPIHashableParameter($0) }) + private func packToParameters(_ value: [String: Optional]) -> AnyAPIParameter { + return .init(value.mapValues { AnyAPIParameter($0) }) } } diff --git a/Tests/ExtensibilityTests.swift b/Tests/ExtensibilityTests.swift index 990645f..92b05c7 100644 --- a/Tests/ExtensibilityTests.swift +++ b/Tests/ExtensibilityTests.swift @@ -18,7 +18,7 @@ final class ExtensibilityTests: XCTestCase { } } -fileprivate struct TestAPI { +fileprivate enum TestAPI { @GET("/api/v1/tuple_param", mockType: .someType) static var testAPI: APIParameterBuilder<()>? = nil } @@ -28,7 +28,7 @@ enum MockType: Hashable { } extension API { - init( + convenience init( wrappedValue: ParameterBuilder? = nil, _ path: String, mockType: MockType diff --git a/push.sh b/push.sh index 3163031..60eeaab 100755 --- a/push.sh +++ b/push.sh @@ -27,13 +27,7 @@ release(){ git checkout -b $release_branch develop - agvtool new-marketing-version $version - - agvtool next-version -all - - build=$(agvtool what-version | tail -n2 | awk -F ' ' '{print $NF}') - - git_message="[Release] version: $version build: $build" + git_message="[Release] version: $version" git add . && git commit -m $git_message