├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .mise.toml ├── .pre-commit-config.yaml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── AFAPIWrapper.xcscheme │ └── APIWrapper.xcscheme ├── Demo.playground ├── Pages │ ├── Advanced.xcplaygroundpage │ │ └── Contents.swift │ ├── Basic.xcplaygroundpage │ │ └── Contents.swift │ └── Combine.xcplaygroundpage │ │ └── Contents.swift ├── Sources │ ├── API+Request.swift │ ├── Log.swift │ └── PostManResponse.swift └── contents.xcplayground ├── LICENSE ├── Package.resolved ├── Package.swift ├── Package@swift-5.9.swift ├── README.md ├── README_CN.md ├── RaAPIWrapper.podspec ├── Rakefile ├── Sources ├── Alamofire │ ├── API+AF.swift │ ├── APIRequestInfo+AF.swift │ └── AnyAPIHashableParameterEncoding.swift └── Core │ ├── PrivacyInfo.xcprivacy │ ├── RequestInfo │ ├── APIHTTPMethod.swift │ ├── APIHeaders.swift │ ├── APIParameter.swift │ ├── APIRequestInfo.swift │ ├── APIRequestUserInfo.swift │ └── AnyAPIHashable │ │ ├── AnyAPIHashable.swift │ │ └── AnyAPIHashableParameter.swift │ └── Wrapper │ ├── API.swift │ ├── APIParameterBuilder.swift │ ├── APIParameterConvertible.swift │ └── HTTPMethod │ ├── CONNECT.swift │ ├── DELETE.swift │ ├── GET.swift │ ├── HEAD.swift │ ├── OPTIONS.swift │ ├── PATCH.swift │ ├── POST.swift │ ├── PUT.swift │ ├── QUERY.swift │ └── TRACE.swift ├── Tests ├── AvailabilityTest.swift ├── ExtensibilityTest.swift └── TestEnum.swift └── script └── ls.rb /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # GitHub recommends pinning actions to a commit SHA. 7 | # To get a newer version, you will need to update the SHA. 8 | # You can also reference a tag or branch, but the action may change without warning. 9 | 10 | name: Swift Build 11 | 12 | on: 13 | pull_request: 14 | branches: [main] 15 | workflow_dispatch: 16 | 17 | jobs: 18 | build: 19 | name: Xcode ${{ matrix.xcode }} on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [macos-14] 24 | xcode: ["15"] 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: jdx/mise-action@v2 28 | - uses: maxim-lobanov/setup-xcode@v1 29 | with: 30 | xcode-version: ${{ matrix.xcode }} 31 | - name: Lint 32 | run: rake swift:lint 33 | - name: Test 34 | run: rake swift:test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,objective-c,swift,swiftpackagemanager,xcode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,objective-c,swift,swiftpackagemanager,xcode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### macOS Patch ### 33 | # iCloud generated files 34 | *.icloud 35 | 36 | ### Objective-C ### 37 | # Xcode 38 | # 39 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 40 | 41 | ## User settings 42 | xcuserdata/ 43 | 44 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 45 | *.xcscmblueprint 46 | *.xccheckout 47 | 48 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 49 | build/ 50 | DerivedData/ 51 | *.moved-aside 52 | *.pbxuser 53 | !default.pbxuser 54 | *.mode1v3 55 | !default.mode1v3 56 | *.mode2v3 57 | !default.mode2v3 58 | *.perspectivev3 59 | !default.perspectivev3 60 | 61 | ## Obj-C/Swift specific 62 | *.hmap 63 | 64 | ## App packaging 65 | *.ipa 66 | *.dSYM.zip 67 | *.dSYM 68 | 69 | # CocoaPods 70 | # We recommend against adding the Pods directory to your .gitignore. However 71 | # you should judge for yourself, the pros and cons are mentioned at: 72 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 73 | # Pods/ 74 | # Add this line if you want to avoid checking in source code from the Xcode workspace 75 | # *.xcworkspace 76 | 77 | # Carthage 78 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 79 | # Carthage/Checkouts 80 | 81 | Carthage/Build/ 82 | 83 | # fastlane 84 | # It is recommended to not store the screenshots in the git repo. 85 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 86 | # For more information about the recommended setup visit: 87 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 88 | 89 | fastlane/report.xml 90 | fastlane/Preview.html 91 | fastlane/screenshots/**/*.png 92 | fastlane/test_output 93 | 94 | # Code Injection 95 | # After new code Injection tools there's a generated folder /iOSInjectionProject 96 | # https://github.com/johnno1962/injectionforxcode 97 | 98 | iOSInjectionProject/ 99 | 100 | ### Objective-C Patch ### 101 | 102 | ### Swift ### 103 | # Xcode 104 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 105 | 106 | 107 | 108 | 109 | 110 | 111 | ## Playgrounds 112 | timeline.xctimeline 113 | playground.xcworkspace 114 | 115 | # Swift Package Manager 116 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 117 | # Packages/ 118 | # Package.pins 119 | # Package.resolved 120 | # *.xcodeproj 121 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 122 | # hence it is not needed unless you have added a package configuration file to your project 123 | 124 | .swiftpm 125 | .build/ 126 | 127 | # CocoaPods 128 | # We recommend against adding the Pods directory to your .gitignore. However 129 | # you should judge for yourself, the pros and cons are mentioned at: 130 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 131 | # Pods/ 132 | # Add this line if you want to avoid checking in source code from the Xcode workspace 133 | # *.xcworkspace 134 | 135 | # Carthage 136 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 137 | # Carthage/Checkouts 138 | 139 | Carthage/Build/ 140 | 141 | # Accio dependency management 142 | Dependencies/ 143 | .accio/ 144 | 145 | # fastlane 146 | # It is recommended to not store the screenshots in the git repo. 147 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 148 | # For more information about the recommended setup visit: 149 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 150 | 151 | 152 | # Code Injection 153 | # After new code Injection tools there's a generated folder /iOSInjectionProject 154 | # https://github.com/johnno1962/injectionforxcode 155 | 156 | 157 | ### SwiftPackageManager ### 158 | Packages 159 | xcuserdata 160 | *.xcodeproj 161 | 162 | 163 | ### Xcode ### 164 | 165 | ## Xcode 8 and earlier 166 | 167 | ### Xcode Patch ### 168 | *.xcodeproj/* 169 | !*.xcodeproj/project.pbxproj 170 | !*.xcodeproj/xcshareddata/ 171 | !*.xcodeproj/project.xcworkspace/ 172 | !*.xcworkspace/contents.xcworkspacedata 173 | /*.gcno 174 | **/xcshareddata/WorkspaceSettings.xcsettings 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/macos,objective-c,swift,swiftpackagemanager,xcode 177 | 178 | ### Projects ### 179 | *.xcodeproj 180 | *.xcworkspace 181 | -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | pre-commit = "3.7.0" 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: [pre-commit, pre-push] 2 | fail_fast: true 3 | 4 | repos: 5 | - repo: local 6 | hooks: 7 | - id: lint 8 | name: Run Lint 9 | language: system 10 | entry: rake swift:lint 11 | stages: [pre-commit] 12 | verbose: true 13 | - id: test 14 | name: Run Test 15 | language: system 16 | entry: rake swift:test 17 | stages: [pre-push] 18 | verbose: true 19 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/AFAPIWrapper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/APIWrapper.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Demo.playground/Pages/Advanced.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | 5 | import Foundation 6 | 7 | import APIWrapper 8 | 9 | // MARK: - VerificationType 10 | 11 | /// : 12 | /// The `RaAPIWrapper` is extremely extensible. 13 | /// You can work with the `userInfo` property to customize the api parameters you need. 14 | /// 15 | /// The `RaAPIWrapper/AF` module then takes advantage of this feature and supports the `ParameterEncoding` field of `Alamofire`. 16 | /// 17 | /// The following code demonstrates how to add a custom parameter to `API`: 18 | 19 | /// will be used later as a custom parameter of the api. 20 | enum VerificationType: Hashable { 21 | case normal 22 | case special 23 | } 24 | 25 | extension API { 26 | /// You can extend the `API` structure to add your custom parameters to the property wrapper 27 | /// by adding custom initialization methods, while keeping the types as you wish. 28 | /// 29 | /// **Note**: The first parameter `wrappedValue` cannot be omitted! 30 | convenience init( 31 | wrappedValue: ParameterBuilder? = nil, 32 | _ path: String, 33 | verification: VerificationType? = nil 34 | ) { 35 | self.init(wrappedValue: wrappedValue, path, userInfo: ["verification": verification]) 36 | } 37 | } 38 | 39 | // MARK: - AdvancedAPI 40 | 41 | enum AdvancedAPI { 42 | /// Finally, the new initialization method declared above is called on 43 | /// the property wrapper to complete the interface definition. 44 | @GET("/api", verification: .normal) 45 | static var testAPI: APIParameterBuilder<()>? = nil 46 | } 47 | 48 | //: [Next](@next) 49 | -------------------------------------------------------------------------------- /Demo.playground/Pages/Basic.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | 3 | import Foundation 4 | 5 | import APIWrapper 6 | 7 | // MARK: - BasicAPI 8 | 9 | /// : 10 | /// 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. 11 | /// 12 | /// The return value of this api depends on the parameters and will return the parameters, headers and other data as is. 13 | 14 | //: To begin by showing some of the most basic uses, look at how the api is defined. 15 | 16 | enum BasicAPI { 17 | /// This is an api for requests using the **GET** method. 18 | /// 19 | /// The full api address is: [](https://postman-echo.com/get?foo1=bar1&foo2=bar2) . 20 | /// The api does not require the caller to pass in any parameters. 21 | @GET("/get?foo1=bar1&foo2=bar2") 22 | static var get: APIParameterBuilder<()>? = nil 23 | } 24 | 25 | //: After defining the api, try to execute the request: 26 | 27 | do { 28 | // Requests the api and parses the return value of the interface. Note the use of the `$` character. 29 | let response = try await BasicAPI.$get.request(to: PostManResponse.self) 30 | 31 | // You can also ignore the return value and focus only on the act of requesting the api itself. 32 | try await BasicAPI.$get.request() 33 | 34 | } catch { 35 | Log.log("❌ get request failure: \(error)") 36 | } 37 | 38 | //: The api with parameters is a little more complicated to define: 39 | 40 | extension BasicAPI { 41 | /// This is an api for requests using the **POST** method. 42 | /// 43 | /// The full api address is: [](https://postman-echo.com/post) . 44 | /// The api is entered as a **tuple** type and requires two parameters, where the second parameter can be `nil`. 45 | @POST("/post") 46 | static var postWithTuple: APIParameterBuilder<(foo1: String, foo2: Int?)>? = .init { 47 | [ 48 | "foo1": $0.foo1, 49 | "foo2": $0.foo2, 50 | ] 51 | } 52 | 53 | /// This is an api for requests using the **POST** method. 54 | /// 55 | /// The full api address is: [](https://postman-echo.com/post) . 56 | /// This api is referenced with the `Arg` type. 57 | @POST("/post") 58 | static var postWithModel: APIParameterBuilder? = .init { $0 } 59 | } 60 | 61 | do { 62 | // Request the api and parse the return value. 63 | let tupleAPIResponse = try await BasicAPI.$postWithTuple.request( 64 | with: (foo1: "foo1", foo2: nil), 65 | to: PostManResponse.self 66 | ) 67 | 68 | /// If you look at the return value, you will see that `foo2` is not passed to the server. 69 | /// This is because `RaAPIWrapper` filters out all parameters with the value `nil`. 70 | 71 | // Try using model as a parameter and you will get the same result. 72 | let modelAPIResponse = try await BasicAPI.$postWithModel.request(with: .init(foo2: "foo2"), to: PostManResponse.self) 73 | 74 | } catch { 75 | Log.log("❌ post request failure: \(error)") 76 | } 77 | 78 | //: [Next](@next) 79 | -------------------------------------------------------------------------------- /Demo.playground/Pages/Combine.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | 5 | import Combine 6 | import Foundation 7 | import ObjectiveC 8 | 9 | import APIWrapper 10 | 11 | // MARK: - CombineAPI 12 | 13 | /// : 14 | /// The design goal of `RaAPIWrapper` is to better encapsulate requests and simplify the request process rather than execute them. 15 | /// 16 | /// So we don't provide any methods for request api. 17 | /// You can define your own request methods by referring to the code in the `Demo/Sources/API+Request.swift` file. 18 | /// 19 | /// Here are 2 request wrappers for `Combine`, which are roughly written for reference only: 20 | 21 | /// For subsequent examples 22 | enum CombineAPI { 23 | @POST("/post") 24 | static var post: APIParameterBuilder? = { $0 } 25 | } 26 | 27 | // MARK: - AnyPublisher 28 | 29 | //: The first way: deliver an `AnyPublisher` object externally and subscribe to it to trigger requests. 30 | 31 | extension API { 32 | func requestPublisher(with params: Parameter) -> AnyPublisher { 33 | let info = createRequestInfo(params) 34 | 35 | // To simplify the demo process, here is a forced unpacking 36 | guard let url = URL(string: "https://postman-echo.com" + info.path) else { 37 | fatalError("url(\(info.path) nil!") 38 | } 39 | 40 | var request = URLRequest(url: url) 41 | request.httpMethod = info.httpMethod.rawValue 42 | 43 | if let parameters = info.parameters { 44 | do { 45 | request.httpBody = try JSONEncoder().encode(parameters) 46 | } catch { 47 | fatalError("Encoder failure: \(error)") 48 | } 49 | 50 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 51 | } 52 | 53 | return URLSession.shared 54 | .dataTaskPublisher(for: request) 55 | .map { data, _ in data } 56 | .mapError { $0 } 57 | .eraseToAnyPublisher() 58 | } 59 | } 60 | 61 | var cancellable = Set() 62 | let publisher = CombineAPI.$post.requestPublisher(with: "123") 63 | publisher 64 | .sink( 65 | receiveCompletion: { Log.log($0) }, 66 | receiveValue: { Log.log(String(data: $0, encoding: .utf8) as Any) } 67 | ) 68 | .store(in: &cancellable) 69 | 70 | // MARK: - PassthroughSubject 71 | 72 | /// : 73 | /// The second one is to provide a `PassthroughSubject` object to the outside world, 74 | /// send parameters when requesting the api, subscribe to the object at other places, 75 | /// accept the parameters and send the request. 76 | 77 | private var kParamSubjectKey = "kParamSubjectKey" 78 | 79 | extension API { 80 | @available(iOS 13.0, *) 81 | public var paramSubject: PassthroughSubject? { 82 | get { 83 | if let value = objc_getAssociatedObject(self, &kParamSubjectKey) as? PassthroughSubject { 84 | return value 85 | } 86 | let paramSubject = PassthroughSubject() 87 | objc_setAssociatedObject(self, &kParamSubjectKey, paramSubject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 88 | return paramSubject 89 | } 90 | set { objc_setAssociatedObject(self, &kParamSubjectKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } 91 | } 92 | 93 | @available(iOS 13.0, *) 94 | public func requestPublisher() -> AnyPublisher? { 95 | paramSubject?.flatMap { self.requestPublisher(with: $0) }.eraseToAnyPublisher() 96 | } 97 | } 98 | 99 | let api = CombineAPI.$post 100 | 101 | api.requestPublisher()? 102 | .sink( 103 | receiveCompletion: { Log.log($0) }, 104 | receiveValue: { Log.log(String(data: $0, encoding: .utf8) as Any) } 105 | ) 106 | .store(in: &cancellable) 107 | 108 | api.paramSubject?.send("233") 109 | api.paramSubject?.send("433") 110 | api.paramSubject?.send(completion: .finished) 111 | 112 | //: [Next](@next) 113 | -------------------------------------------------------------------------------- /Demo.playground/Sources/API+Request.swift: -------------------------------------------------------------------------------- 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 | 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 | public func request(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 | public 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 | 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 | public func request(to type: T.Type) async throws -> T { 45 | 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 | public func request() async throws { 53 | try await request(with: ()) 54 | } 55 | } 56 | 57 | // MARK: - Tools 58 | 59 | extension API { 60 | private 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 | guard let url = URL(string: "https://postman-echo.com" + info.path) else { 65 | fatalError("url(\(info.path) nil!") 66 | } 67 | 68 | Log.log("▶️ Requests will begin soon: \(url.absoluteString)") 69 | 70 | var request = URLRequest(url: url) 71 | request.httpMethod = info.httpMethod.rawValue 72 | 73 | if let parameters = info.parameters { 74 | Log.log("🚧 parameters: \(parameters)") 75 | request.httpBody = try JSONEncoder().encode(parameters) 76 | 77 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 78 | } 79 | 80 | let (data, response) = try await URLSession.shared.data(for: request) 81 | Log.log("✅ \(response.url?.absoluteString ?? "nil") End of request") 82 | 83 | return data 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Demo.playground/Sources/Log.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Log { 4 | static func log(_ content: Any) { 5 | // swiftlint:disable:next no_direct_standard_out_logs 6 | print(content) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Demo.playground/Sources/PostManResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias DemoResponse = Codable & Hashable 4 | 5 | // MARK: - PostManResponse 6 | 7 | public struct PostManResponse: DemoResponse { 8 | public let args: T? 9 | 10 | public let data: T? 11 | 12 | public let url: String 13 | 14 | public let headers: [String: String] 15 | } 16 | 17 | // MARK: - Arg 18 | 19 | public struct Arg: DemoResponse { 20 | private enum CodingKeys: String, CodingKey { 21 | case foo1 22 | case foo2 23 | } 24 | 25 | let foo1: String? 26 | 27 | let foo2: String? 28 | 29 | public init(foo1: String? = nil, foo2: String? = nil) { 30 | self.foo1 = foo1 31 | self.foo2 = foo2 32 | } 33 | 34 | public init(from decoder: Decoder) throws { 35 | let c = try decoder.container(keyedBy: CodingKeys.self) 36 | 37 | foo1 = try c.decodeIfPresent(String.self, forKey: .foo1) 38 | foo2 = try c.decodeIfPresent(String.self, forKey: .foo2) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Demo.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rakuyo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "alamofire", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Alamofire/Alamofire.git", 7 | "state" : { 8 | "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", 9 | "version" : "5.9.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/RakuyoKit/swift.git", 16 | "state" : { 17 | "revision" : "48c5586f3b28bff531f067fef14898c8c862fa25", 18 | "version" : "1.1.3" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-argument-parser", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-argument-parser", 25 | "state" : { 26 | "revision" : "46989693916f56d1186bd59ac15124caef896560", 27 | "version" : "1.3.1" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "APIWrapper", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macOS(.v10_14), 10 | .tvOS(.v12), 11 | .watchOS(.v5), 12 | ], 13 | products: [ 14 | .library(name: "APIWrapper", targets: ["APIWrapper"]), 15 | .library(name: "AFAPIWrapper", targets: ["AFAPIWrapper"]), 16 | ], 17 | dependencies: [ 18 | .package( 19 | url: "https://github.com/Alamofire/Alamofire.git", 20 | .upToNextMajor(from: "5.0.0") 21 | ), 22 | ], 23 | targets: [ 24 | .target( 25 | name: "APIWrapper", 26 | path: "Sources/Core" 27 | ), 28 | .target( 29 | name: "AFAPIWrapper", 30 | dependencies: ["APIWrapper", "Alamofire"], 31 | path: "Sources/Alamofire" 32 | ), 33 | .testTarget( 34 | name: "APIWrapperTests", 35 | dependencies: ["APIWrapper"], 36 | path: "Tests" 37 | ), 38 | ] 39 | ) 40 | 41 | #if swift(>=5.6) 42 | // Add the Swift formatting plugin if possible 43 | package.dependencies.append(.package(url: "https://github.com/RakuyoKit/swift.git", from: "1.1.3")) 44 | #endif 45 | -------------------------------------------------------------------------------- /Package@swift-5.9.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "APIWrapper", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macOS(.v10_14), 10 | .tvOS(.v12), 11 | .watchOS(.v5), 12 | .visionOS(.v1), 13 | ], 14 | products: [ 15 | .library(name: "APIWrapper", targets: ["APIWrapper"]), 16 | .library(name: "AFAPIWrapper", targets: ["AFAPIWrapper"]), 17 | ], 18 | dependencies: [ 19 | .package( 20 | url: "https://github.com/Alamofire/Alamofire.git", 21 | .upToNextMajor(from: "5.9.0") 22 | ), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "APIWrapper", 27 | path: "Sources/Core", 28 | resources: [.copy("PrivacyInfo.xcprivacy")] 29 | ), 30 | .target( 31 | name: "AFAPIWrapper", 32 | dependencies: ["APIWrapper", "Alamofire"], 33 | path: "Sources/Alamofire" 34 | ), 35 | .testTarget( 36 | name: "APIWrapperTests", 37 | dependencies: ["APIWrapper"] 38 | ), 39 | ] 40 | ) 41 | 42 | // Add the Swift formatting plugin if possible 43 | package.dependencies.append(.package(url: "https://github.com/RakuyoKit/swift.git", from: "1.1.3")) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RaAPIWrapper 2 | 3 |

