├── .github └── workflows │ ├── danger.yml │ └── unittest.yml ├── .gitignore ├── .jazzy.yaml ├── .swift-version ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cauli ├── Cauli.h ├── Cauli.swift ├── CauliURLProtocol │ ├── CauliAuthenticationChallengeProxy.swift │ ├── CauliURLProtocol.swift │ ├── CauliURLProtocolDelegate.swift │ └── WeakReference.swift ├── CauliViewController │ ├── CauliViewController.swift │ └── SwitchTableViewCell.swift ├── Configuration │ ├── Configuration.swift │ └── RecordSelector.swift ├── Data structures │ ├── Record.swift │ ├── Response.swift │ ├── Result.swift │ └── URLResponseRepresentable.swift ├── DisplayingFloret.swift ├── Extensions │ ├── Collection+ReduceAsnyc.swift │ ├── NSError+Cauli.swift │ ├── NSError+Codable.swift │ ├── UIWindow+Shake.swift │ ├── URLRequest+Codable.swift │ └── URLSessionConfiguration+Swizzling.swift ├── Floret.swift ├── Florets │ ├── FindReplace │ │ ├── FindReplaceFloret.swift │ │ └── ReplaceDefinition.swift │ ├── HTTPBodyStreamFloret │ │ └── HTTPBodyStreamFloret.swift │ ├── Inspector │ │ ├── Formatter │ │ │ ├── InspectorFloretFormatter.swift │ │ │ ├── InspectorFloretFormatterType.swift │ │ │ └── RecordListFormattedData.swift │ │ ├── InspectorFloret.swift │ │ ├── NSError+NetworkErrorShortString.swift │ │ ├── PrettyPrinter │ │ │ ├── PlaintextPrettyPrinter.swift │ │ │ └── PrettyPrinter.swift │ │ ├── Record List │ │ │ ├── InspectorRecordTableViewCell.swift │ │ │ ├── InspectorTableViewController.swift │ │ │ └── InspectorTableViewDatasource.swift │ │ ├── Record │ │ │ ├── RecordItemTableViewCell.swift │ │ │ ├── RecordTableViewController.swift │ │ │ └── RecordTableViewDatasource.swift │ │ └── TagLabel.swift │ ├── MapRemote │ │ ├── MapRemoteFloret.swift │ │ ├── Mapping.swift │ │ ├── MappingsListDataSource.swift │ │ └── MappingsListViewController.swift │ ├── Mock │ │ ├── MD5Digest.swift │ │ ├── MockFloret.swift │ │ ├── MockFloretStorage.swift │ │ ├── MockRecordSerializer.swift │ │ ├── SwappedRecord.swift │ │ ├── SwappedResponse.swift │ │ └── SwappedURLRequest.swift │ └── NoCache │ │ └── NoCacheFloret.swift ├── Info.plist ├── InterceptingFloret.swift ├── MemoryStorage.swift ├── RecordModifier.swift ├── Resources │ ├── InspectorRecordTableViewCell.xib │ └── SwitchTableViewCell.xib ├── Storage.swift └── ViewControllerShakePresenter.swift ├── CauliTests ├── CauliSpec.swift ├── CauliURLProtocolSpec.swift ├── Fakes │ ├── HTTPURLResponse+Fake.swift │ ├── Record+Fake.swift │ └── URLResponse+Fake.swift ├── FindReplaceFloretSpec.swift ├── Info.plist ├── MemoryStorageSpec.swift ├── NoCacheFloretSpec.swift ├── Stubs │ └── CauliURLProtocolDelegateStub.swift └── URLResponseRepresentableSpec.swift ├── Cauliframework.podspec ├── Cauliframework.xcodeproj ├── project.pbxproj └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── Cauliframework.xcscheme ├── Cauliframework.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Dangerfile ├── Draft.playground ├── Contents.swift └── contents.xcplayground ├── Example └── cauli-ios-example │ ├── Podfile │ ├── Podfile.lock │ ├── cauli-ios-example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── cauli-ios-example.xcscheme │ ├── cauli-ios-example.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── cauli-ios-example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Cauli+Customized.swift │ ├── CustomInspectorFloretFormatter.swift │ ├── File.txt │ ├── Info.plist │ ├── MockFloret │ ├── GET_http_ip_jsontest_com_ │ │ └── b014744502154b59cdd3ed4c2f21c8b6-anonymized │ │ │ ├── record.json │ │ │ └── response.data │ └── default │ │ └── 404 │ │ ├── record.json │ │ └── response.data │ └── Sections │ ├── Requests │ ├── RequestModel.swift │ ├── RequestModelTableViewCell.swift │ └── RequestsTableViewController.swift │ └── WebView │ └── WebViewController.swift ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md └── docs └── guides ├── Architecture.md ├── Configuring Cauli.md ├── Florets.md ├── Frequently Asked Questions.md └── Writing Your Own Plugin.md /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger 2 | 3 | on: 4 | pull_request: 5 | branches: [ master, develop ] 6 | 7 | jobs: 8 | test: 9 | name: Danger 10 | runs-on: macos-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | - name: Install Bundler 15 | run: gem install bundler:2.2.17 16 | - uses: actions/cache@v1 17 | with: 18 | path: vendor/bundle 19 | key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}-3 20 | restore-keys: | 21 | ${{ runner.os }}-gem- 22 | - name: Bundle Install 23 | run: bundle install --path vendor/bundle 24 | - name: Danger 25 | run: bundle exec danger 26 | env: 27 | destination: $ 28 | LANG: 'en_US.UTF-8' 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/unittest.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | 9 | jobs: 10 | test: 11 | runs-on: macos-12 12 | strategy: 13 | matrix: 14 | include: # selection of https://github.com/actions/runner-images/blob/macOS-12/20230916.1/images/macos/macos-12-Readme.md#installed-simulators 15 | - xcode: "13.4.1" 16 | ios: "15.5" 17 | device: "iPhone 8" 18 | - xcode: "14.2" 19 | ios: "16.2" 20 | device: "iPhone 8" 21 | if: github.event_name == 'pull_request' # if only run pull request when multiple trigger workflow 22 | name: "test ios ${{matrix.ios}}" 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: "Select Xcode ${{matrix.xcode}}" 26 | run: sudo xcode-select -switch /Applications/Xcode_${{matrix.xcode}}.app && /usr/bin/xcodebuild -version 27 | - name: Run Unit Tests on ${{matrix.ios}} ${{matrix.device}} 28 | run: xcodebuild -workspace Cauliframework.xcworkspace -scheme 'Cauliframework' -sdk 'iphonesimulator' -destination 'platform=iOS Simulator,name=${{matrix.device}},OS=${{matrix.ios}}' -configuration Debug clean test 29 | build-example-app: 30 | runs-on: macos-12 31 | if: github.event_name == 'pull_request' # if only run pull request when multiple trigger workflow 32 | name: "Build Example App" 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Build Example App 36 | run: xcodebuild -workspace Example/cauli-ios-example/cauli-ios-example.xcworkspace -scheme 'Cauliframework' -destination 'platform=iOS Simulator,name=iPhone 8,OS=16.2' -configuration Release clean build 37 | build-spm: 38 | runs-on: macos-12 39 | if: github.event_name == 'pull_request' # if only run pull request when multiple trigger workflow 40 | name: "Build SPM" 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Build SPM 44 | run: swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios16.2-simulator" 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,swift,xcode,objective-c 3 | 4 | ### Objective-C ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData* 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | # CocoaPods - Refactored to standalone file 36 | 37 | 38 | # Carthage - Refactored to standalone file 39 | 40 | # fastlane 41 | # 42 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 43 | # screenshots whenever they are needed. 44 | # For more information about the recommended setup visit: 45 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 46 | 47 | fastlane/report.xml 48 | fastlane/Preview.html 49 | fastlane/screenshots 50 | fastlane/test_output 51 | 52 | # Code Injection 53 | # 54 | # After new code Injection tools there's a generated folder /iOSInjectionProject 55 | # https://github.com/johnno1962/injectionforxcode 56 | 57 | iOSInjectionProject/ 58 | 59 | ### Objective-C Patch ### 60 | 61 | ### Objective-C.CocoaPods Stack ### 62 | ## CocoaPods GitIgnore Template 63 | 64 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 65 | # - Also handy if you have a lage number of dependant pods 66 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGONRE THE LOCK FILE 67 | #Pods/ 68 | 69 | ### Objective-C.Carthage Stack ### 70 | # Carthage 71 | # 72 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 73 | # Carthage/Checkouts 74 | 75 | Carthage/Build 76 | 77 | ### OSX ### 78 | *.DS_Store 79 | .AppleDouble 80 | .LSOverride 81 | 82 | # Icon must end with two \r 83 | Icon 84 | 85 | # Thumbnails 86 | ._* 87 | 88 | # Files that might appear in the root of a volume 89 | .DocumentRevisions-V100 90 | .fseventsd 91 | .Spotlight-V100 92 | .TemporaryItems 93 | .Trashes 94 | .VolumeIcon.icns 95 | .com.apple.timemachine.donotpresent 96 | 97 | # Directories potentially created on remote AFP share 98 | .AppleDB 99 | .AppleDesktop 100 | Network Trash Folder 101 | Temporary Items 102 | .apdisk 103 | 104 | ### Swift ### 105 | # Xcode 106 | # 107 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 108 | 109 | ## Build generated 110 | 111 | ## Various settings 112 | 113 | ## Other 114 | 115 | ## Obj-C/Swift specific 116 | 117 | ## Playgrounds 118 | timeline.xctimeline 119 | playground.xcworkspace 120 | 121 | # Swift Package Manager 122 | # 123 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 124 | # Packages/ 125 | # Package.pins 126 | .build/ 127 | 128 | # CocoaPods - Refactored to standalone file 129 | 130 | # Carthage - Refactored to standalone file 131 | 132 | # fastlane 133 | # 134 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 135 | # screenshots whenever they are needed. 136 | # For more information about the recommended setup visit: 137 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 138 | 139 | 140 | ### Swift.CocoaPods Stack ### 141 | ## CocoaPods GitIgnore Template 142 | 143 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 144 | # - Also handy if you have a lage number of dependant pods 145 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGONRE THE LOCK FILE 146 | 147 | ### Swift.Carthage Stack ### 148 | # Carthage 149 | # 150 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 151 | # Carthage/Checkouts 152 | 153 | 154 | ### Xcode ### 155 | # Xcode 156 | # 157 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 158 | 159 | ## Build generated 160 | 161 | ## Various settings 162 | 163 | ## Other 164 | 165 | ### Xcode Patch ### 166 | *.xcodeproj/* 167 | !*.xcodeproj/project.pbxproj 168 | !*.xcodeproj/xcshareddata/ 169 | !*.xcworkspace/contents.xcworkspacedata 170 | /*.gcno 171 | 172 | # End of https://www.gitignore.io/api/osx,swift,xcode,objective-c 173 | /docs/generated 174 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: Cauliframework 2 | xcodebuild_arguments: [-workspace, Cauliframework.xcworkspace, -scheme, Cauliframework] 3 | author_url: https://cauli.works 4 | github_url: https://github.com/cauliframework/cauli 5 | output: docs/generated 6 | theme: fullwidth 7 | documentation: docs/guides/*.md 8 | exclude: 9 | - Cauli/CauliURLProtocol/CauliURLProtocol.swift # jazzy wrongly includes this internal file 10 | - Cauli/Extensions/URLRequest+Codable.swift -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - valid_ibinspectable 4 | opt_in_rules: 5 | - empty_count 6 | - array_init 7 | - closure_end_indentation 8 | - closure_spacing 9 | - contains_over_first_not_nil 10 | - explicit_top_level_acl 11 | - fallthrough 12 | - fatal_error_message 13 | - file_name 14 | - first_where 15 | - force_unwrapping 16 | - implicit_return 17 | - implicitly_unwrapped_optional 18 | - lower_acl_than_parent 19 | - missing_docs 20 | - modifier_order 21 | - multiline_function_chains 22 | - multiline_parameters 23 | - nimble_operator 24 | - operator_usage_whitespace 25 | - overridden_super_call 26 | - private_action 27 | - private_outlet 28 | - prohibited_super_call 29 | - redundant_type_annotation 30 | - sorted_first_last 31 | - sorted_imports 32 | - superfluous_disable_command 33 | - trailing_closure 34 | - unavailable_function 35 | - unneeded_parentheses_in_closure_argument 36 | - unused_import 37 | - unused_optional_binding 38 | excluded: 39 | - Pods 40 | - CauliTests 41 | - Example 42 | - Draft.playground 43 | - Package.swift 44 | 45 | force_cast: warning 46 | force_try: warning 47 | 48 | file_length: 49 | warning: 500 50 | error: 1200 51 | 52 | reporter: "xcode" 53 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | * **bugfix** Fixed an issue were the inspector wouldn’t use the formatters filter. [#246](https://github.com/cauliframework/cauli/pull/246) 5 | 6 | ## 1.1 7 | * **feature** Added a `MapRemoteFloret` that can change urls of requests before they are sent. [#216](https://github.com/cauliframework/cauli/issues/216) by @brototyp 8 | * **improvement** Added an optional description to florets [#176](https://github.com/cauliframework/cauli/issues/176) by @Shukuyen 9 | * **improvement** Removed Cocoapods as a developer dependency in favor of SPM. [#238](https://github.com/cauliframework/cauli/pull/238) 10 | * **improvement** Extracted the `InspectorFloretFormatter` to increase customizability if the `InspectorFloret`. [#239](https://github.com/cauliframework/cauli/pull/239) 11 | * **bugfix** Fixed an issue where cauli didn’t pass the redirection information up to the application. [#196](https://github.com/cauliframework/cauli/issues/196) 12 | * **bugfix** Fixed an issue when using Cauli via SPM. [#238](https://github.com/cauliframework/cauli/pull/238) 13 | 14 | ## 1.0.1 15 | * **improvement** Added Support for iOS 8 and 9. [#164](https://github.com/cauliframework/cauli/issues/164) by @brototyp 16 | * **improvement** Redesigned Inspector Floret record list. [#179](https://github.com/cauliframework/cauli/issues/179) by @Shukuyen 17 | * **bugfix** Fixed a bug where a Cauli instance did not consider the enabled state when deciding whether to handle a record or not. [#185](https://github.com/cauliframework/cauli/issues/185) by @pstued 18 | 19 | ## 1.0 20 | * **feature** Added a `HTTPBodyStreamFloret` to improve the compatibilty to requests with `httpBodyStream`s. [#154](https://github.com/cauliframework/cauli/pull/154) by @brototyp 21 | * **improvement** Changed the name of the framework from `Cauli` to `Cauliframework`. [#135](https://github.com/cauliframework/cauli/issues/135) 22 | * **improvement** Changed the `max` and `all` functions on `RecordSelector` to be public. [#136](https://github.com/cauliframework/cauli/issues/136) 23 | * **improvement** Added a search to the Inspector Floret that allows filtering the record list by URL. [#134](https://github.com/cauliframework/cauli/pull/134) by @shukuyen 24 | * **improvement** Added some description to the `CauliViewController`s sections. [#157](https://github.com/cauliframework/cauli/issues/157) by @pstued 25 | * **improvement** Added `preStorageRecordModifier` to `Configuration` and `Storage` to allow for records to be modified before they are stored. [#146](https://github.com/cauliframework/cauli/pull/146) by @shukuyen 26 | * **improvement** Added a done button to dismiss the CauliViewController when the ViewController is displayed via the shake gesture. [#114](https://github.com/cauliframework/cauli/issues/114) by @pstued 27 | * **improvement** Added a PlaintextPrettyPrinter and manual selection [#91](https://github.com/cauliframework/cauli/issues/91) by @brototyp 28 | * **improvement** Splitted the Floret protocol into InterceptingFloret and DisplayingFloret protocols for a better separation of a Florets functionality and responsibility. [#155](https://github.com/cauliframework/cauli/issues/155) by @pstued 29 | * **bugfix** Fixed an issue where records cells were cropped in the InspectorViewController. [#147](https://github.com/cauliframework/cauli/issues/147) 30 | * **bugfix** Fixed a bug where Records would be duplicated in the InspectorViewController. [#148](https://github.com/cauliframework/cauli/issues/148) by @brototyp 31 | * **bugfix** Fixed a bug where the searchbar could cover the first entry in the InspectorViewController. [#144](https://github.com/cauliframework/cauli/issues/144) by @brototyp 32 | * **bugfix** Fixed a bug where searching in the InspectorFloret would not load additional records [#161](https://github.com/cauliframework/cauli/pull/161) by @shukuyen 33 | 34 | ## 0.9 35 | * First public release 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## 👋 How to lend a helping hand? 4 | 5 | You like Cauli and want to help? You are the best! We welcome every kind of support and want to create a open community here. 6 | You want to help but are unsure what? No worries! Here is an incomplete list of things you can support us with. 7 | 8 | * Got an idea? Just **write an issue** and we can start a discussion. 9 | * You already know some Cauli? Maybe there are **issues with questions you can help** with. 10 | * Love **Unit tests**? We got some, but we bet you can improve them. 11 | * Found a piece of bad **documentation**? Most of the initial documentation is written by non native speakers. Maybe you can improve on it. 12 | * Want to roll up your sleeves and hack some code? Maybe you have a great idea for a feature you can add. 13 | 14 | 15 | ## 💻 How to contribute code changes? 16 | 17 | Want to fix a bug, add a feature or improved one? What next? Here is a short guideline on how to get your changes merged. 18 | 19 | * Go ahead and create a **fork** of Cauli 20 | * **Branch off** the development branch 21 | * **Commit** your changes 22 | * Create a **Pull Request** to the development branch 23 | 24 | Somebody will add two eyes to your code and might give some feedback. Got some changes merged? Welcome to the team! Let's try to make debugging better with Cauli! 25 | 26 | 27 | ## 🤔 How to get started? 28 | 29 | Checked out the repository and would like some guidance to find your way? 30 | 31 | * We are using SPM to manage our testing dependencies, [Quick](https://github.com/Quick/Quick), [Nimble](https://github.com/Quick/Nimble) and OHHTTPStubs. 32 | * Just open the `Cauliframework.xcworkspace`. Here you can find all the code that make up Cauli. 33 | * Have you seen the `Example` folder? In there we added an iOS example application. Open the `cauli-ios-example.xcworkspace` and have a look. You can use the example app to play around, test your changes or sketch out a new idea for a Floret. 34 | -------------------------------------------------------------------------------- /Cauli/Cauli.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #import 24 | 25 | //! Project version number for Cauli. 26 | FOUNDATION_EXPORT double CauliVersionNumber; 27 | 28 | //! Project version string for Cauli. 29 | FOUNDATION_EXPORT const unsigned char CauliVersionString[]; 30 | 31 | // In this header, you should import all the public headers of your framework using statements like #import 32 | 33 | 34 | -------------------------------------------------------------------------------- /Cauli/CauliURLProtocol/CauliAuthenticationChallengeProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The CauliAuthenticationChallengeProxy works as a proxy between URLAuthenticationChallengeSender and the `urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)` completionHandler. 26 | internal class CauliAuthenticationChallengeProxy: NSObject, URLAuthenticationChallengeSender { 27 | 28 | private let authChallengeCompletionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void) 29 | 30 | init(authChallengeCompletionHandler: @escaping ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) { 31 | self.authChallengeCompletionHandler = authChallengeCompletionHandler 32 | } 33 | 34 | func performDefaultHandling(for challenge: URLAuthenticationChallenge) { 35 | authChallengeCompletionHandler(.performDefaultHandling, nil) 36 | } 37 | 38 | func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) { 39 | authChallengeCompletionHandler(.useCredential, credential) 40 | } 41 | 42 | func continueWithoutCredential(for challenge: URLAuthenticationChallenge) { 43 | authChallengeCompletionHandler(.performDefaultHandling, nil) 44 | } 45 | 46 | func cancel(_ challenge: URLAuthenticationChallenge) { 47 | authChallengeCompletionHandler(.cancelAuthenticationChallenge, nil) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Cauli/CauliURLProtocol/CauliURLProtocolDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal protocol CauliURLProtocolDelegate: AnyObject { 26 | /// This function will be called to determine if a Record should be handled by this Cauli instance. 27 | /// This function is called multiple times for a request and in different states of being processed. 28 | /// 29 | /// - Parameter record: The Record that should be checked. 30 | /// - Returns: Return yes if this Record should be handled. 31 | func handles(_ record: Record) -> Bool 32 | func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) 33 | func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) 34 | } 35 | -------------------------------------------------------------------------------- /Cauli/CauliURLProtocol/WeakReference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal class WeakReference { 26 | weak var value: AnyObject? 27 | 28 | init(_ value: T?) { 29 | self.value = value as AnyObject 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Cauli/CauliViewController/SwitchTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class SwitchTableViewCell: UITableViewCell { 26 | 27 | static let reuseIdentifier = "SwitchTableViewCell" 28 | static let nibName = "SwitchTableViewCell" 29 | 30 | @IBOutlet private weak var titleLabel: UILabel! 31 | @IBOutlet private weak var descriptionLabel: UILabel! 32 | @IBOutlet private weak var `switch`: UISwitch! 33 | var switchValueChanged: ((Bool) -> Void)? 34 | 35 | func set(title: String, switchValue: Bool, description: String?) { 36 | titleLabel.text = title 37 | descriptionLabel.text = description 38 | `switch`.isOn = switchValue 39 | } 40 | 41 | @IBAction private func switchValueChanged(_ sender: UISwitch) { 42 | switchValueChanged?(sender.isOn) 43 | } 44 | 45 | override func prepareForReuse() { 46 | super.prepareForReuse() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Cauli/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The Configuration is used to configure Cauli at initialization time. 26 | /// The `Configuration.standard` is a sensibly chosen configuration set. 27 | public struct Configuration { 28 | 29 | /// The default Configuration. 30 | /// 31 | /// - Configuration: 32 | /// - recordSelector: Only records to a maximum of 5 MB are considered. 33 | /// - enableShakeGesture: The shake gesture is enabled. 34 | /// - storageCapacity: The storage capacity is limited to 50 records,. 35 | /// - preStorageRecordModifier: A `RecordModifier` that can modify records before they are stored. 36 | public static let standard = Configuration( 37 | recordSelector: RecordSelector.max(bytesize: 5 * 1024 * 1024), 38 | enableShakeGesture: true, 39 | storageCapacity: .records(50) 40 | ) 41 | 42 | /// Defines if a Record should be handled. This can be used to only select Records by a specific domain, a filetype, a maximum filesize or such. 43 | /// 44 | /// ## Examples 45 | /// ```swift 46 | /// // Only records for the domain cauli.works are selected. 47 | /// RecordSelector { $0.originalRequest.url?.host == "cauli.works" } 48 | /// 49 | /// // Only records with a response body of max 10 MB are selected. 50 | /// RecordSelector.max(bytesize: 10 * 1024 * 1024) 51 | /// ``` 52 | public let recordSelector: RecordSelector 53 | 54 | /// If `enableShakeGesture` is set to true, Cauli will try to hook into the 55 | /// `UIWindow.motionEnded(:UIEvent.EventSubtype, with: UIEvent?)` function 56 | /// to display the Cauli UI whenever the device is shaken. 57 | /// If that function is overridden the Cauli automatism doesn't work. In that case, you can 58 | /// use the `Cauli.viewController()` function to display that ViewController manually. 59 | public let enableShakeGesture: Bool 60 | 61 | /// The `storageCapacity` defines the capacity of the storage. 62 | public let storageCapacity: StorageCapacity 63 | 64 | /// This `RecordModifier` allows the `Storage` to modify records before they are stored. 65 | /// This allows you to change details of a record before it is passed along to a presentation floret, like for example the `InspectorFloret`. 66 | public var preStorageRecordModifier: RecordModifier? 67 | 68 | /// Creates a new `Configuration` with the given parameters. Please check the 69 | /// properties of a `Configuration` for their meaning. 70 | public init(recordSelector: RecordSelector, enableShakeGesture: Bool, storageCapacity: StorageCapacity, preStorageRecordModifier: RecordModifier? = nil) { 71 | self.recordSelector = recordSelector 72 | self.enableShakeGesture = enableShakeGesture 73 | self.storageCapacity = storageCapacity 74 | self.preStorageRecordModifier = preStorageRecordModifier 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Cauli/Configuration/RecordSelector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A RecordSelector defines a selection of a Record object. It is used in the `Configuration.recordSelector` to 26 | /// define a subset of Records that should be handled by a Cauli instance. 27 | public struct RecordSelector { 28 | internal let selects: (Record) -> (Bool) 29 | 30 | /// Creates a new RecordSelector. 31 | /// 32 | /// - Parameter selects: A closure describing if a Record should be selected or not. 33 | public init(selects: @escaping (Record) -> (Bool)) { 34 | self.selects = selects 35 | } 36 | } 37 | 38 | /// Provides helper methods to select records with a `RecordSelector` 39 | public extension RecordSelector { 40 | /// Selects every Record. 41 | /// 42 | /// - Returns: Returns a RecordSelector where every Record is selected. 43 | static func all() -> RecordSelector { 44 | RecordSelector { _ in true } 45 | } 46 | 47 | /// Selects Records by a maximum filesize 48 | /// 49 | /// - Parameter bytesize: The maximum size in bytes of the Response body. 50 | /// - Returns: Returns a RecordSelector that selects only the Records where the body is smaller or 51 | /// equal than the required size. 52 | static func max(bytesize: Int) -> RecordSelector { 53 | RecordSelector { record in 54 | guard record.designatedRequest.httpBody?.count ?? 0 <= bytesize else { return false } 55 | switch record.result { 56 | case nil: return true 57 | case .error?: return true 58 | case .result(let response)?: 59 | if let data = response.data { 60 | return data.count <= bytesize 61 | } else if let urlResponse = response.urlResponse as? HTTPURLResponse, 62 | let contentLengthString = urlResponse.allHeaderFields["Content-Length"] as? String, 63 | let contentLength = Int(contentLengthString) { 64 | return contentLength <= bytesize 65 | } 66 | return true 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Cauli/Data structures/Record.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A record represents a full roundtrip of a URLRequest and it's response. 26 | public struct Record { 27 | /// The identifier of the record. As long as the identifier is the same 28 | /// two Records are assumed to be the same. 29 | public var identifier: UUID 30 | 31 | /// The originalRequest is the request passed on to the URL loading system. 32 | /// The originalRequest should not be changed by any floret. 33 | public var originalRequest: URLRequest 34 | 35 | /// The designatedRequest will initially be the same as the originalRequest 36 | /// but can be changed by every Floret. The designatedRequest is the one 37 | /// that will be executed in the end. 38 | public var designatedRequest: URLRequest 39 | 40 | /// The result will be nil until either the URL loading system failed to perform the 41 | /// request, or the response is received. The `data` of the `Response` can be incomplete 42 | /// while receiving. 43 | public var result: Result? 44 | 45 | /// The requestStarted Date is set after all florets finished their `willRequest` function. 46 | public var requestStarted: Date? 47 | 48 | /// The responseReceived Date is set after all florets finished their `didRespond` function. 49 | public var responseReceived: Date? 50 | } 51 | 52 | extension Record: Codable {} 53 | 54 | extension Record { 55 | init(_ request: URLRequest) { 56 | identifier = UUID() 57 | originalRequest = request 58 | designatedRequest = request 59 | } 60 | } 61 | 62 | extension Record { 63 | internal mutating func append(receivedData: Data) throws { 64 | guard case let .result(result)? = result else { 65 | throw NSError.CauliInternal.appendingDataWithoutResponse(receivedData, record: self) 66 | } 67 | var currentData = result.data ?? Data() 68 | currentData.append(receivedData) 69 | self.result = .result(Response(result.urlResponse, data: currentData)) 70 | } 71 | } 72 | 73 | extension Record { 74 | internal func swapped(to path: URL) -> SwappedRecord { 75 | SwappedRecord(self, folder: path) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Cauli/Data structures/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The `Response` wrapps the `URLResponse` and the data received from the server. 26 | public struct Response: Codable { 27 | /// The `Data` received for a request. 28 | public var data: Data? 29 | 30 | /// The `URLResponse` for a request. 31 | public var urlResponse: URLResponse { 32 | get { 33 | urlResponseRepresentable.urlResponse 34 | } 35 | set { 36 | urlResponseRepresentable = URLResponseRepresentable(newValue) 37 | } 38 | } 39 | 40 | /// Initializes a new `Response` with a given `URLResponse` and optional data. 41 | /// 42 | /// - Parameters: 43 | /// - urlResponse: The URLResponse 44 | /// - data: Optional received data 45 | init(_ urlResponse: URLResponse, data: Data?) { 46 | self.data = data 47 | urlResponseRepresentable = URLResponseRepresentable(urlResponse) 48 | } 49 | 50 | private var urlResponseRepresentable: URLResponseRepresentable 51 | } 52 | -------------------------------------------------------------------------------- /Cauli/Data structures/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The Result represents a possible result expecting any given type. 26 | public enum Result { 27 | /// An error occured. 28 | case error(NSError) 29 | /// The successful result itself 30 | case result(Type) 31 | } 32 | 33 | extension Result: Codable { 34 | 35 | private enum CodingKeys: CodingKey { 36 | case error 37 | case result 38 | } 39 | 40 | public init(from decoder: Decoder) throws { 41 | let container = try decoder.singleValueContainer() 42 | if let decodedType = try? container.decode(Type.self) { 43 | self = .result(decodedType) 44 | } else if let internalError = try? container.decode(InternalError.self) { 45 | self = .error(NSError(domain: internalError.domain, code: internalError.code, userInfo: internalError.userInfo)) 46 | } else { 47 | let context = DecodingError.Context.init(codingPath: decoder.codingPath, debugDescription: "Could not create Result") 48 | throw DecodingError.dataCorrupted(context) 49 | } 50 | } 51 | 52 | public func encode(to encoder: Encoder) throws { 53 | var container = encoder.singleValueContainer() 54 | switch self { 55 | case .error(let error): 56 | try container.encode(InternalError(domain: error.domain, code: error.code, userInfo: error.compatibleUserInfo)) 57 | case .result(let type): 58 | try container.encode(type) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Cauli/DisplayingFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | /// A DisplayingFloret provides a ViewController for settings or to display any information. 26 | public protocol DisplayingFloret: Floret { 27 | /// This function is called whenever the Cauli UI will be displayed. 28 | /// If a Floret needs any UI for configuration or to display data you 29 | /// can return a ViewController here. 30 | /// 31 | /// The default implementation returns nil. 32 | /// 33 | /// - Parameter cauli: The Cauli instance this floret will be displayed in. Use this 34 | /// instance to access the storage for example. 35 | /// - Returns: Return a Floret specific ViewController or `nil` if there is none. 36 | func viewController(_ cauli: Cauli) -> UIViewController 37 | } 38 | -------------------------------------------------------------------------------- /Cauli/Extensions/Collection+ReduceAsnyc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal extension Collection { 26 | 27 | typealias AsynchTransformation = (Product, Element, @escaping (Product) -> Void) -> Void 28 | 29 | func cauli_reduceAsync(_ initial: Product, transform: @escaping AsynchTransformation, completion: @escaping (Product) -> Void) { 30 | cauli_reduceAsync(initial, transform: transform, completion: completion, index: startIndex) 31 | } 32 | 33 | fileprivate func cauli_reduceAsync(_ initial: Product, transform: @escaping AsynchTransformation, completion: @escaping (Product) -> Void, index: Index) { 34 | guard index < endIndex else { return completion(initial) } 35 | let element = self[index] 36 | transform(initial, element) { result in 37 | let nextIndex = self.index(after: index) 38 | self.cauli_reduceAsync(result, transform: transform, completion: completion, index: nextIndex) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Cauli/Extensions/NSError+Cauli.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | // swiftlint:disable nesting 26 | extension NSError { 27 | 28 | /// All Cauli related Errors. 29 | public struct Cauli { 30 | 31 | /// The Cauli NSErrorDomain. 32 | public let domain = "works.cauli.framework" 33 | 34 | /// All Cauli Error Codes. 35 | public enum Code {} 36 | 37 | /// All keys used in Cauli Errors. 38 | public enum UserInfoKey {} 39 | } 40 | 41 | internal struct CauliInternal { 42 | internal static let domain = "works.cauli.framework.internal" 43 | internal enum Code: Int { 44 | case failedToAppendData 45 | case appendingDataWithoutResponse 46 | } 47 | 48 | internal enum UserInfoKey: String { 49 | case data 50 | case record 51 | } 52 | 53 | internal static func failedToAppendData(_ data: Data, record: Record) -> NSError { 54 | NSError(domain: domain, 55 | code: Code.failedToAppendData.rawValue, 56 | userInfo: [UserInfoKey.data.rawValue: data, UserInfoKey.record.rawValue: record]) 57 | } 58 | 59 | internal static func appendingDataWithoutResponse(_ data: Data, record: Record) -> NSError { 60 | NSError(domain: domain, 61 | code: Code.appendingDataWithoutResponse.rawValue, 62 | userInfo: [UserInfoKey.data.rawValue: data, UserInfoKey.record.rawValue: record]) 63 | } 64 | } 65 | } 66 | // swiftlint:enable nesting 67 | -------------------------------------------------------------------------------- /Cauli/Extensions/NSError+Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal struct InternalError: Codable { 26 | let domain: String 27 | let code: Int 28 | let userInfo: [String: String] 29 | } 30 | 31 | internal extension NSError { 32 | var compatibleUserInfo: [String: String] { 33 | self.userInfo.reduce([:]) { (result, keyValuePair: (key: String, value: Any)) -> [String: String] in 34 | var newResult = result 35 | let keyString = keyValuePair.key 36 | 37 | if let valueString = keyValuePair.value as? String { 38 | newResult[keyString] = valueString 39 | } 40 | 41 | return newResult 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Cauli/Extensions/UIWindow+Shake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | #if os(iOS) 24 | 25 | import UIKit 26 | 27 | internal struct Notification { 28 | static let shakeMotionDidEnd = NSNotification.Name(rawValue: "cauli_shakeMotionDidEnd") 29 | } 30 | 31 | extension UIWindow { 32 | /// We hook into this function to post a Notification whenever the device is shaken 33 | /// and subsequentially display the Cauli UI. If this function is overridden in the client 34 | /// application, the notification is not posted and the UI hast to be displayed manually. 35 | override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { 36 | if let event = event, 37 | event.type == .motion, 38 | event.subtype == .motionShake { 39 | NotificationCenter.default.post(name: Notification.shakeMotionDidEnd, object: self) 40 | } 41 | super.motionEnded(motion, with: event) 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /Cauli/Extensions/URLSessionConfiguration+Swizzling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | // This extension performes all the swizzling logic 26 | // to replace the `URLSessionConfiguration.default` with one, where 27 | // the `protocolClasses` contain the `CauliURLProtocol` 28 | extension URLSessionConfiguration { 29 | // swiftlint:disable force_unwrapping identifier_name 30 | internal static let cauliDefaultSessionConfigurationGetterMethod = class_getClassMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.default))! 31 | internal static let cauliSwizzledSessionConfigurationGetterMethod = class_getClassMethod(URLSessionConfiguration.self, #selector(URLSessionConfiguration.cauliDefaultSessionConfiguration))! 32 | // swiftlint:enable force_unwrapping identifier_name 33 | 34 | @objc internal class func cauliDefaultSessionConfiguration() -> URLSessionConfiguration { 35 | let configuration = cauliDefaultSessionConfiguration() // since it is swizzled, this will call the original implementation 36 | let protocolClasses = configuration.protocolClasses ?? [] 37 | configuration.protocolClasses = [CauliURLProtocol.self] + protocolClasses 38 | return configuration 39 | } 40 | 41 | @objc internal class func cauliSwizzleDefaultSessionConfigurationGetter() { 42 | method_exchangeImplementations(cauliDefaultSessionConfigurationGetterMethod, cauliSwizzledSessionConfigurationGetterMethod) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Cauli/Floret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A Floret defines a Cauli plugin. It can be used to intercept and modify 26 | /// network Requests as well as Responses. 27 | public protocol Floret { 28 | 29 | /// The name of the Floret. This will be used to identify the floret in the UI. 30 | /// If not implemented, the type will be used per default. 31 | var name: String { get } 32 | 33 | /// An optional description of the floret. This will be displayed alongside the 34 | /// floret name in the UI. Use it to explain in few words what the floret does and 35 | /// how to use it. 36 | var description: String? { get } 37 | } 38 | 39 | // swiftlint:disable missing_docs 40 | public extension Floret { 41 | var name: String { 42 | String(describing: Self.self) 43 | } 44 | 45 | var description: String? { 46 | nil 47 | } 48 | } 49 | // swiftlint:enable missing_docs 50 | -------------------------------------------------------------------------------- /Cauli/Florets/FindReplace/FindReplaceFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A `FindReplaceFloret` uses `RecordModifier`s to modify a `Record` 26 | /// before sending a request and after receiving a response. Use multiple 27 | /// instances of the `FindReplaceFloret`s to group certain RecordModifiers 28 | /// under a given name. 29 | public class FindReplaceFloret: InterceptingFloret { 30 | 31 | public var enabled = true 32 | public let name: String 33 | public var description: String? 34 | 35 | private let willRequestModifiers: [RecordModifier] 36 | private let didRespondModifiers: [RecordModifier] 37 | 38 | /// This init will create a FindReplaceFloret with RecordModifiers to modify Records. 39 | /// 40 | /// - Parameters: 41 | /// - willRequestModifiers: The RecordModifiers used to modify a Record before sending a request. 42 | /// - didRespondModifiers: The RecordModifiers used to modify a Record after receiving a response. 43 | /// - name: Can be used to describe the set of choosen RecordModifiers. The default name is `FindReplaceFloret`. 44 | /// - description: Provide additional description what this floret does that is exposed in the UI 45 | public init(willRequestModifiers: [RecordModifier] = [], didRespondModifiers: [RecordModifier] = [], name: String = "FindReplaceFloret", description: String? = nil) { 46 | self.willRequestModifiers = willRequestModifiers 47 | self.didRespondModifiers = didRespondModifiers 48 | self.name = name 49 | self.description = description 50 | } 51 | 52 | public func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 53 | let record = willRequestModifiers.reduce(record) { record, recordModifier in 54 | recordModifier.modify(record) 55 | } 56 | 57 | completionHandler(record) 58 | } 59 | 60 | public func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 61 | let record = didRespondModifiers.reduce(record) { record, recordModifier in 62 | recordModifier.modify(record) 63 | } 64 | 65 | completionHandler(record) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Cauli/Florets/FindReplace/ReplaceDefinition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | extension FindReplaceFloret { 26 | /// A ReplaceDefinition defines the replacement of a Record 27 | public struct ReplaceDefinition { 28 | /// This closures is called when a Record can be modified. 29 | /// It can return a modified Record or the original Record. 30 | let modifier: (Record) -> (Record) 31 | } 32 | } 33 | 34 | extension FindReplaceFloret.ReplaceDefinition { 35 | /// This init is based on a Records keyPath and allows just to modify 36 | /// a specific property of a Record. 37 | /// 38 | /// - Parameters: 39 | /// - keyPath: The keyPath describing which property of a Record should be modified. 40 | /// - modifier: A closure used to modify the property defined by the keyPath. 41 | /// - Returns: A ReplaceDefinition modifying a specific property of a Record. 42 | /// Use this to initalize a FindReplaceFloret. 43 | public init(keyPath: WritableKeyPath, modifier: @escaping (Type) -> (Type)) { 44 | self.init { record in 45 | var newRecord = record 46 | let oldValue = record[keyPath: keyPath] 47 | let modifiedValue = modifier(oldValue) 48 | newRecord[keyPath: keyPath] = modifiedValue 49 | return newRecord 50 | } 51 | } 52 | } 53 | 54 | extension FindReplaceFloret.ReplaceDefinition { 55 | /// This init will modify the Requests URL. 56 | /// 57 | /// - Parameters: 58 | /// - expression: A RegularExpression describing the part of the Requets URL to modify. 59 | /// - replacement: The replacement string used to replace matches of the RegularExpression. 60 | /// - Returns: A ReplaceDefinition modifying the RequestURL. Use this to initalize a FindReplaceFloret. 61 | public static func modifyUrl(expression: NSRegularExpression, replacement: String) -> FindReplaceFloret.ReplaceDefinition { 62 | let keyPath = \Record.designatedRequest.url 63 | let modifier: (URL?) -> (URL?) = { url in 64 | guard let oldURL = url else { return url } 65 | let urlWithReplacements = replacingOcurrences(of: expression, in: oldURL.absoluteString, with: replacement) 66 | return URL(string: urlWithReplacements) 67 | } 68 | return FindReplaceFloret.ReplaceDefinition(keyPath: keyPath, modifier: modifier) 69 | } 70 | 71 | static func replacingOcurrences(of expression: NSRegularExpression, in string: String, with template: String) -> String { 72 | let range = NSRange(string.startIndex..., in: string) 73 | return expression.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: template) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Cauli/Florets/HTTPBodyStreamFloret/HTTPBodyStreamFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The `HTTPBodyStreamFloret` can modify a `Record` where the request uses a `httpBodyStream` 26 | /// instead of setting the data on the request itself. 27 | /// It will read all data from the stream and set it as data on the request. This is helpful 28 | /// if you want to inspect `Record`s in the storage or want to modify the requests body 29 | /// before it is sent to the server. 30 | public class HTTPBodyStreamFloret: InterceptingFloret { 31 | private static let bufferByteSize = 1024 32 | 33 | private let maximumConvertedByteSize: Int64 34 | public var enabled = true 35 | 36 | /// Will create a new `HTTPBodyStreamFloret` instance. 37 | /// 38 | /// - Parameter maximumConvertedByteSize: The maximum size until which the data should be 39 | /// converted. Once more data is read from the `httpBodyStream`, the record will not be 40 | /// changed and the `httpBodyStream` is kept. Default is 50 * 1024. 41 | public init(maximumConvertedByteSize: Int64 = 50 * 1024 ) { 42 | self.maximumConvertedByteSize = maximumConvertedByteSize 43 | } 44 | 45 | public func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 46 | guard let httpBodyStream = record.designatedRequest.httpBodyStream, 47 | let data = data(reading: httpBodyStream) else { 48 | return completionHandler(record) 49 | } 50 | var record = record 51 | record.designatedRequest.httpBodyStream = nil 52 | record.designatedRequest.httpBody = data 53 | completionHandler(record) 54 | } 55 | 56 | public func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 57 | completionHandler(record) 58 | } 59 | 60 | private func data(reading input: InputStream) -> Data? { 61 | var data = Data() 62 | input.open() 63 | defer { 64 | input.close() 65 | } 66 | 67 | let buffer = UnsafeMutablePointer.allocate(capacity: HTTPBodyStreamFloret.bufferByteSize) 68 | defer { 69 | buffer.deallocate() 70 | } 71 | 72 | while input.hasBytesAvailable { 73 | if data.count > maximumConvertedByteSize { return nil } 74 | let read = input.read(buffer, maxLength: HTTPBodyStreamFloret.bufferByteSize) 75 | data.append(buffer, count: read) 76 | } 77 | 78 | return data 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Formatter/InspectorFloretFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | public class InspectorFloretFormatter: InspectorFloretFormatterType { 26 | public init() {} 27 | public func listFormattedData(for record: Record) -> InspectorFloret.RecordListFormattedData { 28 | let method = record.designatedRequest.httpMethod ?? "" 29 | let path = record.designatedRequest.url?.absoluteString ?? "" 30 | let time: String 31 | if let requestStarted = record.requestStarted { 32 | time = Self.timeFormatter.string(from: requestStarted) 33 | } else { 34 | time = "" 35 | } 36 | let status: String 37 | let statusColor: UIColor 38 | switch record.result { 39 | case nil: 40 | status = "-" 41 | statusColor = Self.grayColor 42 | case .error(let error)?: 43 | status = error.cauli_networkErrorShortString 44 | statusColor = Self.redColor 45 | case .result(let response)?: 46 | if let httpUrlResponse = response.urlResponse as? HTTPURLResponse { 47 | status = "\(httpUrlResponse.statusCode)" 48 | statusColor = colorForHTTPStatusCode(httpUrlResponse.statusCode) 49 | } else { 50 | status = "-" 51 | statusColor = Self.grayColor 52 | } 53 | } 54 | return InspectorFloret.RecordListFormattedData(method: method, path: path, time: time, status: status, statusColor: statusColor) 55 | } 56 | 57 | public func recordMatchesQuery(record: Record, query: String) -> Bool { 58 | guard let urlString = record.designatedRequest.url?.absoluteString else { 59 | return false 60 | } 61 | return urlString.range(of: query, options: String.CompareOptions.caseInsensitive) != nil 62 | } 63 | } 64 | 65 | extension InspectorFloretFormatter { 66 | static let greenColor = UIColor(red: 11 / 255.0, green: 176 / 255.0, blue: 61 / 255.0, alpha: 1) 67 | static let blueColor = UIColor(red: 74 / 255.0, green: 144 / 255.0, blue: 226 / 255.0, alpha: 1) 68 | static let redColor = UIColor(red: 210 / 255.0, green: 46 / 255.0, blue: 14 / 255.0, alpha: 1) 69 | static let grayColor = UIColor(red: 155 / 255.0, green: 155 / 255.0, blue: 155 / 255.0, alpha: 1) 70 | 71 | private func colorForHTTPStatusCode(_ statusCode: Int) -> UIColor { 72 | switch statusCode { 73 | case 0..<300: return Self.greenColor 74 | case 300..<400: return Self.blueColor 75 | case 400..<600: return Self.redColor 76 | default: return Self.grayColor 77 | } 78 | } 79 | } 80 | 81 | extension InspectorFloretFormatter { 82 | static let timeFormatter: DateFormatter = { 83 | let timeFormatter = DateFormatter() 84 | timeFormatter.dateStyle = .none 85 | timeFormatter.timeStyle = .medium 86 | return timeFormatter 87 | }() 88 | } 89 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Formatter/InspectorFloretFormatterType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | /// The `InspectorFloretFormatterType` is used to define how a Record is formatted when 24 | /// shown in the `InspectorFloret`. 25 | public protocol InspectorFloretFormatterType { 26 | /// Returns the `RecordListFormattedData` for a `Record`. 27 | /// - Parameter record: the Record 28 | /// - Returns: The RecordListFormattedData 29 | func listFormattedData(for record: Record) -> InspectorFloret.RecordListFormattedData 30 | /// This function is called when deciding if a `Record` should be shown with a given query, 31 | /// for example when typing in a search field. 32 | /// - Parameters: 33 | /// - record: The Record 34 | /// - query: The Search Query 35 | /// - Returns: Return true, if the Record matches the Query. 36 | func recordMatchesQuery(record: Record, query: String) -> Bool 37 | } 38 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Formatter/RecordListFormattedData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | extension InspectorFloret { 26 | /// The RecordListFormattedData defines the data shown 27 | /// in the Record list in the InspectorFloret. 28 | public struct RecordListFormattedData { 29 | /// The string shown in the method label 30 | public let method: String 31 | /// The string shown in the path label 32 | public let path: String 33 | /// The string shown in the time label 34 | public let time: String 35 | /// The string shown in the status label 36 | public let status: String 37 | /// The background color of the status label. 38 | /// The text color of the status label will be white. 39 | public let statusColor: UIColor 40 | 41 | /// Initializes a new `RecordListFormattedData`. Parameter match property documentation 42 | public init(method: String, path: String, time: String, status: String, statusColor: UIColor) { 43 | self.method = method 44 | self.path = path 45 | self.time = time 46 | self.status = status 47 | self.statusColor = statusColor 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/InspectorFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | /// The InspectorFloret lets you browse through and share your requests and responses from within the application. Just open `Cauli`s viewController. 26 | /// 27 | /// You can 28 | /// * browse through all stored `Records`. 29 | /// * inspect details of a `Record`. 30 | /// * share details of a `Record`. 31 | /// * filter `Record`s by the request URL. 32 | public class InspectorFloret: DisplayingFloret { 33 | 34 | public var description: String? = "Tap to inspect network requests and responses. Data is recorded as long as Cauli is enabled." 35 | 36 | private let formatter: InspectorFloretFormatterType 37 | 38 | /// Public initalizer to create an instance of the `InspectorFloret`. 39 | public init() { 40 | self.formatter = InspectorFloretFormatter() 41 | } 42 | 43 | /// Public initializer to create an instace of the `InspectorFloret` with a custom 44 | /// formatter. See `InspectorFloretFormatterType` for further details. 45 | /// - Parameter formatter: The `InspectorFloretFormatterType` to be used. 46 | public init(formatter: InspectorFloretFormatterType) { 47 | self.formatter = formatter 48 | } 49 | 50 | public func viewController(_ cauli: Cauli) -> UIViewController { 51 | InspectorTableViewController(cauli, formatter: formatter) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/PrettyPrinter/PlaintextPrettyPrinter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class PlaintextPrettyPrinter: UIViewController { 26 | 27 | static var name = "Plaintext" 28 | 29 | private let string: String 30 | private let textView: UITextView = { 31 | let textView = UITextView() 32 | textView.isEditable = false 33 | textView.translatesAutoresizingMaskIntoConstraints = false 34 | return textView 35 | }() 36 | 37 | init(string: String) { 38 | self.string = string 39 | super.init(nibName: nil, bundle: nil) 40 | } 41 | 42 | @available(*, unavailable) 43 | required init?(coder aDecoder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | 47 | override func viewDidLoad() { 48 | super.viewDidLoad() 49 | view.backgroundColor = .white 50 | 51 | view.addSubview(textView) 52 | view.addConstraints([ 53 | NSLayoutConstraint(item: textView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1.0, constant: 0), 54 | NSLayoutConstraint(item: textView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1.0, constant: 0), 55 | NSLayoutConstraint(item: textView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 0), 56 | NSLayoutConstraint(item: textView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.0, constant: 0) 57 | ]) 58 | 59 | textView.text = string 60 | } 61 | 62 | } 63 | 64 | extension PlaintextPrettyPrinter: PrettyPrinter { 65 | static func viewController(for item: Any) -> UIViewController? { 66 | switch item { 67 | case let string as String: return PlaintextPrettyPrinter(string: string) 68 | case let data as Data: return viewController(forJsonData: data) 69 | case let url as URL: return viewController(forUrl: url) 70 | default: return nil 71 | } 72 | } 73 | 74 | static func viewController(forUrl url: URL) -> UIViewController? { 75 | guard url.isFileURL, let data = try? Data(contentsOf: url) else { 76 | return nil 77 | } 78 | return viewController(forJsonData: data) ?? viewController(forPlaintextData: data) 79 | } 80 | 81 | static func viewController(forJsonData data: Data) -> UIViewController? { 82 | guard let jsonObject = try? JSONSerialization.jsonObject(with: data), 83 | let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]), 84 | let jsonString = String(bytes: jsonData, encoding: .utf8) else { return nil } 85 | return PlaintextPrettyPrinter(string: jsonString) 86 | } 87 | 88 | static func viewController(forPlaintextData data: Data) -> UIViewController? { 89 | guard let string = String(bytes: data, encoding: .utf8) else { return nil } 90 | return PlaintextPrettyPrinter(string: string) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/PrettyPrinter/PrettyPrinter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal protocol PrettyPrinter { 26 | static var name: String { get } 27 | static func viewController(for item: Any) -> UIViewController? 28 | } 29 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Record List/InspectorRecordTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class InspectorRecordTableViewCell: UITableViewCell { 26 | 27 | static let reuseIdentifier = "InspectorRecordTableViewCell" 28 | static let nibName = "InspectorRecordTableViewCell" 29 | 30 | @IBOutlet private weak var methodLabel: UILabel! 31 | @IBOutlet private weak var pathLabel: UILabel! 32 | @IBOutlet private weak var timeLabel: UILabel! 33 | @IBOutlet private weak var statusCodeLabel: TagLabel! 34 | 35 | internal func configure(with data: InspectorFloret.RecordListFormattedData, stringToHighlight: String?) { 36 | timeLabel.text = data.time 37 | methodLabel.text = data.method 38 | let pathString = data.path 39 | let pathAttributedString = NSMutableAttributedString(string: pathString) 40 | if let stringToHighlight = stringToHighlight { 41 | var rangeToSearch = pathString.startIndex.. Void), completion: ((_ finished: Bool) -> Void)? = nil) { 55 | if #available(iOS 11, *) { 56 | tableView.performBatchUpdates(updates, completion: completion) 57 | } else { 58 | tableView.beginUpdates() 59 | updates() 60 | tableView.endUpdates() 61 | completion?(true) 62 | } 63 | } 64 | 65 | internal func append(records: [Record], to tableView: UITableView, completion: ((_ finished: Bool) -> Void)? = nil) { 66 | performBatchUpdate(in: tableView, updates: { 67 | let numberOfExistingRecords = filteredItems.count 68 | let numberOfAddedRecords = records.filter(filter.selects).count 69 | let indexPaths = (numberOfExistingRecords..<(numberOfExistingRecords + numberOfAddedRecords)).map { IndexPath(row: $0, section: 0) } 70 | tableView.insertRows(at: indexPaths, with: .bottom) 71 | items += records 72 | }, completion: completion) 73 | } 74 | 75 | } 76 | 77 | extension InspectorTableViewDatasource: UITableViewDataSource { 78 | internal func setup(tableView: UITableView) { 79 | tableView.dataSource = self 80 | let nib = UINib(nibName: InspectorRecordTableViewCell.nibName, bundle: Cauli.bundle) 81 | tableView.register(nib, forCellReuseIdentifier: InspectorRecordTableViewCell.reuseIdentifier) 82 | tableView.rowHeight = UITableView.automaticDimension 83 | tableView.estimatedRowHeight = 82.0 84 | } 85 | 86 | internal func record(at indexPath: IndexPath) -> Record { 87 | filteredItems[indexPath.row] 88 | } 89 | 90 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 91 | filteredItems.count 92 | } 93 | 94 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 95 | guard let cell = tableView.dequeueReusableCell(withIdentifier: InspectorRecordTableViewCell.reuseIdentifier, for: indexPath) as? InspectorRecordTableViewCell else { 96 | fatalError("Unable to dequeue a cell") 97 | } 98 | let data = formatter.listFormattedData(for: record(at: indexPath)) 99 | cell.configure(with: data, stringToHighlight: filterString) 100 | cell.accessoryType = .disclosureIndicator 101 | return cell 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Record/RecordItemTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class RecordItemTableViewCell: UITableViewCell { 26 | 27 | static let reuseIdentifier = "RecordItemTableViewCell" 28 | 29 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | super.init(style: .value2, reuseIdentifier: reuseIdentifier) 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | super.init(coder: aDecoder) 35 | } 36 | 37 | static let textPadding = UIEdgeInsets(top: 14, left: 0, bottom: 14, right: 0) 38 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { 39 | var size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) 40 | guard let textLabel = self.textLabel, let detailTextLabel = self.detailTextLabel else { 41 | return size 42 | } 43 | self.layoutIfNeeded() 44 | let textHeight = max(detailTextLabel.frame.size.height, textLabel.frame.size.height) 45 | size.height = textHeight + RecordItemTableViewCell.textPadding.top + RecordItemTableViewCell.textPadding.bottom 46 | return size 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/Record/RecordTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class RecordTableViewController: UITableViewController { 26 | 27 | let record: Record 28 | let datasource: RecordTableViewDatasource 29 | lazy var shareButton: UIBarButtonItem = { 30 | UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareButtonTapped)) 31 | }() 32 | 33 | init(_ record: Record) { 34 | self.record = record 35 | self.datasource = RecordTableViewDatasource(record) 36 | super.init(nibName: nil, bundle: nil) 37 | } 38 | 39 | @available(*, unavailable) 40 | required init?(coder aDecoder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | override func viewDidLoad() { 45 | super.viewDidLoad() 46 | title = "Record" 47 | datasource.setup(tableView) 48 | tableView.dataSource = datasource 49 | navigationItem.rightBarButtonItem = shareButton 50 | } 51 | 52 | @objc func shareButtonTapped() { 53 | guard let tempPath = MockRecordSerializer.write(record: record) else { return } 54 | 55 | let allRecordFiles = (try? FileManager.default.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil, options: [])) ?? [] 56 | 57 | let viewContoller = UIActivityViewController(activityItems: allRecordFiles, applicationActivities: nil) 58 | present(viewContoller, animated: true, completion: nil) 59 | } 60 | 61 | } 62 | 63 | // UITableViewDelegate 64 | extension RecordTableViewController { 65 | 66 | static let prettyPrinter: [PrettyPrinter.Type] = [PlaintextPrettyPrinter.self] 67 | 68 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 69 | guard let item = datasource.item(at: indexPath), 70 | let sourceCell = tableView.cellForRow(at: indexPath) else { return } 71 | 72 | presentActionSelection(for: item, from: sourceCell) 73 | tableView.deselectRow(at: indexPath, animated: true) 74 | } 75 | 76 | private func presentActionSelection(for item: RecordTableViewDatasource.Item, from cell: UITableViewCell) { 77 | guard let valueGenerator = item.value else { return } 78 | let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 79 | 80 | for prettyPrinter in RecordTableViewController.prettyPrinter { 81 | if let viewController = prettyPrinter.viewController(for: valueGenerator()) { 82 | alertController.addAction(UIAlertAction(title: prettyPrinter.name, style: .default) { [weak self] _ in 83 | self?.navigationController?.pushViewController(viewController, animated: true) 84 | }) 85 | } 86 | } 87 | 88 | alertController.addAction(UIAlertAction(title: "Share", style: .default) { [weak self] _ in 89 | let activityItem = valueGenerator() 90 | self?.presentShareSheet(for: [activityItem], from: cell) 91 | }) 92 | 93 | alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 94 | 95 | present(alertController, animated: true, completion: nil) 96 | } 97 | 98 | private func presentShareSheet(for items: [Any], from cell: UITableViewCell) { 99 | let viewContoller = UIActivityViewController(activityItems: items, applicationActivities: nil) 100 | viewContoller.popoverPresentationController?.sourceView = cell 101 | viewContoller.popoverPresentationController?.sourceRect = cell.bounds 102 | viewContoller.popoverPresentationController?.permittedArrowDirections = [.up, .down] 103 | present(viewContoller, animated: true, completion: nil) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Cauli/Florets/Inspector/TagLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | @IBDesignable 26 | internal class TagLabel: UILabel { 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | commonInit() 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | super.init(coder: aDecoder) 35 | commonInit() 36 | } 37 | 38 | private func commonInit() { 39 | layer.cornerRadius = 3 40 | clipsToBounds = true 41 | textColor = UIColor.white 42 | } 43 | 44 | static let edgeInsets = UIEdgeInsets.init(top: 1, left: 4, bottom: 1, right: 4) 45 | override func drawText(in rect: CGRect) { 46 | super.drawText(in: rect.inset(by: TagLabel.edgeInsets)) 47 | } 48 | 49 | override var intrinsicContentSize: CGSize { 50 | let size = super.intrinsicContentSize 51 | return CGSize(width: size.width + TagLabel.edgeInsets.left + TagLabel.edgeInsets.right, height: size.height + TagLabel.edgeInsets.top + TagLabel.edgeInsets.bottom) 52 | } 53 | 54 | @available(iOS 9.0, *) 55 | override var firstBaselineAnchor: NSLayoutYAxisAnchor { 56 | let anchor = super.firstBaselineAnchor 57 | return anchor // is there any way to move this anchor down by `TagLabel.edgeInsets.top`? 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Cauli/Florets/MapRemote/MapRemoteFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | /// The `MapRemoteFloret` can modify the url before the request is performed. 26 | /// This is esp. helpful when using a staging or testing server. 27 | /// 28 | /// The `MapRemoteFloret` can only modify the url of a request. If you need to update headers please use the `FindReplaceFloret`. 29 | /// 30 | /// Example configuration. For more examples check the `Mapping` documentation. 31 | /// ```swift 32 | /// let httpsifyMapping = Mapping(name: "https-ify", sourceLocation: MappingLocation(scheme: "http"), destinationLocation: MappingLocation(scheme: "https")) 33 | /// let mapLocal = Mapping(name: "map local", sourceLocation: MappingLocation(), destinationLocation: MappingLocation(host: "localhost") 34 | /// let floret = MapRemoteFloret(mappings: [httpsifyMapping, mapLocal]) 35 | /// Cauli([floret]) 36 | /// ``` 37 | public class MapRemoteFloret: InterceptingFloret { 38 | 39 | internal var userDefaults = UserDefaults() 40 | public var enabled: Bool { 41 | get { 42 | userDefaults.bool(forKey: "Cauli.MapRemoteFloret.enabled") 43 | } 44 | set { 45 | userDefaults.setValue(newValue, forKey: "Cauli.MapRemoteFloret.enabled") 46 | } 47 | } 48 | 49 | public var description: String? { 50 | "The MapRemoteFloret can modify the url of a request before performed. \n Currently \(enabledMappings.count) mappings are enabled." 51 | } 52 | 53 | private let mappings: [Mapping] 54 | private var enabledMappings: Set { 55 | get { 56 | guard let mappings = userDefaults.array(forKey: "Cauli.MapRemoteFloret.enabledMappings") as? [String] else { return [] } 57 | return Set(mappings) 58 | } 59 | set { 60 | userDefaults.setValue(Array(newValue), forKey: "Cauli.MapRemoteFloret.enabledMappings") 61 | } 62 | } 63 | 64 | /// Instantiates a new `MapRemoteFloret` instance with an array of mappings. 65 | /// - Parameter mappings: An array of mappings. Mappings will be evaluated in the order of this array. 66 | public init(mappings: [Mapping]) { 67 | self.mappings = mappings 68 | } 69 | 70 | func isMappingEnabled(_ mapping: Mapping) -> Bool { 71 | enabledMappings.contains(mapping.name) 72 | } 73 | 74 | func setMapping(_ mapping: Mapping, enabled: Bool) { 75 | if enabled { 76 | enabledMappings.insert(mapping.name) 77 | } else { 78 | enabledMappings.remove(mapping.name) 79 | } 80 | } 81 | 82 | public func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 83 | let mappedRecord = mappings.filter { enabledMappings.contains($0.name) }.reduce(record) { record, mapping -> Record in 84 | guard let requestUrl = record.designatedRequest.url, mapping.sourceLocation.matches(url: requestUrl) else { return record } 85 | var record = record 86 | let updatedUrl = mapping.destinationLocation.updating(url: requestUrl) 87 | record.designatedRequest.url = updatedUrl 88 | return record 89 | } 90 | completionHandler(mappedRecord) 91 | } 92 | 93 | public func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 94 | completionHandler(record) 95 | } 96 | 97 | public func viewController(_ cauli: Cauli) -> UIViewController { 98 | MappingsListViewController(mapRemoteFloret: self, mappings: mappings) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Cauli/Florets/MapRemote/MappingsListDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class MappingsListDataSource: NSObject, UITableViewDataSource { 26 | 27 | private let mapRemoteFloret: MapRemoteFloret 28 | private let mappings: [Mapping] 29 | 30 | init(mapRemoteFloret: MapRemoteFloret, mappings: [Mapping]) { 31 | self.mapRemoteFloret = mapRemoteFloret 32 | self.mappings = mappings 33 | } 34 | 35 | func setup(_ tableView: UITableView) { 36 | let bundle = Bundle(for: SwitchTableViewCell.self) 37 | tableView.register(UINib(nibName: SwitchTableViewCell.nibName, bundle: bundle), forCellReuseIdentifier: SwitchTableViewCell.reuseIdentifier) 38 | } 39 | 40 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 41 | mappings.count 42 | } 43 | 44 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 45 | guard let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.reuseIdentifier, for: indexPath) as? SwitchTableViewCell else { 46 | fatalError("Unable to dequeue a cell") 47 | } 48 | let mapping = mappings[indexPath.row] 49 | cell.set(title: mapping.name, switchValue: mapRemoteFloret.isMappingEnabled(mapping), description: nil) 50 | cell.switchValueChanged = { [weak mapRemoteFloret] newValue in 51 | mapRemoteFloret?.setMapping(mapping, enabled: newValue) 52 | } 53 | return cell 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Cauli/Florets/MapRemote/MappingsListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class MappingsListViewController: UITableViewController { 26 | 27 | private let dataSource: MappingsListDataSource 28 | 29 | init(mapRemoteFloret: MapRemoteFloret, mappings: [Mapping]) { 30 | self.dataSource = MappingsListDataSource(mapRemoteFloret: mapRemoteFloret, mappings: mappings) 31 | super.init(nibName: nil, bundle: nil) 32 | dataSource.setup(tableView) 33 | tableView.dataSource = dataSource 34 | } 35 | 36 | @available(*, unavailable) 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Cauli/Florets/Mock/MockRecordSerializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal class MockRecordSerializer { 26 | 27 | /// Returns a path to a temporary folder where a record is written to. 28 | /// 29 | /// - Parameter record: The record to write on disk. 30 | /// - Returns: The path to the temporary folder. 31 | static func write(record: Record) -> URL? { 32 | let tempPath = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString, isDirectory: true) 33 | 34 | do { 35 | try FileManager.default.createDirectory(at: tempPath, withIntermediateDirectories: true, attributes: nil) 36 | let swappedRecord = record.swapped(to: tempPath) 37 | guard let data = self.data(for: swappedRecord) else { return nil } 38 | let filepath = tempPath.appendingPathComponent("record.json") 39 | try data.write(to: filepath) 40 | } catch { 41 | try? FileManager.default.removeItem(at: tempPath) 42 | return nil 43 | } 44 | 45 | return tempPath 46 | } 47 | 48 | static func record(from folder: URL) -> Record? { 49 | let filepath = folder.appendingPathComponent("record.json") 50 | if let data = try? Data(contentsOf: filepath), 51 | let swappedRecord = MockRecordSerializer.record(with: data) { 52 | return swappedRecord.record(in: folder) 53 | } else { 54 | return nil 55 | } 56 | } 57 | 58 | private static func data(for record: SwappedRecord) -> Data? { 59 | let encoder = JSONEncoder() 60 | encoder.outputFormatting = .prettyPrinted 61 | guard let data = try? encoder.encode(record) else { return nil } 62 | return data 63 | } 64 | 65 | private static func record(with data: Data) -> SwappedRecord? { 66 | let decoder = JSONDecoder() 67 | guard let record = try? decoder.decode(SwappedRecord.self, from: data) else { return nil } 68 | return record 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Cauli/Florets/Mock/SwappedRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal struct SwappedRecord: Codable { 26 | let identifier: UUID 27 | let originalRequest: SwappedURLRequest 28 | let designatedRequest: SwappedURLRequest 29 | let result: Result? 30 | let requestStarted: Date? 31 | let responseReceived: Date? 32 | } 33 | 34 | extension SwappedRecord { 35 | init(_ record: Record, folder: URL) { 36 | self.identifier = record.identifier 37 | self.originalRequest = SwappedURLRequest(record.originalRequest, bodyFilepath: folder.appendingPathComponent("originalRequest.data")) 38 | self.designatedRequest = SwappedURLRequest(record.designatedRequest, bodyFilepath: folder.appendingPathComponent("designatedRequest.data")) 39 | if let result = record.result { 40 | switch result { 41 | case .error(let error): 42 | self.result = .error(error) 43 | case .result(let response): 44 | let swappedResponse = SwappedResponse(response, dataFilepath: folder.appendingPathComponent("response.data")) 45 | self.result = .result(swappedResponse) 46 | } 47 | } else { 48 | self.result = nil 49 | } 50 | 51 | self.requestStarted = record.requestStarted 52 | self.responseReceived = record.responseReceived 53 | } 54 | 55 | func record(in folder: URL) -> Record { 56 | var record = Record(self.originalRequest.urlRequest(in: folder)) 57 | 58 | record.identifier = self.identifier 59 | record.designatedRequest = self.designatedRequest.urlRequest(in: folder) 60 | if let result = self.result { 61 | switch result { 62 | case .error(let error): 63 | record.result = .error(error) 64 | case .result(let swappedResponse): 65 | let response = swappedResponse.response(in: folder) 66 | record.result = .result(response) 67 | } 68 | } else { 69 | record.result = nil 70 | } 71 | record.requestStarted = self.requestStarted 72 | record.responseReceived = self.responseReceived 73 | 74 | return record 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Cauli/Florets/Mock/SwappedResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal struct SwappedResponse: Codable { 26 | let dataFilename: String? 27 | var urlResponse: URLResponse { 28 | get { 29 | urlResponseRepresentable.urlResponse 30 | } 31 | set { 32 | urlResponseRepresentable = URLResponseRepresentable(newValue) 33 | } 34 | } 35 | 36 | init(_ urlResponse: URLResponse, dataFilename: String?) { 37 | self.dataFilename = dataFilename 38 | urlResponseRepresentable = URLResponseRepresentable(urlResponse) 39 | } 40 | 41 | var urlResponseRepresentable: URLResponseRepresentable 42 | } 43 | 44 | extension SwappedResponse { 45 | init(_ response: Response, dataFilepath: URL) { 46 | self.urlResponseRepresentable = URLResponseRepresentable(response.urlResponse) 47 | 48 | if let data = response.data, 49 | (try? data.write(to: dataFilepath)) != nil { 50 | self.dataFilename = dataFilepath.lastPathComponent 51 | } else { 52 | self.dataFilename = nil 53 | } 54 | } 55 | 56 | func response(in folder: URL) -> Response { 57 | var response = Response(urlResponse, data: nil) 58 | response.data = nil 59 | 60 | if let dataFilename = dataFilename, 61 | let data = try? Data(contentsOf: folder.appendingPathComponent(dataFilename)) { 62 | response.data = data 63 | } 64 | 65 | return response 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Cauli/Florets/Mock/SwappedURLRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal struct SwappedURLRequest: Codable { 26 | let url: URL 27 | let cachePolicy: URLRequest.CachePolicy 28 | let timeoutInterval: TimeInterval 29 | let mainDocumentURL: URL? 30 | let allowsCellularAccess: Bool 31 | let httpMethod: String? 32 | let allHTTPHeaderFields: [String: String]? 33 | let bodyFilename: String? 34 | let httpShouldHandleCookies: Bool 35 | let httpShouldUsePipelining: Bool 36 | } 37 | 38 | extension SwappedURLRequest { 39 | init(_ urlRequest: URLRequest, bodyFilepath: URL) { 40 | // swiftlint:disable force_unwrapping 41 | self.url = urlRequest.url ?? URL(string: "unknown")! 42 | // swiftlint:enable force_unwrapping 43 | self.cachePolicy = urlRequest.cachePolicy 44 | self.timeoutInterval = urlRequest.timeoutInterval 45 | self.mainDocumentURL = urlRequest.mainDocumentURL 46 | self.allowsCellularAccess = urlRequest.allowsCellularAccess 47 | self.httpMethod = urlRequest.httpMethod 48 | self.allHTTPHeaderFields = urlRequest.allHTTPHeaderFields 49 | self.httpShouldHandleCookies = urlRequest.httpShouldHandleCookies 50 | self.httpShouldUsePipelining = urlRequest.httpShouldUsePipelining 51 | 52 | if let body = urlRequest.httpBody, 53 | (try? body.write(to: bodyFilepath)) != nil { 54 | self.bodyFilename = bodyFilepath.lastPathComponent 55 | } else { 56 | self.bodyFilename = nil 57 | } 58 | } 59 | 60 | func urlRequest(in folder: URL) -> URLRequest { 61 | var request = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: timeoutInterval) 62 | request.mainDocumentURL = mainDocumentURL 63 | request.allowsCellularAccess = allowsCellularAccess 64 | request.httpMethod = httpMethod 65 | request.allHTTPHeaderFields = allHTTPHeaderFields 66 | request.httpShouldHandleCookies = httpShouldHandleCookies 67 | request.httpShouldUsePipelining = httpShouldUsePipelining 68 | 69 | if let bodyFilename = bodyFilename, 70 | let body = try? Data(contentsOf: folder.appendingPathComponent(bodyFilename)) { 71 | request.httpBody = body 72 | } 73 | 74 | return request 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Cauli/Florets/NoCache/NoCacheFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// The NoCacheFloret modifies the desigantedRequest and the response of a record 26 | /// to prevent returning cached data or writing data to the cache. 27 | /// 28 | /// On the designatedRequest it will: 29 | /// * change the **cachePolicy** to **.reloadIgnoringLocalCacheData** 30 | /// * **remove** the value for the **If-Modified-Since** key on allHTTPHeaderFields 31 | /// * **remove** the value for the **If-None-Match** key on allHTTPHeaderFields 32 | /// * change **Cache-Control** to **no-cache** 33 | /// 34 | /// On the response it will: 35 | /// * **remove** the value for the **Last-Modified** key on allHTTPHeaderFields 36 | /// * **remove** the value for the **ETag** key on allHTTPHeaderFields 37 | /// * change **Expires** to **0** 38 | /// * change **Cache-Control** to **no-cache** 39 | public class NoCacheFloret: FindReplaceFloret { 40 | 41 | /// Public initalizer to create an instance of the `NoCacheFloret`. 42 | public required init() { 43 | let willRequestReplaceDefinition = RecordModifier(keyPath: \Record.designatedRequest) { designatedRequest -> (URLRequest) in 44 | var request = designatedRequest 45 | request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData 46 | request.setValue(nil, forHTTPHeaderField: "If-Modified-Since") 47 | request.setValue(nil, forHTTPHeaderField: "If-None-Match") 48 | request.setValue("no-cache", forHTTPHeaderField: "Cache-Control") 49 | 50 | return request 51 | } 52 | 53 | let didRespondReplaceDefinition = RecordModifier(keyPath: \Record.result) { result in 54 | guard case .result(let response)? = result, let httpURLResponse = response.urlResponse as? HTTPURLResponse, let url = httpURLResponse.url else { return result } 55 | 56 | var allHTTPHeaderFields = httpURLResponse.allHeaderFields as? [String: String] ?? [:] 57 | allHTTPHeaderFields.removeValue(forKey: "Last-Modified") 58 | allHTTPHeaderFields.removeValue(forKey: "ETag") 59 | allHTTPHeaderFields["Expires"] = "0" 60 | allHTTPHeaderFields["Cache-Control"] = "no-cache" 61 | 62 | guard let newHTTPURLRespones = HTTPURLResponse(url: url, statusCode: httpURLResponse.statusCode, httpVersion: nil, headerFields: allHTTPHeaderFields) else { return result } 63 | 64 | return Result.result(Response(newHTTPURLRespones, data: response.data)) 65 | } 66 | 67 | super.init(willRequestModifiers: [willRequestReplaceDefinition], didRespondModifiers: [didRespondReplaceDefinition], name: "NoCacheFloret") 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Cauli/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Cauli/InterceptingFloret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// An InterceptingFloret can process requests and responses. 26 | public protocol InterceptingFloret: Floret { 27 | 28 | /// If an InterceptingFloret is disabled both functions `willRequest` and `didRespond` will 29 | /// not be called anymore. A InterceptingFloret doesn't need to perform any specific action. 30 | var enabled: Bool { get set } 31 | 32 | /// This function will be called before a request is performed. The InterceptingFlorets will be 33 | /// called in the order the Cauli instance got initialized with. 34 | /// 35 | /// Using this function you can: 36 | /// - inspect the request 37 | /// - modify the request (update the `designatedRequest`) 38 | /// - fail the request (set the `result` to `.error()`) 39 | /// - return a cached or pre-calculated response (set the `result` to `.result()`) 40 | /// 41 | /// - Parameters: 42 | /// - record: The `Record` that represents the request before it was performed. 43 | /// - completionHandler: Call this completion handler exactly once with the 44 | /// original or modified `Record`. 45 | func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) 46 | 47 | /// This function will be called after a request is performed and the response arrived. 48 | /// The InterceptingFlorets will be called in the order the Cauli instance got initialized with. 49 | /// 50 | /// Using this function you can: 51 | /// - modify the request 52 | /// 53 | /// - Parameters: 54 | /// - record: The `Record` that represents the request after it was performed. 55 | /// - completionHandler: Call this completion handler exactly once with the 56 | /// original or modified `Record`. 57 | func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) 58 | } 59 | -------------------------------------------------------------------------------- /Cauli/MemoryStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | internal final class MemoryStorage: Storage { 26 | 27 | var records: [Record] = [] 28 | var capacity: StorageCapacity { 29 | didSet { 30 | ensureCapacity() 31 | } 32 | } 33 | var preStorageRecordModifier: RecordModifier? 34 | 35 | init(capacity: StorageCapacity, preStorageRecordModifier: RecordModifier? = nil) { 36 | self.capacity = capacity 37 | self.preStorageRecordModifier = preStorageRecordModifier 38 | } 39 | 40 | func store(_ record: Record) { 41 | assert(Thread.isMainThread, "\(#file):\(#line) must run on the main thread!") 42 | let modifiedRecord = preStorageRecordModifier?.modify(record) ?? record 43 | if let recordIndex = records.firstIndex(where: { $0.identifier == modifiedRecord.identifier }) { 44 | records[recordIndex] = modifiedRecord 45 | } else { 46 | records.insert(modifiedRecord, at: 0) 47 | ensureCapacity() 48 | } 49 | } 50 | 51 | func records(_ count: Int, after record: Record?) -> [Record] { 52 | assert(Thread.isMainThread, "\(#file):\(#line) must run on the main thread!") 53 | let index: Int 54 | if let record = record, 55 | let recordIndex = records.firstIndex(where: { $0.identifier == record.identifier }) { 56 | index = recordIndex + 1 57 | } else { 58 | index = 0 59 | } 60 | let maxCount = min(count, records.count - index) 61 | return Array(records[index..<(index + maxCount)]) 62 | } 63 | 64 | private func ensureCapacity() { 65 | switch capacity { 66 | case .unlimited: return 67 | case .records(let maximumRecordCount): 68 | records = Array(records.prefix(maximumRecordCount)) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Cauli/RecordModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A RecordModifier defines the modification of a Record 26 | public struct RecordModifier { 27 | /// modify closure defines a Record modification. It can return the original record or a modified record. 28 | /// It should not return a new record/a record with a new UUID. 29 | let modify: (Record) -> (Record) 30 | } 31 | 32 | extension RecordModifier { 33 | /// This init is based on a Records keyPath and allows just to modify 34 | /// a specific property of a Record. 35 | /// 36 | /// - Parameters: 37 | /// - keyPath: The keyPath describing which property of a Record should be modified. 38 | /// - modifier: A closure used to modify the property defined by the keyPath. 39 | /// - Returns: A RecordModifier is modifying a specific property of a Record. 40 | /// Used for example to initalize a FindReplaceFloret. 41 | public init(keyPath: WritableKeyPath, modifier: @escaping (Type) -> (Type)) { 42 | self.init { record in 43 | var newRecord = record 44 | let oldValue = record[keyPath: keyPath] 45 | let modifiedValue = modifier(oldValue) 46 | newRecord[keyPath: keyPath] = modifiedValue 47 | return newRecord 48 | } 49 | } 50 | 51 | /// This init will modify the Requests URL. 52 | /// 53 | /// - Parameters: 54 | /// - expression: A RegularExpression describing the part of the Requets URL to modify. 55 | /// - template: The substitution template used when replacing matching instances of the expression. 56 | /// - Returns: A RecordModifier is modifying the RequestURL. Used to initalize a FindReplaceFloret. 57 | public static func modifyUrl(expression: NSRegularExpression, template: String) -> RecordModifier { 58 | let keyPath = \Record.designatedRequest.url 59 | let modifier: (URL?) -> (URL?) = { url in 60 | guard let oldURL = url else { return url } 61 | let urlWithReplacements = replacingOcurrences(of: expression, in: oldURL.absoluteString, with: template) 62 | return URL(string: urlWithReplacements) 63 | } 64 | return RecordModifier(keyPath: keyPath, modifier: modifier) 65 | } 66 | 67 | static func replacingOcurrences(of expression: NSRegularExpression, in string: String, with template: String) -> String { 68 | let range = NSRange(string.startIndex..., in: string) 69 | return expression.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: template) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Cauli/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | /// A Storage is used to store and retrieve Records. It can be either in memory or on disk. 26 | public protocol Storage { 27 | 28 | /// The `capacity` defines the capacity of the storage. 29 | var capacity: StorageCapacity { get set } 30 | 31 | /// A `RecordModifier` that can modify each `Record` before it is persisted to a `Storage`. 32 | /// This allows you to modify requests and responses after they are executed but before they are passed along to other florets. 33 | var preStorageRecordModifier: RecordModifier? { get set } 34 | 35 | /// Initialize a Store with capacity and optional pre storage record modifier 36 | /// 37 | /// - Parameters: 38 | /// - capacity: `StorageCapacity` of the storage 39 | /// - preStorageRecordModifier: `RecordModifier` that can modify a `Record` before it is stored 40 | init(capacity: StorageCapacity, preStorageRecordModifier: RecordModifier?) 41 | 42 | /// Adds a record to the storage. Updates a possibly existing record. 43 | /// A record is the same if it's identifier is the same. 44 | /// 45 | /// - Parameter record: The record to add to the storage. 46 | func store(_ record: Record) 47 | 48 | /// Returns a number of records after the referenced record. 49 | /// Records are sorted by order there were added to the storage, 50 | /// Might return less than `count` if there are no more records. 51 | /// 52 | /// - Parameters: 53 | /// - count: The number of records that should be returned. 54 | /// - after: The record after which there should be new records returned. 55 | /// - Returns: The records after the referenced record, sorted from latest to oldest. 56 | func records(_ count: Int, after: Record?) -> [Record] 57 | } 58 | 59 | /// Defines the capacity of a storage. 60 | public enum StorageCapacity: Equatable { 61 | /// The capacity is unlimited. 62 | case unlimited 63 | /// The capacity is limited to a certain number of records. 64 | case records(Int) 65 | } 66 | -------------------------------------------------------------------------------- /Cauli/ViewControllerShakePresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | 25 | internal class ViewControllerShakePresenter { 26 | 27 | private var viewController: (() -> (UIViewController?)) 28 | 29 | /// This initializes a `ViewControllerShakePresenter` which will, when the device is shaked, 30 | /// modally present a `UINavigationController`. The passed ViewController will be the 31 | /// `rootViewController` of the `UINavigationController`. 32 | /// 33 | /// - Parameter viewController: A viewController that will be the `rootViewController` of a UINavigationController. 34 | init(_ viewController: @escaping () -> (UIViewController?)) { 35 | self.viewController = viewController 36 | self.shakeMotionDidEndObserver = NotificationCenter.default.addObserver(forName: Notification.shakeMotionDidEnd, object: nil, queue: nil) { [weak self] _ in 37 | self?.toggleViewController() 38 | } 39 | } 40 | 41 | private var shakeMotionDidEndObserver: NSObjectProtocol? 42 | 43 | private func presentingViewController() -> UIViewController? { 44 | var presentingViewController = UIApplication.shared.keyWindow?.rootViewController 45 | while let presentedViewController = presentingViewController?.presentedViewController { 46 | presentingViewController = presentedViewController 47 | } 48 | return presentingViewController 49 | } 50 | 51 | weak var presentedViewController: UIViewController? 52 | private func toggleViewController() { 53 | let presentingViewController = self.presentingViewController() 54 | if let presentedViewController = presentedViewController, 55 | presentedViewController.presentingViewController != nil { 56 | presentedViewController.dismiss(animated: true, completion: nil) 57 | } else if let viewController = self.viewController() { 58 | let navigationController = UINavigationController(rootViewController: viewController) 59 | let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissPresentedViewController)) 60 | viewController.navigationItem.rightBarButtonItem = doneBarButtonItem 61 | presentedViewController = navigationController 62 | presentingViewController?.present(navigationController, animated: true, completion: nil) 63 | } 64 | } 65 | 66 | @objc private func dismissPresentedViewController() { 67 | presentedViewController?.dismiss(animated: true, completion: nil) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CauliTests/CauliSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Cauliframework 24 | import Foundation 25 | import Quick 26 | import Nimble 27 | 28 | class CauliSpec: QuickSpec { 29 | override func spec() { 30 | describe("init") { 31 | it("should not create a retain cycle") { 32 | var strongCauli: Cauli? = Cauli([], configuration: Configuration.standard) 33 | weak var weakCauli: Cauli? = strongCauli 34 | strongCauli = nil 35 | expect(weakCauli).to(beNil()) 36 | } 37 | it("should initialize with standard configuration when no parameter was given") { 38 | let cauli = Cauli([]) 39 | let standardConfiguration = Configuration.standard 40 | expect(cauli.storage.capacity) == standardConfiguration.storageCapacity 41 | } 42 | it("should configure the storage with the correct storage capacity") { 43 | let cauli = Cauli([], configuration: Configuration(recordSelector: RecordSelector.init(selects: {_ in true }), enableShakeGesture: true, storageCapacity: .unlimited)) 44 | expect(cauli.storage.capacity) == .unlimited 45 | 46 | let cauli2 = Cauli([], configuration: Configuration(recordSelector: RecordSelector.init(selects: {_ in true }), enableShakeGesture: true, storageCapacity: .records(10))) 47 | expect(cauli2.storage.capacity) == .records(10) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CauliTests/Fakes/HTTPURLResponse+Fake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | extension HTTPURLResponse { 26 | 27 | class override var fake: HTTPURLResponse { 28 | return HTTPURLResponse(url: URL(string: "https://cauli.works/fake")!, 29 | statusCode: 200, 30 | httpVersion: nil, 31 | headerFields: ["fake":"fake"])! 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /CauliTests/Fakes/Record+Fake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | @testable import Cauliframework 25 | 26 | extension Record { 27 | static func fake() -> Record { 28 | return fake(with: URL(string: "spec_fake_url")!) 29 | } 30 | static func fake(with url: URL) -> Record { 31 | return Record(URLRequest(url: url)) 32 | } 33 | func setting(identifier: UUID) -> Record { 34 | return Record(identifier: identifier, originalRequest: originalRequest, designatedRequest: designatedRequest, result: result, requestStarted: nil, responseReceived: nil) 35 | } 36 | func setting(originalRequestUrl url: URL) -> Record { 37 | return Record(identifier: identifier, originalRequest: URLRequest(url: url), designatedRequest: designatedRequest, result: result, requestStarted: requestStarted, responseReceived: responseReceived) 38 | } 39 | mutating func setting(designatedRequestsHeaderFields headerFields: [String: String]?) { 40 | designatedRequest.allHTTPHeaderFields?.keys.forEach { 41 | designatedRequest.setValue(nil, forHTTPHeaderField: $0) 42 | } 43 | 44 | headerFields?.forEach { 45 | designatedRequest.setValue($1, forHTTPHeaderField: $0) 46 | } 47 | } 48 | mutating func setting(designatedRequestsCachePolicy cachePolicy: [String: String]?) { 49 | designatedRequest.cachePolicy = .reloadIgnoringLocalCacheData 50 | } 51 | mutating func setting(httpURLResponse response: HTTPURLResponse) { 52 | result = .result(Response(response, data: nil)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CauliTests/Fakes/URLResponse+Fake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | import Foundation 24 | 25 | extension URLResponse { 26 | 27 | @objc class var fake: URLResponse { 28 | return URLResponse(url: URL(string: "https://cauli.works/fake")!, 29 | mimeType: "fake/fake", 30 | expectedContentLength: 1337, 31 | textEncodingName: nil) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /CauliTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /CauliTests/Stubs/CauliURLProtocolDelegateStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @testable import Cauliframework 24 | import Foundation 25 | 26 | class CauliURLProtocolDelegateStub: CauliURLProtocolDelegate { 27 | var willRequestClosure: ((inout Record) -> Void)? 28 | var didRespondClosure: ((inout Record) -> Void)? 29 | 30 | func handles(_ record: Record) -> Bool { 31 | return true 32 | } 33 | 34 | func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 35 | if let willRequestClosure = willRequestClosure { 36 | var updatedRecord = record 37 | willRequestClosure(&updatedRecord) 38 | completionHandler(updatedRecord) 39 | } else { 40 | completionHandler(record) 41 | } 42 | } 43 | 44 | func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) { 45 | if let didRespondClosure = didRespondClosure { 46 | var updatedRecord = record 47 | didRespondClosure(&updatedRecord) 48 | completionHandler(updatedRecord) 49 | } else { 50 | completionHandler(record) 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /CauliTests/URLResponseRepresentableSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018 cauli.works 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | // 22 | 23 | @testable import Cauliframework 24 | import Foundation 25 | import Quick 26 | import Nimble 27 | 28 | class URLResponseRepresentableSpec: QuickSpec { 29 | 30 | override func spec() { 31 | describe("init") { 32 | it("should initialize with a URLResponse") { 33 | let fakeURLResponse = URLResponse.fake 34 | let urlResponseRepresentable: URLResponseRepresentable = URLResponseRepresentable(fakeURLResponse) 35 | 36 | guard case .urlResponse(let urlResponse) = urlResponseRepresentable else { 37 | fail("expect the enum case to equal `urlResponse(URLResponse)`") 38 | return 39 | } 40 | 41 | expect(urlResponse) == fakeURLResponse 42 | } 43 | 44 | it("should initialize with a HTTPURLResponse") { 45 | let fakeHTTPURLRespone = HTTPURLResponse.fake 46 | let urlResponseRepresentable = URLResponseRepresentable(fakeHTTPURLRespone) 47 | 48 | guard case .httpURLResponse(let httpURLResponse) = urlResponseRepresentable else { 49 | fail("expect the enum case to equal `httpURLResponse(HTTPURLResponse)`") 50 | return 51 | } 52 | 53 | expect(httpURLResponse.url) == fakeHTTPURLRespone.url 54 | expect(httpURLResponse.statusCode) == fakeHTTPURLRespone.statusCode 55 | expect(httpURLResponse.allHeaderFields["fake"] as? String) == fakeHTTPURLRespone.allHeaderFields["fake"] as? String 56 | } 57 | } 58 | 59 | describe("codable") { 60 | it("should decode an encoded URLResponseRepresentable", closure: { 61 | let fakeResponseToEncode = URLResponse.fake 62 | let urlResponseRepresentable = URLResponseRepresentable(fakeResponseToEncode) 63 | let encodedData: Data? = try? JSONEncoder().encode([urlResponseRepresentable]) 64 | 65 | guard let data = encodedData else { 66 | fail("encodedData is nil") 67 | return 68 | } 69 | 70 | guard let decodedValue = try? JSONDecoder().decode([URLResponseRepresentable].self, from: data), 71 | let decodedURLResponseRepresentable = decodedValue.first, 72 | case .urlResponse(let urlResponse) = decodedURLResponseRepresentable else { 73 | fail("expect to decode URLResponseRepresentable from data") 74 | return 75 | } 76 | 77 | expect(urlResponse.url?.absoluteString) == fakeResponseToEncode.url?.absoluteString 78 | expect(urlResponse.expectedContentLength) == fakeResponseToEncode.expectedContentLength 79 | expect(urlResponse.textEncodingName).to(beNil()) 80 | }) 81 | } 82 | } 83 | 84 | private func data(for fileName: String) -> Data? { 85 | guard let path = Bundle(for: type(of: self)).path(forResource: fileName, ofType: nil), 86 | let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return nil } 87 | 88 | return data 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Cauliframework.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint Cauli.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | s.name = "Cauliframework" 18 | s.version = "1.1" 19 | s.summary = "Helps you to intercept network requests." 20 | # This description is used to generate tags and improve search results. 21 | # * Think: What does it do? Why did you write it? What is the focus? 22 | # * Try to keep it short, snappy and to the point. 23 | # * Write the description between the DESC delimiters below. 24 | # * Finally, don't worry about the indent, CocoaPods strips it! 25 | s.description = <<-DESC 26 | Cauli is a network debugging framework featuring a plugin infrastructure to hook into selected request and responses as well as recording and displaying performed requests. It provides a wide range of possibilities. For example from inspecting network traffic to mock UnitTests. Missing something fancy? How about writing your own Plugin. 27 | DESC 28 | 29 | s.homepage = "https://cauli.works" 30 | s.license = { :type => "MIT", :file => "LICENSE" } 31 | s.authors = { "Cornelius Horstmann" => "cornelius@brototyp.de", "Pascal Stüdlein" => "mail@pascal-stuedlein.de" } 32 | s.ios.deployment_target = "8.0" 33 | 34 | # watchOS and tvOS are currently not tested 35 | # s.watchos.deployment_target = "3.0" 36 | # s.tvos.deployment_target = "10.0" 37 | s.source = { :git => "https://github.com/cauliframework/cauli.git", :tag => "#{s.version}" } 38 | s.source_files = "Cauli", "Cauli/**/*.swift" 39 | s.swift_version = '5.0' 40 | 41 | s.resources = 'Cauli/**/*.xib' 42 | 43 | end 44 | -------------------------------------------------------------------------------- /Cauliframework.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // Copyright (c) 2018 cauli.works 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | // 27 | 28 | 29 | -------------------------------------------------------------------------------- /Cauliframework.xcodeproj/xcshareddata/xcschemes/Cauliframework.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Cauliframework.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Cauliframework.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cauliframework.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "nimble", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Quick/Nimble.git", 7 | "state" : { 8 | "revision" : "7a46a5fc86cb917f69e3daf79fcb045283d8f008", 9 | "version" : "8.1.2" 10 | } 11 | }, 12 | { 13 | "identity" : "ohhttpstubs", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/AliSoftware/OHHTTPStubs", 16 | "state" : { 17 | "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", 18 | "version" : "9.1.0" 19 | } 20 | }, 21 | { 22 | "identity" : "quick", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/Quick/Quick", 25 | "state" : { 26 | "revision" : "0b4ed6c706dd0cce923b5019a605a9bcc6b1b600", 27 | "version" : "2.0.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # Sometimes it's a README fix, or something like that - which isn't relevant for 2 | # including in a project's CHANGELOG for example 3 | declared_trivial = github.pr_title.include? "#trivial" 4 | has_pod_changes = !git.modified_files.grep(/^Cauli\//).empty? 5 | 6 | # Make it more obvious that a PR is a work in progress and shouldn't be merged yet 7 | warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" or github.pr_title.include? "WIP:" 8 | 9 | has_changelog_updates = git.modified_files.include?("CHANGELOG.md") 10 | if has_pod_changes && !has_changelog_updates && !declared_trivial 11 | fail("Please include a CHANGELOG entry to credit yourself! \nYou can find it at /CHANGELOG.md.", :sticky => false) 12 | markdown <<-MARKDOWN 13 | ### Example CHANGELOG entry 14 | 15 | ```markdown 16 | * **feature/improvement/bugfix** #{github.pr_title}\s\s 17 | [#issue_number](https://github.com/cauliframework/cauli/issues/issue_number) by @#{github.pr_author} 18 | ``` 19 | 20 | MARKDOWN 21 | 22 | markdown <<-MARKDOWN 23 | ### Trivial PR? 24 | 25 | If you think your PR is trivial and neither a CHANGELOG entry nor a README update is necessary, you can add a `#trivial` to your PR title. The bot will pick that up and will not warn about a missing CHANGELOG or README entry. 26 | MARKDOWN 27 | end 28 | 29 | has_readme_updates = git.modified_files.include?("README.md") 30 | if has_pod_changes && !has_readme_updates && !declared_trivial 31 | warn("Are there any changes that should be explained in the `README.md`?") 32 | end 33 | 34 | # Perform swiftlint and comment violations inline 35 | swiftlint.config_file = '.swiftlint.yml' 36 | swiftlint.verbose = true 37 | swiftlint.lint_files inline_mode: true 38 | -------------------------------------------------------------------------------- /Draft.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | enum Result { 4 | case error(Error) 5 | case result(Type) 6 | } 7 | 8 | protocol Record { 9 | var identifier: UUID { get } 10 | // var createdAt: Date { get } 11 | // var storedAt: Date { get } 12 | var originalRequest: URLRequest { get } 13 | var designatedRequest: URLRequest { get } 14 | var result: Result<(URLResponse, Data?)> { get } 15 | // var additionalInformation: [String: Codable] { get } 16 | } 17 | 18 | protocol Floret { 19 | func willRequest(_ record: Record) -> Record 20 | func didRespond(_ record: Record) -> Record 21 | } 22 | 23 | protocol Storage { 24 | func store(_ record: Record) 25 | func records(_ count: Int, after: Record?) -> [Record] 26 | } 27 | 28 | final class Cauli { 29 | 30 | public var enabled: Bool 31 | private let storage: Storage 32 | 33 | init(_ florets: [Floret]) { 34 | fatalError("implement me") 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Draft.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '10.0' 3 | 4 | target 'cauli-ios-example' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for cauli-ios-example 9 | pod 'Cauliframework', path: '../../' 10 | end 11 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Cauliframework (1.0.1) 3 | 4 | DEPENDENCIES: 5 | - Cauliframework (from `../../`) 6 | 7 | EXTERNAL SOURCES: 8 | Cauliframework: 9 | :path: "../../" 10 | 11 | SPEC CHECKSUMS: 12 | Cauliframework: 311e383f595cc3969c93afbcc80bf45c71da468f 13 | 14 | PODFILE CHECKSUM: 6aa874b392e490b17f77e20dbd808153ee0fbd15 15 | 16 | COCOAPODS: 1.10.1 17 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example.xcodeproj/xcshareddata/xcschemes/cauli-ios-example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 06.08.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cauliframework 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 18 | Cauli.customShared.run() 19 | Cauli.mockFloret.enabled = true 20 | return true 21 | } 22 | 23 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 24 | return true 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Cauli+Customized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cauli+Customized.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 04.10.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cauliframework 11 | 12 | internal extension Cauli { 13 | static let findReplaceFloret: FindReplaceFloret = { 14 | let expresssion = try! NSRegularExpression(pattern: "^http://", options: []) 15 | let httpsUrl = RecordModifier.modifyUrl(expression: expresssion, template: "https://") 16 | let floret = FindReplaceFloret(willRequestModifiers: [httpsUrl], name: "https-ify Floret") 17 | floret.description = "You can set this text while creating your floret!" 18 | return floret 19 | }() 20 | static let mockFloret: MockFloret = { 21 | let floret = MockFloret() 22 | floret.addMapping() { request, floret in 23 | guard let host = request.url?.host, 24 | host == "invalidurl.invalid" else { return nil } 25 | return floret.resultForPath("2e018ce0e517c39e2717efb0151e3b65/97f6f69543dd9e669ee47a0cabbb40b8") 26 | } 27 | floret.addMapping(forUrlPath: "/404") { request, floret in 28 | return floret.resultForPath("default/404") 29 | } 30 | return floret 31 | }() 32 | static let inspectorFloret = InspectorFloret() 33 | static let customInspectorFloret = InspectorFloret(formatter: CustomInspectorFloretFormatter()) 34 | static let mapRemoteFloret: MapRemoteFloret = { 35 | let mapping = Mapping(name: "invalidurl.invalid", sourceLocation: MappingLocation(host: "invalidurl.invalid"), destinationLocation: MappingLocation(path: "/rewritten")) 36 | let floret = MapRemoteFloret(mappings: [mapping]) 37 | floret.enabled = true 38 | return floret 39 | }() 40 | static let customShared = Cauli([HTTPBodyStreamFloret(), findReplaceFloret, mockFloret, inspectorFloret, mapRemoteFloret, customInspectorFloret]) 41 | } 42 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/CustomInspectorFloretFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomInspectorFloretFormatter.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 25.09.23. 6 | // Copyright © 2023 brototyp.de. All rights reserved. 7 | // 8 | 9 | import Cauliframework 10 | 11 | class CustomInspectorFloretFormatter: InspectorFloretFormatterType { 12 | 13 | private let defaultFormatter = InspectorFloretFormatter() 14 | 15 | func listFormattedData(for record: Cauliframework.Record) -> Cauliframework.InspectorFloret.RecordListFormattedData { 16 | guard case .error(let error) = record.result else { 17 | return defaultFormatter.listFormattedData(for: record) 18 | } 19 | let defaultData = defaultFormatter.listFormattedData(for: record) 20 | let newMethod = "\(defaultData.method) | \(error.domain).\(error.code)" 21 | return Cauliframework.InspectorFloret.RecordListFormattedData(method: newMethod, path: defaultData.path, time: defaultData.time, status: defaultData.status, statusColor: defaultData.statusColor) 22 | } 23 | 24 | func recordMatchesQuery(record: Cauliframework.Record, query: String) -> Bool { 25 | defaultFormatter.recordMatchesQuery(record: record, query: query) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/File.txt: -------------------------------------------------------------------------------- 1 | This is just a simple payload 2 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/MockFloret/GET_http_ip_jsontest_com_/b014744502154b59cdd3ed4c2f21c8b6-anonymized/record.json: -------------------------------------------------------------------------------- 1 | { 2 | "result" : { 3 | "urlResponseRepresentable" : { 4 | "httpURLResponse" : { 5 | "url" : "http:\/\/ip.jsontest.com\/", 6 | "statusCode" : 200, 7 | "headerFields" : { 8 | "Content-Type" : "application\/json; charset=ISO-8859-1", 9 | "X-Cloud-Trace-Context" : "25e39a14effdf99d0c5dd8d8db258ac7", 10 | "Server" : "Google Frontend", 11 | "Date" : "Tue, 18 Dec 2018 13:47:50 GMT", 12 | "Content-Length" : "23", 13 | "Access-Control-Allow-Origin" : "*" 14 | } 15 | } 16 | }, 17 | "dataFilename" : "response.data" 18 | }, 19 | "requestStarted" : 566833670.01283097, 20 | "originalRequest" : { 21 | "httpMethod" : "GET", 22 | "httpShouldHandleCookies" : true, 23 | "allHTTPHeaderFields" : { 24 | 25 | }, 26 | "url" : "http:\/\/ip.jsontest.com\/", 27 | "cachePolicy" : 0, 28 | "allowsCellularAccess" : true, 29 | "httpShouldUsePipelining" : false, 30 | "timeoutInterval" : 60 31 | }, 32 | "identifier" : "AEA4F334-2185-41E5-B024-687FC2F7053F", 33 | "designatedRequest" : { 34 | "httpMethod" : "GET", 35 | "httpShouldHandleCookies" : true, 36 | "allHTTPHeaderFields" : { 37 | 38 | }, 39 | "url" : "http:\/\/ip.jsontest.com\/", 40 | "cachePolicy" : 0, 41 | "allowsCellularAccess" : true, 42 | "httpShouldUsePipelining" : false, 43 | "timeoutInterval" : 60 44 | } 45 | } -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/MockFloret/GET_http_ip_jsontest_com_/b014744502154b59cdd3ed4c2f21c8b6-anonymized/response.data: -------------------------------------------------------------------------------- 1 | {"ip": "77.11.38.000"} 2 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/MockFloret/default/404/record.json: -------------------------------------------------------------------------------- 1 | { 2 | "result" : { 3 | "urlResponseRepresentable" : { 4 | "httpURLResponse" : { 5 | "url" : "https:\/\/httpstat.us\/404", 6 | "statusCode" : 404, 7 | "headerFields" : { 8 | "Server" : "Microsoft-IIS\/10.0", 9 | "Set-Cookie" : "ARRAffinity=77c477e3e649643e5771873e1a13179fb00983bc73c71e196bf25967fd453df9;Path=\/;HttpOnly;Domain=httpstat.us", 10 | "Cache-Control" : "private", 11 | "X-Powered-By" : "ASP.NET", 12 | "X-AspNetMvc-Version" : "5.1", 13 | "Content-Type" : "text\/plain; charset=utf-8", 14 | "Content-Length" : "13", 15 | "Date" : "Tue, 18 Dec 2018 14:37:42 GMT", 16 | "X-AspNet-Version" : "4.0.30319", 17 | "Access-Control-Allow-Origin" : "*" 18 | } 19 | } 20 | }, 21 | "dataFilename" : "response.data" 22 | }, 23 | "requestStarted" : 566836661.78855896, 24 | "originalRequest" : { 25 | "httpMethod" : "GET", 26 | "httpShouldHandleCookies" : true, 27 | "allHTTPHeaderFields" : { 28 | 29 | }, 30 | "url" : "https:\/\/httpstat.us\/404", 31 | "cachePolicy" : 0, 32 | "allowsCellularAccess" : true, 33 | "httpShouldUsePipelining" : false, 34 | "timeoutInterval" : 60 35 | }, 36 | "identifier" : "B47A0B34-37DF-41BF-9B6B-A31075AAF237", 37 | "designatedRequest" : { 38 | "httpMethod" : "GET", 39 | "httpShouldHandleCookies" : true, 40 | "allHTTPHeaderFields" : { 41 | 42 | }, 43 | "url" : "https:\/\/httpstat.us\/404", 44 | "cachePolicy" : 0, 45 | "allowsCellularAccess" : true, 46 | "httpShouldUsePipelining" : false, 47 | "timeoutInterval" : 60 48 | } 49 | } -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/MockFloret/default/404/response.data: -------------------------------------------------------------------------------- 1 | 404 Not Found -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Sections/Requests/RequestModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestModel.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 17.11.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RequestModel { 12 | var name: String { get } 13 | var url: URL { get } 14 | func dataTask(in session: URLSession, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask 15 | } 16 | 17 | struct DownloadRequestModel: RequestModel { 18 | let name: String 19 | let url: URL 20 | 21 | func dataTask(in session: URLSession, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 22 | return session.dataTask(with: url, completionHandler: completionHandler) 23 | } 24 | } 25 | 26 | struct UploadRequestModel: RequestModel { 27 | let name: String 28 | let url: URL 29 | let data: Data 30 | 31 | func dataTask(in session: URLSession, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 32 | var urlRequest = URLRequest(url: url) 33 | urlRequest.httpMethod = "POST" 34 | return session.uploadTask(with: urlRequest, from: data, completionHandler: completionHandler) 35 | } 36 | } 37 | 38 | extension UploadRequestModel { 39 | init(name: String, url: URL, localFileName: String) { 40 | let fileUrl = Bundle.main.url(forResource: localFileName, withExtension: nil)! 41 | let data = try! Data(contentsOf: fileUrl) 42 | self.init(name: name, url: url, data: data) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Sections/Requests/RequestModelTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestModelTableViewCell.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 17.11.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RequestModelTableViewCell: UITableViewCell { 12 | 13 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 14 | super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | super.init(coder: aDecoder) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Sections/Requests/RequestsTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 06.08.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Cauliframework 11 | 12 | class RequestsTableViewController: UITableViewController { 13 | 14 | lazy var urlSession: URLSession = { 15 | return URLSession(configuration: .default, delegate: self, delegateQueue: nil) 16 | }() 17 | 18 | let requestModels: [RequestModel] = [ 19 | DownloadRequestModel(name: "httpstat.us/200", url: URL(string: "https://httpstat.us/200")!), 20 | DownloadRequestModel(name: "httpstat.us/301", url: URL(string: "https://httpstat.us/301")!), 21 | DownloadRequestModel(name: "httpstat.us/304", url: URL(string: "https://httpstat.us/304")!), 22 | DownloadRequestModel(name: "httpstat.us/404", url: URL(string: "https://httpstat.us/404")!), 23 | DownloadRequestModel(name: "api.ipify.org", url: URL(string: "http://api.ipify.org/?format=json")!), 24 | DownloadRequestModel(name: "invalidurl.invalid", url: URL(string: "https://invalidurl.invalid/")!), 25 | DownloadRequestModel(name: "HTTP Basic Auth", url: URL(string: "https://jigsaw.w3.org/HTTP/Basic/")!), 26 | UploadRequestModel(name: "Upload", url: URL(string: "https://httpbin.org/post")!, localFileName: "File.txt") 27 | ] 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | tableView.register(RequestModelTableViewCell.self, forCellReuseIdentifier: "RequestModelTableViewCell") 32 | } 33 | 34 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 35 | return requestModels.count 36 | } 37 | 38 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 39 | let cell = tableView.dequeueReusableCell(withIdentifier: "RequestModelTableViewCell", for: indexPath) 40 | let requestModel = requestModels[indexPath.row] 41 | 42 | cell.textLabel?.text = requestModel.name 43 | 44 | return cell 45 | } 46 | 47 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 48 | tableView.deselectRow(at: indexPath, animated: true) 49 | let requestModel = requestModels[indexPath.row] 50 | let cell = tableView.cellForRow(at: indexPath) 51 | cell?.detailTextLabel?.text = nil 52 | let activityIndicator = UIActivityIndicatorView(style: .gray) 53 | activityIndicator.isHidden = false 54 | activityIndicator.startAnimating() 55 | cell?.accessoryView = activityIndicator 56 | 57 | let dataTask = requestModel.dataTask(in: urlSession) { (data, response, error) in 58 | DispatchQueue.main.sync { 59 | if let httpUrlResponse = response as? HTTPURLResponse { 60 | let label = UILabel() 61 | label.text = "\(httpUrlResponse.statusCode)" 62 | label.sizeToFit() 63 | cell?.accessoryView = label 64 | if let data = data { 65 | cell?.detailTextLabel?.text = String(bytes: data, encoding: .utf8) 66 | } 67 | } else { 68 | cell?.accessoryView = nil 69 | } 70 | } 71 | } 72 | dataTask.resume() 73 | } 74 | 75 | } 76 | 77 | extension RequestsTableViewController: URLSessionTaskDelegate { 78 | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 79 | if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) { 80 | let credential = URLCredential(user: "guest", password: "guest", persistence: .forSession) 81 | completionHandler(.useCredential, credential) 82 | } else { 83 | completionHandler(.performDefaultHandling, nil) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/cauli-ios-example/cauli-ios-example/Sections/WebView/WebViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewController.swift 3 | // cauli-ios-example 4 | // 5 | // Created by Cornelius Horstmann on 17.11.18. 6 | // Copyright © 2018 brototyp.de. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import WebKit 11 | 12 | class WebViewController: UIViewController { 13 | private let webView: WKWebView = WKWebView() 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | view.addSubview(webView) 18 | webView.frame = view.bounds 19 | 20 | webView.load(URLRequest(url: URL(string: "https://cauli.works")!)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "jazzy" 4 | gem "danger" 5 | gem "danger-swiftlint" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.1.1) 7 | base64 8 | bigdecimal 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | connection_pool (>= 2.2.5) 11 | drb 12 | i18n (>= 1.6, < 2) 13 | minitest (>= 5.1) 14 | mutex_m 15 | tzinfo (~> 2.0) 16 | addressable (2.8.5) 17 | public_suffix (>= 2.0.2, < 6.0) 18 | algoliasearch (1.27.5) 19 | httpclient (~> 2.8, >= 2.8.3) 20 | json (>= 1.5.1) 21 | atomos (0.1.3) 22 | base64 (0.1.1) 23 | bigdecimal (3.1.4) 24 | claide (1.1.0) 25 | claide-plugins (0.9.2) 26 | cork 27 | nap 28 | open4 (~> 1.3) 29 | cocoapods (1.13.0) 30 | addressable (~> 2.8) 31 | claide (>= 1.0.2, < 2.0) 32 | cocoapods-core (= 1.13.0) 33 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 34 | cocoapods-downloader (>= 1.6.0, < 2.0) 35 | cocoapods-plugins (>= 1.0.0, < 2.0) 36 | cocoapods-search (>= 1.0.0, < 2.0) 37 | cocoapods-trunk (>= 1.6.0, < 2.0) 38 | cocoapods-try (>= 1.1.0, < 2.0) 39 | colored2 (~> 3.1) 40 | escape (~> 0.0.4) 41 | fourflusher (>= 2.3.0, < 3.0) 42 | gh_inspector (~> 1.0) 43 | molinillo (~> 0.8.0) 44 | nap (~> 1.0) 45 | ruby-macho (>= 2.3.0, < 3.0) 46 | xcodeproj (>= 1.23.0, < 2.0) 47 | cocoapods-core (1.13.0) 48 | activesupport (>= 5.0, < 8) 49 | addressable (~> 2.8) 50 | algoliasearch (~> 1.0) 51 | concurrent-ruby (~> 1.1) 52 | fuzzy_match (~> 2.0.4) 53 | nap (~> 1.0) 54 | netrc (~> 0.11) 55 | public_suffix (~> 4.0) 56 | typhoeus (~> 1.0) 57 | cocoapods-deintegrate (1.0.5) 58 | cocoapods-downloader (1.6.3) 59 | cocoapods-plugins (1.0.0) 60 | nap 61 | cocoapods-search (1.0.1) 62 | cocoapods-trunk (1.6.0) 63 | nap (>= 0.8, < 2.0) 64 | netrc (~> 0.11) 65 | cocoapods-try (1.2.0) 66 | colored2 (3.1.2) 67 | concurrent-ruby (1.2.2) 68 | connection_pool (2.4.1) 69 | cork (0.3.0) 70 | colored2 (~> 3.1) 71 | danger (9.3.2) 72 | claide (~> 1.0) 73 | claide-plugins (>= 0.9.2) 74 | colored2 (~> 3.1) 75 | cork (~> 0.1) 76 | faraday (>= 0.9.0, < 3.0) 77 | faraday-http-cache (~> 2.0) 78 | git (~> 1.13) 79 | kramdown (~> 2.3) 80 | kramdown-parser-gfm (~> 1.0) 81 | no_proxy_fix 82 | octokit (~> 6.0) 83 | terminal-table (>= 1, < 4) 84 | danger-swiftlint (0.33.0) 85 | danger 86 | rake (> 10) 87 | thor (~> 0.19) 88 | drb (2.1.1) 89 | ruby2_keywords 90 | escape (0.0.4) 91 | ethon (0.16.0) 92 | ffi (>= 1.15.0) 93 | faraday (2.7.11) 94 | base64 95 | faraday-net_http (>= 2.0, < 3.1) 96 | ruby2_keywords (>= 0.0.4) 97 | faraday-http-cache (2.5.0) 98 | faraday (>= 0.8) 99 | faraday-net_http (3.0.2) 100 | ffi (1.16.3) 101 | fourflusher (2.3.1) 102 | fuzzy_match (2.0.4) 103 | gh_inspector (1.1.3) 104 | git (1.18.0) 105 | addressable (~> 2.8) 106 | rchardet (~> 1.8) 107 | httpclient (2.8.3) 108 | i18n (1.14.1) 109 | concurrent-ruby (~> 1.0) 110 | jazzy (0.14.4) 111 | cocoapods (~> 1.5) 112 | mustache (~> 1.1) 113 | open4 (~> 1.3) 114 | redcarpet (~> 3.4) 115 | rexml (~> 3.2) 116 | rouge (>= 2.0.6, < 5.0) 117 | sassc (~> 2.1) 118 | sqlite3 (~> 1.3) 119 | xcinvoke (~> 0.3.0) 120 | json (2.6.3) 121 | kramdown (2.4.0) 122 | rexml 123 | kramdown-parser-gfm (1.1.0) 124 | kramdown (~> 2.0) 125 | liferaft (0.0.6) 126 | mini_portile2 (2.8.4) 127 | minitest (5.20.0) 128 | molinillo (0.8.0) 129 | mustache (1.1.1) 130 | mutex_m (0.1.2) 131 | nanaimo (0.3.0) 132 | nap (1.1.0) 133 | netrc (0.11.0) 134 | no_proxy_fix (0.1.2) 135 | octokit (6.1.1) 136 | faraday (>= 1, < 3) 137 | sawyer (~> 0.9) 138 | open4 (1.3.4) 139 | public_suffix (4.0.7) 140 | rake (13.0.6) 141 | rchardet (1.8.0) 142 | redcarpet (3.6.0) 143 | rexml (3.2.6) 144 | rouge (4.1.3) 145 | ruby-macho (2.5.1) 146 | ruby2_keywords (0.0.5) 147 | sassc (2.4.0) 148 | ffi (~> 1.9) 149 | sawyer (0.9.2) 150 | addressable (>= 2.3.5) 151 | faraday (>= 0.17.3, < 3) 152 | sqlite3 (1.6.7) 153 | mini_portile2 (~> 2.8.0) 154 | terminal-table (3.0.2) 155 | unicode-display_width (>= 1.1.1, < 3) 156 | thor (0.20.3) 157 | typhoeus (1.4.0) 158 | ethon (>= 0.9.0) 159 | tzinfo (2.0.6) 160 | concurrent-ruby (~> 1.0) 161 | unicode-display_width (2.5.0) 162 | xcinvoke (0.3.0) 163 | liferaft (~> 0.0.6) 164 | xcodeproj (1.23.0) 165 | CFPropertyList (>= 2.3.3, < 4.0) 166 | atomos (~> 0.1.3) 167 | claide (>= 1.0.2, < 2.0) 168 | colored2 (~> 3.1) 169 | nanaimo (~> 0.3.0) 170 | rexml (~> 3.2.4) 171 | 172 | PLATFORMS 173 | ruby 174 | 175 | DEPENDENCIES 176 | danger 177 | danger-swiftlint 178 | jazzy 179 | 180 | BUNDLED WITH 181 | 2.2.17 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Corenlius Horstmann and Pascal Stüdlein 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // 3 | // Cauliframework 4 | // 5 | 6 | import PackageDescription 7 | 8 | let package = Package( 9 | name: "Cauliframework", 10 | platforms: [ 11 | .iOS(.v11), 12 | ], 13 | products: [ 14 | .library( 15 | name: "Cauliframework", 16 | targets: ["Cauliframework"] 17 | ) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "Cauliframework", 22 | dependencies: [], 23 | path: "Cauli", 24 | resources: [ 25 | .process("Resources/*") 26 | ] 27 | ) 28 | ], 29 | swiftLanguageVersions: [.v5] 30 | ) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Cauli](https://cauli.works/logo.png) 2 | 3 | [![Tests](https://github.com/cauliframework/cauli/workflows/Tests/badge.svg)](https://github.com/cauliframework/cauli/actions) 4 | [![SPM Compatible](https://camo.githubusercontent.com/86f8561418bbd6240d5c39dbf80b83a3dc1e85e69fe58da808f0168194dcc0d3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5377696674504d2d436f6d70617469626c652d627269676874677265656e2e737667)](https://github.com/cauliframework/cauli/blob/develop/Package.swift) 5 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/cauliframework/cauli/blob/develop/LICENSE) 6 | [![Jazzy documentation](https://cauli.works/docs/badge.svg)](https://cauli.works/docs/) 7 | 8 | Cauli is a network debugging framework featuring a plugin infrastructure to hook into selected request and responses as well as recording and displaying performed requests. It provides a wide range of possibilities. For example from [inspecting](https://cauli.works/docs/florets.html#InspectorFloret) network traffic to [mock](https://cauli.works/docs/florets.html#MockFloret) UnitTests. Missing something fancy? How about [writing your own Plugin](https://cauli.works/docs/writing-your-own-plugin.html). 9 | 10 | ## Features 11 | 12 | 🌏 Hooks into the [URL Loading System](https://cauli.works/docs/frequently-asked-questions.html) 13 | 🧩 [Existing](https://cauli.works/docs/florets.html) set of Plugins (Florets) 14 | 🔧 [Extensible](https://cauli.works/docs/writing-your-own-plugin.html) Plugin Infrastructure 15 | 16 | ### Documentation 17 | 18 | * [Architecture](https://cauli.works/docs/architecture.html) 19 | * [Configuring Cauli](https://cauli.works/docs/configuring-cauli.html) 20 | * [Plugins / Florets](https://cauli.works/docs/florets.html) 21 | * [Writing Your Own Plugin / Floret](https://cauli.works/docs/writing-your-own-plugin.html) 22 | * [Frequently Asked Questions](https://cauli.works/docs/frequently-asked-questions.html) 23 | 24 | ## Getting Started 25 | 26 | ### Installation 27 | 28 | #### [Swift Package Manager](https://swift.org/package-manager/) 29 | 30 | The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. Once you have your Swift package set up, add the following to your `Package.swift` file. 31 | 32 | ```swift 33 | dependencies: [ 34 | .package(url: "https://github.com/cauliframework/cauli.git", from: "1.1") 35 | ] 36 | ``` 37 | 38 | ### Setup 39 | 40 | Add an `import Cauliframework` to your `AppDelegate` and call the `run` function on the shared instace in the `application(:, didFinishLaunchingWithOptions:)`. Make sure to call `run` before instantiating any `URLSession`. Otherwise Cauli can't intercept network requests and create any records. 41 | 42 | ```swift 43 | import Cauliframework 44 | 45 | public class AppDelegate: UIApplicationDelegate { 46 | public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 47 | Cauli.shared.run() 48 | // perform your usual application setup 49 | return true 50 | } 51 | } 52 | ``` 53 | 54 | This will configure Cauli to hook into every request, setup the core florets (plugins) ([InspectorFloret](https://cauli.works/docs/Classes/InspectorFloret.html)) and configures a shake gesture for the Cauli UI. 55 | 56 | ## Contributing 57 | Please read [CONTRIBUTING](CONTRIBUTING.md) for details. 58 | 59 | ## License 60 | Cauli is available under the [MIT license](LICENSE). 61 | -------------------------------------------------------------------------------- /docs/guides/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | Cauli will hook into the [URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system) by registering a custom [URLProtocol](https://developer.apple.com/documentation/foundation/urlprotocol) and swizzling the [URLSessionConfiguration.default](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411560-default) with one, where the [protocolClasses](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411050-protocolclasses) contain a custom [URLProtocol](https://developer.apple.com/documentation/foundation/urlprotocol). 3 | 4 | Therefore it's recommended to create your Cauli instance as soon as possible, preferred in the [`application:didFinishLaunchingWithOptions:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622921-application?language=objc) to ensure that all network requests can be intercepted. -------------------------------------------------------------------------------- /docs/guides/Configuring Cauli.md: -------------------------------------------------------------------------------- 1 | # Configuring Cauli 2 | 3 | To get started Cauli offers a shared instance already setup and ready to use but you can instantiate and configure your own. 4 | 5 | ## Instantiating Cauli 6 | 7 | Please make sure you create your Cauli instance as early as possible, especially before any request is started so Cauli can intercept every request. If you don't want to start Cauli at the beginning just don't call the `run` function. Check the [Architecture](Architecture) for more details. 8 | 9 | ```swift 10 | let configuration = Configuration( 11 | recordSelector: RecordSelector.max(bytesize: 5 * 1024 * 1024), 12 | enableShakeGesture: true, 13 | storageCapacity: .records(50)) 14 | let cauli = Cauli([ InspectorFloret() ], configuration: configuration) 15 | ``` 16 | 17 | Please check the [Docs](https://cauli.works/docs/Structs/Configuration.html) for all configuration options. 18 | 19 | ## Run / Pause Cauli 20 | 21 | ```swift 22 | cauli.run() 23 | cauli.pause() 24 | ``` 25 | 26 | ## Manually Display the Cauli UI 27 | 28 | If you don't want to use the shake gesture you can display the Cauli UI manually. 29 | 30 | ```swift 31 | // Disable the shake gesture using the Configuration 32 | let configuration = Configuration( 33 | recordSelector: RecordSelector.max(bytesize: 5 * 1024 * 1024), 34 | enableShakeGesture: false, 35 | storageCapacity: .records(50)) 36 | let cauli = Cauli([ InspectorFloret() ], configuration: configuration) 37 | 38 | // Create the Cauli ViewController to display it manually. 39 | // This ViewController expects to be displayed in a navigation stack, as it can 40 | // try to push other ViewControllers to the navigation stack. 41 | let cauliViewController = cauli.viewController() 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/guides/Florets.md: -------------------------------------------------------------------------------- 1 | # Florets 2 | 3 | ## InspectorFloret 4 | 5 | The [InspectorFloret](https://cauli.works/docs/Classes/InspectorFloret.html) lets you browse through and share your requests and responses from within the application. Just shake your device (if you are using the default configuration). 6 | 7 | ## MockFloret 8 | 9 | The [MockFloret](https://cauli.works/docs/Classes/MockFloret.html) helps you to easily mock your network requests for tests or to reproduce a bug. You can can use it to record the responses for later mocking and even is compatible to the Format used in the [InspectorFloret](https://cauli.works/docs/Classes/InspectorFloret.html). 10 | 11 | ## NoCacheFloret 12 | 13 | The [NoCacheFloret](https://cauli.works/docs/Classes/NoCacheFloret.html) modifies the designatedRequest and the response of a record to prevent returning cached data or writing data to the cache. 14 | 15 | ## FindReplaceFloret 16 | 17 | The [FindReplaceFloret](https://cauli.works/docs/Classes/FindReplaceFloret.html) can be used to easily replace any part of a request or response by using a [RecordModifier](https://cauli.works/docs/Classes/FindReplaceFloret/RecordModifier.html) 18 | 19 | ## MapRemoteFloret 20 | 21 | The [MapRemoteFloret](https://cauli.works/docs/Classes/MapRemoteFloret.html) can modify the url of a request before the request is performed. This is esp. helpful when using a staging or testing server. 22 | -------------------------------------------------------------------------------- /docs/guides/Frequently Asked Questions.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## Can I use Cauli in my production builds? 4 | Cauli is designed for developing purposes. Even if it doesn't use any private API we recommend against using it in a production environment. 5 | 6 | ## What kind of network traffic is considered? 7 | Cauli hooks into the [URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system). This means it only considers network traffic of `URLSessions` as well as traffic from [legacy URL Loading Systems](https://developer.apple.com/documentation/foundation/url_loading_system/legacy_url_loading_systems) such as `NSURLConnection` or `UIWebView`. 8 | Currently only the `URLSession.default` and `URLSessions` initialised with the `URLSessionConfiguration.default` are supported. 9 | 10 | ## Why are WKWebView and SFSafariViewController not supported? 11 | 12 | WKWebView runs out of process, meaning it is not using the URL loading system of your application and thus are not passing through the `URLProtocol`. In iOS 11 Apple added the [`setURLSchemeHandler:forURLScheme:`](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler?language=objc) on the [WKWebViewConfiguration](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration?language=objc) which can be used to intercept requests, but it is not possible to register a custom `WKURLSchemeHandler` for http or https schemes. There are [alternative solutions](https://github.com/wilddylan/WKWebViewWithURLProtocol/blob/master/WKWebViewWithURLProtocol/NSURLProtocol%2BWKWebViewSupport.m) which leverage private API, but Cauli should stay private API free. 13 | `SFSafariViewController` runs out of process as well and is sandboxed. There are no APIs to intercept requests. 14 | 15 | ## How can I modify Records before they are displayed in displaying florets, like the InspectorFloret? 16 | You may want to modify certain aspects of recorded requests or responses before they are displayed in the `Cauli` view controller. For example you have some confidential information in the request headers, like an API key or auth token that you don't want to expose. 17 | 18 | Displaying florets fetch their data from a `Storage` before it is displayed. This gives you the opportunity to modify the data (a `Record`) before it is stored. You can use a `RecordModifier` for this, that is passed to your `Cauli` `Configuration` on init: 19 | 20 | ```swift 21 | let recordModifier = RecordModifier(keyPath: \Record.designatedRequest) { designatedRequest -> (URLRequest) in 22 | let headerKey = "X-AUTH-NAME" 23 | var request = designatedRequest 24 | request.setValue(String(repeating: "*", count: (request.value(forHTTPHeaderField: headerKey) ?? "").count), forHTTPHeaderField: headerKey) 25 | return request 26 | } 27 | 28 | let configuration = Configuration( 29 | recordSelector: RecordSelector.max(bytesize: 5 * 1024 * 1024), 30 | enableShakeGesture: true, 31 | storageCapacity: .records(50), 32 | preStorageRecordModifier: recordModifier 33 | ) 34 | 35 | let cauli = Cauli([ InspectorFloret() ], configuration: configuration) 36 | ``` 37 | 38 | This will rewrite all request headers with the key "X-AUTH-NAME" to contain only asterisks, but only after the request was executed with the correct header. 39 | -------------------------------------------------------------------------------- /docs/guides/Writing Your Own Plugin.md: -------------------------------------------------------------------------------- 1 | # Writing your own Plugin 2 | 3 | You can write your own plugin if you want to process requests before they get send, responses after they got received or if you want to provide an additional UI. 4 | 5 | ## Before implementing your own Plugin 6 | 7 | Before implementing your own idea, you have to decide which Floret base protocol suits you best. You can also implement multiple. 8 | 9 | **[InterceptingFloret](https://cauli.works/docs/Protocols/InterceptingFloret.html):** This protocol let's you process requests and responses. 10 | **[DisplayingFloret](https://cauli.works/docs/Protocols/DisplayingFloret.html):** This protocol let's you create a custom UIViewController for your floret. 11 | **[Floret](https://cauli.works/docs/Protocols/Floret.html):** The Floret protocol is the base for the protocols described above. It should not be implemented directly, since there will be no effect. 12 | 13 | ## Accessing the storage 14 | 15 | Cauli manages the storage of `Record`s. Every `Record` selected by the [RecordSelector](https://cauli.works/docs/Structs/RecordSelector.html) is stored in memory. When displaying your [DisplayingFlorets](https://cauli.works/docs/Protocols/DisplayingFloret.html) ViewController you have access to the respective Cauli instance and thus to it's [Storage](https://cauli.works/docs/Protocols/Storage.html). 16 | --------------------------------------------------------------------------------