├── .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 | # 
2 |
3 | [](https://github.com/cauliframework/cauli/actions)
4 | [](https://github.com/cauliframework/cauli/blob/develop/Package.swift)
5 | [](https://github.com/cauliframework/cauli/blob/develop/LICENSE)
6 | [](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 |
--------------------------------------------------------------------------------