├── .circleci └── config.yml ├── .codecov.yml ├── .editorconfig ├── .gitignore ├── .swiftlint.yml ├── CONTRIBUTING.md ├── Package.swift ├── README.md ├── Sources ├── AsyncExt │ ├── Exports.swift │ ├── Future+Bool.swift │ ├── Future+Comparable.swift │ ├── Future+Equatable.swift │ └── Future+Optional.swift ├── FluentExt │ ├── Exports.swift │ ├── Extensions │ │ ├── Model+Query.swift │ │ ├── QueryBuilder+Filter.swift │ │ ├── QueryBuilder+Sort.swift │ │ ├── Request+Filter.swift │ │ └── Request+Sort.swift │ ├── FilterConfig.swift │ ├── FilterValue.swift │ ├── QueryFilterMethod.swift │ ├── SQL+Contains.swift │ └── SingleValueDecoder.swift ├── ServiceExt │ ├── Environment+DotEnv.swift │ ├── Environment+Getters.swift │ └── Exports.swift └── VaporExt │ ├── Encodable+Response.swift │ └── Exports.swift ├── Tests ├── AsyncExtTests │ ├── Future+BoolTests.swift │ ├── Future+ComparableTests.swift │ ├── Future+EquatableTests.swift │ ├── Helpers │ │ └── CustomError.swift │ └── XCTestManifests.swift ├── FluentExtTests │ ├── StubTests.swift │ └── XCTestManifests.swift ├── LinuxMain.swift ├── ServiceExtTests │ ├── Environment+DotEnvTests.swift │ ├── Environment+GettersTests.swift │ └── XCTestManifests.swift └── VaporExtTests │ ├── StubTests.swift │ └── XCTestManifests.swift └── license /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | macos-4.2: 5 | macos: 6 | xcode: "10.0.0" 7 | steps: 8 | - checkout 9 | - run: brew install libressl 10 | - run: swift package generate-xcodeproj 11 | - run: xcodebuild -scheme VaporExt-Package -enableCodeCoverage YES test 12 | - run: bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" 13 | 14 | macos-release-4.2: 15 | macos: 16 | xcode: "10.0.0" 17 | steps: 18 | - checkout 19 | - run: brew install libressl 20 | - run: swift build -c release 21 | 22 | macos-4.1: 23 | macos: 24 | xcode: "9.4.1" 25 | steps: 26 | - checkout 27 | - run: brew install libressl 28 | - run: swift test 29 | 30 | macos-release-4.1: 31 | macos: 32 | xcode: "9.4.1" 33 | steps: 34 | - checkout 35 | - run: brew install libressl 36 | - run: swift build -c release 37 | 38 | linux-4.1: 39 | docker: 40 | - image: swift:4.1.3 41 | steps: 42 | - checkout 43 | - run: swift build 44 | - run: swift test 45 | 46 | linux-release-4.1: 47 | docker: 48 | - image: swift:4.1.3 49 | steps: 50 | - checkout 51 | - run: swift build -c release 52 | 53 | linux-4.2: 54 | docker: 55 | - image: norionomura/swift:swift-4.2-branch 56 | steps: 57 | - checkout 58 | - run: swift build 59 | - run: swift test 60 | 61 | linux-release-4.2: 62 | docker: 63 | - image: norionomura/swift:swift-4.2-branch 64 | steps: 65 | - checkout 66 | - run: swift build -c release 67 | 68 | swiftlint: 69 | docker: 70 | - image: norionomura/swiftlint 71 | steps: 72 | - checkout 73 | - run: swiftlint 74 | 75 | workflows: 76 | version: 2 77 | tests: 78 | jobs: 79 | - swiftlint 80 | # Swift 4.1 81 | - linux-4.1 82 | - linux-release-4.1 83 | - macos-4.1 84 | - macos-release-4.1 85 | 86 | # Swift 4.2 87 | - linux-4.2 88 | - linux-release-4.2 89 | - macos-4.2 90 | - macos-release-4.2 91 | 92 | nightly: 93 | triggers: 94 | - schedule: 95 | cron: "0 0 * * *" 96 | filters: 97 | branches: 98 | only: 99 | - master 100 | jobs: 101 | - linux-4.1 102 | - linux-4.2 103 | - macos-4.1 104 | - macos-4.2 105 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: 80...100 5 | 6 | status: 7 | project: true 8 | patch: true 9 | changes: true 10 | 11 | ignore: 12 | - Tests/**/* 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | # Set default charset 8 | [*.{js,py,swift,m,json}] 9 | charset = utf-8 10 | 11 | # 4 space indentation 12 | [*.swift] 13 | indent_style = space 14 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | Package.resolved 6 | DerivedData/ 7 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | 5 | nesting: 6 | type_level: 2 7 | 8 | opt_in_rules: 9 | - closure_end_indentation 10 | - empty_count 11 | - file_header 12 | - force_unwrapping 13 | - literal_expression_end_indentation 14 | 15 | force_cast: error 16 | force_try: error 17 | force_unwrapping: error 18 | 19 | line_length: 120 20 | 21 | identifier_name: 22 | excluded: 23 | - i 24 | - id 25 | - FILTER_CONFIG_REGEX 26 | 27 | closure_end_indentation: error 28 | 29 | file_header: 30 | severity: error 31 | required_pattern: | 32 | \/\/ 33 | \/\/ .*?\.swift 34 | \/\/ (Async|Fluent|Service|Vapor)Ext 35 | \/\/ 36 | \/\/ Created by .*? on \d{2}\/\d{2}\/\d{2}\. 37 | \/\/ Copyright © \d{4} Vapor Community\. All rights reserved\. 38 | 39 | reporter: "emoji" 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains information and guidelines about contributing to this project. Please read it before you start participating. 4 | 5 | **Topics** 6 | 7 | - [Asking Questions](#asking-questions) 8 | - [Ways to Contribute](#ways-to-contribute) 9 | - [Adding new Extensions](#adding-new-extensions) 10 | - [Adding documentation](#adding-documentation) 11 | - [Adding changelog entries](#adding-changelog-entries) 12 | - [Reporting Issues](#reporting-issues) 13 | 14 | --- 15 | 16 | ## Asking Questions 17 | 18 | We don't use GitHub as a support forum. 19 | For any usage questions that are not specific to the project itself, please ask on [Vapor Slack](https://vapor.team) instead. 20 | By doing so, you'll be more likely to quickly solve your problem, and you'll allow anyone else with the same question to find the answer. 21 | This also allows us to focus on improving the project for others. 22 | 23 | --- 24 | 25 | ## Ways to Contribute 26 | 27 | You can contribute to the project in a variety of ways: 28 | 29 | - Improve documentation 🙏 30 | - Add more extensions 👍 31 | - Add missing unit tests 😅 32 | - Fixing or reporting bugs 😱 33 | 34 | If you're new to Open Source or Swift the Vapor community is a great place to get involved 35 | 36 | **Your contribution is always welcomed, no contribution is too small.** 37 | 38 | --- 39 | 40 | ## Adding new Extensions 41 | 42 | Please refer to the following rules before submitting a pull request with your new extensions: 43 | 44 | - Add your contributions to [**master branch** ](https://github.com/vapor-community/vapor-ext/tree/master): - by doing this we can merge new pull-requests into **master** branch as soon as they are accepted, and add them to the next releases once they are fully tested. 45 | - Mention the original source of extension source (if possible) as a comment inside extension: 46 | 47 | ```swift 48 | public extension SomeType { 49 | public name: SomeType { 50 | // https://stackoverflow.com/somepage 51 | // .. code 52 | } 53 | } 54 | ``` 55 | 56 | - A pull request should only add one extension at a time. 57 | - All extensions should follow [Swift API Design Guidelines](https://developer.apple.com/videos/play/wwdc2016/403/) 58 | - Always declare extensions as **public**. 59 | - All extensions names should be as clear as possible. 60 | - All extensions should be well documented. see [Adding documentation](#adding-documentation) 61 | - This library is to extend the standards types available in Async package. 62 | - Extensions could be: - Enums - Instance properties & type properties - Instance methods & type methods - Initializers 63 | - All extensions should be tested. 64 | - Files are named based on the type that the contained extensions extend and located inside a related target. 65 | - (example: `Future` extensions related to `Bool` types are found in "**Future+Bool.swift**" file insine `AsyncExt` target) 66 | - All extensions files and test files have a one to one relation. 67 | - (example: all tests for "**Future+Bool.swift**" are found in the "**Future+BoolTests.swift**" file) 68 | - There should be a one to one relationship between extensions and their backing tests. 69 | - Tests should be named using the same API of the extension it backs. 70 | - (example: `Future` extension method `equal` is named `testEqual`) 71 | - All test files are named based on the extensions which it tests. 72 | - (example: all Future extensions tests are found in "**FutureExtensionsTests.swift**" file) 73 | - Extensions and tests are ordered inside files in the following order: 74 | 75 | ```swift 76 | // MARK: - enums 77 | public enum { 78 | // ... 79 | } 80 | 81 | // MARK: - Properties 82 | public extension SomeType {} 83 | 84 | // MARK: - Methods 85 | public extension SomeType {} 86 | 87 | // MARK: - Initializers 88 | public extension SomeType {} 89 | ``` 90 | 91 | --- 92 | 93 | ## Adding documentation 94 | 95 | Use the following template to add documentation for extensions 96 | 97 | > Replace placeholders inside <> 98 | 99 | > Remove any extra lines, eg. if method does not return any value, delete the `- Returns:` line 100 | 101 | #### Documentation template for units with single parameter: 102 | 103 | ```swift 104 | /// . 105 | /// 106 | /// 107 | /// 108 | /// - Parameter : . 109 | /// - Throws: 110 | /// - Returns: 111 | ``` 112 | 113 | #### Documentation template for units with multiple parameters: 114 | 115 | ```swift 116 | /// . 117 | /// 118 | /// 119 | /// 120 | /// - Parameters: 121 | /// - : . 122 | /// - : . 123 | /// - Throws: 124 | /// - Returns: 125 | ``` 126 | 127 | #### Documentation template for enums: 128 | 129 | ```swift 130 | /// AsyncExt: . 131 | /// 132 | /// - : 133 | /// - : 134 | /// - : 135 | /// - : 136 | ``` 137 | 138 | #### Power Tip: 139 | 140 | In Xcode select a method and press `command` + `alt` + `/` to create a documentation template! 141 | 142 | --- 143 | 144 | ## Adding changelog entries 145 | 146 | The [Changelog](https://github.com/vapor-community/async-extensions/blob/master/CHANGELOG.md) is a file which contains a curated, chronologically ordered list of notable changes for each version of a project. Please make sure to add a changelog entry describing your contribution to it everyting there is a notable change. 147 | 148 | The [Changelog Guidelines](https://github.com/vapor-community/async-extensions/blob/master/CHANGELOG_GUIDELINES.md) contains instructions for maintaining (or adding new entries) to the Changelog. 149 | 150 | --- 151 | 152 | ## Reporting Issues 153 | 154 | A great way to contribute to the project is to send a detailed issue when you encounter an problem. 155 | We always appreciate a well-written, thorough bug report. 156 | 157 | Check that the project issues database doesn't already include that problem or suggestion before submitting an issue. 158 | If you find a match, add a quick "**+1**" or "**I have this problem too**". 159 | Doing this helps prioritize the most common problems and requests. 160 | 161 | **When reporting issues, please include the following:** 162 | 163 | - What did you do? 164 | - What did you expect to happen? 165 | - What happened instead? 166 | - VaporExt version 167 | - Swift version 168 | - Platform(s) (macOS or Linux) 169 | - Demo Project (if available) 170 | 171 | This information will help us review and fix your issue faster. 172 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "VaporExt", 7 | products: [ 8 | .library(name: "AsyncExt", targets: ["AsyncExt"]), 9 | .library(name: "FluentExt", targets: ["FluentExt"]), 10 | .library(name: "ServiceExt", targets: ["ServiceExt"]), 11 | .library(name: "VaporExt", targets: ["VaporExt"]), 12 | ], 13 | dependencies: [ 14 | // 💧 A server-side Swift web framework. 15 | .package(url: "https://github.com/gperdomor/vapor.git", from: "3.0.0"), 16 | // 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging. 17 | .package(url: "https://github.com/vapor/core.git", from: "3.0.0"), 18 | // 🖋 Swift ORM framework (queries, models, and relations) for building NoSQL and SQL database integrations. 19 | .package(url: "https://github.com/vapor/fluent.git", from: "3.0.0"), 20 | // 📦 Dependency injection / inversion of control framework. 21 | .package(url: "https://github.com/vapor/service.git", from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // AsyncExt 25 | .target(name: "AsyncExt", dependencies: ["Async", "Core"]), 26 | .testTarget(name: "AsyncExtTests", dependencies: ["AsyncExt"]), 27 | 28 | // FluentExt 29 | .target(name: "FluentExt", dependencies: ["Fluent", "FluentSQL", "Debugging", "Vapor"]), 30 | .testTarget(name: "FluentExtTests", dependencies: ["FluentExt"]), 31 | 32 | // ServiceExt 33 | .target(name: "ServiceExt", dependencies: ["Service"]), 34 | .testTarget(name: "ServiceExtTests", dependencies: ["ServiceExt"]), 35 | 36 | .target(name: "VaporExt", dependencies: ["AsyncExt", "FluentExt", "ServiceExt"]), 37 | .testTarget(name: "VaporExtTests", dependencies: ["VaporExt"]), 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Vapor Version 4 | 5 | 6 | MIT License 7 | 8 | 9 | Continuous Integration 10 | 11 | 12 | 13 | 14 | 15 | Swift 4.1 16 | 17 |

