├── .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 |