├── .github
└── workflows
│ ├── ci.yml
│ └── codecov.yml
├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcuserdata
│ │ └── ijaewon.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ └── xcschemes
│ ├── APIRouter.xcscheme
│ └── URLRouter.xcscheme
├── CHANGELOG.md
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── URLRouter
│ ├── BodyBuilder.swift
│ ├── HeaderBuilder.swift
│ ├── QueryBuilder.swift
│ ├── RequestBuilder.swift
│ ├── RouterBuilder.swift
│ └── URLBuilder.swift
└── Tests
└── URLRouterTests
├── BodyBuilderTests.swift
├── HeaderBuilder.swift
├── QueryBuilderTests.swift
├── RequestBuilderTests.swift
├── RouterBuilderTests.swift
└── URLBuilderTests.swift
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | pull_request:
8 | schedule:
9 | - cron: 30 1 * * *
10 |
11 | jobs:
12 | test:
13 | runs-on: macos-12
14 | env:
15 | PROJECT: URLRouter.xcodeproj
16 | SCHEME: URLRouter-Package
17 | CODECOV_PACKAGE_NAME: URLRouter
18 | strategy:
19 | matrix:
20 | env:
21 | - sdk: iphonesimulator
22 | destination: platform=iOS Simulator,name=iPhone 14 Pro,OS=16.2
23 |
24 | - sdk: macosx
25 | destination: platform=macOS,arch=x86_64
26 |
27 | - sdk: appletvsimulator
28 | destination: platform=tvOS Simulator,name=Apple TV,OS=16.0
29 |
30 | - sdk: applewatchsimulator
31 | destination: platform=watchOS Simulator,OS=9.0,name=Apple Watch Series 5 (44mm)
32 |
33 | steps:
34 | - uses: actions/checkout@v3
35 | - name: List SDKs and Devices
36 | run: xcodebuild -showsdks && xcrun xctrace list devices
37 | - name: Generate Xcode Project
38 | run: swift package generate-xcodeproj
39 | - name: Build and Test
40 | run: |
41 | xcodebuild clean build test \
42 | -project "$PROJECT" \
43 | -scheme "$SCHEME" \
44 | -destination "$DESTINATION" \
45 | -configuration Debug \
46 | -enableCodeCoverage YES
47 | env:
48 | SDK: ${{ matrix.env.sdk }}
49 | DESTINATION: ${{ matrix.env.destination }}
50 | - name: Upload Code Coverage
51 | run: |
52 | bash <(curl -s https://codecov.io/bash) \
53 | -X xcodeplist \
54 | -J "$CODECOV_PACKAGE_NAME"
55 | env:
56 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | patch:
4 | default:
5 | enabled: no
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 | *.xcuserstate
11 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/ijaewon.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devyhan/URLRouter/1fac1d8a61979c00885c35cabb10c3e36b54a2ff/.swiftpm/xcode/package.xcworkspace/xcuserdata/ijaewon.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/APIRouter.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
45 |
46 |
48 |
54 |
55 |
56 |
57 |
58 |
68 |
69 |
75 |
76 |
82 |
83 |
84 |
85 |
87 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/URLRouter.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
67 |
68 |
74 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog 📝
2 |
3 | 
4 | |Release|0.1.5|
5 | |:---|:---|
6 | |Changed|Changed the APIRouter name to URLRouter which is a universal meaning #33 by @devyhan|
7 | |Fixed|Fixed the public scope of the static parameter and static function of `Scheme` components #32 by @devyhan
Fixed explicitly disable codecov/patch coverage #35 by @devyhan|
8 |
9 | ---
10 |
11 | 
12 | |Release|0.1.4|
13 | |:---|:---|
14 | |Added|Supported to `QueryBuilder` for using multiple `Query(_, value:)` declaration #29 by @devyhan|
15 | |Fixed|Modified a `Param` component of the `BodyBuilder` to a `Field` component #22 by @devyhan
Modified the Github workflow to work when pushed to main branch #25 by @devyhan|
16 | |Removed|Removed image files that use data capacity #23 by @devyhan|
17 |
18 | ---
19 |
20 | 
21 | |Release|0.1.3|
22 | |:---|:---|
23 | |Added|Supported to the `HttpRequest` method as static parameter #16 by @devyhan
Supported to the URL `Scheme` as static parameter and function #18 by @devyhan
24 | |Fixed|Fixed the phenomenon of a double CI test call #19 by @devyhan|
25 |
26 | ---
27 |
28 | 
29 | |Release|0.1.2|
30 | |:---|:---|
31 | |Fixed|Fixed the public scope of the `Field` components of `HeaderBuilder` #9 by @devyhan
Fixed the public scope of the `buildEither(component:)` components of `RouterBuilder` #12 by @devyhan
Fixed the public scope of the `buildEither(component:)` components of `URLBuilder` #13 by @devyhan|
32 |
33 | ---
34 |
35 | 
36 | |Release|0.1.1|
37 | |:---|:---|
38 | |Fixed|Fixed struct the `Router` parameter for get `URLRequest` #6 by @devyhan
Fixed the public scope of the `Param` components of `BodyBuilder` #4 by @devyhan|
39 |
40 | ---
41 |
42 | 
43 | |Release|0.1.0|
44 | |:---|:---|
45 |
46 | ---
47 |
48 | ### *[Currenly Release Info](https://github.com/devyhan/APIRouter/releases)*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 YoHan Cho
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.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "URLRouter",
7 | platforms: [
8 | .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)
9 | ],
10 | products: [
11 | .library(
12 | name: "URLRouter",
13 | targets: ["URLRouter"]),
14 | ],
15 | dependencies: [],
16 | targets: [
17 | .target(
18 | name: "URLRouter",
19 | dependencies: []),
20 | .testTarget(
21 | name: "URLRouterTests",
22 | dependencies: ["URLRouter"]),
23 | ]
24 | )
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://codecov.io/gh/devyhan/URLRouter)
3 | [](https://swiftpackageindex.com/devyhan/URLRouter)
4 | [](https://swiftpackageindex.com/devyhan/URLRouter)
5 |
6 |
7 |
8 |
9 |
10 | ## What's URLRouter 📟
11 | ***URLRouter*** is provides an easy way to manage multiple URL endpoints in Swift.
12 | It provides a simple interface for managing multiple endpoints and allows developers to interact with them in a single, unified manner.
13 | It also provides a way for developers to create custom endpoints DSL(Domain-Specific Languages) and to manage their own settings for each endpoint.
14 | Additionally, it provides a way to track the status of each endpoint and to easily detect any changes or updates that have been made.
15 |
16 | Similar to Swift Evolution's [Regex builder DSL](https://github.com/apple/swift-evolution/blob/main/proposals/0351-regex-builder.md), URL string literal and a more powerful pattern result builder to help make Swift URL string processing fast and easy and without mistakes. Ultimately, with ***URLRouter***, changes are easy to detect and useful for maintenance.
17 |
18 | 🤔 *Ask questions you’re wondering about [here](https://github.com/devyhan/URLRouter/discussions/new?category=q-a).*
19 | 💡 *Share ideas [here](https://github.com/devyhan/URLRouter/discussions/new).*
20 |
21 | ## Installation 📦
22 | - Using [Swift Package Manager](https://swift.org/package-manager)
23 |
24 | ```swift
25 | import PackageDescription
26 |
27 | let package = Package(
28 | name: "SomeApp",
29 | dependencies: [
30 | .Package(url: "https://github.com/devyhan/URLRouter", majorVersion: ""),
31 | ]
32 | )
33 | ```
34 |
35 | ## Configure URLRouter 📝
36 | ### Implement URLs Namespace
37 | - To implement URLs namespace we create a new type that will house the domain and behavior of the URLs by conforming to `URLRoutable`.
38 | ```swift
39 | import URLRouter
40 |
41 | public enum URLs: URLRoutable {
42 | ...
43 | }
44 | ```
45 | ### HttpHeader declaration
46 | - Using `HeaderBuilder` to `httpHeader` declaration.
47 | ```swift
48 | Request {
49 | ...
50 | Header {
51 | Field("HEADERVALUE", forKey: "HEADERKEY")
52 | Field("HEADERVALUE1", forKey: "HEADERKEY1")
53 | Field("HEADERVALUE2", forKey: "HEADERKEY2")
54 | ...
55 | }
56 | ...
57 | }
58 | ```
59 | - Using `Dictionary` to `httpHeader` declaration.
60 | ```swift
61 | Request {
62 | ...
63 | Header([
64 | "HEADERKEY": "HEADERVALUE",
65 | "HEADERKEY1": "HEADERVALUE1",
66 | "HEADERKEY2": "HEADERVALUE2",
67 | ...
68 | ])
69 | ...
70 | }
71 | ```
72 | ---
73 | ### HttpBody declaration
74 | - Using `HeaderBuilder` to `httpHeader` declaration.
75 | ```swift
76 | Request {
77 | ...
78 | Body {
79 | Field("VALUE", forKey: "KEY")
80 | Field("VALUE1", forKey: "KEY1")
81 | Field("VALUE2", forKey: "KEY2")
82 | ...
83 | }
84 | ...
85 | }
86 | ```
87 | - Using `Dictionary` to `httpHeader` declaration.
88 | ```swift
89 | Request {
90 | ...
91 | Body([
92 | "KEY": "VALUE",
93 | "KEY1": "VALUE1",
94 | "KEY2": "VALUE2",
95 | ...
96 | ])
97 | ...
98 | }
99 | ```
100 | ---
101 | ### HttpMethod declaration
102 | - Using `Method(_ method:)` to `httpMethod` declaration.
103 | ```swift
104 | Request {
105 | ...
106 | Method(.get)
107 | ...
108 | }
109 | ```
110 | - Using `static let method:` to `httpMethod` declaration.
111 | ```swift
112 | Request {
113 | ...
114 | Method.get
115 | ...
116 | }
117 | ```
118 | ---
119 | ### URL declaration
120 | - Using `URL(_ url:)` to `URL` declaration.
121 | ```swift
122 | Request {
123 | ...
124 | URL("https://www.baseurl.com/comments?postId=1")
125 | ...
126 | }
127 | ```
128 | - Using `URLBuilder` to `URL` declaration and `URLComponents` declaration.
129 | ```swift
130 | Request {
131 | ...
132 | URL {
133 | Scheme(.https)
134 | Host("www.baseurl.com")
135 | Path("comments")
136 | Query("postId", value: "1")
137 | }
138 | ...
139 | }
140 | // https://www.baseurl.com/comments?postId=1
141 | ```
142 | - Using `BaseURL(_ url:)` for `URL` override.
143 | ```swift
144 | Request {
145 | BaseURL("https://www.baseurl.com")
146 | URL {
147 | Path("comments")
148 | Query("postId", value: "1")
149 | }
150 | }
151 | // https://www.baseurl.com/comments?postId=1
152 |
153 | Router {
154 | BaseURL("https://www.baseurl.com")
155 | Request {
156 | URL {
157 | Scheme(.https)
158 | Host("www.overrideurl.com")
159 | Path("comments")
160 | Query("postId", value: "1")
161 | }
162 | }
163 | }
164 | // https://www.overrideurl.com/comments?postId=1
165 | ```
166 | #### URL Scheme declaration
167 | - Using `Scheme(_ scheme:)` to `Scheme` declaration.
168 | ```swift
169 | Request {
170 | ...
171 | URL {
172 | Scheme(.https)
173 | ...
174 | }
175 | ...
176 | }
177 | ```
178 | - Using `static let scheme:` to `Scheme` declaration.
179 | ```swift
180 | Request {
181 | ...
182 | URL {
183 | Scheme.https
184 | ...
185 | }
186 | ...
187 | }
188 | ```
189 | #### URL Query declaration
190 | - Using `Dictionary` to `Query` declaration.
191 | ```swift
192 | Request {
193 | ...
194 | URL {
195 | Query(
196 | [
197 | "first": "firstQuery",
198 | "second": "secondQuery",
199 | ...
200 | ]
201 | )
202 | }
203 | ...
204 | }
205 | ```
206 | - Using `Query(_, value:)` to `Query` declaration.
207 | ```swift
208 | Request {
209 | ...
210 | URL {
211 | Query("test", value: "query")
212 | Query("test", value: "query")
213 | ...
214 | }
215 | ...
216 | }
217 | ```
218 | - Using `Field(_, forKey:)` to `Query` declaration.
219 | ```swift
220 | Request {
221 | ...
222 | URL {
223 | Query {
224 | Field("firstQuery", forKey: "first")
225 | Field("secondQuery", forKey: "second")
226 | ...
227 | }
228 | ...
229 | }
230 | ...
231 | }
232 | ```
233 | ---
234 | ### How to configure and use ***URLRouter*** in a real world project?
235 | - Just create URLRouter.swift in your project! Happy hacking! 😁
236 | ```swift
237 | import URLRouter
238 |
239 | enum URLs: URLRoutable {
240 | // DOC: https://docs.github.com/ko/rest/repos/repos?apiVersion=2022-11-28#list-organization-repositories
241 | case listOrganizationRepositories(organizationName: String)
242 | // DOC: https://docs.github.com/ko/rest/repos/repos?apiVersion=2022-11-28#create-an-organization-repository
243 | case createAnOrganizationRepository(organizationName: String, repositoryInfo: RepositoryInfo)
244 | // DOC: https://docs.github.com/ko/rest/search?apiVersion=2022-11-28#search-repositories
245 | case searchRepositories(query: String)
246 | case deeplink(path: String = "home")
247 |
248 | struct RepositoryInfo {
249 | let name: String
250 | let description: String
251 | let homePage: String
252 | let `private`: Bool
253 | let hasIssues: Bool
254 | let hasProjects: Bool
255 | let hasWiki: Bool
256 | }
257 |
258 | var router: URLRouter {
259 | URLRouter {
260 | BaseURL("http://api.github.com")
261 | switch self {
262 | case let .listOrganizationRepositories(organizationName):
263 | Request {
264 | Method.post
265 | Header {
266 | Field("application/vnd.github+json", forKey: "Accept")
267 | Field("Bearer ", forKey: "Authorization")
268 | Field("2022-11-28", forKey: "X-GitHub-Api-Version")
269 | }
270 | URL {
271 | Path("orgs/\(organizationName)/repos")
272 | }
273 | }
274 | case let .createAnOrganizationRepository(organizationName, repositoryInfo):
275 | Request {
276 | Method.post
277 | Header {
278 | Field("application/vnd.github+json", forKey: "Accept")
279 | Field("Bearer ", forKey: "Authorization")
280 | Field("2022-11-28", forKey: "X-GitHub-Api-Version")
281 | }
282 | URL {
283 | Path("orgs/\(organizationName)/repos")
284 | }
285 | Body {
286 | Field(repositoryInfo.name, forKey: "name")
287 | Field(repositoryInfo.description, forKey: "description")
288 | Field(repositoryInfo.homePage, forKey: "homepage")
289 | Field(repositoryInfo.private, forKey: "private")
290 | Field(repositoryInfo.hasIssues, forKey: "has_issues")
291 | Field(repositoryInfo.hasProjects, forKey: "has_projects")
292 | Field(repositoryInfo.hasWiki, forKey: "has_wiki")
293 | }
294 | }
295 | case let .searchRepositories(query):
296 | Request {
297 | Method.get
298 | Header {
299 | Field("application/vnd.github+json", forKey: "Accept")
300 | Field("Bearer ", forKey: "Authorization")
301 | Field("2022-11-28", forKey: "X-GitHub-Api-Version")
302 | }
303 | URL {
304 | Path("search/repositories")
305 | Query("q", value: query)
306 | }
307 | }
308 | case let .deeplink(path):
309 | URL {
310 | Scheme.custom("example-deeplink")
311 | Host("detail")
312 | Path(path)
313 | Query {
314 | Field("postId", forKey: "1")
315 | Field("createdAt", forKey: "2021-04-27T04:39:54.261Z")
316 | }
317 | }
318 | }
319 | }
320 | }
321 | }
322 |
323 | // http://api.github.com/orgs/organization/repos
324 | let listOrganizationRepositoriesUrl = URLs.listOrganizationRepositories(organizationName: "organization").router?.urlRequest?.url
325 |
326 | // http://api.github.com/search/repositories?q=urlrouter
327 | let searchRepositoriesUrl = URLs.searchRepositories(query: "urlrouter").router?.urlRequest?.url
328 |
329 | // example-deeplink://detail/comments?1=postId&2021-04-27T04:39:54.261Z=createdA
330 | let deeplink = URLs.deeplink(path: "detail").router.url
331 | ```
332 | - Using ***URLRouter*** to provide `URLRequest`.
333 | ```swift
334 | let repositoryInfo: URLs.RepositoryInfo = .init(name: "Hello-World", description: "This is your first repository", homePage: "https://github.com", private: false, hasIssues: true, hasProjects: true, hasWiki: false)
335 | let request = URLs.createAnOrganizationRepository(organizationName: "SomeOrganization", repositoryInfo: repositoryInfo).router?.urlRequest
336 |
337 | URLSession.shared.dataTask(with: request) { data, response, error in
338 | ...
339 | ```
340 | - Using ***URLRouter*** to provide deeplink `URL` and check to match this `URL`.
341 | ```swift
342 | class AppDelegate: UIResponder, UIApplicationDelegate {
343 | ...
344 | func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
345 | let detailDeeplink = URLs.deeplink(path: "detail").router.url
346 | if detailDeeplink == url {
347 | ...
348 | }
349 | ...
350 | ```
351 | ## License
352 |
353 | ***URLRouter*** is under MIT license. See the [LICENSE](LICENSE) file for more info.
354 |
355 | ---
356 | 
357 | [](https://twitter.com/devyhan93)
--------------------------------------------------------------------------------
/Sources/URLRouter/BodyBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol BodyProtocol {
4 | func build(_ body: inout Body)
5 | }
6 |
7 | @resultBuilder
8 | public struct BodyBuilder {
9 | public static func buildBlock(_ components: BodyProtocol...) -> BodyProtocol {
10 | CombinedBody(components)
11 | }
12 |
13 | public static func buildArray(_ components: [BodyProtocol]) -> BodyProtocol {
14 | CombinedBody(components)
15 | }
16 |
17 | public static func buildEither(first component: BodyProtocol) -> BodyProtocol {
18 | CombinedBody([component])
19 | }
20 |
21 | public static func buildEither(second component: BodyProtocol) -> BodyProtocol {
22 | CombinedBody([component])
23 | }
24 | }
25 |
26 | private struct CombinedBody: BodyProtocol {
27 | private let children: [BodyProtocol]
28 |
29 | init(_ children: [BodyProtocol]) {
30 | self.children = children
31 | }
32 |
33 | func build(_ body: inout Body) {
34 | children.forEach {
35 | $0.build(&body)
36 | }
37 | }
38 | }
39 |
40 | public extension Body {
41 | init(@BodyBuilder _ params: () -> BodyProtocol) {
42 | let combineBody = params()
43 | var body = Body([:])
44 | combineBody.build(&body)
45 | self = body
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/URLRouter/HeaderBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol HeaderProtocol {
4 | func build(_ header: inout Header)
5 | }
6 |
7 | @resultBuilder
8 | public struct HeaderBuilder {
9 | public static func buildBlock(_ components: HeaderProtocol...) -> HeaderProtocol {
10 | CombinedHeader(components)
11 | }
12 |
13 | public static func buildArray(_ components: [HeaderProtocol]) -> HeaderProtocol {
14 | CombinedHeader(components)
15 | }
16 |
17 | public static func buildEither(first component: HeaderProtocol) -> HeaderProtocol {
18 | CombinedHeader([component])
19 | }
20 |
21 | public static func buildEither(second component: HeaderProtocol) -> HeaderProtocol {
22 | CombinedHeader([component])
23 | }
24 | }
25 |
26 | private struct CombinedHeader: HeaderProtocol {
27 | private let children: [HeaderProtocol]
28 |
29 | init(_ children: [HeaderProtocol]) {
30 | self.children = children
31 | }
32 |
33 | func build(_ header: inout Header) {
34 | children.forEach {
35 | $0.build(&header)
36 | }
37 | }
38 | }
39 |
40 | public extension Header {
41 | init(@HeaderBuilder _ fields: () -> HeaderProtocol) {
42 | let combineHeader = fields()
43 | var header = Header([:])
44 | combineHeader.build(&header)
45 | self = header
46 | }
47 | }
48 |
49 | public struct Field: HeaderProtocol, BodyProtocol, QueryProtocol {
50 | private let value: Any
51 | private let key: String
52 |
53 | public init(_ value: Any, forKey key: String) {
54 | self.value = value
55 | self.key = key
56 | }
57 |
58 | public func build(_ header: inout Header) {
59 | var headers: Dictionary = [:]
60 | for item in header.headers {
61 | headers.updateValue(String(item.value), forKey: item.key)
62 | }
63 | if let value = value as? String {
64 | headers.updateValue(String(value), forKey: key)
65 | }
66 | header = Header(headers)
67 | }
68 |
69 | public func build(_ body: inout Body) {
70 | var dictionary: Dictionary = [:]
71 | for item in body.body {
72 | dictionary.updateValue(item.value, forKey: item.key)
73 | }
74 | dictionary.updateValue(value, forKey: key)
75 | body = Body(dictionary)
76 | }
77 |
78 | public func build(_ query: inout Query) {
79 | var queries: [URLQueryItem] = []
80 | for item in query.queries {
81 | queries.append(item)
82 | }
83 | if let value = value as? String {
84 | queries.append(URLQueryItem(name: key, value: value))
85 | }
86 | query = Query(queries)
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/URLRouter/QueryBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol QueryProtocol {
4 | func build(_ query: inout Query)
5 | }
6 |
7 | @resultBuilder
8 | public struct QueryBuilder {
9 | public static func buildBlock(_ components: QueryProtocol...) -> QueryProtocol {
10 | CombinedQuery(components)
11 | }
12 |
13 | public static func buildArray(_ components: [QueryProtocol]) -> QueryProtocol {
14 | CombinedQuery(components)
15 | }
16 |
17 | public static func buildEither(first component: QueryProtocol) -> QueryProtocol {
18 | CombinedQuery([component])
19 | }
20 |
21 | public static func buildEither(second component: QueryProtocol) -> QueryProtocol {
22 | CombinedQuery([component])
23 | }
24 | }
25 |
26 | private struct CombinedQuery: QueryProtocol {
27 | private let children: [QueryProtocol]
28 |
29 | init(_ children: [QueryProtocol]) {
30 | self.children = children
31 | }
32 |
33 | func build(_ query: inout Query) {
34 | children.forEach {
35 | $0.build(&query)
36 | }
37 | }
38 | }
39 |
40 | public extension Query {
41 | init(@QueryBuilder _ fields: () -> QueryProtocol) {
42 | let combineQuery = fields()
43 | var queries = Query([:])
44 | combineQuery.build(&queries)
45 | self = queries
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/URLRouter/RequestBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// [HttpMethod Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods#:~:text=HTTP%20request%20methods,-HTTP%20defines%20a)
4 | public enum HttpMethod: String {
5 | case options = "OPTIONS"
6 | case get = "GET"
7 | case head = "HEAD"
8 | case post = "POST"
9 | case put = "PUT"
10 | case patch = "PATCH"
11 | case delete = "DELETE"
12 | case trace = "TRACE"
13 | case connect = "CONNECT"
14 | }
15 |
16 | public protocol RequestProtocol {
17 | func build(_ request: inout Request)
18 | }
19 |
20 | @resultBuilder
21 | public struct RequestBuilder {
22 | public static func buildBlock(_ components: RequestProtocol...) -> RequestProtocol {
23 | CombinedRequest(components)
24 | }
25 | }
26 |
27 | struct CombinedRequest: RequestProtocol {
28 | private let children: Array
29 |
30 | init(_ children: Array) {
31 | self.children = children
32 | }
33 |
34 | func build(_ request: inout Request) {
35 | children.forEach {
36 | $0.build(&request)
37 | }
38 | }
39 | }
40 |
41 | public extension Request {
42 | init(@RequestBuilder _ build: () -> RequestProtocol) {
43 | let combinedRequest = build()
44 | let url = Foundation.URL(string: "CANNOT_FIND_DEFAULT_URL")!
45 | var request = Request(URLRequest(url: url))
46 | combinedRequest.build(&request)
47 | self = request
48 | }
49 | }
50 |
51 | public struct Method: RequestProtocol {
52 | private let method: HttpMethod
53 |
54 | /// A parser of OPTIONS request method.
55 | public static let options = Self(.options)
56 |
57 | /// A parser of GET request method.
58 | public static let get = Self(.get)
59 |
60 | /// A parser of HEAD request method.
61 | public static let head = Self(.head)
62 |
63 | /// A parser of POST requests.
64 | public static let post = Self(.post)
65 |
66 | /// A parser of PUT requests.
67 | public static let put = Self(.put)
68 |
69 | /// A parser of PATCH requests.
70 | public static let patch = Self(.patch)
71 |
72 | /// A parser of DELETE requests.
73 | public static let delete = Self(.delete)
74 |
75 | /// A parser of TRACE requests.
76 | public static let trace = Self(.trace)
77 |
78 | /// A parser of CONNECT requests.
79 | public static let connect = Self(.connect)
80 |
81 | public init(_ method: HttpMethod) {
82 | self.method = method
83 | }
84 |
85 | public func build(_ request: inout Request) {
86 | request.urlRequest?.httpMethod = method.rawValue
87 | }
88 | }
89 |
90 | public struct Header: RequestProtocol {
91 | let headers: Dictionary
92 |
93 | public init(_ headers: Dictionary) {
94 | self.headers = headers
95 | }
96 |
97 | public func build(_ request: inout Request) {
98 | for header in headers {
99 | request.urlRequest?.addValue(header.value, forHTTPHeaderField: header.key)
100 | }
101 | }
102 | }
103 |
104 | public struct Body: RequestProtocol {
105 | let body: Dictionary
106 |
107 | public init(_ body: Dictionary) {
108 | self.body = body
109 | }
110 |
111 | public func build(_ request: inout Request) {
112 | do {
113 | let jsonData = try JSONSerialization.data(withJSONObject: self.body, options: .fragmentsAllowed)
114 | request.urlRequest?.httpBody = jsonData
115 | } catch {
116 | print("Error \(error)")
117 | }
118 | }
119 | }
120 |
121 | public struct URL: RequestProtocol, URLRouterProtocol {
122 | var components: URLComponents?
123 | var queryItems: Array = []
124 |
125 | public init(_ url: String) {
126 | self.components = URLComponents(string: url)
127 | }
128 |
129 | public func build(_ request: inout Request) {
130 | request.urlComponents = self.components
131 | request.urlRequest?.url = self.components?.url
132 | if !queryItems.isEmpty {
133 | request.urlComponents?.queryItems = self.queryItems
134 | request.urlRequest?.url = request.urlComponents?.url
135 | }
136 | }
137 |
138 | public func build(_ router: inout URLRouter) {
139 | router.urlComponents = self.components
140 | router.urlRequest?.url = self.components?.url
141 | router.url = self.components?.url
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/Sources/URLRouter/RouterBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol URLRoutable {
4 | var router: URLRouter { get }
5 | }
6 |
7 | public protocol URLRouterProtocol {
8 | func build(_ router: inout URLRouter)
9 | }
10 |
11 | @resultBuilder
12 | public struct RouterBuilder {
13 | public static func buildBlock(_ components: URLRouterProtocol...) -> URLRouterProtocol {
14 | CombinedRouter(components)
15 | }
16 |
17 | public static func buildArray(_ components: [URLRouterProtocol]) -> URLRouterProtocol {
18 | CombinedRouter(components)
19 | }
20 |
21 | public static func buildEither(first component: URLRouterProtocol) -> URLRouterProtocol {
22 | CombinedRouter([component])
23 | }
24 |
25 | public static func buildEither(second component: URLRouterProtocol) -> URLRouterProtocol {
26 | CombinedRouter([component])
27 | }
28 | }
29 |
30 | private struct CombinedRouter: URLRouterProtocol {
31 | private let children: Array
32 |
33 | init(_ children: Array) {
34 | self.children = children
35 | }
36 |
37 | func build(_ router: inout URLRouter) {
38 | children.forEach {
39 | $0.build(&router)
40 | }
41 | }
42 | }
43 |
44 | public extension URLRouter {
45 | init(@RouterBuilder _ build: @escaping () -> URLRouterProtocol) {
46 | let CombinedRouter = build()
47 | var router = URLRouter(Request(URLRequest(url: Foundation.URL(string: "CANNOT_FIND_BASE_URL")!)))
48 | CombinedRouter.build(&router)
49 | self = router
50 | }
51 | }
52 |
53 | public struct URLRouter: URLRouterProtocol {
54 | var request: Request
55 | public var url: Foundation.URL?
56 | public var urlRequest: URLRequest?
57 | public var urlComponents: URLComponents?
58 |
59 | public init(_ request: Request) {
60 | self.request = request
61 | }
62 |
63 | public func build(_ router: inout URLRouter) {
64 | router.request = self.request
65 | }
66 | }
67 |
68 | public struct Request: URLRouterProtocol {
69 | var urlRequest: URLRequest?
70 | var urlComponents: URLComponents?
71 |
72 | public init(_ urlRequest: URLRequest?) {
73 | self.urlRequest = urlRequest
74 | }
75 |
76 | public func build(_ router: inout URLRouter) {
77 | if let url = buildUrl(&router) {
78 | router.request.urlRequest = URLRequest(url: url)
79 | router.request.urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
80 | }
81 |
82 | router.request.urlRequest?.httpBody = self.urlRequest?.httpBody
83 | router.request.urlRequest?.httpMethod = self.urlRequest?.httpMethod
84 | router.request.urlRequest?.allHTTPHeaderFields = self.urlRequest?.allHTTPHeaderFields
85 |
86 | router.urlRequest = router.request.urlRequest
87 | router.urlComponents = router.request.urlComponents
88 | }
89 |
90 | private func buildUrl(_ router: inout URLRouter) -> Foundation.URL? {
91 | var url: Foundation.URL?
92 | if let urlRequestString = urlRequest?.url?.absoluteString,
93 | let urlComponentsString = urlComponents?.url?.absoluteString {
94 | if urlRequestString != "CANNOT_FIND_DEFAULT_URL" {
95 | let urlString = urlRequestString > urlComponentsString ? urlRequestString : urlComponentsString
96 | url = Foundation.URL(string: urlString, relativeTo: router.request.urlRequest?.url)
97 | }
98 | }
99 | return url
100 | }
101 | }
102 |
103 | public struct BaseURL: URLRouterProtocol {
104 | let url: String
105 |
106 | public init(_ url: String) {
107 | self.url = url
108 | }
109 |
110 | public func build(_ router: inout URLRouter) {
111 | router.request = Request(URLRequest(url: Foundation.URL(string: self.url)!))
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/URLRouter/URLBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum URLScheme {
4 | case http, https, mqtt, mqtts, ws, wss
5 | case custom(String)
6 |
7 | var rawValue: String {
8 | switch self {
9 | case .http:
10 | return "http"
11 | case .https:
12 | return "https"
13 | case .mqtt:
14 | return "mqtt"
15 | case .mqtts:
16 | return "mqtts"
17 | case .ws:
18 | return "ws"
19 | case .wss:
20 | return "wss"
21 | case let .custom(value):
22 | return value
23 | }
24 | }
25 | }
26 |
27 | public protocol HttpUrlProtocol {
28 | func build(_ url: inout URL)
29 | }
30 |
31 | @resultBuilder
32 | public struct URLBuilder {
33 | public static func buildBlock(_ components: HttpUrlProtocol...) -> HttpUrlProtocol {
34 | CombinedURL(components)
35 | }
36 |
37 | public static func buildArray(_ components: [HttpUrlProtocol]) -> HttpUrlProtocol {
38 | CombinedURL(components)
39 | }
40 |
41 | public static func buildEither(first component: HttpUrlProtocol) -> HttpUrlProtocol {
42 | CombinedURL([component])
43 | }
44 |
45 | public static func buildEither(second component: HttpUrlProtocol) -> HttpUrlProtocol {
46 | CombinedURL([component])
47 | }
48 | }
49 |
50 | private struct CombinedURL: HttpUrlProtocol {
51 | private let children: [HttpUrlProtocol]
52 |
53 | init(_ children: [HttpUrlProtocol]) {
54 | self.children = children
55 | }
56 |
57 | func build(_ url: inout URL) {
58 | children.forEach {
59 | $0.build(&url)
60 | }
61 | }
62 | }
63 |
64 | public extension URL {
65 | init(@URLBuilder _ build: () -> HttpUrlProtocol) {
66 | let combineUrl = build()
67 | var url = URL(String())
68 | combineUrl.build(&url)
69 | self = url
70 | }
71 | }
72 |
73 | public struct Scheme: HttpUrlProtocol {
74 | private let scheme: URLScheme
75 |
76 | public static let http = Self(.http)
77 |
78 | public static let https = Self(.https)
79 |
80 | public static let mqtt = Self(.mqtt)
81 |
82 | public static let mqtts = Self(.mqtts)
83 |
84 | public static func custom(_ value: String) -> Self {
85 | Self(.custom(value))
86 | }
87 |
88 | public init(_ scheme: URLScheme) {
89 | self.scheme = scheme
90 | }
91 |
92 | public func build(_ url: inout URL) {
93 | url.components?.scheme = self.scheme.rawValue
94 | }
95 | }
96 |
97 | public struct Host: HttpUrlProtocol {
98 | private let host: String
99 |
100 | public init(_ host: String) {
101 | self.host = host
102 | }
103 |
104 | public func build(_ url: inout URL) {
105 | url.components?.host = self.host
106 | }
107 | }
108 |
109 | public struct Path: HttpUrlProtocol {
110 | private let path: String
111 |
112 | public init(_ path: String) {
113 | self.path = path.prefix(1) != "/" ? "/" + path : path
114 | }
115 |
116 | public func build(_ url: inout URL) {
117 | url.components?.path = self.path
118 | }
119 | }
120 |
121 | public struct Query: HttpUrlProtocol {
122 | var queries: Array = []
123 |
124 | public init(_ queries: [URLQueryItem]) {
125 | self.queries = queries
126 | }
127 |
128 | public init(_ name: String, value: String?) {
129 | self.queries.append(URLQueryItem(name: name, value: value))
130 | }
131 |
132 | public init(_ queries: Dictionary) {
133 | for query in queries {
134 | self.queries.append(URLQueryItem(name: query.key, value: query.value))
135 | }
136 | }
137 |
138 | public func build(_ url: inout URL) {
139 | if self.queries.count > 1 {
140 | url.components?.queryItems = self.queries
141 | } else {
142 | if let query = self.queries.first {
143 | url.queryItems.append(URLQueryItem(name: query.name, value: query.value))
144 | }
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/BodyBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class BodyBuilderTests: XCTestCase {
6 | func testSwitchConditionalStatementWorkingForBuildEitherInBodyBuilder() {
7 | enum BodyOptions {
8 | case one
9 | case two
10 |
11 | var request: Request {
12 | Request {
13 | Body {
14 | switch self {
15 | case .one:
16 | Field("VALUE", forKey: "OPTIONONE")
17 | case .two:
18 | Field("VALUE", forKey: "OPTIONTWO")
19 | }
20 | }
21 | }
22 | }
23 | }
24 |
25 | if let optionOneBody = BodyOptions.one.request.urlRequest?.httpBody,
26 | let optionOneBodyString = String(data: optionOneBody, encoding: .utf8),
27 | let optionTwoBody = BodyOptions.two.request.urlRequest?.httpBody,
28 | let optionTwoBodyString = String(data: optionTwoBody, encoding: .utf8) {
29 | XCTAssertEqual(optionOneBodyString, "{\"OPTIONONE\":\"VALUE\"}")
30 | XCTAssertEqual(optionTwoBodyString, "{\"OPTIONTWO\":\"VALUE\"}")
31 | }
32 | }
33 |
34 | func testIfConditionalStatementWorkingForBuildEitherInUrlBuilder() {
35 | enum BodyOptions {
36 | case one
37 | case two
38 |
39 | var request: Request {
40 | Request {
41 | Body {
42 | if self == .one {
43 | Field("VALUE", forKey: "OPTIONONE")
44 | } else {
45 | Field("VALUE", forKey: "OPTIONTWO")
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | if let optionOneBody = BodyOptions.one.request.urlRequest?.httpBody,
53 | let optionOneBodyString = String(data: optionOneBody, encoding: .utf8),
54 | let optionTwoBody = BodyOptions.two.request.urlRequest?.httpBody,
55 | let optionTwoBodyString = String(data: optionTwoBody, encoding: .utf8) {
56 | XCTAssertEqual(optionOneBodyString, "{\"OPTIONONE\":\"VALUE\"}")
57 | XCTAssertEqual(optionTwoBodyString, "{\"OPTIONTWO\":\"VALUE\"}")
58 | }
59 | }
60 |
61 | func testForLoopStatementWorkingForBuildEitherInBodyBuilder() {
62 | let params = [
63 | "key1": "value1",
64 | "key2": "value2",
65 | "key3": "value3",
66 | "key4": "value4",
67 | ]
68 |
69 | let request = Request {
70 | Body {
71 | for param in params {
72 | Field(param.value, forKey: param.key)
73 | }
74 | }
75 | }
76 |
77 | if let httpBody = request.urlRequest?.httpBody,
78 | let bodyString = String(data: httpBody, encoding: .utf8) {
79 | XCTAssertEqual(bodyString.sorted(), ["\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", "\"", ",", ",", ",", "1", "1", "2", "2", "3", "3", "4", "4", ":", ":", ":", ":", "a", "a", "a", "a", "e", "e", "e", "e", "e", "e", "e", "e", "k", "k", "k", "k", "l", "l", "l", "l", "u", "u", "u", "u", "v", "v", "v", "v", "y", "y", "y", "y", "{", "}"])
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/HeaderBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class HeaderBuilderTests: XCTestCase {
6 | func testSwitchConditionStatemntWorkingForBuildEitherInHeaderBuilder() {
7 | enum HeaderOptions {
8 | case one
9 | case two
10 |
11 | var request: Request {
12 | Request {
13 | Header {
14 | switch self {
15 | case .one:
16 | Field("VALUE", forKey: "OPTIONONE")
17 | case .two:
18 | Field("VALUE", forKey: "OPTIONTWO")
19 | }
20 | }
21 | }
22 | }
23 | }
24 |
25 | if let optionOneHeaderFields = HeaderOptions.one.request.urlRequest?.allHTTPHeaderFields,
26 | let optionTwoHeaderFields = HeaderOptions.two.request.urlRequest?.allHTTPHeaderFields {
27 | XCTAssertEqual(optionOneHeaderFields, ["OPTIONONE":"VALUE"])
28 | XCTAssertEqual(optionTwoHeaderFields, ["OPTIONTWO":"VALUE"])
29 | }
30 | }
31 |
32 | func testIfConditionalStatementWorkingForBuildEitherInUrlBuilder() {
33 | enum HeaderOptions {
34 | case one
35 | case two
36 |
37 | var request: Request {
38 | Request {
39 | Header {
40 | if self == .one {
41 | Field("VALUE", forKey: "OPTIONONE")
42 | } else {
43 | Field("VALUE", forKey: "OPTIONTWO")
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | if let optionOneHeaderFields = HeaderOptions.one.request.urlRequest?.allHTTPHeaderFields,
51 | let optionTwoHeaderFields = HeaderOptions.two.request.urlRequest?.allHTTPHeaderFields {
52 | XCTAssertEqual(optionOneHeaderFields, ["OPTIONONE":"VALUE"])
53 | XCTAssertEqual(optionTwoHeaderFields, ["OPTIONTWO":"VALUE"])
54 | }
55 | }
56 |
57 | func testForLoopStatementWorkingForBuildEitherInHeaderBuilder() {
58 | let fields = [
59 | "key1": "value1",
60 | "key2": "value2",
61 | "key3": "value3",
62 | "key4": "value4",
63 | ]
64 |
65 | let request = Request {
66 | Header {
67 | for field in fields {
68 | Field(field.value, forKey: field.key)
69 | }
70 | }
71 | }
72 |
73 | if let sortedAllHTTPHeaderFields = request.urlRequest?.allHTTPHeaderFields {
74 | XCTAssertEqual(sortedAllHTTPHeaderFields, ["key1": "value1", "key2": "value2", "key3": "value3", "key4": "value4"])
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/QueryBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class QueryBUilderTests: XCTestCase {
6 | func testGeneratedMultyUrlQueryWithQueryBuilder() {
7 | let request = Request {
8 | URL {
9 | Query {
10 | Field("firstQuery", forKey: "first")
11 | Field("secondQuery", forKey: "second")
12 | }
13 | }
14 | }
15 |
16 | if let queryString = request.urlRequest?.url?.absoluteString {
17 | XCTAssertEqual(queryString.first, "?")
18 | XCTAssertEqual(queryString.contains("first=firstQuery"), true)
19 | XCTAssertEqual(queryString.contains("second=secondQuery"), true)
20 | XCTAssertEqual(queryString.split(separator: "&").count, 2)
21 | }
22 | }
23 |
24 | func testSwitchConditionStatementWorkingForBuildEitherInQueryBuilder() {
25 | enum QueryOptions {
26 | case one
27 | case two
28 |
29 | var request: Request {
30 | Request {
31 | URL {
32 | Query {
33 | switch self {
34 | case .one:
35 | Field("firstQuery", forKey: "first")
36 | case .two:
37 | Field("secondQuery", forKey: "second")
38 | }
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
45 | if let optionOneQueryFields = QueryOptions.one.request.urlComponents?.queryItems,
46 | let optionTwoQueryFields = QueryOptions.two.request.urlComponents?.queryItems {
47 | XCTAssertEqual(optionOneQueryFields.first?.debugDescription, "first=firstQuery")
48 | XCTAssertEqual(optionTwoQueryFields.first?.debugDescription, "second=secondQuery")
49 | }
50 | }
51 |
52 | func testIfConditionalStatementWorkingForBuildEitherInQueryBuilder() {
53 | enum QueryOptions {
54 | case one
55 | case two
56 |
57 | var request: Request {
58 | Request {
59 | URL {
60 | Query {
61 | if self == .one {
62 | Field("firstQuery", forKey: "first")
63 | } else {
64 | Field("secondQuery", forKey: "second")
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | if let optionOneQueryFields = QueryOptions.one.request.urlComponents?.queryItems,
73 | let optionTwoQueryFields = QueryOptions.two.request.urlComponents?.queryItems {
74 | XCTAssertEqual(optionOneQueryFields.first?.debugDescription, "first=firstQuery")
75 | XCTAssertEqual(optionTwoQueryFields.first?.debugDescription, "second=secondQuery")
76 | }
77 | }
78 |
79 | func testForLoopStatementWorkingForBuildEitherInQueryBuilder() {
80 | let fields = [
81 | "first": "firstQuery",
82 | "second": "secondQuery",
83 | "third": "thirdQuery"
84 | ]
85 |
86 | let request = Request {
87 | URL {
88 | Query {
89 | for field in fields {
90 | Field(field.value, forKey: field.key)
91 | }
92 | }
93 | }
94 | }
95 |
96 | var mockQueryItems: [URLQueryItem] = []
97 | for field in fields {
98 | mockQueryItems.append(URLQueryItem(name: field.key, value: field.value))
99 | }
100 |
101 | if let queryItems = request.urlComponents?.queryItems {
102 | XCTAssertEqual(queryItems, mockQueryItems)
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/RequestBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class RequestBuilderTests: XCTestCase {
6 | func testGeneratedHttpRequestBodyWithBodyBuilder() {
7 | let request = Request {
8 | Body {
9 | Field("VALUE", forKey: "KEY")
10 | }
11 | }
12 |
13 | if let requestBody = request.urlRequest?.httpBody,
14 | let bodyString = String(data: requestBody, encoding: .utf8)
15 | {
16 | XCTAssertEqual(bodyString, "{\"KEY\":\"VALUE\"}")
17 | }
18 | }
19 |
20 | func testGeneratedHttpRequestBodyWithObjectThroughInitializer() {
21 | let request = Request {
22 | Body(["KEY": "VALUE"])
23 | }
24 |
25 | if let requestBody = request.urlRequest?.httpBody,
26 | let bodyString = String(data: requestBody, encoding: .utf8)
27 | {
28 | XCTAssertEqual(bodyString, "{\"KEY\":\"VALUE\"}")
29 | }
30 | }
31 |
32 | func testGeneratedHttpRequestHeaderWithHeaderBuilder() {
33 | let request = Request {
34 | Header {
35 | Field("JSON_VALUE", forKey: "JSON_KEY")
36 | }
37 | }
38 |
39 | if let requestHeader = request.urlRequest?.value(forHTTPHeaderField: "JSON_KEY") {
40 | XCTAssertEqual(requestHeader, "JSON_VALUE")
41 | }
42 | }
43 |
44 | func testGeneratedHttpRequestHeaderWithRequestBuilder() {
45 | let request = Request {
46 | Header(["KEY": "VALUE"])
47 | }
48 |
49 | if let requestHeader = request.urlRequest?.value(forHTTPHeaderField: "KEY") {
50 | XCTAssertEqual(requestHeader, "VALUE")
51 | }
52 | }
53 |
54 | func testGeneratedMethodProtocolWithRequestBuilder() {
55 | let optionsUrlRequest = Request {
56 | Method(.options)
57 | }
58 | let getUrlRequest = Request {
59 | Method(.get)
60 | }
61 | let headUrlRequest = Request {
62 | Method(.head)
63 | }
64 | let postUrlRequest = Request {
65 | Method(.post)
66 | }
67 | let putUrlRequest = Request {
68 | Method(.put)
69 | }
70 | let patchUrlRequest = Request {
71 | Method(.patch)
72 | }
73 | let deleteUrlRequest = Request {
74 | Method(.delete)
75 | }
76 | let traceUrlRequest = Request {
77 | Method(.trace)
78 | }
79 | let connectUrlRequest = Request {
80 | Method(.connect)
81 | }
82 |
83 | XCTAssertEqual(optionsUrlRequest.urlRequest?.httpMethod, "OPTIONS")
84 | XCTAssertEqual(getUrlRequest.urlRequest?.httpMethod, "GET")
85 | XCTAssertEqual(headUrlRequest.urlRequest?.httpMethod, "HEAD")
86 | XCTAssertEqual(postUrlRequest.urlRequest?.httpMethod, "POST")
87 | XCTAssertEqual(putUrlRequest.urlRequest?.httpMethod, "PUT")
88 | XCTAssertEqual(patchUrlRequest.urlRequest?.httpMethod, "PATCH")
89 | XCTAssertEqual(deleteUrlRequest.urlRequest?.httpMethod, "DELETE")
90 | XCTAssertEqual(traceUrlRequest.urlRequest?.httpMethod, "TRACE")
91 | XCTAssertEqual(connectUrlRequest.urlRequest?.httpMethod, "CONNECT")
92 | }
93 |
94 | func testGeneratedMethodProtocolWithStaticParameterOfRequestBuilder() {
95 | let optionsUrlRequest = Request {
96 | Method.options
97 | }
98 | let getUrlRequest = Request {
99 | Method.get
100 | }
101 | let headUrlRequest = Request {
102 | Method.head
103 | }
104 | let postUrlRequest = Request {
105 | Method.post
106 | }
107 | let putUrlRequest = Request {
108 | Method.put
109 | }
110 | let patchUrlRequest = Request {
111 | Method.patch
112 | }
113 | let deleteUrlRequest = Request {
114 | Method.delete
115 | }
116 | let traceUrlRequest = Request {
117 | Method.trace
118 | }
119 | let connectUrlRequest = Request {
120 | Method.connect
121 | }
122 |
123 | XCTAssertEqual(optionsUrlRequest.urlRequest?.httpMethod, "OPTIONS")
124 | XCTAssertEqual(getUrlRequest.urlRequest?.httpMethod, "GET")
125 | XCTAssertEqual(headUrlRequest.urlRequest?.httpMethod, "HEAD")
126 | XCTAssertEqual(postUrlRequest.urlRequest?.httpMethod, "POST")
127 | XCTAssertEqual(putUrlRequest.urlRequest?.httpMethod, "PUT")
128 | XCTAssertEqual(patchUrlRequest.urlRequest?.httpMethod, "PATCH")
129 | XCTAssertEqual(deleteUrlRequest.urlRequest?.httpMethod, "DELETE")
130 | XCTAssertEqual(traceUrlRequest.urlRequest?.httpMethod, "TRACE")
131 | XCTAssertEqual(connectUrlRequest.urlRequest?.httpMethod, "CONNECT")
132 | }
133 |
134 | func testGeneratedAssembleURLRequestWithRequestBuilder() {
135 | let request = Request {
136 | Body(["KEY": "VALUE"])
137 | Header {
138 | Field("HEADERVALUE", forKey: "HEADERKEY")
139 | }
140 | Method(.get)
141 | URL("https://www.urltest.com/test/path?first=query&second=query")
142 | }
143 |
144 | var urlRequest = URLRequest(url: Foundation.URL(string: "https://www.urltest.com/test/path?first=query&second=query")!)
145 | let bodyDictionary = ["KEY": "VALUE"]
146 | let body = try! JSONSerialization.data(withJSONObject: bodyDictionary, options: .fragmentsAllowed)
147 | let header = ["HEADERKEY": "HEADERVALUE"]
148 | urlRequest.httpBody = body
149 | urlRequest.httpMethod = "GET"
150 | urlRequest.allHTTPHeaderFields = header
151 |
152 | if let request = request.urlRequest {
153 | XCTAssertEqual(request, urlRequest)
154 | }
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/RouterBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class RouterBuilderTests: XCTestCase {
6 | func testGeneratedRouterWithRouterBuilder() {
7 | let router = URLRouter {
8 | Request {
9 | Body {
10 | Field("VALUE", forKey: "KEY")
11 | }
12 | Method(.get)
13 | Header {
14 | Field("HEADERVALUE", forKey: "HEADERKEY")
15 | }
16 | URL {
17 | Scheme(.https)
18 | Host("www.baseurl.com")
19 | Path("comments")
20 | Query("postId", value: "1")
21 | }
22 | }
23 | }
24 |
25 | var mcokUrlRequest = URLRequest(url: Foundation.URL(string: "https://www.baseurl.com/comments?postId=1")!)
26 | let bodyDictionary = ["KEY": "VALUE"]
27 | let body = try! JSONSerialization.data(withJSONObject: bodyDictionary, options: .fragmentsAllowed)
28 | let header = ["HEADERKEY": "HEADERVALUE"]
29 | mcokUrlRequest.httpBody = body
30 | mcokUrlRequest.httpMethod = "GET"
31 | mcokUrlRequest.allHTTPHeaderFields = header
32 |
33 | if let urlRequest = router.urlRequest {
34 | XCTAssertEqual(urlRequest, mcokUrlRequest)
35 | }
36 | }
37 |
38 | func testGeneratedRouterWithRouterBuilderUsingBaseURL() {
39 | let router = URLRouter {
40 | BaseURL("https://www.baseurl.com")
41 | Request {
42 | Body {
43 | Field("VALUE", forKey: "KEY")
44 | }
45 | Method(.get)
46 | Header {
47 | Field("HEADERVALUE", forKey: "HEADERKEY")
48 | }
49 | URL {
50 | Path("comments")
51 | Query("postId", value: "1")
52 | }
53 | }
54 | }
55 |
56 | var mockUrlRequest = URLRequest(url: Foundation.URL(string: "https://www.baseurl.com/comments?postId=1")!)
57 | let bodyDictionary = ["KEY": "VALUE"]
58 | let body = try! JSONSerialization.data(withJSONObject: bodyDictionary, options: .fragmentsAllowed)
59 | let header = ["HEADERKEY": "HEADERVALUE"]
60 | mockUrlRequest.httpBody = body
61 | mockUrlRequest.httpMethod = "GET"
62 | mockUrlRequest.allHTTPHeaderFields = header
63 |
64 | if let urlRequest = router.urlRequest {
65 | XCTAssertEqual(urlRequest, mockUrlRequest)
66 | }
67 | }
68 |
69 | func testRouterSwiftchBranching() {
70 | enum URLs {
71 | case one, two, homeDeeplink, detailDeeplink
72 |
73 | var router: URLRouter {
74 | URLRouter {
75 | BaseURL("https://www.baseurl.com")
76 | switch self {
77 | case .one:
78 | Request {
79 | Body {
80 | Field("VALUE", forKey: "KEY")
81 | }
82 | Method(.get)
83 | Header {
84 | Field("HEADERVALUE", forKey: "HEADERKEY")
85 | }
86 | URL {
87 | Path("comments")
88 | Query("postId", value: "1")
89 | }
90 | }
91 | case .two:
92 | Request {
93 | Body {
94 | Field("VALUE", forKey: "KEY")
95 | }
96 | Method(.get)
97 | Header {
98 | Field("HEADERVALUE", forKey: "HEADERKEY")
99 | }
100 | URL {
101 | Path("comments")
102 | Query("postId", value: "2")
103 | }
104 | }
105 | case .homeDeeplink:
106 | URL {
107 | Scheme.custom("example-deeplink")
108 | Host("home")
109 | }
110 | case .detailDeeplink:
111 | URL {
112 | Scheme.custom("example-deeplink")
113 | Host("detail")
114 | Path("comments")
115 | Query {
116 | Field("postId", forKey: "1")
117 | Field("createdAt", forKey: "2021-04-27T04:39:54.261Z")
118 | }
119 | }
120 | }
121 | }
122 | }
123 | }
124 |
125 | let bodyDictionary = ["KEY": "VALUE"]
126 | let body = try! JSONSerialization.data(withJSONObject: bodyDictionary, options: .fragmentsAllowed)
127 | let header = ["HEADERKEY": "HEADERVALUE"]
128 |
129 | var mockOptionOneUrlRequest = URLRequest(url: Foundation.URL(string: "https://www.baseurl.com/comments?postId=1")!)
130 | mockOptionOneUrlRequest.httpBody = body
131 | mockOptionOneUrlRequest.httpMethod = "GET"
132 | mockOptionOneUrlRequest.allHTTPHeaderFields = header
133 |
134 | var mockOptionTwoUrlRequest = URLRequest(url: Foundation.URL(string: "https://www.baseurl.com/comments?postId=2")!)
135 | mockOptionTwoUrlRequest.httpBody = body
136 | mockOptionTwoUrlRequest.httpMethod = "GET"
137 | mockOptionTwoUrlRequest.allHTTPHeaderFields = header
138 |
139 | let mockHomeDeeplinkUrl = Foundation.URL(string: "example-deeplink://home")!
140 | let mockDetailDeeplinkUrl = Foundation.URL(string: "example-deeplink://detail/comments?1=postId&2021-04-27T04:39:54.261Z=createdAt")!
141 |
142 | if let optionOneUrlRequest = URLs.one.router.urlRequest,
143 | let optionTwoUrlRequest = URLs.two.router.urlRequest,
144 | let homeDeeplinkUrl = URLs.homeDeeplink.router.url,
145 | let detailDeeplinkUrl = URLs.detailDeeplink.router.url {
146 | XCTAssertEqual(optionOneUrlRequest, mockOptionOneUrlRequest)
147 | XCTAssertEqual(optionTwoUrlRequest, mockOptionTwoUrlRequest)
148 | XCTAssertEqual(homeDeeplinkUrl, mockHomeDeeplinkUrl)
149 | XCTAssertEqual(detailDeeplinkUrl, mockDetailDeeplinkUrl)
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Tests/URLRouterTests/URLBuilderTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import XCTest
3 | @testable import URLRouter
4 |
5 | final class URLBuilderTests: XCTestCase {
6 | func testGeneratedUrlSchemeWithURLBuilder() {
7 | let httpUrlRequest = Request {
8 | URL {
9 | Scheme(.http)
10 | }
11 | }
12 | let httpsUrlRequest = Request {
13 | URL {
14 | Scheme(.https)
15 | }
16 | }
17 | let mqttUrlRequest = Request {
18 | URL {
19 | Scheme(.mqtt)
20 | }
21 | }
22 | let mqttsUrlRequest = Request {
23 | URL {
24 | Scheme(.mqtts)
25 | }
26 | }
27 | let wsUrlRequest = Request {
28 | URL {
29 | Scheme(.ws)
30 | }
31 | }
32 | let wssUrlRequest = Request {
33 | URL {
34 | Scheme(.wss)
35 | }
36 | }
37 | let customUrlRequest = Request {
38 | URL {
39 | Scheme(.custom("custom"))
40 | }
41 | }
42 |
43 | if let httpUrlString = httpUrlRequest.urlRequest?.url?.absoluteString,
44 | let httpsUrlString = httpsUrlRequest.urlRequest?.url?.absoluteString,
45 | let mqttUrlString = mqttUrlRequest.urlRequest?.url?.absoluteString,
46 | let mqttsUrlString = mqttsUrlRequest.urlRequest?.url?.absoluteString,
47 | let wsUrlString = wsUrlRequest.urlRequest?.url?.absoluteString,
48 | let wssUrlString = wssUrlRequest.urlRequest?.url?.absoluteString,
49 | let customUrlString = customUrlRequest.urlRequest?.url?.absoluteString {
50 | XCTAssertEqual(httpUrlString, "http:")
51 | XCTAssertEqual(httpsUrlString, "https:")
52 | XCTAssertEqual(mqttUrlString, "mqtt:")
53 | XCTAssertEqual(mqttsUrlString, "mqtts:")
54 | XCTAssertEqual(wsUrlString, "ws:")
55 | XCTAssertEqual(wssUrlString, "wss:")
56 | XCTAssertEqual(customUrlString, "custom:")
57 | }
58 | }
59 |
60 | func testGeneratedUrlSchemeWithStaticParameterAndFunctionOfUrlBuilder() {
61 | let httpUrlRequest = Request {
62 | URL {
63 | Scheme.http
64 | }
65 | }
66 | let httpsUrlRequest = Request {
67 | URL {
68 | Scheme.https
69 | }
70 | }
71 | let mqttUrlRequest = Request {
72 | URL {
73 | Scheme.mqtt
74 | }
75 | }
76 | let mqttsUrlRequest = Request {
77 | URL {
78 | Scheme.mqtts
79 | }
80 | }
81 | let customUrlRequest = Request {
82 | URL {
83 | Scheme.custom("custom")
84 | }
85 | }
86 |
87 | if let httpUrlString = httpUrlRequest.urlRequest?.url?.absoluteString,
88 | let httpsUrlString = httpsUrlRequest.urlRequest?.url?.absoluteString,
89 | let mqttUrlString = mqttUrlRequest.urlRequest?.url?.absoluteString,
90 | let mqttsUrlString = mqttsUrlRequest.urlRequest?.url?.absoluteString,
91 | let customUrlString = customUrlRequest.urlRequest?.url?.absoluteString {
92 | XCTAssertEqual(httpUrlString, "http:")
93 | XCTAssertEqual(httpsUrlString, "https:")
94 | XCTAssertEqual(mqttUrlString, "mqtt:")
95 | XCTAssertEqual(mqttsUrlString, "mqtts:")
96 | XCTAssertEqual(customUrlString, "custom:")
97 | }
98 | }
99 |
100 | func testGeneratedUrlHostWithURLBuilder() {
101 | let request = Request {
102 | URL {
103 | Host("host.com")
104 | }
105 | }
106 |
107 | if let urlString = request.urlRequest?.url?.absoluteString {
108 | XCTAssertEqual(urlString, "//host.com")
109 | }
110 | }
111 |
112 | func testGeneratedUrlPathWithURLBuilder() {
113 | let request = Request {
114 | URL {
115 | Path("test/path")
116 | }
117 | }
118 |
119 | if let pathString = request.urlRequest?.url?.absoluteString {
120 | XCTAssertEqual(pathString, "/test/path")
121 | }
122 | }
123 |
124 | func testRemovedPrefixSlashToUrlPath() {
125 | let request = Request {
126 | URL {
127 | Path("/test/path")
128 | }
129 | }
130 |
131 | if let pathString = request.urlRequest?.url?.absoluteString {
132 | XCTAssertEqual(pathString, "/test/path")
133 | }
134 | }
135 |
136 | func testGeneratedUrlQueryWithURLBuilder() {
137 | let request = Request {
138 | URL {
139 | Query("test", value: "query")
140 | }
141 | }
142 |
143 | if let queryString = request.urlRequest?.url?.absoluteString {
144 | XCTAssertEqual(queryString, "?test=query")
145 | }
146 | }
147 |
148 | func testGeneratedMultyUrlQueryWithURLBuilder() {
149 | let request = Request {
150 | URL {
151 | Query(
152 | [
153 | "first": "firstQuery",
154 | "second": "secondQuery"
155 | ]
156 | )
157 | }
158 | }
159 |
160 | if let queryString = request.urlRequest?.url?.absoluteString {
161 | XCTAssertEqual(queryString.first, "?")
162 | XCTAssertEqual(queryString.contains("first=firstQuery"), true)
163 | XCTAssertEqual(queryString.contains("second=secondQuery"), true)
164 | XCTAssertEqual(queryString.split(separator: "&").count, 2)
165 | }
166 | }
167 |
168 | func testGeneratedUrlWithURLBuilder() {
169 | let request = Request {
170 | URL {
171 | Scheme(.https)
172 | Host("www.urltest.com")
173 | Path("test/path")
174 | Query("test", value: "query")
175 | }
176 | }
177 |
178 | if let url = request.urlRequest?.url?.absoluteString {
179 | XCTAssertEqual(url, "https://www.urltest.com/test/path?test=query")
180 | }
181 | }
182 |
183 | func testSwitchConditionalStatementWorkingForBuildEitherInUrlBuilder() {
184 | enum SchemeOptions {
185 | case https
186 | case http
187 |
188 | var request: Request {
189 | Request {
190 | URL {
191 | switch self {
192 | case .https:
193 | Scheme(.https)
194 | case .http:
195 | Scheme(.http)
196 | }
197 | Host("www.urltest.com")
198 | }
199 | }
200 | }
201 | }
202 |
203 | if let httpUrlString = SchemeOptions.http.request.urlRequest?.url?.absoluteString,
204 | let httpsUrlstring = SchemeOptions.https.request.urlRequest?.url?.absoluteString {
205 | XCTAssertEqual(httpUrlString, "http://www.urltest.com")
206 | XCTAssertEqual(httpsUrlstring, "https://www.urltest.com")
207 | }
208 | }
209 |
210 | func testIfConditionalStatementWorkingForBuildEitherInUrlBuilder() {
211 | enum SchemeOptions {
212 | case https
213 | case http
214 |
215 | var request: Request {
216 | Request {
217 | URL {
218 | if self == .http {
219 | Scheme(.http)
220 | } else {
221 | Scheme(.https)
222 | }
223 | Host("www.urltest.com")
224 | }
225 | }
226 | }
227 | }
228 |
229 | if let httpUrlString = SchemeOptions.http.request.urlRequest?.url?.absoluteString,
230 | let httpsUrlString = SchemeOptions.https.request.urlRequest?.url?.absoluteString {
231 | XCTAssertEqual(httpUrlString, "http://www.urltest.com")
232 | XCTAssertEqual(httpsUrlString, "https://www.urltest.com")
233 | }
234 | }
235 |
236 | func testForLoopStatementWorkingForBuildEitherInUrlBuilder() {
237 | let queries = [
238 | "query1": "value1",
239 | "query2": "value2",
240 | "query3": "value3",
241 | "query4": "value4",
242 | ]
243 |
244 | let request = Request {
245 | URL {
246 | Scheme(.https)
247 | Host("www.urltest.com")
248 |
249 | for query in queries {
250 | Query(query.key, value: query.value)
251 | }
252 | }
253 | }
254 |
255 | if let queryItems = request.urlComponents?.queryItems {
256 | XCTAssertEqual(queryItems.contains(URLQueryItem(name: "query1", value: "value1")), true)
257 | XCTAssertEqual(queryItems.contains(URLQueryItem(name: "query2", value: "value2")), true)
258 | XCTAssertEqual(queryItems.contains(URLQueryItem(name: "query3", value: "value3")), true)
259 | XCTAssertEqual(queryItems.contains(URLQueryItem(name: "query4", value: "value4")), true)
260 | }
261 | }
262 | }
263 |
--------------------------------------------------------------------------------