18 | 19 | # VaporExt 20 | 21 | VaporExt is a collection of **Swift extensions**, with handy methods, syntactic sugar, and performance improvements for wide range of Vapor types and classes. 22 | 23 | ## Requirements: 24 | 25 | - Swift 4.1+ 26 | 27 | ## Installation 28 | 29 | You can use The Swift Package Manager to install VaporExt by adding the proper description to your Package.swift file: 30 | 31 |
import PackageDescription
 32 | 
 33 | let package = Package(
 34 |     name: "YOUR_PROJECT_NAME",
 35 |     targets: [],
 36 |     dependencies: [
 37 |         .package(url: "https://github.com/vapor-community/vapor-ext.git", from: "0.1.0"),
 38 |     ]
 39 | )
 40 | 
41 | 42 |

Note that the Swift Package Manager is still in early design and development, for more information checkout its GitHub Page

43 | 44 | ## What is included? 45 | 46 | The extensions are grouped in 3 modules, `AsyncExt`, `FluentExt` and `ServiceExt`, and the `VaporExt` which acts as a helper to export the extensions included in the previous packages. 47 | 48 | ### AsyncExt 49 | 50 | - New Future methods: 51 | - `true(or error:)` 52 | - `false(or error:)` 53 | - `greater(than value:)` 54 | - `greater(than value:, or error:)` 55 | - `greaterOrEqual(to value:)` 56 | - `greaterOrEqual(to value:, or error:)` 57 | - `less(than value:)` 58 | - `less(than value:, or error:)` 59 | - `lessOrEqual(to value:)` 60 | - `lessOrEqual(to value:, or error:)` 61 | - `equal(to value:)` 62 | - `equal(to value:, or error:)` 63 | - `notEqual(to value:)` 64 | - `notEqual(to value:, or error:)` 65 | 66 | ### FluentExt 67 | 68 | - New Model functions: 69 | - `query(by:, on:, withSoftDeleted:)` to create a query for the model and apply a filter of criteria. 70 | - `findAll(sortBy:, on:, withSoftDeleted:)` to find all models and apply some sorting criteria. 71 | - `find(by:, sortBy:, on:, withSoftDeleted:)` to find models and apply some filters and sorting criteria. 72 | - `findOne(by:, on:, withSoftDeleted:)` to find first model that matches the filters criteria. 73 | - `count(on:, withSoftDeleted:)` to count the number of registers of the model. 74 | - `count(by:, on:, withSoftDeleted:)` to count the number of registers of the model that matches some criteria. 75 | - New Binary operators: 76 | - `!~=` for not has suffix comparison. 77 | - `!=~` for not has prefix comparison. 78 | - `!~~` for not contains comparison. 79 | - New filter methods: 80 | - `filter(keyPath:, at parameter:, on req:)` to handle automatic filters based in query params. 81 | - New sort methods: 82 | - `sort(keyPath:, at queryParam:, as parameter:, default direction:, on req:)` to handle automatic sorting based in query params. 83 | - `sort(keyPath:, as parameter:, default direction:, on req:)` to handle automatic sorting based in query params. 84 | - `sort(by:)` to apply some sorting criteria. 85 | - New Request extensions to build FilterOperator and QuerySort from query params (Usefull if you use a Repository system): 86 | - `filter(keyPath:, at parameter:)` to build FilterOperator based in query params. 87 | - `sort(keyPath:, at queryParam:, as parameter:)` to build QuerySort based in query params. 88 | - `sort(keyPath:, at queryParam:, as parameter:, default direction:)` to build QuerySort based in query params. 89 | - `sort(keyPath:, as parameter:)` to build QuerySort based in query params. 90 | - `sort(keyPath:, as parameter:, default direction:)` to build QuerySort based in query params. 91 | 92 | #### Query params syntax for filters: 93 | 94 | You can set the filter method with this format `parameter=method:value` and the new filter method will build the filter based on the `method`, example: 95 | 96 | - `username=example@domain.com` or `username=eq:example@domain.com` will use `equal` comparison. 97 | - `username=neq:example@domain.com` will use `not equal` comparison. 98 | - `username=in:example@domain.com,other@domain.com` will use `in` comparison. 99 | - `username=nin:example@domain.com,other@domain.com` will use `not in` comparison. 100 | - `price=gt:3000` will use `greater than` comparison. 101 | - `price=gte:3000` will use `greater than or equal` comparison. 102 | - `price=lt:3000` will use `less than` comparison. 103 | - `price=lte:3000` will use `less than or equal` comparison. 104 | - `username=sw:example` will filter using `starts with` comparison. 105 | - `username=nsw:example` will filter using `not starts with` comparison. 106 | - `username=ew:domain.com` will filter using `ends with` comparison. 107 | - `username=new:domain.com` will filter using `not ends with` comparison. 108 | - `username=ct:domain.com` will filter using `contains` comparison. 109 | - `username=nct:domain.com` will filter using `not contains` comparison. 110 | 111 | ```swift 112 | return try User.query(on: req) 113 | .filter(\User.username, at: "username", on: req) 114 | .filter(\User.enabled, at: "enabled", on: req) 115 | .filter(\User.createdAt, at: "created_at", on: req) 116 | .filter(\User.updatedAt, at: "updated_at", on: req) 117 | .filter(\User.deletedAt, at: "deleted_at", on: req) 118 | ``` 119 | 120 | #### Query params syntax for sorting: 121 | 122 | You can set the sorts methods with this format `sort=field:direction,field:direction` and the new sort method will build the query based on that configuration, for example 123 | 124 | - With `sort=username:asc,created_at:desc` you can get you users sorted by username with ASC direction and firstname in DESC direction 125 | 126 | ```swift 127 | return try User.query(on: req) 128 | .sort(\User.username, as: "username", on: req) 129 | .sort(\User.createdAt, as: "created_at", default: .ascending, on: req) // if created_at is not present in the url, then the sort is applied using the default direction 130 | ``` 131 | 132 | #### Request extensions example: 133 | 134 | ```swift 135 | func index(_ req: Request) throws -> Future<[User]> { 136 | let repository = try req.make(UserRepository.self) 137 | 138 | let criteria: [FilterOperator] = try [ 139 | req.filter(\User.name, at: "name"), 140 | req.filter(\User.username, at: "username"), 141 | req.filter(\User.enabled, at: "enabled") 142 | ].compactMap { $0 } 143 | 144 | var sort: [User.Database.QuerySort] = try [ 145 | req.sort(\User.name, as: "name"), 146 | req.sort(\User.username, as: "username"), 147 | req.sort(\User.createdAt, as: "created_at"), 148 | ].compactMap { $0 } 149 | 150 | if sort.isEmpty { 151 | let defaultSort = try req.sort(\User.name, as: "name", default: .ascending) 152 | sort.append(defaultSort) 153 | } 154 | 155 | return repository.findBy(criteria: criteria, orderBy: sort, on: req) 156 | } 157 | ``` 158 | 159 | ### ServiceExt 160 | 161 | - New Enviroment methods: 162 | - New method `dotenv(filename:)` to add `.env`support! 163 | - New generic `get(_ key:)` method to get values as Int and Bool 164 | - New generic `get(_ key:, _ fallback:)` method to get values as Int and Bool and String, with a fallback values 165 | 166 | ### VaporExt 167 | 168 | - New Future methods: 169 | - new `toResponse(on req:, as status:, contentType:)` to transform a Future to a Future 170 | 171 | ## Get involved: 172 | 173 | We want your feedback. 174 | Please refer to [contributing guidelines](https://github.com/vapor-community/vapor-ext/tree/master/CONTRIBUTING.md) before participating. 175 | 176 | ## Discord Channel: 177 | 178 | It is always nice to talk with other people using VaporExt and exchange experiences, so come [join our Discord channel](http://vapor.team). 179 | 180 | ## Credits 181 | 182 | This package is developed and maintained by [Gustavo Perdomo](https://github.com/gperdomor) with the collaboration of all vapor community. 183 | 184 | ## License 185 | 186 | VaporExt is released under an MIT license. See [license](license) for more information. 187 | -------------------------------------------------------------------------------- /Sources/AsyncExt/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/30/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @_exported import Async 10 | -------------------------------------------------------------------------------- /Sources/AsyncExt/Future+Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Bool.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | 11 | // MARK: - Methods 12 | 13 | public extension Future where T == Bool { 14 | /// Check if the current value is `true`. 15 | /// 16 | /// Future.map(on: worker) { true }.true(or: CustomError()) // true 17 | /// 18 | /// Future.map(on: worker) { false }.true(or: CustomError()) // Throws CustomError 19 | /// 20 | /// - Parameter error: The error to be thrown. 21 | /// - Returns: The result of the comparison wrapped in a Future. 22 | /// - Throws: Throws the passed error in the opposite case. 23 | func `true`(or error: Error) throws -> Future { 24 | return map(to: Bool.self) { boolean in 25 | if !boolean { 26 | throw error 27 | } 28 | return boolean 29 | } 30 | } 31 | 32 | /// Check if the current value is `false`. 33 | /// 34 | /// Future.map(on: worker) { false }.false(or: CustomError()) // true 35 | /// 36 | /// Future.map(on: worker) { true }.false(or: CustomError()) // Throws CustomError 37 | /// 38 | /// - Parameter error: The error to be thrown. 39 | /// - Returns: The result of the comparison wrapped in a Future. 40 | /// - Throws: Throws the passed error in the opposite case. 41 | func `false`(or error: Error) throws -> Future { 42 | return map(to: Bool.self) { boolean in 43 | if boolean { 44 | throw error 45 | } 46 | return !boolean 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/AsyncExt/Future+Comparable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Comparable.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | 11 | // MARK: - Methods 12 | 13 | public extension Future where T: Comparable { 14 | /// Check if the current value is greater than the passed value. 15 | /// 16 | /// Future.map(on: worker) { 5 }.greater(than: 4) // true 17 | /// 18 | /// Future.map(on: worker) { 5 }.greater(than: 5) // false 19 | /// 20 | /// - Parameter value: The value to compare. 21 | /// - Returns: The result of the comparison wrapped in a Future. 22 | func greater(than value: T) -> Future { 23 | return map(to: Bool.self) { current in 24 | current > value 25 | } 26 | } 27 | 28 | /// Check if the current value is greater than the passed value. 29 | /// 30 | /// Future.map(on: worker) { 5 }.greater(than: 4, or: CustomError()) // true 31 | /// 32 | /// Future.map(on: worker) { 5 }.greater(than: 5, or: CustomError()) // throws CustomError 33 | /// 34 | /// - Parameters: 35 | /// - value: The value to compare. 36 | /// - error: The error to be thrown. 37 | /// - Returns: The result of the comparison wrapped in a Future. 38 | /// - Throws: Throws the passed error if the current value is not greater than the passed value. 39 | func greater(than value: T, or error: Error) throws -> Future { 40 | return try greater(than: value).true(or: error) 41 | } 42 | 43 | /// Check if the current value is greater or equal to the passed value. 44 | /// 45 | /// Future.map(on: worker) { 5 }.greaterOrEqual(to: 4) // true 46 | /// 47 | /// Future.map(on: worker) { 5 }.greaterOrEqual(to: 5) // true 48 | /// 49 | /// - Parameter value: The value to compare. 50 | /// - Returns: The result of the comparison wrapped in a Future. 51 | func greaterOrEqual(to value: T) -> Future { 52 | return map(to: Bool.self) { current in 53 | current >= value 54 | } 55 | } 56 | 57 | /// Check if the current value is greater or equal to the passed value. 58 | /// 59 | /// Future.map(on: worker) { 5 }.greaterOrEqual(to: 5, or: CustomError()) // true 60 | /// 61 | /// Future.map(on: worker) { 5 }.greaterOrEqual(to: 7, or: CustomError()) // throws CustomError 62 | /// 63 | /// - Parameters: 64 | /// - value: The value to compare. 65 | /// - error: The error to be thrown. 66 | /// - Returns: The result of the comparison wrapped in a Future. 67 | /// - Throws: Throws the passed error if the current value is not greater or equal to the passed value. 68 | func greaterOrEqual(to value: T, or error: Error) throws -> Future { 69 | return try greaterOrEqual(to: value).true(or: error) 70 | } 71 | 72 | /// Check if the current value is less than the passed value. 73 | /// 74 | /// Future.map(on: worker) { 5 }.less(than: 10) // true 75 | /// 76 | /// Future.map(on: worker) { 5 }.less(than: 5) // false 77 | /// 78 | /// - Parameter value: The value to compare. 79 | /// - Returns: The result of the comparison wrapped in a Future. 80 | func less(than value: T) -> Future { 81 | return map(to: Bool.self) { current in 82 | current < value 83 | } 84 | } 85 | 86 | /// Check if the current value is less than the passed value. 87 | /// 88 | /// Future.map(on: worker) { 5 }.less(than: 10, or: CustomError()) // true 89 | /// 90 | /// Future.map(on: worker) { 5 }.less(than: 5, or: CustomError()) // throws CustomError 91 | /// 92 | /// - Parameters: 93 | /// - value: The value to compare. 94 | /// - error: The error to be thrown. 95 | /// - Returns: The result of the comparison wrapped in a Future. 96 | /// - Throws: Throws the passed error if the current value is not less than the passed value. 97 | func less(than value: T, or error: Error) throws -> Future { 98 | return try less(than: value).true(or: error) 99 | } 100 | 101 | /// Check if the current value is less or equal to the passed value. 102 | /// 103 | /// Future.map(on: worker) { 5 }.lessOrEqual(to: 10) // true 104 | /// 105 | /// Future.map(on: worker) { 5 }.lessOrEqual(to: 5) // true 106 | /// 107 | /// - Parameter value: The value to compare. 108 | /// - Returns: The result of the comparison wrapped in a Future. 109 | func lessOrEqual(to value: T) -> Future { 110 | return map(to: Bool.self) { current in 111 | current <= value 112 | } 113 | } 114 | 115 | /// Check if the current value is less or equal to the passed value. 116 | /// 117 | /// Future.map(on: worker) { 5 }.lessOrEqual(to: 10, or: CustomError()) // true 118 | /// 119 | /// Future.map(on: worker) { 5 }.lessOrEqual(to: 7, or: CustomError()) // throws CustomError 120 | /// 121 | /// - Parameters: 122 | /// - value: The value to compare. 123 | /// - error: The error to be thrown. 124 | /// - Returns: The result of the comparison wrapped in a Future. 125 | /// - Throws: Throws the passed error if the current value is not less or equal to the passed value. 126 | func lessOrEqual(to value: T, or error: Error) throws -> Future { 127 | return try lessOrEqual(to: value).true(or: error) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/AsyncExt/Future+Equatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Equatable.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | 11 | // MARK: - Methods 12 | 13 | public extension Future where T: Equatable { 14 | /// Check if the current value is equal to the passed value. 15 | /// 16 | /// Future.map(on: worker) { 5 }.equal(to: 5) // true 17 | /// 18 | /// Future.map(on: worker) { "some string" }.equal(to: "other string") // false 19 | /// 20 | /// - Parameter value: The value to compare. 21 | /// - Returns: The result of the comparison wrapped in a Future. 22 | func equal(to value: T) -> Future { 23 | return map(to: Bool.self) { current in 24 | current == value 25 | } 26 | } 27 | 28 | /// Check if the current value is equal to the passed value 29 | /// 30 | /// Future.map(on: worker) { 5 }.equal(to: 5, or: CustomError()) // true 31 | /// 32 | /// Future.map(on: worker) { 5 }.equal(to: 3, or: CustomError()) // throws CustomError 33 | /// 34 | /// - Parameters: 35 | /// - value: The value to compare. 36 | /// - error: The error to be thrown. 37 | /// - Returns: The result of the comparison wrapped in a Future. 38 | /// - Throws: Throws the passed error if values are not equals. 39 | func equal(to value: T, or error: Error) throws -> Future { 40 | return try map(to: Bool.self) { current in 41 | current == value 42 | }.true(or: error) 43 | } 44 | 45 | /// Check if the current value is different to the passed value 46 | /// 47 | /// Future.map(on: worker) { 5 }.notEqual(to: 5) // false 48 | /// 49 | /// Future.map(on: worker) { "some string" }.notEqual(to: "other string") // true 50 | /// 51 | /// - Parameter value: The value to compare. 52 | /// - Returns: The result of the comparison wrapped in a Future. 53 | func notEqual(to value: T) -> Future { 54 | return map(to: Bool.self) { current in 55 | current != value 56 | } 57 | } 58 | 59 | /// Check if the current value is different to the passed value. 60 | /// 61 | /// Future.map(on: worker) { 5 }.notEqual(to: 5, or: CustomError()) // throws CustomError 62 | /// 63 | /// Future.map(on: worker) { 5 }.notEqual(to: 3, or: CustomError()) // true 64 | /// 65 | /// - Parameters: 66 | /// - value: The value to compare. 67 | /// - error: The error to be thrown. 68 | /// - Returns: The result of the comparison wrapped in a Future. 69 | /// - Throws: Throws the passed error if values are not equals. 70 | func notEqual(to value: T, or error: Error) throws -> Future { 71 | return try map(to: Bool.self) { current in 72 | current != value 73 | }.true(or: error) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/AsyncExt/Future+Optional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+Optional.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 10/31/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | import Core 11 | 12 | public extension Future where Expectation: OptionalType { 13 | func `nil`(or error: @autoclosure @escaping () -> Error) -> Future { 14 | return map { optional in 15 | if let _ = optional.wrapped { 16 | throw error() 17 | } 18 | 19 | return optional.wrapped 20 | } 21 | } 22 | 23 | func `nil`() -> Future { 24 | return map { optional in 25 | optional.wrapped == nil 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/FluentExt/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @_exported import Fluent 10 | @_exported import FluentSQL 11 | -------------------------------------------------------------------------------- /Sources/FluentExt/Extensions/Model+Query.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model+Query.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 09/20/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Fluent 10 | import Vapor 11 | 12 | extension Model where Database: QuerySupporting { 13 | /// Creates a query for this model type on the supplied connection. 14 | /// 15 | /// - Parameters: 16 | /// - filters: Some `FilterOperator`s to be applied. 17 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 18 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 19 | /// - Returns: QueryBuilder 20 | public static func query(by filters: [FilterOperator], on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> QueryBuilder { 21 | var query = Self.query(on: conn, withSoftDeleted: withSoftDeleted) 22 | 23 | filters.forEach { filter in 24 | query = query.filter(filter) 25 | } 26 | 27 | return query 28 | } 29 | 30 | /// Collects all the results of this model type into an array. 31 | /// 32 | /// - Parameters: 33 | /// - sortBy: Some `QuerySort`s to be applied. 34 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 35 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 36 | /// - Returns: A `Future` containing the results. 37 | public static func findAll(sortBy: [Self.Database.QuerySort]? = nil, on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future<[Self]> { 38 | return Self.findAll(as: Self.self, sortBy: sortBy, on: conn, withSoftDeleted: withSoftDeleted) 39 | } 40 | 41 | /// Collects all the results of this model type into an array. 42 | /// 43 | /// - Parameters: 44 | /// - as: Sets the query to decode Model type D when run. The Model’s entity will be used. 45 | /// - sortBy: Some `QuerySort`s to be applied. 46 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 47 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 48 | /// - Returns: A `Future` containing the results. 49 | public static func findAll(as: M.Type, sortBy: [Self.Database.QuerySort]? = nil, on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future<[M]> where M: Model { 50 | return Self 51 | .query(on: conn, withSoftDeleted: withSoftDeleted) 52 | .sort(by: sortBy) 53 | .decode(M.self) 54 | .all() 55 | } 56 | 57 | /// Collects all the results of this model type that matches with filters criteria, into an array. 58 | /// 59 | /// - Parameters: 60 | /// - criteria: Some `FilterOperator`s to be applied. 61 | /// - sortBy: Some `QuerySort`s to be applied. 62 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 63 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 64 | /// - Returns: A `Future` containing the results. 65 | public static func find(by criteria: [FilterOperator] = [], sortBy: [Self.Database.QuerySort]? = nil, on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future<[Self]> { 66 | return Self.find(as: Self.self, by: criteria, sortBy: sortBy, on: conn, withSoftDeleted: withSoftDeleted) 67 | } 68 | 69 | /// Collects all the results of this model type that matches with filters criteria, into an array. 70 | /// 71 | /// - Parameters: 72 | /// - as: Sets the query to decode Model type D when run. The Model’s entity will be used. 73 | /// - criteria: Some `FilterOperator`s to be applied. 74 | /// - sortBy: Some `QuerySort`s to be applied. 75 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 76 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 77 | /// - Returns: A `Future` containing the results. 78 | public static func find(as: M.Type, by criteria: [FilterOperator] = [], sortBy: [Self.Database.QuerySort]? = nil, on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future<[M]> where M: Model { 79 | return Self 80 | .query(by: criteria, on: conn, withSoftDeleted: withSoftDeleted) 81 | .sort(by: sortBy) 82 | .decode(M.self) 83 | .all() 84 | } 85 | 86 | /// Search the first model of this type, that matches the filters criteria. 87 | /// 88 | /// - Parameters: 89 | /// - criteria: Some `FilterOperator`s to be applied. 90 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 91 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 92 | /// - Returns: A `Future` containing the result. 93 | public static func findOne(by criteria: [FilterOperator], on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future { 94 | return Self.findOne(as: Self.self, by: criteria, on: conn, withSoftDeleted: withSoftDeleted) 95 | } 96 | 97 | /// Search the first model of this type, that matches the filters criteria. 98 | /// 99 | /// - Parameters: 100 | /// - as: Sets the query to decode Model type D when run. The Model’s entity will be used. 101 | /// - criteria: Some `FilterOperator`s to be applied. 102 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 103 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 104 | /// - Returns: A `Future` containing the result. 105 | public static func findOne(as: M.Type, by criteria: [FilterOperator], on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future where M: Model { 106 | return Self 107 | .query(by: criteria, on: conn, withSoftDeleted: withSoftDeleted) 108 | .decode(M.self) 109 | .first() 110 | } 111 | 112 | /// Returns the number of registers for this model. 113 | /// 114 | /// - Parameters: 115 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 116 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 117 | /// - Returns: A `Future` containing the count. 118 | public static func count(on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future { 119 | return Self.query(on: conn, withSoftDeleted: withSoftDeleted).count() 120 | } 121 | 122 | /// Returns the number of results for this model, after apply the filter criteria. 123 | /// 124 | /// - Parameters: 125 | /// - criteria: Some `FilterOperator`s to be applied. 126 | /// - conn: Something `DatabaseConnectable` to create the `QueryBuilder` on. 127 | /// - withSoftDeleted: If `true`, soft-deleted models will be included in the results. Defaults to `false`. 128 | /// - Returns: A `Future` containing the count. 129 | public static func count(by criteria: [FilterOperator], on conn: DatabaseConnectable, withSoftDeleted: Bool = false) -> Future { 130 | return Self 131 | .query(by: criteria, on: conn, withSoftDeleted: withSoftDeleted) 132 | .count() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/FluentExt/Extensions/QueryBuilder+Filter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryBuilder+Filter.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Fluent 10 | import FluentSQL 11 | import Vapor 12 | 13 | public extension QueryBuilder where Result: Model, Result.Database == Database { 14 | /// Applies filter criteria over a keypath using criteria configured in a request query params. 15 | /// 16 | /// If your request user is /users?username=ew:gmail.com 17 | /// `\User.query(on: req).filter(\User.username, at: email, on: req)` 18 | /// then, the previous code will extract the filter config for the key `email` of your url params 19 | /// and will build a query filter for the \User.username keypath where their values ends with `gmail.com` 20 | /// 21 | /// - Parameters: 22 | /// - keyPath: the model keypath 23 | /// - parameter: the parameter name in filter config from request url params. 24 | /// - req: the request. 25 | /// - Returns: Self 26 | /// - Throws: FluentError 27 | func filter(_ keyPath: KeyPath, at parameter: String, on req: Request) throws -> Self where T: Codable { 28 | let decoder = try req.make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 29 | 30 | guard let config = req.query[String.self, at: parameter] else { 31 | return self 32 | } 33 | 34 | let filterConfig = config.toFilterConfig 35 | 36 | let method = filterConfig.method 37 | let value = filterConfig.value 38 | 39 | let multiple = [.in, .notIn].contains(method) 40 | var parsed: FilterValue 41 | 42 | if multiple { 43 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 44 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 45 | } else { 46 | let str = "\(parameter)=\(value)" 47 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 48 | } 49 | 50 | switch (method, parsed) { 51 | case let (.equal, .single(value)): // Equal 52 | return filter(keyPath == value) 53 | case let (.notEqual, .single(value)): // Not Equal 54 | return filter(keyPath != value) 55 | case let (.greaterThan, .single(value)): // Greater Than 56 | return filter(keyPath > value) 57 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 58 | return filter(keyPath >= value) 59 | case let (.lessThan, .single(value)): // Less Than 60 | return filter(keyPath < value) 61 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 62 | return filter(keyPath <= value) 63 | case let (.in, .multiple(value)): // In 64 | return filter(keyPath ~~ value) 65 | case let (.notIn, .multiple(value)): // Not In 66 | return filter(keyPath !~ value) 67 | default: 68 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 69 | } 70 | } 71 | } 72 | 73 | public extension QueryBuilder where Result: Model, Result.Database == Database, Result.Database.QueryFilterMethod: SQLBinaryOperator { 74 | /// Applies filter criteria over a keypath using criteria configured in a request query params. 75 | /// 76 | /// - Parameters: 77 | /// - keyPath: the model keypath 78 | /// - parameter: the parameter name in filter config from request url params. 79 | /// - req: the request. 80 | /// - Returns: Self 81 | /// - Throws: FluentError 82 | func filter(_ keyPath: KeyPath, at parameter: String, on req: Request) throws -> Self { 83 | let decoder = try req.make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 84 | 85 | guard let config = req.query[String.self, at: parameter] else { 86 | return self 87 | } 88 | 89 | let filterConfig = config.toFilterConfig 90 | 91 | let method = filterConfig.method 92 | let value = filterConfig.value 93 | 94 | let multiple = [.in, .notIn].contains(method) 95 | var parsed: FilterValue 96 | 97 | if multiple { 98 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 99 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 100 | } else { 101 | let str = "\(parameter)=\(value)" 102 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 103 | } 104 | 105 | switch (method, parsed) { 106 | case let (.equal, .single(value)): // Equal 107 | return filter(keyPath == value) 108 | case let (.notEqual, .single(value)): // Not Equal 109 | return filter(keyPath != value) 110 | case let (.greaterThan, .single(value)): // Greater Than 111 | return filter(keyPath > value) 112 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 113 | return filter(keyPath >= value) 114 | case let (.lessThan, .single(value)): // Less Than 115 | return filter(keyPath < value) 116 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 117 | return filter(keyPath <= value) 118 | case let (.contains, .single(value)): // Contains 119 | return filter(keyPath ~~ value) 120 | case let (.notContains, .single(value)): // Not Contains 121 | return filter(keyPath !~~ value) 122 | case let (.startsWith, .single(value)): // Start With / Preffix 123 | return filter(keyPath =~ value) 124 | case let (.endsWith, .single(value)): // Ends With / Suffix 125 | // return self.filter(keyPath ~= value) 126 | return filter(.make(keyPath, .like, ["%" + value])) 127 | case let (.notStartsWith, .single(value)): // No Start With / Preffix 128 | return filter(keyPath !=~ value) 129 | case let (.notEndsWith, .single(value)): // No Ends With / Suffix 130 | return filter(keyPath !~= value) 131 | case let (.in, .multiple(value)): // In 132 | return filter(keyPath ~~ value) 133 | case let (.notIn, .multiple(value)): // Not In 134 | return filter(keyPath !~ value) 135 | default: 136 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 137 | } 138 | } 139 | 140 | /// Applies filter criteria over a keypath using criteria configured in a request query params. 141 | /// 142 | /// - Parameters: 143 | /// - keyPath: the model keypath 144 | /// - parameter: the parameter name in filter config from request url params. 145 | /// - req: the request. 146 | /// - Returns: Self 147 | /// - Throws: FluentError 148 | func filter(_ keyPath: KeyPath, at parameter: String, on req: Request) throws -> Self { 149 | let decoder = try req.make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 150 | 151 | guard let config = req.query[String.self, at: parameter] else { 152 | return self 153 | } 154 | 155 | let filterConfig = config.toFilterConfig 156 | 157 | let method = filterConfig.method 158 | let value = filterConfig.value 159 | 160 | let multiple = [.in, .notIn].contains(method) 161 | var parsed: FilterValue 162 | 163 | if multiple { 164 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 165 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 166 | } else { 167 | let str = "\(parameter)=\(value)" 168 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 169 | } 170 | 171 | switch (method, parsed) { 172 | case let (.equal, .single(value)): // Equal 173 | return filter(keyPath == value) 174 | case let (.notEqual, .single(value)): // Not Equal 175 | return filter(keyPath != value) 176 | case let (.greaterThan, .single(value)): // Greater Than 177 | return filter(keyPath > value) 178 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 179 | return filter(keyPath >= value) 180 | case let (.lessThan, .single(value)): // Less Than 181 | return filter(keyPath < value) 182 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 183 | return filter(keyPath <= value) 184 | case let (.contains, .single(value)): // Contains 185 | return filter(keyPath ~~ value) 186 | case let (.notContains, .single(value)): // Not Contains 187 | return filter(keyPath !~~ value) 188 | case let (.startsWith, .single(value)): // Start With / Preffix 189 | return filter(keyPath =~ value) 190 | case let (.endsWith, .single(value)): // Ends With / Suffix 191 | // return self.filter(keyPath ~= value) 192 | return filter(.make(keyPath, .like, ["%" + value])) 193 | case let (.notStartsWith, .single(value)): // No Start With / Preffix 194 | return filter(keyPath !=~ value) 195 | case let (.notEndsWith, .single(value)): // No Ends With / Suffix 196 | return filter(keyPath !~= value) 197 | case let (.in, .multiple(value)): // In 198 | return filter(keyPath ~~ value) 199 | case let (.notIn, .multiple(value)): // Not In 200 | return filter(keyPath !~ value) 201 | default: 202 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/FluentExt/Extensions/QueryBuilder+Sort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryBuilder+Sort.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Fluent 10 | import Vapor 11 | 12 | public extension QueryBuilder where Result: Model, Result.Database == Database { 13 | /// Applies sort criteria over a keypath using criteria configured in a request query params. 14 | /// 15 | /// - Parameters: 16 | /// - keyPath: the model keypath. 17 | /// - queryParam: the sorting parameter name in the query params url. 18 | /// - parameter: the parameter name in sorting config. 19 | /// - direction: Default direction to apply if no value is found in url query params. 20 | /// - req: the request. 21 | /// - Returns: Self 22 | /// - Throws: FluentError 23 | func sort(_ keyPath: KeyPath, at queryParam: String, as parameter: String, default direction: Database.QuerySortDirection? = nil, on req: Request) throws -> Self { 24 | if let sort = req.query[String.self, at: queryParam] { 25 | let sortOpts = sort.components(separatedBy: ",") 26 | 27 | for option in sortOpts { 28 | let splited = option.components(separatedBy: ":") 29 | 30 | let field = splited[0] 31 | 32 | if field != parameter { 33 | continue 34 | } 35 | 36 | let direction = splited.count == 1 ? "asc" : splited[1] 37 | 38 | guard ["asc", "desc"].contains(direction) else { 39 | throw FluentError(identifier: "invalidSortConfiguration", reason: "Invalid sort config for '\(option)'") 40 | } 41 | 42 | return self.sort(keyPath, direction == "asc" ? Database.querySortDirectionAscending : Database.querySortDirectionDescending) 43 | } 44 | } 45 | 46 | if let direction = direction { 47 | return sort(keyPath, direction) 48 | } 49 | 50 | return self 51 | } 52 | 53 | /// Applies sort criteria over a keypath using criteria configured in a request query params. 54 | /// 55 | /// - Parameters: 56 | /// - keyPath: the model keypath. 57 | /// - parameter: the parameter name in sorting config. 58 | /// - direction: Default direction to apply if no value is found in url query params. 59 | /// - req: the request. 60 | /// - Returns: Self 61 | /// - Throws: FluentError 62 | func sort(_ keyPath: KeyPath, as parameter: String, default direction: Database.QuerySortDirection? = nil, on req: Request) throws -> Self { 63 | return try sort(keyPath, at: "sort", as: parameter, default: direction, on: req) 64 | } 65 | 66 | /// Applies sort criteria over a keypath using criteria configured in a request query params 67 | /// 68 | /// - Parameter sorts: Some `QuerySort`s to be applied. 69 | /// - Returns: Self 70 | func sort(by sorts: [Database.QuerySort]? = nil) -> Self { 71 | sorts?.forEach { sort in 72 | Database.querySortApply(sort, to: &query) 73 | } 74 | 75 | return self 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/FluentExt/Extensions/Request+Filter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Filter.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 08/09/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Vapor 10 | 11 | public extension Request { 12 | /// Build filter criteria over a keypath using criteria configured in a request query params. 13 | /// 14 | /// - Parameters: 15 | /// - keyPath: the model keypath 16 | /// - parameter: the parameter name in filter config from request url params. 17 | /// - Returns: FilterOperator 18 | /// - Throws: FluentError 19 | func filter(_ keyPath: KeyPath, at parameter: String) throws -> FilterOperator? where T: Codable, Result: Model { 20 | let decoder = try make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 21 | 22 | guard let config = self.query[String.self, at: parameter] else { 23 | return nil 24 | } 25 | 26 | let filterConfig = config.toFilterConfig 27 | 28 | let method = filterConfig.method 29 | let value = filterConfig.value 30 | 31 | let multiple = [.in, .notIn].contains(method) 32 | var parsed: FilterValue 33 | 34 | if multiple { 35 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 36 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 37 | } else { 38 | let str = "\(parameter)=\(value)" 39 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 40 | } 41 | 42 | switch (method, parsed) { 43 | case let (.equal, .single(value)): // Equal 44 | return keyPath == value // self.filter(keyPath == value) 45 | case let (.notEqual, .single(value)): // Not Equal 46 | return keyPath != value // self.filter(keyPath != value) 47 | case let (.greaterThan, .single(value)): // Greater Than 48 | return keyPath > value // self.filter(keyPath > value) 49 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 50 | return keyPath >= value // self.filter(keyPath >= value) 51 | case let (.lessThan, .single(value)): // Less Than 52 | return keyPath < value // self.filter(keyPath < value) 53 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 54 | return keyPath <= value // self.filter(keyPath <= value) 55 | case let (.in, .multiple(value)): // In 56 | return keyPath ~~ value // self.filter(keyPath ~~ value) 57 | case let (.notIn, .multiple(value)): // Not In 58 | return keyPath !~ value // self.filter(keyPath !~ value) 59 | default: 60 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 61 | } 62 | } 63 | 64 | /// Build filter criteria over a keypath using criteria configured in a request query params. 65 | /// 66 | /// - Parameters: 67 | /// - keyPath: the model keypath 68 | /// - parameter: the parameter name in filter config from request url params. 69 | /// - Returns: FilterOperator 70 | /// - Throws: FluentError 71 | func filter(_ keyPath: KeyPath, at parameter: String) throws -> FilterOperator? where Result: Model, Result.Database.QueryFilterMethod: SQLBinaryOperator { 72 | let decoder = try make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 73 | 74 | guard let config = self.query[String.self, at: parameter] else { 75 | return nil 76 | } 77 | 78 | let filterConfig = config.toFilterConfig 79 | 80 | let method = filterConfig.method 81 | let value = filterConfig.value 82 | 83 | let multiple = [.in, .notIn].contains(method) 84 | var parsed: FilterValue 85 | 86 | if multiple { 87 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 88 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 89 | } else { 90 | let str = "\(parameter)=\(value)" 91 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 92 | } 93 | 94 | switch (method, parsed) { 95 | case let (.equal, .single(value)): // Equal 96 | return keyPath == value 97 | case let (.notEqual, .single(value)): // Not Equal 98 | return keyPath != value 99 | case let (.greaterThan, .single(value)): // Greater Than 100 | return keyPath > value 101 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 102 | return keyPath >= value 103 | case let (.lessThan, .single(value)): // Less Than 104 | return keyPath < value 105 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 106 | return keyPath <= value 107 | case let (.contains, .single(value)): // Contains 108 | return keyPath ~~ value 109 | case let (.notContains, .single(value)): // Not Contains 110 | return keyPath !~~ value 111 | case let (.startsWith, .single(value)): // Start With / Preffix 112 | return keyPath =~ value 113 | case let (.endsWith, .single(value)): // Ends With / Suffix 114 | return keyPath ~= value 115 | case let (.notStartsWith, .single(value)): // No Start With / Preffix 116 | return keyPath !=~ value 117 | case let (.notEndsWith, .single(value)): // No Ends With / Suffix 118 | return keyPath !~= value 119 | case let (.in, .multiple(value)): // In 120 | return keyPath ~~ value 121 | case let (.notIn, .multiple(value)): // Not In 122 | return keyPath !~ value 123 | default: 124 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 125 | } 126 | } 127 | 128 | /// Build filter criteria over a keypath using criteria configured in a request query params. 129 | /// 130 | /// - Parameters: 131 | /// - keyPath: the model keypath 132 | /// - parameter: the parameter name in filter config from request url params. 133 | /// - Returns: FilterOperator 134 | /// - Throws: FluentError 135 | func filter(_ keyPath: KeyPath, at parameter: String) throws -> FilterOperator? where Result: Model, Result.Database.QueryFilterMethod: SQLBinaryOperator { 136 | let decoder = try make(ContentCoders.self).requireDataDecoder(for: .urlEncodedForm) 137 | 138 | guard let config = self.query[String.self, at: parameter] else { 139 | return nil 140 | } 141 | 142 | let filterConfig = config.toFilterConfig 143 | 144 | let method = filterConfig.method 145 | let value = filterConfig.value 146 | 147 | let multiple = [.in, .notIn].contains(method) 148 | var parsed: FilterValue 149 | 150 | if multiple { 151 | let str = value.components(separatedBy: ",").map { "\(parameter)[]=\($0)" }.joined(separator: "&") 152 | parsed = try .multiple(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 153 | } else { 154 | let str = "\(parameter)=\(value)" 155 | parsed = try .single(decoder.decode(SingleValueDecoder.self, from: str).get(at: [parameter.makeBasicKey()])) 156 | } 157 | 158 | switch (method, parsed) { 159 | case let (.equal, .single(value)): // Equal 160 | return keyPath == value 161 | case let (.notEqual, .single(value)): // Not Equal 162 | return keyPath != value 163 | case let (.greaterThan, .single(value)): // Greater Than 164 | return keyPath > value 165 | case let (.greaterThanOrEqual, .single(value)): // Greater Than Or Equal 166 | return keyPath >= value 167 | case let (.lessThan, .single(value)): // Less Than 168 | return keyPath < value 169 | case let (.lessThanOrEqual, .single(value)): // Less Than Or Equal 170 | return keyPath <= value 171 | case let (.contains, .single(value)): // Contains 172 | return keyPath ~~ value 173 | case let (.notContains, .single(value)): // Not Contains 174 | return keyPath !~~ value 175 | case let (.startsWith, .single(value)): // Start With / Preffix 176 | return keyPath =~ value 177 | case let (.endsWith, .single(value)): // Ends With / Suffix 178 | return keyPath ~= value 179 | case let (.notStartsWith, .single(value)): // No Start With / Preffix 180 | return keyPath !=~ value 181 | case let (.notEndsWith, .single(value)): // No Ends With / Suffix 182 | return keyPath !~= value 183 | case let (.in, .multiple(value)): // In 184 | return keyPath ~~ value 185 | case let (.notIn, .multiple(value)): // Not In 186 | return keyPath !~ value 187 | default: 188 | throw FluentError(identifier: "invalidFilterConfiguration", reason: "Invalid filter config for '\(config)'") 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/FluentExt/Extensions/Request+Sort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Request+Sort.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 08/09/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Vapor 10 | 11 | public extension Request { 12 | /// Build sort criteria over a keypath using criteria configured in a request query params. 13 | /// 14 | /// - Parameters: 15 | /// - keyPath: the model keypath. 16 | /// - queryParam: the sorting parameter name in the query params url. 17 | /// - parameter: the parameter name in sorting config. 18 | /// - Returns: QuerySort criteria 19 | /// - Throws: FluentError 20 | func sort(_ keyPath: KeyPath, at queryParam: String, as parameter: String) throws -> M.Database.QuerySort? where M: Model { 21 | if let sort = self.query[String.self, at: queryParam] { 22 | let sortOpts = sort.components(separatedBy: ",") 23 | 24 | for option in sortOpts { 25 | let splited = option.components(separatedBy: ":") 26 | 27 | let field = splited[0] 28 | 29 | if field != parameter { 30 | continue 31 | } 32 | 33 | let direction = splited.count == 1 ? "asc" : splited[1] 34 | 35 | guard ["asc", "desc"].contains(direction) else { 36 | throw FluentError(identifier: "invalidSortConfiguration", reason: "Invalid sort config for '\(option)'") 37 | } 38 | 39 | let dir = direction == "asc" ? M.Database.querySortDirectionAscending : M.Database.querySortDirectionDescending 40 | 41 | return M.Database.querySort(M.Database.queryField(.keyPath(keyPath)), dir) 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | /// Build sort criteria over a keypath using criteria configured in a request query params. 49 | /// 50 | /// - Parameters: 51 | /// - keyPath: the model keypath. 52 | /// - queryParam: the sorting parameter name in the query params url. 53 | /// - parameter: the parameter name in sorting config. 54 | /// - direction: Default direction to apply if no value is found in url query params. 55 | /// - Returns: QuerySort criteria 56 | /// - Throws: FluentError 57 | func sort(_ keyPath: KeyPath, at queryParam: String, as parameter: String, default direction: M.Database.QuerySortDirection) throws -> M.Database.QuerySort where M: Model { 58 | if let sort = try self.sort(keyPath, at: queryParam, as: parameter) { 59 | return sort 60 | } 61 | 62 | return M.Database.querySort(M.Database.queryField(.keyPath(keyPath)), direction) 63 | } 64 | 65 | /// Build sort criteria over a keypath using criteria configured in a request query params. 66 | /// 67 | /// - Parameters: 68 | /// - keyPath: the model keypath. 69 | /// - parameter: the parameter name in sorting config. 70 | /// - Returns: QuerySort criteria 71 | /// - Throws: FluentError 72 | func sort(_ keyPath: KeyPath, as parameter: String) throws -> M.Database.QuerySort? where M: Model { 73 | return try sort(keyPath, at: "sort", as: parameter) 74 | } 75 | 76 | /// Build sort criteria over a keypath using criteria configured in a request query params. 77 | /// 78 | /// - Parameters: 79 | /// - keyPath: the model keypath. 80 | /// - parameter: the parameter name in sorting config. 81 | /// - direction: Default direction to apply if no value is found in url query params. 82 | /// - Returns: QuerySort criteria 83 | /// - Throws: FluentError 84 | func sort(_ keyPath: KeyPath, as parameter: String, default direction: M.Database.QuerySortDirection) throws -> M.Database.QuerySort where M: Model { 85 | return try sort(keyPath, at: "sort", as: parameter, default: direction) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/FluentExt/FilterConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterConfig.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 08/14/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct FilterConfig { 12 | var method: QueryFilterMethod 13 | var value: String 14 | } 15 | 16 | internal let FILTER_CONFIG_REGEX = "(\(QueryFilterMethod.allCases.map { $0.rawValue }.joined(separator: "|")))?:?(.*)" 17 | 18 | internal extension String { 19 | var toFilterConfig: FilterConfig { 20 | let result = capturedGroups(withRegex: FILTER_CONFIG_REGEX) 21 | 22 | var method = QueryFilterMethod.equal 23 | 24 | if let rawValue = result[0], let qfm = QueryFilterMethod(rawValue: rawValue) { 25 | method = qfm 26 | } 27 | 28 | return FilterConfig(method: method, value: result[1] ?? "") 29 | } 30 | 31 | func capturedGroups(withRegex pattern: String) -> [String?] { 32 | var results = [String?]() 33 | 34 | var regex: NSRegularExpression 35 | do { 36 | regex = try NSRegularExpression(pattern: pattern, options: []) 37 | } catch { 38 | return results 39 | } 40 | 41 | let matches = regex.matches(in: self, options: [], range: NSRange(location: 0, length: count)) 42 | 43 | guard let match = matches.first else { return results } 44 | 45 | let lastRangeIndex = match.numberOfRanges - 1 46 | guard lastRangeIndex >= 1 else { return results } 47 | 48 | for i in 1 ... lastRangeIndex { 49 | let capturedGroupIndex = match.range(at: i) 50 | 51 | let matchedString: String? = capturedGroupIndex.location == NSNotFound ? nil : NSString(string: self).substring(with: capturedGroupIndex) 52 | 53 | results.append(matchedString) 54 | } 55 | 56 | return results 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/FluentExt/FilterValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterValue.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | internal enum FilterValue { 10 | case single(S) 11 | case multiple(M) 12 | } 13 | -------------------------------------------------------------------------------- /Sources/FluentExt/QueryFilterMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryFilterMethod.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | internal enum QueryFilterMethod: String { 10 | case equal = "eq" 11 | case notEqual = "neq" 12 | case `in` = "in" 13 | case notIn = "nin" 14 | case greaterThan = "gt" 15 | case greaterThanOrEqual = "gte" 16 | case lessThan = "lt" 17 | case lessThanOrEqual = "lte" 18 | case startsWith = "sw" 19 | case notStartsWith = "nsw" 20 | case endsWith = "ew" 21 | case notEndsWith = "new" 22 | case contains = "ct" 23 | case notContains = "nct" 24 | } 25 | 26 | #if swift(>=4.2) 27 | extension QueryFilterMethod: CaseIterable {} 28 | #else 29 | extension QueryFilterMethod { 30 | static var allCases: [QueryFilterMethod] { 31 | return [ 32 | .equal, 33 | .notEqual, 34 | .in, 35 | .notIn, 36 | .greaterThan, 37 | .greaterThanOrEqual, 38 | .lessThan, 39 | .lessThanOrEqual, 40 | .startsWith, 41 | .notStartsWith, 42 | .endsWith, 43 | .notEndsWith, 44 | .contains, 45 | .notContains, 46 | ] 47 | } 48 | } 49 | #endif 50 | -------------------------------------------------------------------------------- /Sources/FluentExt/SQL+Contains.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SQL+Contains.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Fluent 10 | import FluentSQL 11 | 12 | infix operator !~= 13 | /// Not has suffix 14 | public func !~= (lhs: KeyPath, rhs: String) -> FilterOperator 15 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 16 | return .make(lhs, .notLike, ["%" + rhs]) 17 | } 18 | 19 | /// Not has suffix 20 | public func !~= (lhs: KeyPath, rhs: String) -> FilterOperator 21 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 22 | return .make(lhs, .notLike, ["%" + rhs]) 23 | } 24 | 25 | infix operator !=~ 26 | /// Not has prefix. 27 | public func !=~ (lhs: KeyPath, rhs: String) -> FilterOperator 28 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 29 | return .make(lhs, .notLike, [rhs + "%"]) 30 | } 31 | 32 | /// Not has prefix. 33 | public func !=~ (lhs: KeyPath, rhs: String) -> FilterOperator 34 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 35 | return .make(lhs, .notLike, [rhs + "%"]) 36 | } 37 | 38 | infix operator !~~ 39 | /// Not contains. 40 | public func !~~ (lhs: KeyPath, rhs: String) -> FilterOperator 41 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 42 | return .make(lhs, .notLike, ["%" + rhs + "%"]) 43 | } 44 | 45 | /// Not contains. 46 | public func !~~ (lhs: KeyPath, rhs: String) -> FilterOperator 47 | where D: QuerySupporting, D.QueryFilterMethod: SQLBinaryOperator { 48 | return .make(lhs, .notLike, ["%" + rhs + "%"]) 49 | } 50 | -------------------------------------------------------------------------------- /Sources/FluentExt/SingleValueDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingleValueDecoder.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Vapor 10 | 11 | // swiftlint:disable force_unwrapping 12 | /// Decodes nested, single values from data at a key path. 13 | internal struct SingleValueDecoder: Decodable { 14 | let decoder: Decoder 15 | init(from decoder: Decoder) throws { 16 | self.decoder = decoder 17 | } 18 | 19 | func get(at keyPath: [BasicKey]) throws -> D where D: Decodable { 20 | let unwrapper = self 21 | var state = try ContainerState.keyed(unwrapper.decoder.container(keyedBy: BasicKey.self)) 22 | 23 | var keys = Array(keyPath.reversed()) 24 | if keys.isEmpty { 25 | return try unwrapper.decoder.singleValueContainer().decode(D.self) 26 | } 27 | 28 | while let key = keys.popLast() { 29 | switch keys.count { 30 | case 0: 31 | switch state { 32 | case let .keyed(keyed): 33 | return try keyed.decode(D.self, forKey: key) 34 | case var .unkeyed(unkeyed): 35 | return try unkeyed.nestedContainer(keyedBy: BasicKey.self) 36 | .decode(D.self, forKey: key) 37 | } 38 | case 1...: 39 | let next = keys.last! 40 | if let index = next.intValue { 41 | switch state { 42 | case let .keyed(keyed): 43 | var new = try keyed.nestedUnkeyedContainer(forKey: key) 44 | state = try .unkeyed(new.skip(to: index)) 45 | case var .unkeyed(unkeyed): 46 | var new = try unkeyed.nestedUnkeyedContainer() 47 | state = try .unkeyed(new.skip(to: index)) 48 | } 49 | } else { 50 | switch state { 51 | case let .keyed(keyed): 52 | state = try .keyed(keyed.nestedContainer(keyedBy: BasicKey.self, forKey: key)) 53 | case var .unkeyed(unkeyed): 54 | state = try .keyed(unkeyed.nestedContainer(keyedBy: BasicKey.self)) 55 | } 56 | } 57 | default: fatalError("Unexpected negative key count") 58 | } 59 | } 60 | fatalError("`while let key = keys.popLast()` should never fallthrough") 61 | } 62 | } 63 | 64 | private enum ContainerState { 65 | case keyed(KeyedDecodingContainer) 66 | case unkeyed(UnkeyedDecodingContainer) 67 | } 68 | 69 | private extension UnkeyedDecodingContainer { 70 | mutating func skip(to count: Int) throws -> UnkeyedDecodingContainer { 71 | for _ in 0 ..< count { 72 | _ = try nestedContainer(keyedBy: BasicKey.self) 73 | } 74 | return self 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ServiceExt/Environment+DotEnv.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment+DotEnv.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Service 11 | 12 | #if os(Linux) 13 | import Glibc 14 | #else 15 | import Darwin 16 | #endif 17 | 18 | // MARK: - Methods 19 | 20 | public extension Environment { 21 | /// Loads environment variables from .env files. 22 | /// 23 | /// - Parameter filename: name of your env file. 24 | static func dotenv(filename: String = ".env") { 25 | guard let path = getAbsolutePath(for: filename), 26 | let contents = try? String(contentsOfFile: path, encoding: .utf8) else { 27 | return 28 | } 29 | 30 | let lines = contents.split(whereSeparator: { $0 == "\n" || $0 == "\r\n" }) 31 | 32 | for line in lines { 33 | // ignore comments 34 | if line.starts(with: "#") { 35 | continue 36 | } 37 | 38 | // ignore lines that appear empty 39 | if line.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { 40 | continue 41 | } 42 | 43 | // extract key and value which are separated by an equals sign 44 | let parts = line.components(separatedBy: "=") 45 | 46 | let key = parts[0].trimmingCharacters(in: .whitespacesAndNewlines) 47 | var value = parts[1].trimmingCharacters(in: .whitespacesAndNewlines) 48 | 49 | // remove surrounding quotes from value & convert remove escape character before any embedded quotes 50 | if value[value.startIndex] == "\"" && value[value.index(before: value.endIndex)] == "\"" { 51 | value.remove(at: value.startIndex) 52 | value.remove(at: value.index(before: value.endIndex)) 53 | value = value.replacingOccurrences(of: "\\\"", with: "\"") 54 | } 55 | 56 | // remove surrounding single quotes from value & convert remove escape character before any embedded quotes 57 | if value[value.startIndex] == "'" && value[value.index(before: value.endIndex)] == "'" { 58 | value.remove(at: value.startIndex) 59 | value.remove(at: value.index(before: value.endIndex)) 60 | value = value.replacingOccurrences(of: "'", with: "'") 61 | } 62 | 63 | setenv(key, value, 1) 64 | } 65 | } 66 | 67 | /// Determine absolute path of the given argument relative to the current directory. 68 | /// 69 | /// - Parameter relativePath: relative path of the file. 70 | /// - Returns: the absolute path if exists. 71 | private static func getAbsolutePath(for filename: String) -> String? { 72 | let fileManager = FileManager.default 73 | let currentPath = DirectoryConfig.detect().workDir.finished(with: "/") 74 | let filePath = currentPath + filename 75 | if fileManager.fileExists(atPath: filePath) { 76 | return filePath 77 | } else { 78 | return nil 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ServiceExt/Environment+Getters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment+Getters.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Service 10 | 11 | #if os(Linux) 12 | import Glibc 13 | #else 14 | import Darwin 15 | #endif 16 | 17 | // MARK: - Methods 18 | 19 | public extension Environment { 20 | /// Gets a key from the process environment. 21 | /// 22 | /// - Parameter key: the environment variable name. 23 | /// - Returns: the environment variable value if exists. 24 | static func get(_ key: String) -> Int? { 25 | if let value: String = self.get(key), let parsed = Int(value) { 26 | return parsed 27 | } 28 | 29 | return nil 30 | } 31 | 32 | /// Gets a key from the process environment. 33 | /// 34 | /// - Parameter key: the environment variable name. 35 | /// - Returns: the environment variable value if exists. 36 | static func get(_ key: String) -> Bool? { 37 | if let value: String = self.get(key), let parsed = value.lowercased().bool { 38 | return parsed 39 | } 40 | 41 | return nil 42 | } 43 | 44 | /// Gets a key from the process environment. 45 | /// 46 | /// - Parameters: 47 | /// - key: the environment variable name. 48 | /// - fallback: the default value. 49 | /// - Returns: the environment variable value if exists, otherwise the `fallback` value. 50 | static func get(_ key: String, _ fallback: String) -> String { 51 | return get(key) ?? fallback 52 | } 53 | 54 | /// Gets a key from the process environment. 55 | /// 56 | /// - Parameters: 57 | /// - key: the environment variable name. 58 | /// - fallback: the default value. 59 | /// - Returns: the environment variable value if exists, otherwise the `fallback` value. 60 | static func get(_ key: String, _ fallback: Int) -> Int { 61 | guard let value: Int = self.get(key) else { 62 | return fallback 63 | } 64 | 65 | return value 66 | } 67 | 68 | /// Gets a key from the process environment. 69 | /// 70 | /// - Parameters: 71 | /// - key: the environment variable name. 72 | /// - fallback: the default value. 73 | /// - Returns: the environment variable value if exists, otherwise the `fallback` value. 74 | static func get(_ key: String, _ fallback: Bool) -> Bool { 75 | guard let value: Bool = self.get(key) else { 76 | return fallback 77 | } 78 | 79 | return value 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ServiceExt/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @_exported import Service 10 | -------------------------------------------------------------------------------- /Sources/VaporExt/Encodable+Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encodable+Response.swift 3 | // VaporExt 4 | // 5 | // Created by Gustavo Perdomo on 09/06/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | public extension Future where T: Encodable { 10 | /// Transforms a Future to a Future using the passed configuration 11 | /// 12 | /// - Parameters: 13 | /// - req: the request 14 | /// - status: the status of the new response 15 | /// - contentType: The MediaType to encode the data 16 | /// - Returns: A Future with the encodable data 17 | /// - Throws: Errors errors during encoding 18 | func toResponse(on req: Request, as status: HTTPStatus, contentType: MediaType = .json) throws -> Future { 19 | return map(to: Response.self) { encodable in 20 | let response = req.response() 21 | try response.content.encode(encodable, as: contentType) 22 | response.http.status = status 23 | 24 | return response 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/VaporExt/Exports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Exports.swift 3 | // VaporExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @_exported import AsyncExt 10 | @_exported import FluentExt 11 | @_exported import ServiceExt 12 | 13 | @_exported import Vapor 14 | -------------------------------------------------------------------------------- /Tests/AsyncExtTests/Future+BoolTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+BoolTests.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | @testable import AsyncExt 11 | import XCTest 12 | 13 | // MARK: - Methods 14 | 15 | final class FutureBoolTests: XCTestCase { 16 | let worker: Worker = EmbeddedEventLoop() 17 | 18 | func testTrue() throws { 19 | let futTrue = Future.map(on: worker) { true } 20 | let futFalse = Future.map(on: worker) { false } 21 | 22 | XCTAssertEqual(try futTrue.true(or: CustomError()).wait(), true) 23 | XCTAssertThrowsError(try futFalse.true(or: CustomError()).wait()) 24 | } 25 | 26 | func testFalse() throws { 27 | let futTrue = Future.map(on: worker) { true } 28 | let futFalse = Future.map(on: worker) { false } 29 | 30 | XCTAssertThrowsError(try futTrue.false(or: CustomError()).wait()) 31 | XCTAssertEqual(try futFalse.false(or: CustomError()).wait(), true) 32 | } 33 | 34 | func testLinuxTestSuiteIncludesAllTests() throws { 35 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 36 | let thisClass = type(of: self) 37 | let linuxCount = thisClass.allTests.count 38 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 39 | 40 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 41 | #endif 42 | } 43 | 44 | static let allTests = [ 45 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 46 | ("testTrue", testTrue), 47 | ("testFalse", testFalse) 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /Tests/AsyncExtTests/Future+ComparableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+ComparableTests.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | @testable import AsyncExt 11 | import XCTest 12 | 13 | final class FutureComparableTests: XCTestCase { 14 | let worker: Worker = EmbeddedEventLoop() 15 | 16 | func testGreater() throws { 17 | let fut1 = Future.map(on: worker) { 34 } 18 | 19 | XCTAssertEqual(try fut1.greater(than: 10).wait(), true) 20 | XCTAssertEqual(try fut1.greater(than: 34).wait(), false) 21 | XCTAssertEqual(try fut1.greater(than: 50).wait(), false) 22 | 23 | XCTAssertNoThrow(try fut1.greater(than: 10, or: CustomError()).wait()) 24 | XCTAssertThrowsError(try fut1.greater(than: 34, or: CustomError()).wait()) 25 | XCTAssertThrowsError(try fut1.greater(than: 50, or: CustomError()).wait()) 26 | } 27 | 28 | func testGreaterOrEqual() throws { 29 | let fut1 = Future.map(on: worker) { 34 } 30 | 31 | XCTAssertEqual(try fut1.greaterOrEqual(to: 10).wait(), true) 32 | XCTAssertEqual(try fut1.greaterOrEqual(to: 34).wait(), true) 33 | XCTAssertEqual(try fut1.greaterOrEqual(to: 50).wait(), false) 34 | 35 | XCTAssertNoThrow(try fut1.greaterOrEqual(to: 10, or: CustomError()).wait()) 36 | XCTAssertNoThrow(try fut1.greaterOrEqual(to: 34, or: CustomError()).wait()) 37 | XCTAssertThrowsError(try fut1.greaterOrEqual(to: 50, or: CustomError()).wait()) 38 | } 39 | 40 | func testLess() throws { 41 | let fut1 = Future.map(on: worker) { 34 } 42 | 43 | XCTAssertEqual(try fut1.less(than: 10).wait(), false) 44 | XCTAssertEqual(try fut1.less(than: 34).wait(), false) 45 | XCTAssertEqual(try fut1.less(than: 50).wait(), true) 46 | 47 | XCTAssertThrowsError(try fut1.less(than: 10, or: CustomError()).wait()) 48 | XCTAssertThrowsError(try fut1.less(than: 34, or: CustomError()).wait()) 49 | XCTAssertNoThrow(try fut1.less(than: 50, or: CustomError()).wait()) 50 | } 51 | 52 | func testLessOrEqual() throws { 53 | let fut1 = Future.map(on: worker) { 34 } 54 | 55 | XCTAssertEqual(try fut1.lessOrEqual(to: 10).wait(), false) 56 | XCTAssertEqual(try fut1.lessOrEqual(to: 34).wait(), true) 57 | XCTAssertEqual(try fut1.lessOrEqual(to: 50).wait(), true) 58 | 59 | XCTAssertThrowsError(try fut1.lessOrEqual(to: 10, or: CustomError()).wait()) 60 | XCTAssertNoThrow(try fut1.lessOrEqual(to: 34, or: CustomError()).wait()) 61 | XCTAssertNoThrow(try fut1.lessOrEqual(to: 50, or: CustomError()).wait()) 62 | } 63 | 64 | func testLinuxTestSuiteIncludesAllTests() throws { 65 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 66 | let thisClass = type(of: self) 67 | let linuxCount = thisClass.allTests.count 68 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 69 | 70 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 71 | #endif 72 | } 73 | 74 | static let allTests = [ 75 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 76 | ("testGreater", testGreater), 77 | ("testGreaterOrEqual", testGreaterOrEqual), 78 | ("testLess", testLess), 79 | ("testLessOrEqual", testLessOrEqual) 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /Tests/AsyncExtTests/Future+EquatableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Future+EquatableTests.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import Async 10 | @testable import AsyncExt 11 | import XCTest 12 | 13 | final class FutureEquatableTests: XCTestCase { 14 | let worker: Worker = EmbeddedEventLoop() 15 | 16 | func testEqual() throws { 17 | let fut1 = Future.map(on: worker) { 34 } 18 | let fut2 = Future.map(on: worker) { "string" } 19 | 20 | XCTAssertEqual(try fut1.equal(to: 34).wait(), true) 21 | XCTAssertEqual(try fut1.equal(to: 30).wait(), false) 22 | 23 | XCTAssertNoThrow(try fut1.equal(to: 34, or: CustomError()).wait()) 24 | XCTAssertThrowsError(try fut1.equal(to: 30, or: CustomError()).wait()) 25 | 26 | XCTAssertEqual(try fut2.equal(to: "string").wait(), true) 27 | XCTAssertEqual(try fut2.equal(to: "not-equal").wait(), false) 28 | 29 | XCTAssertNoThrow(try fut2.equal(to: "string", or: CustomError()).wait()) 30 | XCTAssertThrowsError(try fut2.equal(to: "not-equal", or: CustomError()).wait()) 31 | } 32 | 33 | func testNotEqual() throws { 34 | let fut1 = Future.map(on: worker) { 34 } 35 | let fut2 = Future.map(on: worker) { "string" } 36 | 37 | XCTAssertEqual(try fut1.notEqual(to: 34).wait(), false) 38 | XCTAssertEqual(try fut1.notEqual(to: 30).wait(), true) 39 | 40 | XCTAssertThrowsError(try fut1.notEqual(to: 34, or: CustomError()).wait()) 41 | XCTAssertNoThrow(try fut1.notEqual(to: 30, or: CustomError()).wait()) 42 | 43 | XCTAssertEqual(try fut2.notEqual(to: "string").wait(), false) 44 | XCTAssertEqual(try fut2.notEqual(to: "not-equal").wait(), true) 45 | 46 | XCTAssertThrowsError(try fut2.notEqual(to: "string", or: CustomError()).wait()) 47 | XCTAssertNoThrow(try fut2.notEqual(to: "not-equal", or: CustomError()).wait()) 48 | } 49 | 50 | func testLinuxTestSuiteIncludesAllTests() throws { 51 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 52 | let thisClass = type(of: self) 53 | let linuxCount = thisClass.allTests.count 54 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 55 | 56 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 57 | #endif 58 | } 59 | 60 | static let allTests = [ 61 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 62 | ("testEqual", testEqual), 63 | ("testNotEqual", testNotEqual) 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /Tests/AsyncExtTests/Helpers/CustomError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomError.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | struct CustomError: Error {} 10 | -------------------------------------------------------------------------------- /Tests/AsyncExtTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // AsyncExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | #if !os(macOS) 12 | public func allTests() -> [XCTestCaseEntry] { 13 | return [ 14 | testCase(FutureBoolTests.allTests), 15 | testCase(FutureComparableTests.allTests), 16 | testCase(FutureEquatableTests.allTests) 17 | ] 18 | } 19 | #endif 20 | -------------------------------------------------------------------------------- /Tests/FluentExtTests/StubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FluentExtTests.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @testable import FluentExt 10 | import XCTest 11 | 12 | final class StubTests: XCTestCase { 13 | func testExample() { 14 | // This is an example of a functional test case. 15 | // Use XCTAssert and related functions to verify your tests produce the correct 16 | // results. 17 | XCTAssertEqual(true, true) 18 | } 19 | 20 | func testLinuxTestSuiteIncludesAllTests() throws { 21 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 22 | let thisClass = type(of: self) 23 | let linuxCount = thisClass.allTests.count 24 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 25 | 26 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 27 | #endif 28 | } 29 | 30 | static let allTests = [ 31 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 32 | ("testExample", testExample) 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Tests/FluentExtTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // FluentExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | #if !os(macOS) 12 | public func allTests() -> [XCTestCaseEntry] { 13 | return [ 14 | testCase(StubTests.allTests) 15 | ] 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxMain.swift 3 | // VaporExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import AsyncExtTests 12 | @testable import FluentExtTests 13 | @testable import ServiceExtTests 14 | @testable import VaporExtTests 15 | 16 | var tests = [XCTestCaseEntry]() 17 | 18 | tests += AsyncExtTests.allTests() 19 | tests += FluentExtTests.allTests() 20 | tests += ServiceExtTests.allTests() 21 | tests += VaporExtTests.allTests() 22 | 23 | XCTMain(tests) 24 | -------------------------------------------------------------------------------- /Tests/ServiceExtTests/Environment+DotEnvTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentTests.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @testable import ServiceExt 10 | import XCTest 11 | 12 | #if os(Linux) 13 | import Glibc 14 | #else 15 | import Darwin 16 | #endif 17 | 18 | final class EnvironmentDotEnvTests: XCTestCase { 19 | let path = "\(DirectoryConfig.detect().workDir)/_____test.env" 20 | 21 | override func setUp() { 22 | let mockEnv = 23 | """ 24 | # example comment 25 | 26 | SERVICE_EXT_STRING_QUOTED="Hello Vapor Quoted" 27 | SERVICE_EXT_STRING_QUOTED_2=Hello "Vapor" 28 | SERVICE_EXT_STRING=Hello Vapor 29 | SERVICE_EXT_INT=107 30 | 31 | SERVICE_EXT_BOOL=true 32 | """ 33 | 34 | FileManager.default.createFile(atPath: path, contents: mockEnv.data(using: .utf8), attributes: nil) 35 | } 36 | 37 | override func tearDown() { 38 | try? FileManager.default.removeItem(atPath: path) 39 | } 40 | 41 | func testDotEnv() { 42 | Environment.dotenv(filename: "_____test.env") 43 | 44 | var string: String? = Environment.get("SERVICE_EXT_STRING_QUOTED") 45 | XCTAssertEqual(string, "Hello Vapor Quoted") 46 | 47 | string = Environment.get("SERVICE_EXT_STRING_QUOTED_2") 48 | XCTAssertEqual(string, "Hello \"Vapor\"") 49 | 50 | string = Environment.get("SERVICE_EXT_STRING") 51 | XCTAssertEqual(string, "Hello Vapor") 52 | 53 | let int: Int? = Environment.get("SERVICE_EXT_INT") 54 | XCTAssertEqual(int, 107) 55 | 56 | let bool: Bool? = Environment.get("SERVICE_EXT_BOOL") 57 | XCTAssertEqual(bool, true) 58 | } 59 | 60 | func testLinuxTestSuiteIncludesAllTests() throws { 61 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 62 | let thisClass = type(of: self) 63 | let linuxCount = thisClass.allTests.count 64 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 65 | 66 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 67 | #endif 68 | } 69 | 70 | static let allTests = [ 71 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 72 | ("testDotEnv", testDotEnv) 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /Tests/ServiceExtTests/Environment+GettersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentTests.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @testable import ServiceExt 10 | import XCTest 11 | 12 | final class EnvironmentTests: XCTestCase { 13 | func testGetEnvAsInt() { 14 | setenv("VAPOR_EXT_VAR_INT", "8080", 1) 15 | 16 | var parsed: Int? = Environment.get("VAPOR_EXT_VAR_INT") 17 | XCTAssertEqual(parsed, 8080) 18 | 19 | parsed = Environment.get("VAPOR_EXT_VAR_______") 20 | XCTAssertNil(parsed) 21 | } 22 | 23 | func testGetEnvAsBool() { 24 | setenv("VAPOR_EXT_VAR_BOOL_1", "TRUE", 1) 25 | setenv("VAPOR_EXT_VAR_BOOL_2", "True", 1) 26 | setenv("VAPOR_EXT_VAR_BOOL_3", "true", 1) 27 | setenv("VAPOR_EXT_VAR_BOOL_4", "1", 1) 28 | 29 | setenv("VAPOR_EXT_VAR_BOOL_5", "FALSE", 1) 30 | setenv("VAPOR_EXT_VAR_BOOL_6", "False", 1) 31 | setenv("VAPOR_EXT_VAR_BOOL_7", "false", 1) 32 | setenv("VAPOR_EXT_VAR_BOOL_8", "0", 1) 33 | 34 | print("" + (Environment.get("VAPOR_EXT_VAR_BOOL_1") ?? "")) 35 | var parsed: Bool? = Environment.get("VAPOR_EXT_VAR_BOOL_1") 36 | XCTAssertEqual(parsed, true) 37 | 38 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_1") 39 | XCTAssertEqual(parsed, true) 40 | 41 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_2") 42 | XCTAssertEqual(parsed, true) 43 | 44 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_3") 45 | XCTAssertEqual(parsed, true) 46 | 47 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_4") 48 | XCTAssertEqual(parsed, true) 49 | 50 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_5") 51 | XCTAssertEqual(parsed, false) 52 | 53 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_6") 54 | XCTAssertEqual(parsed, false) 55 | 56 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_7") 57 | XCTAssertEqual(parsed, false) 58 | 59 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_8") 60 | XCTAssertEqual(parsed, false) 61 | 62 | parsed = Environment.get("VAPOR_EXT_VAR_BOOL_______") 63 | XCTAssertNil(parsed) 64 | } 65 | 66 | func testFallback() { 67 | setenv("VAPOR_EXT_VAR_FALLBACK", "1", 1) 68 | 69 | // Int 70 | var fallbackInt = Environment.get("VAPOR_EXT_VAR_FALLBACK", 9999) 71 | XCTAssertEqual(fallbackInt, 1) 72 | 73 | fallbackInt = Environment.get("X_____", 9999) 74 | XCTAssertEqual(fallbackInt, 9999) 75 | 76 | // Int 77 | var fallbackBool = Environment.get("VAPOR_EXT_VAR_FALLBACK", false) 78 | XCTAssertEqual(fallbackBool, true) 79 | 80 | fallbackBool = Environment.get("X_____", false) 81 | XCTAssertEqual(fallbackBool, false) 82 | 83 | // String 84 | var fallbackString = Environment.get("VAPOR_EXT_VAR_FALLBACK", "This is a test") 85 | XCTAssertEqual(fallbackString, "1") 86 | 87 | fallbackString = Environment.get("X_____", "This is a test") 88 | XCTAssertEqual(fallbackString, "This is a test") 89 | } 90 | 91 | func testLinuxTestSuiteIncludesAllTests() throws { 92 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 93 | let thisClass = type(of: self) 94 | let linuxCount = thisClass.allTests.count 95 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 96 | 97 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 98 | #endif 99 | } 100 | 101 | static let allTests = [ 102 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 103 | ("testGetEnvAsInt", testGetEnvAsInt), 104 | ("testGetEnvAsBool", testGetEnvAsBool), 105 | ("testFallback", testFallback) 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /Tests/ServiceExtTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // ServiceExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | #if !os(macOS) 12 | public func allTests() -> [XCTestCaseEntry] { 13 | return [ 14 | testCase(EnvironmentTests.allTests) 15 | ] 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /Tests/VaporExtTests/StubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VaporExtTests.swift 3 | // VaporExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | @testable import VaporExt 10 | import XCTest 11 | 12 | final class StubTests: XCTestCase { 13 | func testExample() { 14 | // This is an example of a functional test case. 15 | // Use XCTAssert and related functions to verify your tests produce the correct 16 | // results. 17 | XCTAssertEqual(true, true) 18 | } 19 | 20 | func testLinuxTestSuiteIncludesAllTests() throws { 21 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 22 | let thisClass = type(of: self) 23 | let linuxCount = thisClass.allTests.count 24 | let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount) 25 | 26 | XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests") 27 | #endif 28 | } 29 | 30 | static let allTests = [ 31 | ("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests), 32 | ("testExample", testExample) 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Tests/VaporExtTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTestManifests.swift 3 | // VaporExt 4 | // 5 | // Created by Gustavo Perdomo on 07/28/18. 6 | // Copyright © 2018 Vapor Community. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | #if !os(macOS) 12 | public func allTests() -> [XCTestCaseEntry] { 13 | return [ 14 | testCase(StubTests.allTests) 15 | ] 16 | } 17 | #endif 18 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Gustavo Perdomo 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 | --------------------------------------------------------------------------------