4 | 5 | 6 |

7 |

8 | 9 | 10 |

11 | 12 | > [中文](https://github.com/RakuyoKit/RaAPIWrapper/blob/main/README_CN.md) 13 | 14 | `RaAPIWrapper` uses `@propertyWrapper` to achieve a similar effect to that of defining network requests in the Android `Retrofit` library. 15 | 16 | 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. 17 | 18 | ## Say it before 19 | 20 | **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. 21 | 22 | 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. 23 | 24 | ## Requirements 25 | 26 | - **iOS 12**、**macOS 10.14**、**watchOS 5.0**、**tvOS 12** or later. 27 | - **Xcode 14** or later required. 28 | - **Swift 5.7** or later required. 29 | 30 | ## Example 31 | 32 | ```swift 33 | @GET("/api/v1/no_param") 34 | static var noParamAPI: APIParameterBuilder<()>? = nil 35 | 36 | @POST("/api/v1/tuple_param") 37 | static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = .init { 38 | // `Dictionary` and `Array` can be used directly as parameters. 39 | ["id": $0.id, "name": $0.name] 40 | } 41 | 42 | @POST("/post") 43 | static var postWithModel: APIParameterBuilder? = .init { 44 | // When the parameter `Arg` complies with the `APIParameter` (`Encodable & Hashable`) protocol, 45 | // it can be used directly as a parameter. 46 | $0 47 | } 48 | ``` 49 | 50 | ## Install 51 | 52 | ### CocoaPods 53 | 54 | ```ruby 55 | pod 'RaAPIWrapper' 56 | ``` 57 | 58 | If your project relies on `Alamofire`, then you may also consider relying on `RaAPIWrapper/AF`. This module provides a wrapper for `ParameterEncoding`. 59 | 60 | ### Swift Package Manager 61 | 62 | - File > Swift Packages > Add Package Dependency 63 | - Add https://github.com/RakuyoKit/RaAPIWrapper.git 64 | - Select "Up to Next Major" and fill in the corresponding version number 65 | 66 | Or add the following to your `Package.swift` file: 67 | 68 | ```swift 69 | dependencies: [ 70 | .package( 71 | url: "https://github.com/RakuyoKit/RaAPIWrapper.git", 72 | .upToNextMajor(from: "1.2.4") 73 | ) 74 | ] 75 | ``` 76 | 77 | ## Usage 78 | 79 | Please refer to the example in `Demo.playground`. 80 | 81 | > 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. 82 | 83 | ## License 84 | 85 | `RaAPIWrapper` is available under the **MIT** license. For more information, see [LICENSE](LICENSE). 86 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # RaAPIWrapper 2 | 3 |

4 | 5 | 6 |

7 |

8 | 9 | 10 |

11 | 12 | `RaAPIWrapper` 利用 `@propertyWrapper` 来实现类似于 Android `Retrofit` 库中定义网络请求的效果。 13 | 14 | 在同一个文件中包含大量的网络请求接口时,`RaAPIWrapper` 可以帮助您以更聚合的形式定义每一个请求,让您不用在文件内来回跳转。 15 | 16 | ## 说在前面 17 | 18 | **特别注意!**:`RaAPIWrapper` 仅仅是一个**定义**网络请求的语法糖。您需要在此基础上借助 `Alamofire`、`Moya` 、其他第三方网络框架或者直接调用 `URLSession` 来发起网络请求。 19 | 20 | 好的一点是,您只需要修改少量的代码,甚至无需修改代码,就可以很简单地将 `RaAPIWrapper` 集成进您已有的项目,`RaAPIWrapper` 可以很好的和您项目中现有的网络框架共存。 21 | 22 | ## 基本要求 23 | 24 | - 运行 **iOS 12**、**macOS 10.14**、**watchOS 5.0**、**tvOS 12** 及以上版本的设备。 25 | - 使用 **Xcode 14** 及以上版本编译运行。 26 | - **Swift 5.7** 及以上版本。 27 | 28 | ## 示例 29 | 30 | ```swift 31 | @GET("/api/v1/no_param") 32 | static var noParamAPI: APIParameterBuilder<()>? = nil 33 | 34 | @POST("/api/v1/tuple_param") 35 | static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = .init { 36 | // 字典和数组可直接作为参数使用 37 | ["id": $0.id, "name": $0.name] 38 | } 39 | 40 | @POST("/post") 41 | static var postWithModel: APIParameterBuilder? = .init { 42 | // 当参数 `Arg` 遵守 `APIParameter`(`Encodable & Hashable`) 协议时,可直接作为参数使用。 43 | $0 44 | } 45 | ``` 46 | 47 | ## 安装 48 | 49 | ### CocoaPods 50 | 51 | ```ruby 52 | pod 'RaAPIWrapper' 53 | ``` 54 | 55 | 如果您的项目依赖了 `Alamofire`,那么您还可以考虑依赖 `RaAPIWrapper/AF`。该模块提供了针对 `ParameterEncoding` 的封装。 56 | 57 | ### Swift Package Manager 58 | 59 | - 依次选择 File > Swift Packages > Add Package Dependency 60 | - 输入 https://github.com/RakuyoKit/RaAPIWrapper.git 61 | - 选择 "Up to Next Major" 并填入对应的版本号 62 | 63 | 或者将下面的内容添加到 `Package.swift` 文件中: 64 | 65 | ```swift 66 | dependencies: [ 67 | .package( 68 | url: "https://github.com/RakuyoKit/RaAPIWrapper.git", 69 | .upToNextMajor(from: "1.2.4") 70 | ) 71 | ] 72 | ``` 73 | 74 | ## 使用 75 | 76 | 请参考 `Demo.playground` 中的示例。 77 | 78 | > 因为 playground 以 Swift Package Manager 的形式依赖 `RaAPIWrapper`,所以请先通过 `Package.swift` 打开项目,再从左侧的导航栏中选择 `Demo.playground`,运行相关内容。 79 | 80 | ## License 81 | 82 | `RaAPIWrapper` 在 **MIT** 许可下可用。 有关更多信息,请参见 [LICENSE](LICENSE) 文件。 83 | -------------------------------------------------------------------------------- /RaAPIWrapper.podspec: -------------------------------------------------------------------------------- 1 | # pod lib lint --verbose --allow-warnings RaAPIWrapper.podspec 2 | # pod trunk push --verbose --allow-warnings RaAPIWrapper.podspec 3 | 4 | Pod::Spec.new do |s| 5 | 6 | s.name = 'RaAPIWrapper' 7 | 8 | s.version = '1.2.4' 9 | 10 | s.summary = 'Makes it easier to define a network request.' 11 | 12 | s.description = 'Use `@propertyWrapper to provide the necessary data for network requests in a more aggregated form.' 13 | 14 | s.homepage = 'https://github.com/RakuyoKit/RaAPIWrapper' 15 | 16 | s.license = 'MIT' 17 | 18 | s.author = { 'Rakuyo' => 'rakuyo.mo@gmail.com' } 19 | 20 | s.source = { :git => 'https://github.com/RakuyoKit/RaAPIWrapper.git', :tag => s.version.to_s } 21 | 22 | s.requires_arc = true 23 | 24 | s.ios.deployment_target = '12.0' 25 | s.osx.deployment_target = '10.14' 26 | s.tvos.deployment_target = '12.0' 27 | s.watchos.deployment_target = '5.0' 28 | 29 | s.swift_versions = ['5'] 30 | 31 | s.module_name = 'APIWrapper' 32 | 33 | s.default_subspec = "Core" 34 | 35 | s.subspec "Core" do |cs| 36 | cs.source_files = 'Sources/Core/**/*' 37 | 38 | cs.resource_bundles = { 'RaAPIWrapper' => ['Sources/Core/PrivacyInfo.xcprivacy'] } 39 | end 40 | 41 | s.subspec "AF" do |cs| 42 | cs.source_files = 'Sources/Alamofire/*' 43 | 44 | cs.dependency "RaAPIWrapper/Core" 45 | cs.dependency "Alamofire", '>= 5.8.0', '< 6.0.0' 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | module Tools 2 | def mise_exec_prefix 3 | 'mise exec --' 4 | end 5 | 6 | def ci_mode? 7 | ENV['CI'] == 'true' 8 | end 9 | 10 | def ci_debug_mode? 11 | ENV['RUNNER_DEBUG'] == '1' || ENV['ACTIONS_RUNNER_DEBUG'] == 'true' 12 | end 13 | end 14 | 15 | # Reference other rake files to avoid adding the -f parameter when executing the rake command 16 | FileList['**/*.rb'].each { |rf| require_relative rf } 17 | 18 | namespace :env do 19 | include Tools 20 | 21 | desc 'Init env' 22 | task :init do 23 | if File.exist?('.mise.toml') 24 | install_mise 25 | puts "mise installed: #{`mise --version`}" 26 | sh "mise install" 27 | end 28 | 29 | if File.exist?('.pre-commit-config.yaml') 30 | Rake::Task['env:githook'].invoke 31 | end 32 | end 33 | 34 | desc 'Install git hook' 35 | task :githook do 36 | sh "#{mise_exec_prefix} pre-commit install" 37 | end 38 | 39 | def install_mise 40 | output = `which mise >/dev/null 2>&1` 41 | if $?.success? 42 | return 43 | end 44 | 45 | puts "mise not found, installing..." 46 | sh "curl https://mise.run | sh" 47 | 48 | case ENV['SHELL'] 49 | when /bash/ 50 | sh 'echo "eval \"\$(~/.local/bin/mise activate bash)\"" >> ~/.bashrc' 51 | sh "source ~/.bashrc" 52 | 53 | when /zsh/ 54 | sh 'echo "eval \"\$(~/.local/bin/mise activate zsh)\"" >> ~/.zshrc' 55 | sh "zsh -c 'source ~/.zshrc'" 56 | 57 | else 58 | puts "Unknown shell env!" 59 | exit 1 60 | end 61 | end 62 | end 63 | 64 | namespace :swift do 65 | FORMAT_COMMAND = 'swift package --allow-writing-to-package-directory format' 66 | 67 | desc 'Run Format' 68 | task :format do 69 | sh FORMAT_COMMAND 70 | end 71 | 72 | desc 'Run Lint' 73 | task :lint do 74 | sh FORMAT_COMMAND + ' --lint' 75 | end 76 | 77 | desc 'Test' 78 | task :test do 79 | sh 'swift test' 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /Sources/Alamofire/API+AF.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API+AF.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/19. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !COCOAPODS 12 | import APIWrapper 13 | #endif 14 | 15 | let parameterEncodingKey = "af_parameter_encoding" 16 | 17 | extension API { 18 | public convenience init( 19 | wrappedValue: ParameterBuilder?, 20 | _ path: String, 21 | specialBaseURL: URL? = nil, 22 | header: HeaderBuilder? = nil, 23 | parameterEncoding: AnyAPIParameterEncoding, 24 | userInfo: APIRequestUserInfo = [:] 25 | ) { 26 | var _userInfo = userInfo 27 | _userInfo[parameterEncodingKey] = parameterEncoding 28 | 29 | self.init( 30 | wrappedValue: wrappedValue, 31 | path, 32 | specialBaseURL: specialBaseURL, 33 | header: header, 34 | userInfo: _userInfo 35 | ) 36 | } 37 | } 38 | 39 | extension API { 40 | /// Encoding of `Parameters`. 41 | public var parameterEncoding: AnyAPIParameterEncoding? { 42 | userInfo[parameterEncodingKey] as? AnyAPIParameterEncoding 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Alamofire/APIRequestInfo+AF.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRequestInfo+AF 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/19. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !COCOAPODS 12 | import APIWrapper 13 | #endif 14 | 15 | extension APIRequestInfo { 16 | /// Encoding of `Parameters`. 17 | public var parameterEncoding: AnyAPIParameterEncoding? { 18 | userInfo[parameterEncodingKey] as? AnyAPIParameterEncoding 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Alamofire/AnyAPIHashableParameterEncoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyHashableAPIParameterEncoding.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import Alamofire 12 | 13 | #if !COCOAPODS 14 | import APIWrapper 15 | #endif 16 | 17 | /// Represents an arbitrary api parameter. 18 | public typealias AnyAPIParameterEncoding = AnyAPIHashableParameterEncoding 19 | 20 | // MARK: - AnyAPIHashableParameterEncoding 21 | 22 | /// Make `AlamofireParameterEncoding` follow `Hashable` protocol. 23 | public struct AnyAPIHashableParameterEncoding: AnyAPIHashable { 24 | public typealias Value = ParameterEncoding 25 | 26 | public typealias Input = Hashable & Value 27 | 28 | public let value: Value 29 | 30 | public let equals: (Value) -> Bool 31 | 32 | public let hash: (_ hasher: inout Hasher) -> Void 33 | 34 | public init(_ value: T) { 35 | self.value = value 36 | equals = { ($0 as? T == value) } 37 | hash = { $0.combine(value) } 38 | } 39 | } 40 | 41 | // MARK: - AnyAPIParameterEncoding + ParameterEncoding 42 | 43 | extension AnyAPIParameterEncoding: ParameterEncoding { 44 | public func encode( 45 | _ urlRequest: Alamofire.URLRequestConvertible, 46 | with parameters: Alamofire.Parameters? 47 | ) throws -> URLRequest { 48 | try value.encode(urlRequest, with: parameters) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Core/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyTracking 9 | 10 | NSPrivacyTrackingDomains 11 | 12 | 13 | NSPrivacyCollectedDataTypes 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/APIHTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIHTTPMethod.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - APIHTTPMethod 12 | 13 | /// The HTTP method to use when requesting the api. 14 | /// 15 | /// With the `ExpressibleByStringLiteral` protocol, 16 | /// you can initialize the object directly with string literals 17 | public struct APIHTTPMethod: RawRepresentable { 18 | public typealias RawValue = String 19 | 20 | public var rawValue: RawValue 21 | 22 | public init(rawValue: RawValue) { 23 | self.rawValue = rawValue 24 | } 25 | } 26 | 27 | // MARK: ExpressibleByStringLiteral 28 | 29 | extension APIHTTPMethod: ExpressibleByStringLiteral { 30 | public init(stringLiteral value: RawValue) { 31 | self.init(rawValue: value) 32 | } 33 | } 34 | 35 | // MARK: Hashable 36 | 37 | extension APIHTTPMethod: Hashable { } 38 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/APIHeaders.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIHeaders.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Defines the type of header you can pass into the `API` property wrapper. 12 | public typealias APIHeaders = [String: String] 13 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/APIParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIParameter.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2023/01/13. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Used to constrain what types can be used as api parameters. 12 | public typealias APIParameter = AnyAPIHashableParameter.Input 13 | 14 | /// Represents an arbitrary api parameter. 15 | public typealias AnyAPIParameter = AnyAPIHashableParameter 16 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/APIRequestInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRequestInfo.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/25. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - APIRequestInfo 12 | 13 | /// Information needed to request the api 14 | public struct APIRequestInfo { 15 | /// The path to the requested api 16 | public let path: String 17 | 18 | /// A special api base url. 19 | /// 20 | /// Should be a setting item independent of your global configuration. 21 | public let specialBaseURL: URL? 22 | 23 | /// Type representing HTTP methods 24 | public let httpMethod: APIHTTPMethod 25 | 26 | /// API header 27 | public let header: APIHeaders? 28 | 29 | /// Parameters of the requested api 30 | public let parameters: AnyAPIParameter? 31 | 32 | /// An additional storage space. 33 | /// You can use this property to store some custom data. 34 | public let userInfo: APIRequestUserInfo 35 | 36 | public init( 37 | path: String, 38 | specialBaseURL: URL? = nil, 39 | httpMethod: APIHTTPMethod, 40 | header: APIHeaders? = nil, 41 | parameterBuild: APIParameterBuilder? = nil, 42 | parameterInput: ParamType? = nil, 43 | userInfo: APIRequestUserInfo = [:] 44 | ) { 45 | self.path = path 46 | self.specialBaseURL = specialBaseURL 47 | self.httpMethod = httpMethod 48 | self.header = header 49 | self.userInfo = userInfo 50 | 51 | parameters = { 52 | guard 53 | let parameter = parameterInput, 54 | let build = parameterBuild 55 | else { 56 | return nil 57 | } 58 | 59 | let result = build(parameter) 60 | if let value = result as? AnyAPIParameter { 61 | return value 62 | } 63 | return .init(result) 64 | }() 65 | } 66 | } 67 | 68 | // MARK: Hashable 69 | 70 | extension APIRequestInfo: Hashable { } 71 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/APIRequestUserInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRequestUserInfo.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/25. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Defines the type of custom data you can pass into the `API` property wrapper. 12 | public typealias APIRequestUserInfo = [AnyHashable: AnyHashable] 13 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyAPIHashable.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - AnyAPIHashable 12 | 13 | /// You can make any property or protocol follow the `Hashable` protocol through this protocol. 14 | /// 15 | /// Define a type, then make it follow the `AnyAPIHashable` protocol, 16 | /// then specify the type of Value as the type you want to make it follow the Hashable. 17 | /// That's all you need to do to get the job done. 18 | /// 19 | /// See `AnyAPIHashableParameter` or `AnyAPIHashableParameterEncoding` for details on how to use it. 20 | /// the former makes the `Encodable` protocol follow `Hashable`, 21 | /// the latter makes `Alamofire.ParameterEncoding` do the same thing. 22 | public protocol AnyAPIHashable: Hashable, CustomStringConvertible, CustomDebugStringConvertible { 23 | /// The type itself that you want to make follow the Hashable protocol. 24 | /// For example: `typealias Value = Encodable` 25 | associatedtype Value 26 | 27 | /// Storing the original object. 28 | var value: Value { get } 29 | 30 | /// Used to implement `Equatable`. 31 | /// 32 | /// When using this type, you do not need to care about the specifics of the value. 33 | var equals: (Value) -> Bool { get } 34 | 35 | /// Used to implement `Hashable`. 36 | /// 37 | /// When using this type, you do not need to care about the specifics of the value. 38 | var hash: (_ hasher: inout Hasher) -> Void { get } 39 | } 40 | 41 | // MARK: - Hashable 42 | 43 | extension AnyAPIHashable { 44 | public static func == (lhs: Self, rhs: Self) -> Bool { 45 | lhs.equals(rhs.value) 46 | } 47 | 48 | public func hash(into hasher: inout Hasher) { 49 | hash(&hasher) 50 | } 51 | } 52 | 53 | // MARK: - CustomStringConvertible 54 | 55 | extension AnyAPIHashable { 56 | public var description: String { 57 | // When printing the logs, only the objects that need attention are kept by themselves. 58 | .init(describing: value) 59 | } 60 | } 61 | 62 | // MARK: - CustomDebugStringConvertible 63 | 64 | extension AnyAPIHashable { 65 | public var debugDescription: String { 66 | // In debug mode, focus on the full content 67 | .init(describing: self) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/Core/RequestInfo/AnyAPIHashable/AnyAPIHashableParameter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyAPIHashableParameter.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - AnyAPIHashableParameter 12 | 13 | /// Make `Encodable` follow `Hashable` protocol. 14 | public struct AnyAPIHashableParameter: AnyAPIHashable { 15 | public typealias Value = Encodable 16 | 17 | public typealias Input = Hashable & Value 18 | 19 | public let value: Value 20 | 21 | public let equals: (Value) -> Bool 22 | 23 | public let hash: (_ hasher: inout Hasher) -> Void 24 | 25 | public init(_ value: T) { 26 | self.value = value 27 | equals = { ($0 as? T == value) } 28 | hash = { $0.combine(value) } 29 | } 30 | } 31 | 32 | // MARK: Encodable 33 | 34 | extension AnyAPIHashableParameter: Encodable { 35 | public func encode(to encoder: Encoder) throws { 36 | try value.encode(to: encoder) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/25. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - APIHTTPMethodWrapper 12 | 13 | /// Used to encapsulate the `APIHTTPMethod` object provided to the `API`. 14 | public protocol APIHTTPMethodWrapper { 15 | static var httpMethod: APIHTTPMethod { get } 16 | } 17 | 18 | // MARK: - API 19 | 20 | /// API wrapper. Used to wrap the data needed to request an api. 21 | @propertyWrapper 22 | public class API { 23 | public typealias HeaderBuilder = (Parameter) -> APIHeaders 24 | 25 | public typealias ParameterBuilder = APIParameterBuilder 26 | 27 | /// Enables you to access `@propertyWrapper` objects via the `$` symbol. 28 | public var projectedValue: API { self } 29 | 30 | /// Parameter constructor for the api. 31 | public var wrappedValue: ParameterBuilder? 32 | 33 | /// A special api base url. 34 | /// 35 | /// Should be a setting item independent of your global configuration. 36 | public let specialBaseURL: URL? 37 | 38 | /// The path to the requested api. 39 | public let path: String 40 | 41 | /// Used to construct the api request header. 42 | public let headerBuilder: HeaderBuilder? 43 | 44 | /// An additional storage space. 45 | /// You can use this property to store some custom data. 46 | public let userInfo: APIRequestUserInfo 47 | 48 | public init( 49 | wrappedValue: ParameterBuilder?, 50 | _ path: String, 51 | specialBaseURL: URL? = nil, 52 | header: HeaderBuilder? = nil, 53 | userInfo: APIRequestUserInfo = [:] 54 | ) { 55 | self.wrappedValue = wrappedValue 56 | self.path = path 57 | self.specialBaseURL = specialBaseURL 58 | headerBuilder = header 59 | self.userInfo = userInfo 60 | } 61 | } 62 | 63 | extension API { 64 | /// The HTTP method to use when requesting the api. 65 | /// 66 | /// Open this property so that you can access the request method 67 | /// directly through the `@propertyWrapper` object. 68 | public static var httpMethod: HTTPMethod.Type { HTTPMethod.self } 69 | 70 | /// Creates an `APIRequestInfo` object. 71 | /// 72 | /// Used to generate the final, minimal api data. 73 | public func createRequestInfo(_ parameter: Parameter) -> APIRequestInfo { 74 | .init( 75 | path: path, 76 | specialBaseURL: specialBaseURL, 77 | httpMethod: Self.httpMethod.httpMethod, 78 | header: headerBuilder?(parameter), 79 | parameterBuild: wrappedValue, 80 | parameterInput: parameter, 81 | userInfo: userInfo 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/APIParameterBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIParameterBuilder.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2023/01/13. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - APIParameterBuilder 12 | 13 | public struct APIParameterBuilder { 14 | public typealias Output = any APIParameter 15 | 16 | public typealias ParameterBuilder = (Input) -> Output 17 | 18 | private let parameterBuild: ParameterBuilder 19 | 20 | public init(_ build: @escaping ParameterBuilder) { 21 | parameterBuild = build 22 | } 23 | } 24 | 25 | extension APIParameterBuilder { 26 | public init(_ build: @escaping (Input) -> [Any?]) { 27 | parameterBuild = { build($0).toParameters } 28 | } 29 | 30 | public init(_ build: @escaping (Input) -> [String: Any?]) { 31 | parameterBuild = { build($0).toParameters } 32 | } 33 | } 34 | 35 | extension APIParameterBuilder { 36 | func callAsFunction(_ input: Input) -> Output { 37 | parameterBuild(input) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/APIParameterConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIParameterConvertible.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/25. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - APIParameterConvertible 12 | 13 | /// Means that the type can be converted to an interface parameter for requesting an api. 14 | protocol APIParameterConvertible { 15 | /// Converts the target to an Encodable-compliant type. 16 | var toParameters: AnyAPIParameter { get } 17 | } 18 | 19 | // MARK: - Array + APIParameterConvertible 20 | 21 | extension Array: APIParameterConvertible { 22 | /// For `[AnyObject]` arrays, or a mixed array with very complex elements. 23 | var toParameters: AnyAPIParameter { 24 | let filtered = (self as [Any?]).compactMap { $0 } 25 | 26 | let result: [AnyAPIParameter] = filtered.compactMap { 27 | if let value = $0 as? AnyAPIParameter { return value } 28 | if let value = $0 as? (any APIParameter) { return .init(value) } 29 | if let value = $0 as? APIParameterConvertible { return value.toParameters } 30 | return mapAnyObjectToEncodable($0 as AnyObject) 31 | } 32 | 33 | assert( 34 | filtered.count == result.count, 35 | "There are elements in the container type that are not nil that cannot be converted to AnyAPIParameter type. This behavior will cause the parameter to be discarded and not sent to the server. This behavior is usually dangerous and needs to be fixed, please check your code!" 36 | ) 37 | 38 | return .init(result) 39 | } 40 | } 41 | 42 | // MARK: - Dictionary + APIParameterConvertible 43 | 44 | extension Dictionary: APIParameterConvertible where Key == String { 45 | /// For a `[String: AnyObject]` dictionary, or a mixed dictionary with very complex elements. 46 | var toParameters: AnyAPIParameter { 47 | let filtered = (self as [String: Any?]).compactMapValues { $0 } 48 | 49 | let result: [String: AnyAPIParameter] = filtered.compactMapValues { 50 | if let value = $0 as? AnyAPIParameter { return value } 51 | if let value = $0 as? (any APIParameter) { return .init(value) } 52 | if let value = $0 as? APIParameterConvertible { return value.toParameters } 53 | 54 | if let value = $0 as? (any RawRepresentable) { 55 | return mapAnyObjectToEncodable(value.rawValue as AnyObject) 56 | } 57 | return mapAnyObjectToEncodable($0 as AnyObject) 58 | } 59 | 60 | assert( 61 | filtered.count == result.count, 62 | "There are elements in the container type that are not nil that cannot be converted to AnyAPIParameter type. This behavior will cause the parameter to be discarded and not sent to the server. This behavior is usually dangerous and needs to be fixed, please check your code!" 63 | ) 64 | 65 | return .init(result) 66 | } 67 | } 68 | 69 | // MARK: - Tools 70 | 71 | extension APIParameterConvertible { 72 | fileprivate func mapAnyObjectToEncodable(_ value: AnyObject) -> AnyAPIParameter? { 73 | if let result = value as? String { return .init(result) } 74 | if let result = value as? Character { return .init(String(result)) } 75 | if let result = value as? Int { return .init(result) } 76 | if let result = value as? UInt { return .init(result) } 77 | if let result = value as? Double { return .init(result) } 78 | if let result = value as? Float { return .init(result) } 79 | if let result = value as? Bool { return .init(result) } 80 | if let result = value as? Data { return .init(result) } 81 | if let result = value as? Date { return .init(result) } 82 | if let result = value as? [String: Any] { return result.toParameters } 83 | if let result = value as? [Any] { return result.toParameters } 84 | 85 | return nil 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/CONNECT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CONNECT.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - ConnectHTTPMethod 12 | 13 | public enum ConnectHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "CONNECT" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `CONNECT` api. 18 | public typealias CONNECT = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/DELETE.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DELETE.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - DeleteHTTPMethod 12 | 13 | public enum DeleteHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "DELETE" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `DELETE` api. 18 | public typealias DELETE = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/GET.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GET.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/26. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - GetHTTPMethod 12 | 13 | public enum GetHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "GET" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `GET` api. 18 | public typealias GET = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/HEAD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HEAD.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - HeadHTTPMethod 12 | 13 | public enum HeadHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "HEAD" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `HEAD` api. 18 | public typealias HEAD = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/OPTIONS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OPTIONS.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - OptionsHTTPMethod 12 | 13 | public enum OptionsHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "OPTIONS" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `OPTIONS` api. 18 | public typealias OPTIONS = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/PATCH.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PATCH.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - PatchHTTPMethod 12 | 13 | public enum PatchHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "PATCH" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `PATCH` api. 18 | public typealias PATCH = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/POST.swift: -------------------------------------------------------------------------------- 1 | // 2 | // POST.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/26. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - PostHTTPMethod 12 | 13 | public enum PostHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "POST" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `POST` api. 18 | public typealias POST = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/PUT.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PUT.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - PutHTTPMethod 12 | 13 | public enum PutHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "PUT" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `PUT` api. 18 | public typealias PUT = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/QUERY.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QUERY.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - QueryHTTPMethod 12 | 13 | public enum QueryHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "QUERY" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `QUERY` api. 18 | public typealias QUERY = API 19 | -------------------------------------------------------------------------------- /Sources/Core/Wrapper/HTTPMethod/TRACE.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TRACE.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - TraceHTTPMethod 12 | 13 | public enum TraceHTTPMethod: APIHTTPMethodWrapper { 14 | public static var httpMethod: APIHTTPMethod { "TRACE" } 15 | } 16 | 17 | /// Encapsulates the data needed to request the `TRACE` api. 18 | public typealias TRACE = API 19 | -------------------------------------------------------------------------------- /Tests/AvailabilityTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvailabilityTest.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/8/26. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import APIWrapper 11 | 12 | // MARK: - AvailabilityTests 13 | 14 | final class AvailabilityTests: XCTestCase { 15 | func testTupleParamAPI() throws { 16 | let param: (id: Int, name: String?) = (1, nil) 17 | let info = TestAPI.$tupleParamAPI.createRequestInfo(param) 18 | 19 | // Verify that `nil` is filtered out in `info.parameters` 20 | XCTAssertEqual(info.parameters, packToParameters(["id": 1])) 21 | XCTAssertNotEqual(info.parameters, packToParameters(["id": 1, "name": Optional.none])) 22 | 23 | XCTAssertEqual(info.httpMethod, PostHTTPMethod.httpMethod) 24 | XCTAssertEqual(info.path, TestAPI.path) 25 | XCTAssertNil(info.specialBaseURL) 26 | } 27 | 28 | func testEnumParamAPI() throws { 29 | let info = TestAPI.$enumAPI.createRequestInfo(.one) 30 | 31 | XCTAssertEqual(info.parameters, packToParameters(["id": TestEnum.one.rawValue])) 32 | } 33 | 34 | private func packToParameters(_ value: [String: Int?]) -> AnyAPIParameter { 35 | .init(value.mapValues { AnyAPIParameter($0) }) 36 | } 37 | } 38 | 39 | // MARK: - TestAPI 40 | 41 | private enum TestAPI { 42 | static let path = "/api/v1/tuple_param" 43 | 44 | @POST(Self.path) 45 | static var tupleParamAPI: APIParameterBuilder<(id: Int, name: String?)>? = .init { 46 | ["id": $0.id, "name": $0.name] 47 | } 48 | 49 | @POST(Self.path) 50 | static var test1API: APIParameterBuilder<(id: Int, name: String)>? = .init { 51 | ["id": $0.id, "name": $0.name] 52 | } 53 | 54 | @POST(Self.path) 55 | static var test2API: APIParameterBuilder<(id: String, name: String)>? = .init { 56 | ["id": $0.id, "name": $0.name] 57 | } 58 | 59 | @POST("/api/v1/enum_param") 60 | static var enumAPI: APIParameterBuilder? = .init { 61 | ["id": $0] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/ExtensibilityTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExtensibilityTest.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2022/12/15. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import APIWrapper 11 | 12 | // MARK: - ExtensibilityTests 13 | 14 | final class ExtensibilityTests: XCTestCase { 15 | func testExample() throws { 16 | let info = TestAPI.$testAPI.createRequestInfo(()) 17 | 18 | XCTAssertEqual(info.userInfo, ["mockType": MockType.someType]) 19 | XCTAssertNotEqual(info.userInfo, [:]) 20 | } 21 | } 22 | 23 | // MARK: - TestAPI 24 | 25 | private enum TestAPI { 26 | @GET("/api/v1/tuple_param", mockType: .someType) 27 | static var testAPI: APIParameterBuilder<()>? = nil 28 | } 29 | 30 | // MARK: - MockType 31 | 32 | enum MockType: Hashable { 33 | case someType 34 | } 35 | 36 | extension API { 37 | convenience init( 38 | wrappedValue: ParameterBuilder? = nil, 39 | _ path: String, 40 | mockType: MockType 41 | ) { 42 | self.init(wrappedValue: wrappedValue, path, userInfo: ["mockType": mockType]) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/TestEnum.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestEnum.swift 3 | // RaAPIWrapper 4 | // 5 | // Created by Rakuyo on 2023/5/16. 6 | // Copyright © 2024 RakuyoKit. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum TestEnum: Int { 12 | case one = 1 13 | case two = 2 14 | } 15 | -------------------------------------------------------------------------------- /script/ls.rb: -------------------------------------------------------------------------------- 1 | namespace :lib do 2 | require 'find' 3 | include Tools 4 | 5 | desc 'Release new version' 6 | task :release do 7 | podspec_file = Find.find('.').detect { |path| path =~ /\.podspec$/ && !path.include?('.build/') } 8 | 9 | name = nil 10 | version = nil 11 | 12 | File.readlines(podspec_file).each do |line| 13 | if line =~ /s.name/ 14 | name = line.split('=').last.strip.delete("'") 15 | elsif line =~ /s.version/ 16 | version = line.split('=').last.strip.delete("'") 17 | break 18 | end 19 | end 20 | 21 | puts "Releasing version #{version} of #{name}" 22 | 23 | execute_pod('lib', 'lint', name) 24 | 25 | main_branch = 'main' 26 | develop_branch = 'develop' 27 | release_branch = "release/#{version}" 28 | 29 | source_branch = if has_develop_branch? 30 | develop_branch 31 | else 32 | main_branch 33 | end 34 | 35 | sh "git checkout -b #{release_branch} #{source_branch}" 36 | git_message = "release: version #{version}" 37 | sh "git add . && git commit -m '#{git_message}' --no-verify --allow-empty" 38 | 39 | git_merge(release_branch, main_branch, "Merge branch '#{release_branch}'") 40 | 41 | sh "git tag #{version}" 42 | git_push(version) 43 | 44 | if source_branch == develop_branch 45 | git_merge(version, develop_branch, "Merge tag '#{version}' into #{develop_branch}") 46 | end 47 | 48 | sh "git branch -d #{release_branch}" 49 | 50 | execute_pod('trunk', 'push', name) 51 | end 52 | 53 | def execute_pod(command, subcommand, name) 54 | sh "pod #{command} #{subcommand} #{name}.podspec --allow-warnings --skip-tests" 55 | end 56 | 57 | def has_develop_branch? 58 | system "git show-branch develop &>/dev/null" 59 | end 60 | 61 | def git_merge(from_branch, to_branch, merge_message) 62 | sh "git checkout #{to_branch}" 63 | sh "git merge --no-ff -m '#{merge_message}' #{from_branch} --no-verify" 64 | git_push(to_branch, "--no-verify") 65 | end 66 | 67 | def git_push(branch, options = "") 68 | sh "git push origin #{branch} #{options}" 69 | end 70 | end 71 | --------------------------------------------------------------------------------