├── .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 | ![Relative date](https://img.shields.io/date/1671761657) 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 | ![Relative date](https://img.shields.io/date/1671605792) 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 | ![Relative date](https://img.shields.io/date/1671433162) 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 | ![Relative date](https://img.shields.io/date/1671097337) 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 | ![Relative date](https://img.shields.io/date/1671068209) 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 | ![Relative date](https://img.shields.io/date/1670996156) 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 | ![main](https://github.com/devyhan/urlrouter/actions/workflows/ci.yml/badge.svg?branch=main) 2 | [![codecov](https://codecov.io/gh/devyhan/URLRouter/branch/main/graph/badge.svg?token=ZQNDOX2VDF)](https://codecov.io/gh/devyhan/URLRouter) 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdevyhan%2FURLRouter%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/devyhan/URLRouter) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdevyhan%2FURLRouter%2Fbadge%3Ftype%3Dplatforms)](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 | ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/devyhan/urlrouter?style=social) 357 | [![Twitter Follow @devyhan93](https://img.shields.io/twitter/follow/devyhan93?style=social)](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 | --------------------------------------------------------------------------------