Repository: spring-media/Carlos Branch: master Commit: 26ca894e3cef Files: 164 Total size: 784.3 KB Directory structure: gitextract_a123yuin/ ├── .github/ │ └── workflows/ │ └── pr.yml ├── .gitignore ├── .gitmodules ├── .swiftformat ├── .swiftlint.yml ├── .swiftpm/ │ └── xcode/ │ ├── package.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── Carlos.xcscheme ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Carlos.podspec ├── Carlos.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── Carlos iOS.xcscheme │ └── Carlos watchOS.xcscheme ├── Carlos.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ ├── IDEWorkspaceChecks.plist │ └── swiftpm/ │ └── Package.resolved ├── Example/ │ ├── CarlosMacSample/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── CarlosTvSample/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ │ ├── App Icon - Large.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon - Small.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Top Shelf Image.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── LaunchImage.launchimage/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── CarlosWatchSample/ │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Interface.storyboard │ │ └── Info.plist │ ├── CarlosWatchSample Extension/ │ │ ├── Assets.xcassets/ │ │ │ ├── Contents.json │ │ │ └── placeholder.imageset/ │ │ │ └── Contents.json │ │ ├── CountryRow.swift │ │ ├── ExtensionDelegate.swift │ │ ├── Info.plist │ │ └── InterfaceController.swift │ ├── Example/ │ │ ├── AppDelegate.swift │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── BaseCacheViewController.swift │ │ ├── ComplexCacheSampleViewController.swift │ │ ├── ConditionedCacheSampleViewController.swift │ │ ├── DataCacheSampleViewController.swift │ │ ├── DelayedNetworkFetcher.swift │ │ ├── ExampleCell.swift │ │ ├── ExamplesListViewController.swift │ │ ├── ImageCacheSampleViewController.swift │ │ ├── Images.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── JSONCacheSampleViewController.swift │ │ ├── MemoryWarningSampleViewController.swift │ │ ├── PooledCacheSampleViewController.swift │ │ ├── SwitchCacheSampleViewController.swift │ │ └── UserDefaultsCacheSampleViewController.swift │ └── Example.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── Gemfile ├── LICENSE ├── MIGRATING.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources/ │ └── Carlos/ │ ├── CacheLevels/ │ │ ├── BatchAllCache.swift │ │ ├── Composed.swift │ │ ├── Conditioned.swift │ │ ├── DiskCacheLevel.swift │ │ ├── Fetcher.swift │ │ ├── MemoryCacheLevel.swift │ │ ├── NSUserDefaultsCacheLevel.swift │ │ ├── NetworkFetcher.swift │ │ └── PoolCache.swift │ ├── CacheProvider.swift │ ├── Carlos.swift │ ├── Core/ │ │ ├── BasicCache.swift │ │ ├── BasicFetcher.swift │ │ ├── Errors.swift │ │ ├── ExpensiveObject.swift │ │ ├── Extensions.swift │ │ ├── FetcherValueTransformation.swift │ │ ├── FunctionComposition.swift │ │ ├── Logger.swift │ │ ├── MemoryWarning.swift │ │ ├── StringConvertible.swift │ │ └── UnfairLock.swift │ ├── Info.plist │ ├── Operations/ │ │ ├── CacheLevel+Batch.swift │ │ ├── KeyTransformation.swift │ │ ├── Normalize.swift │ │ ├── PostProcess.swift │ │ ├── SwitchCache.swift │ │ └── ValueTransformation.swift │ └── Transformers/ │ ├── ComposedOneWayTransformer.swift │ ├── ComposedTwoWayTransformer.swift │ ├── ConditionedOneWayTransformer.swift │ ├── ConditionedOutputProcessing.swift │ ├── ConditionedTwoWayTransformer.swift │ ├── ConditionedValueTransformation.swift │ ├── ImageTransformer.swift │ ├── JSONTransformer.swift │ ├── OneWayTransformer.swift │ ├── StringTransformer.swift │ ├── Transformers.swift │ └── TwoWayTransformer.swift ├── Tests/ │ └── CarlosTests/ │ ├── BasicCacheTests.swift │ ├── BasicFetcherTests.swift │ ├── BatchTests.swift │ ├── CacheProviderTests.swift │ ├── CompositionTests.swift │ ├── ConditionedCacheTests.swift │ ├── ConditionedOneWayTransformationBoxTests.swift │ ├── ConditionedOutputProcessingTests.swift │ ├── ConditionedTransformersTests.swift │ ├── ConditionedTwoWayTransformationBoxTests.swift │ ├── ConditionedValueTransformationTests.swift │ ├── DiskCacheTests.swift │ ├── Fakes/ │ │ ├── Base64EncodedImage.swift │ │ ├── CacheLevelFake.swift │ │ └── FetcherFake.swift │ ├── FetcherValueTransformationTests.swift │ ├── ImageTransformerTests.swift │ ├── Info.plist │ ├── JSONTransformerTests.swift │ ├── KeyTransformationTests.swift │ ├── MKDistanceFormatterTransformerTests.swift │ ├── MemoryCacheLevelTests.swift │ ├── MemoryWarningNotificationTests.swift │ ├── NSDateFormatterTransformerTests.swift │ ├── NSNumberFormatterTransformerTests.swift │ ├── NSUserDefaultsCacheLevelTests.swift │ ├── NetworkFetcherTests.swift │ ├── NormalizationTests.swift │ ├── OneWayTransformationBoxTests.swift │ ├── OneWayTransformerCompositionTests.swift │ ├── PoolCacheTests.swift │ ├── PostProcessTests.swift │ ├── StringConvertibleTests.swift │ ├── StringTransformerTests.swift │ ├── SwitchCacheTests.swift │ ├── TwoWayTransformationBoxTests.swift │ ├── TwoWayTransformerCompositionTests.swift │ └── ValueTransformationTests.swift └── fastlane/ ├── Fastfile ├── README.md └── Scanfile ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/pr.yml ================================================ name: PR on: pull_request: branches: - master jobs: pr: runs-on: macos-11 steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.6 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Prepare run: bundle install - name: Test run: bundle exec fastlane test ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## macOS .DS_Store ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ .bundle/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/ # fastlane # # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the # screenshots whenever they are needed. # For more information about the recommended setup visit: # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md fastlane/report.xml fastlane/screenshots ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .swiftformat ================================================ # file options --exclude Carthage,Build,.build,.swiftpm,SourcePackages --symlinks ignore --swiftversion 5.3 # format options --allman false --binarygrouping 4,8 --commas inline --comments indent --decimalgrouping 3,6 --elseposition same-line --empty void --exponentcase lowercase --exponentgrouping disabled --fractiongrouping disabled --guardelse same-line --header ignore --hexgrouping 4,8 --hexliteralcase uppercase --ifdef indent --indent 2 --indentcase false --importgrouping testable-bottom --linebreaks lf --maxwidth none --octalgrouping 4,8 --patternlet hoist --operatorfunc spaced --nospaceoperators ..<, ... --self remove --semicolons inline --stripunusedargs always --trimwhitespace always --wraparguments before-first --wrapcollections before-first ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: - redundant_string_enum_value - line_length - trailing_whitespace - identifier_name - opening_brace opt_in_rules: - closure_spacing - conditional_returns_on_newline - empty_count - explicit_init - fatal_error_message - first_where - force_unwrapping - overridden_super_call - prohibited_super_call - redundant_nil_coalescing - operator_usage_whitespace - array_init - contains_over_first_not_nil - joined_default_parameter - line_length - literal_expression_end_indentation - multiline_parameters - override_in_extension - pattern_matching_keywords - redundant_string_enum_value - sorted_first_last - unneeded_parentheses_in_closure_argument - vertical_parameter_alignment_on_call - nimble_operator - quick_discouraged_call - quick_discouraged_focused_test - quick_discouraged_pending_test - single_test_class # TODO: maybe turn it on one day... (a lot of fixes) # - closure_end_indentation # - let_var_whitespace # - implicitly_unwrapped_optional # - private_outlet # - object_literal # - sorted_imports # - strict_fileprivate # - switch_case_on_newline included: - Sources - Tests - Example excluded: - SourcePackages - .build - Build - .swiftpm - Carthage cyclomatic_complexity: warning: 25 type_body_length: warning: 300 error: 500 file_length: warning: 500 error: 800 type_name: min_length: 3 max_length: warning: 50 error: 60 excluded: - Id - Ad nesting: type_level: warning: 3 error: 5 superfluous_disable_command: warning force_unwrapping: error trailing_semicolon: error reporter: "xcode" ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/Carlos.xcscheme ================================================ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 1.0.0 **Breaking Changes** - Swift 5.3 and Xcode 12 support - The codebase has been migrated from PiedPiper to Combine - The minimum supported OS version set to Combine's minimum supported version: iOS 13, macOS 10.15, watchOS 6, tvOS 13. - Removed `Dispatched.swift` and `RequestCapperCache.swift` because the functionality they provided could be easily re-implemented using Combine operators. **New Features** - `Carlos` is now powered by Combine which means you can use awesome Combine provided operators on the Carlos cached values! ## 0.10.0 **New Features** - Swift Package Manager Support - Xcode 11.5 Migration ## 0.9 **Breaking Changes** - Swift 3.0 support (for Swift 2.3 use specific commit `5d354c829d766568f164c386c59de21357b5ccff` instead) - `batchGetAll` has been removed and replaced with a reified `allBatch` (see **New features**) - All deprecated functions have been removed - All custom operators have been removed in favor of their function counterparts - macOS and tvOS support has been temporarily dropped and will be probably re-added in the future - `set` method on `CacheLevel` now returns a `Future` enabling error-handling and progress-tracking of `set` calls. **New Features** - It's now possible to lift a `CacheLevel` into one that operates on a sequence of keys and returns a sequence of values. You can use `allBatch` to create a concrete `BatchAllCache`. You can use `get` on this cache if you want to pass a list of keys and get the success callback when **all** of them succeed and the failure callback **as soon as one** of them fails (old behavior of `batchGetAll`), or you can compose or transform an `allBatch` cache just like any another `CacheLevel`. Consult the `README.md` for an example. ## 0.8 **Breaking Changes** - The codebase has been migrated to Swift 2.2 - `Promise` now has only an empty `init`. If you used one of the convenience `init` (with `value:`, with `error:` or with `value:error:`), they now moved to `Future`. ## 0.7 **Breaking Changes** - `onCompletion` argument now is a closure accepting a `Result` as a parameter instead of a tuple `(value: T?, error: ErrorType?)`. `Result` is the usual `enum` (aka `Either`) that can be `.Success(T)`, `.Error(ErrorType)` or `Cancelled` in case of canceled computations. - Please add a `import PiedPiper` line everywhere you make use of Carlos' `Future`s or `Promise`s, since with 0.7 we now ship a separate `Pied Piper` framework. - `AsyncComputation` has been removed from the public API. Please use `OneWayTransformer` (or `CacheLevel`) instead now. **Deprecated** - APIs using closures instead of `Fetcher`, `CacheLevel` or `OneWayTransformer` parameters are now deprecated in favor of their counterparts. They will be removed from Carlos with the 1.0 release. **New Features** - It's now possible to batch a set of fetch requests. You can use `batchGetAll` if you want to pass a list of keys and get the success callback when **all** of them succeed and the failure callback **as soon as one** of them fails, or `batchGetSome` if you want to pass a list of keys and get the success callback when all of them completed (successfully or not) but only get the list of successful responses back. **Fixes** - Correctly updates access date on the disk cache when calling `set` on a `DiskCacheLevel` ## 0.6 **New features** - It's now possible to conditionally post-process values fetched from `CacheLevel`s (or fetch closures) on the key used to fetch the value. Use the function `conditionedPostProcess` or consult the `README.md` for more information - It's now possible to conditionally transform values fetched from (or set on) `CacheLevel`s on the key used to fetch (or set) the value. Use the function `conditionedValueTransformation` or consult the `README.md` for more information **Fixes** - `Carthage` integration works again **Minor improvements** - `CacheProvider` now has accessors to retrieve shared instances of the built-in caches (`sharedImageCache`, `sharedDataCache` and `sharedJSONCache`) ## 0.5 **New features** - `Promise` can now be canceled. Call `cancel()` to cancel a `Promise`. Be notified of a canceled operation with the `onCancel` function. Use `onCancel` to setup the cancel behavior of your custom operation. Remember that an operation can only be canceled once, and can only be *executing*, *canceled*, *failed* or *succeeded* at any given time. - It's now possible to apply a condition to a `OneWayTransformer`. You can call `conditioned` on the instance of `OneWayTransformer` to decorate and pass the condition on the input. This means you can effectively implement conditioned key transformations on `CacheLevel`s. Moreover, you can implement conditioned post processing transformations as well. For this, though, keep in mind that the input of the `OneWayTransformer` will be the output of the cache, not the key. - It's now possible to apply a condition to a `TwoWayTransformer`. You can call `conditioned` on the instance of `TwoWayTransformer` to decorate and pass two conditions: the one to apply for the forward transformation and the one to apply for the inverse transformation, that will take of course different input types. This means you can effectively implement conditioned value transformations on `CacheLevel`s. - A new `NSUserDefaultsCacheLevel` is now included in `Carlos`. You can use this `CacheLevel` to persist values on `NSUserDefaults`, and you can even use multiple instances of this level to persist sandboxed sets of values - It's now possible to dispatch a `CacheLevel` or a fetch closure on a given GCD queue. Use the `dispatch` protocol extension or the `~>>` operator and pass the specific `dispatch_queue_t`. Global functions are not provided since we're moving towards a global-functions-free API for `Carlos 1.0` **Major changes** - **API Breaking**: `CacheRequest` is now renamed to `Future`. All the public API return `Future` instances now, and you can use `Promise` for your custom cache levels and fetchers - **API Breaking**: `OneWayTransformer` and `TwoWayTransformer` are now asynchronous, i.e. they return a `Future` instead of a `T` directly - **API Breaking**: all the `conditioned` variants now take an asynchronous condition closure, i.e. the closure has to return a `Future` instead of a `(Bool, ErrorType)` tuple - All the global functions are now **deprecated**. They will be removed from the public API with the release of `Carlos 1.0` **Minor improvements** - `Promise` can now be initialized with an `Optional` and an `ErrorType`, correctly behaving depending on the optional value - `Promise` now has a `mimic` function that takes a `Future` and succeeds or fails when the given `Future` does so - `ImageTransformer` now applies its tranformations on a background queue - `JSONTransformer` now passes the right error when the transformations fail - `CacheProvider.dataCache` now pools requests on the network **and** disk levels, so pooled requests don't result in multiple `set` calls on the disk level - It's now possible to `cancel` operations coming from a `NetworkFetcher` - `Int`, `Float`, `Double` and `Character` conform to `ExpensiveObject` now with a unit (`1`) cost - Added a `MIGRATING.md` to the repo and to the Wiki that explains how to migrate to new versions of `Carlos` (only for breaking changes) ## 0.4 **Major changes** - Adds a `Fetcher` protocol that you can use to create your custom fetchers. - Adds the possibility to transform values coming out of `Fetcher` instances through `OneWayTransformer` objects without forcing them to be `TwoWayTransformer` as in the case of transforming values of `CacheLevel` instances - Adds a `JSONCache` function to `CacheProvider` - Adds output processers to process/sanitize values coming out of `CacheLevel`s (see `postProcess`) - Adds a way to compose multiple `OneWayTransformer`s through functions, operators and protocol extensions - Adds a way to compose multiple `TwoWayTransformer`s through functions, operators and protocol extensions - Adds a `normalize` function and protocol extension transforming `CacheLevel` instances into `BasicCache` ones to make it easier to store instance properties - Adds a `JSONTransformer` class conforming to `TwoWayTransformer` - Adds a `ImageTransformer` class for the iOS and WatchOS 2 frameworks conforming to `TwoWayTransformer` - Adds a `StringTransformer` class conforming to `TwoWayTransformer` **Minor improvements** - `invert` is now available as a protocol extension to the `TwoWayTransformer` protocol **WatchOS 2** - Adds `WatchOS 2` support through `CocoaPods` **tvOS** - Adds framework support for `tvOS` ## 0.3 **Major changes** - Codebase converted to `Swift 2.0` - Adds `WatchOS 2` support - Adds `Mac OS X 10.9+` support **API-Breaking changes** - `CacheRequest.onFailure` now passes an `ErrorType` instead of an `NSError` **Minor improvements** - Adds an `onCompletion` method to the `CacheRequest` class, that will be called in both success and failure cases ## 0.2 **Major changes** - Includes a `CacheProvider` class to create commonly used caches - Includes a Playground to quickly test Carlos and custom cache architectures - includes a new `switchLevels` function to have multiple cache lanes **Minor improvements** - Improves `DiskCacheLevel` and `MemoryCacheLevel` by having protocol-based keys - Defines safer Transformers (either `OneWayTransformer` or `TwoWayTransformer`) that return Optionals. If a conversion fails, set operations silently fail and get operations fail with a meaningful error. - Extends the `conditioned` function and the `` operator to support fetch closures - Improves the code documentation **Bugfixes** - Fixes an issue where the `NetworkFetcher` would not correctly handle multiple get requests for the same URL ## 0.1 - First release ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Carlos If you want to contribute to this repo, please: - Create an issue explaining your problem and your solution - Clone the repo on your local machine - Create a branch with the issue number and a short abstract of the feature name - Implement your solution - Write tests (untested features won't be merged) - When all the tests are written and green, create a pull request, with a short description of the approach taken ================================================ FILE: Carlos.podspec ================================================ # # Be sure to run `pod lib lint Carlos.podspec' to ensure this is a # valid spec and remove all comments before submitting the spec. # Pod::Spec.new do |s| s.name = "Carlos" s.version = "1.0.0" s.summary = "A simple but flexible cache." s.description = <<-DESC Carlos is a small set of classes convenience operators to realize custom, flexible and powerful cache layers in your iOS, watchOS, tvOS and Mac OS X applications. DESC s.homepage = "https://github.com/spring-media/Carlos" s.license = 'MIT' s.author = { "Vittorio Monaco" => "vittorio.monaco1@gmail.com" } s.source = { :git => "https://github.com/spring-media/Carlos.git", :tag => s.version.to_s } s.swift_version = '5.0' s.ios.deployment_target = '13.0' s.watchos.deployment_target = '6.0' s.source_files = 'Sources/Carlos/*.swift' s.watchos.exclude_files = 'Sources/Carlos/MemoryWarning.swift' s.requires_arc = true end ================================================ FILE: Carlos.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 5205729B1BB8716E0098DD09 /* StringTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205729A1BB8716E0098DD09 /* StringTransformer.swift */; }; 520572A91BB8752B0098DD09 /* StringTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520572A81BB8752B0098DD09 /* StringTransformerTests.swift */; }; 5207E6991BE13D2E0062DEAC /* ExpensiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */; }; 5207E6AD1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */; }; 520DBB4F1CF6307500F9ABE3 /* BatchAllCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */; }; 5222107C1C501C5400E682D1 /* ConditionedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */; }; 5222108B1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */; }; 522210901C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108F1C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift */; }; 523257D21B51993000A10A56 /* TwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D11B51993000A10A56 /* TwoWayTransformationBoxTests.swift */; }; 523257D41B519AB900A10A56 /* OneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D31B519AB900A10A56 /* OneWayTransformationBoxTests.swift */; }; 5235BAD71BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5235BAD61BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift */; }; 524019BC1C0AF9ED00749957 /* CacheProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524019BB1C0AF9ED00749957 /* CacheProviderTests.swift */; }; 5243C4B31B77670F00737B3B /* StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4B21B77670F00737B3B /* StringConvertible.swift */; }; 5243C4BF1B77686300737B3B /* StringConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4BE1B77686300737B3B /* StringConvertibleTests.swift */; }; 52463F6E1C181D1B0034D032 /* ConditionedOutputProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */; }; 52463F7D1C18264B0034D032 /* ConditionedOutputProcessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7C1C18264B0034D032 /* ConditionedOutputProcessingTests.swift */; }; 52463F7F1C1837CF0034D032 /* ConditionedValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */; }; 5250E3801BB1345700EB7388 /* Normalize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E37F1BB1345700EB7388 /* Normalize.swift */; }; 5250E3841BB137BF00EB7388 /* NormalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E3831BB137BF00EB7388 /* NormalizationTests.swift */; }; 5257BBB41BBFC3FD00787819 /* ComposedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */; }; 5257BBC21BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBC11BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift */; }; 526005AF1BB84AB200D96242 /* ImageTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */; }; 526078251BE2590E0022040B /* ConditionedTransformersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526078241BE2590E0022040B /* ConditionedTransformersTests.swift */; }; 52615CD41BBC8123001DA9BE /* ComposedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */; }; 52615CE21BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CE11BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift */; }; 526339751B52435C00074CB9 /* KeyTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339741B52435C00074CB9 /* KeyTransformation.swift */; }; 526339771B52450C00074CB9 /* ConditionedCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339761B52450C00074CB9 /* ConditionedCacheTests.swift */; }; 5263397B1B52536700074CB9 /* KeyTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397A1B52536700074CB9 /* KeyTransformationTests.swift */; }; 5263397D1B525D1600074CB9 /* MemoryCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397C1B525D1600074CB9 /* MemoryCacheLevelTests.swift */; }; 5263397F1B52760F00074CB9 /* ValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397E1B52760F00074CB9 /* ValueTransformationTests.swift */; }; 526339811B52805000074CB9 /* MemoryWarningNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339801B52805000074CB9 /* MemoryWarningNotificationTests.swift */; }; 526339831B52866900074CB9 /* BasicCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339821B52866900074CB9 /* BasicCacheTests.swift */; }; 526339851B52A82800074CB9 /* CompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339841B52A82800074CB9 /* CompositionTests.swift */; }; 526339891B52B51C00074CB9 /* DiskCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339881B52B51C00074CB9 /* DiskCacheTests.swift */; }; 52635A4F1B4F0F6F00F187EE /* BasicCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */; }; 52635A511B4F0F6F00F187EE /* Carlos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3E1B4F0F6F00F187EE /* Carlos.swift */; }; 52635A521B4F0F6F00F187EE /* Composed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3F1B4F0F6F00F187EE /* Composed.swift */; }; 52635A531B4F0F6F00F187EE /* Conditioned.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A401B4F0F6F00F187EE /* Conditioned.swift */; }; 52635A551B4F0F6F00F187EE /* DiskCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */; }; 52635A561B4F0F6F00F187EE /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A431B4F0F6F00F187EE /* Errors.swift */; }; 52635A571B4F0F6F00F187EE /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A441B4F0F6F00F187EE /* Extensions.swift */; }; 52635A581B4F0F6F00F187EE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A451B4F0F6F00F187EE /* Logger.swift */; }; 52635A591B4F0F6F00F187EE /* MemoryCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */; }; 52635A5A1B4F0F6F00F187EE /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */; }; 52635A5B1B4F0F6F00F187EE /* NetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */; }; 52635A5C1B4F0F6F00F187EE /* OneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */; }; 52635A5D1B4F0F6F00F187EE /* PoolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */; }; 52635A5F1B4F0F6F00F187EE /* ValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */; }; 52635A601B4F0F6F00F187EE /* Transformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4D1B4F0F6F00F187EE /* Transformers.swift */; }; 52635A611B4F0F6F00F187EE /* TwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */; }; 5274A99B1BC05CB0006962E8 /* FetcherValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */; }; 5274A9A01BC0613C006962E8 /* BasicFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */; }; 5274A9CD1BC06976006962E8 /* BasicFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9CC1BC06976006962E8 /* BasicFetcherTests.swift */; }; 5274A9D91BC069E7006962E8 /* FetcherValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9D81BC069E7006962E8 /* FetcherValueTransformationTests.swift */; }; 527C65471B7A66D90005023B /* SwitchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65461B7A66D90005023B /* SwitchCache.swift */; }; 527C65531B7A89440005023B /* SwitchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65521B7A89440005023B /* SwitchCacheTests.swift */; }; 5291C8531BB7FAA800C4E15E /* JSONTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */; }; 52B7621C1BB84F160087CD91 /* JSONTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */; }; 52BBFD561B51BA140000084A /* CacheLevelFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD551B51BA140000084A /* CacheLevelFake.swift */; }; 52BBFD581B51BAD20000084A /* PoolCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD571B51BAD20000084A /* PoolCacheTests.swift */; }; 52BED3501CE87C38002C045A /* Carlos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52635A1D1B4F0F3D00F187EE /* Carlos.framework */; }; 52D5F3481C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3471C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift */; }; 52D5F3541C52C32000BA3452 /* ConditionedValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3531C52C32000BA3452 /* ConditionedValueTransformationTests.swift */; }; 52D7DDAD1C54F4E1007E5328 /* CacheLevel+Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */; }; 52D7DDBC1C552854007E5328 /* BatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDBB1C552854007E5328 /* BatchTests.swift */; }; 52E0CBF91BBC4E1800F20C22 /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; }; 52E197021BBC6B27004BF6C5 /* PostProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */; }; 52F0644F1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */; }; 52F064511B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064501B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift */; }; 52F064551B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064541B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift */; }; AD22FB2D1B70BBF6007F319F /* CacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */; }; C71C59751B6A722800AE5294 /* NetworkFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71C59731B6A721C00AE5294 /* NetworkFetcherTests.swift */; }; CC0313E124ACBBC8002AB2BE /* Base64EncodedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0313DF24ACBBC8002AB2BE /* Base64EncodedImage.swift */; }; CC0313E724ACCF94002AB2BE /* Transformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4D1B4F0F6F00F187EE /* Transformers.swift */; }; CC0313E824ACCF94002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */; }; CC0313E924ACCF94002AB2BE /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A441B4F0F6F00F187EE /* Extensions.swift */; }; CC0313EA24ACCF94002AB2BE /* ConditionedValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */; }; CC0313EB24ACCF94002AB2BE /* NetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */; }; CC0313EC24ACCF94002AB2BE /* SwitchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65461B7A66D90005023B /* SwitchCache.swift */; }; CC0313ED24ACCF94002AB2BE /* MemoryCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */; }; CC0313EE24ACCF94002AB2BE /* Carlos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3E1B4F0F6F00F187EE /* Carlos.swift */; }; CC0313EF24ACCF94002AB2BE /* CacheLevel+Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */; }; CC0313F024ACCF94002AB2BE /* BasicCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */; }; CC0313F224ACCF94002AB2BE /* ImageTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */; }; CC0313F324ACCF94002AB2BE /* ValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */; }; CC0313F424ACCF94002AB2BE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A451B4F0F6F00F187EE /* Logger.swift */; }; CC0313F524ACCF94002AB2BE /* ComposedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */; }; CC0313F624ACCF94002AB2BE /* KeyTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339741B52435C00074CB9 /* KeyTransformation.swift */; }; CC0313F724ACCF94002AB2BE /* CacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */; }; CC0313F824ACCF94002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */; }; CC0313F924ACCF94002AB2BE /* BatchAllCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */; }; CC0313FC24ACCF94002AB2BE /* ComposedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */; }; CC0313FD24ACCF94002AB2BE /* StringTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205729A1BB8716E0098DD09 /* StringTransformer.swift */; }; CC0313FE24ACCF94002AB2BE /* FetcherValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */; }; CC0313FF24ACCF94002AB2BE /* PoolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */; }; CC03140024ACCF94002AB2BE /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; }; CC03140224ACCF94002AB2BE /* OneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */; }; CC03140324ACCF94002AB2BE /* ConditionedOutputProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */; }; CC03140424ACCF94002AB2BE /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A431B4F0F6F00F187EE /* Errors.swift */; }; CC03140524ACCF94002AB2BE /* ExpensiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */; }; CC03140624ACCF94002AB2BE /* TwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */; }; CC03140724ACCF94002AB2BE /* CacheProvider+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */; }; CC03140824ACCF94002AB2BE /* DiskCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */; }; CC03140924ACCF94002AB2BE /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */; }; CC03140A24ACCF94002AB2BE /* Composed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3F1B4F0F6F00F187EE /* Composed.swift */; }; CC03140B24ACCF94002AB2BE /* Conditioned.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A401B4F0F6F00F187EE /* Conditioned.swift */; }; CC03140C24ACCF94002AB2BE /* JSONTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */; }; CC03140D24ACCF94002AB2BE /* StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4B21B77670F00737B3B /* StringConvertible.swift */; }; CC03140E24ACCF94002AB2BE /* BasicFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */; }; CC03140F24ACCF94002AB2BE /* Normalize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E37F1BB1345700EB7388 /* Normalize.swift */; }; CC03141024ACCF94002AB2BE /* ConditionedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */; }; CC03142024ACCF99002AB2BE /* ImageTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */; }; CC03142124ACCF99002AB2BE /* MemoryWarningNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339801B52805000074CB9 /* MemoryWarningNotificationTests.swift */; }; CC03142224ACCF99002AB2BE /* StringConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4BE1B77686300737B3B /* StringConvertibleTests.swift */; }; CC03142324ACCF99002AB2BE /* DiskCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339881B52B51C00074CB9 /* DiskCacheTests.swift */; }; CC03142424ACCF99002AB2BE /* ConditionedTwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3471C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift */; }; CC03142524ACCF99002AB2BE /* MKDistanceFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064541B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift */; }; CC03142624ACCF99002AB2BE /* StringTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520572A81BB8752B0098DD09 /* StringTransformerTests.swift */; }; CC03142724ACCF99002AB2BE /* ConditionedOutputProcessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7C1C18264B0034D032 /* ConditionedOutputProcessingTests.swift */; }; CC03142924ACCF99002AB2BE /* OneWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CE11BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift */; }; CC03142A24ACCF99002AB2BE /* JSONTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */; }; CC03142B24ACCF99002AB2BE /* OneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D31B519AB900A10A56 /* OneWayTransformationBoxTests.swift */; }; CC03142C24ACCF99002AB2BE /* ConditionedOneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108F1C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift */; }; CC03142D24ACCF99002AB2BE /* SwitchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65521B7A89440005023B /* SwitchCacheTests.swift */; }; CC03142F24ACCF99002AB2BE /* CacheLevelFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD551B51BA140000084A /* CacheLevelFake.swift */; }; CC03143124ACCF99002AB2BE /* BatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDBB1C552854007E5328 /* BatchTests.swift */; }; CC03143224ACCF99002AB2BE /* NSDateFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */; }; CC03143324ACCF99002AB2BE /* FetcherValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9D81BC069E7006962E8 /* FetcherValueTransformationTests.swift */; }; CC03143424ACCF99002AB2BE /* Base64EncodedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0313DF24ACBBC8002AB2BE /* Base64EncodedImage.swift */; }; CC03143524ACCF99002AB2BE /* BasicCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339821B52866900074CB9 /* BasicCacheTests.swift */; }; CC03143624ACCF99002AB2BE /* ValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397E1B52760F00074CB9 /* ValueTransformationTests.swift */; }; CC03143724ACCF99002AB2BE /* TwoWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBC11BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift */; }; CC03143824ACCF99002AB2BE /* CacheProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524019BB1C0AF9ED00749957 /* CacheProviderTests.swift */; }; CC03143924ACCF99002AB2BE /* MemoryCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397C1B525D1600074CB9 /* MemoryCacheLevelTests.swift */; }; CC03143A24ACCF99002AB2BE /* KeyTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397A1B52536700074CB9 /* KeyTransformationTests.swift */; }; CC03143B24ACCF99002AB2BE /* ConditionedTransformersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526078241BE2590E0022040B /* ConditionedTransformersTests.swift */; }; CC03143C24ACCF99002AB2BE /* CompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339841B52A82800074CB9 /* CompositionTests.swift */; }; CC03143D24ACCF99002AB2BE /* PoolCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD571B51BAD20000084A /* PoolCacheTests.swift */; }; CC03143E24ACCF99002AB2BE /* ConditionedCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339761B52450C00074CB9 /* ConditionedCacheTests.swift */; }; CC03143F24ACCF99002AB2BE /* NSNumberFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064501B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift */; }; CC03144024ACCF99002AB2BE /* NetworkFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71C59731B6A721C00AE5294 /* NetworkFetcherTests.swift */; }; CC03144124ACCF99002AB2BE /* BasicFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9CC1BC06976006962E8 /* BasicFetcherTests.swift */; }; CC03144224ACCF99002AB2BE /* ConditionedValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3531C52C32000BA3452 /* ConditionedValueTransformationTests.swift */; }; CC03144324ACCF99002AB2BE /* PostProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */; }; CC03144424ACCF99002AB2BE /* NSUserDefaultsCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5235BAD61BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift */; }; CC03144524ACCF99002AB2BE /* NormalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E3831BB137BF00EB7388 /* NormalizationTests.swift */; }; CC03144624ACCF99002AB2BE /* TwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D11B51993000A10A56 /* TwoWayTransformationBoxTests.swift */; }; CC03145924ACCFA0002AB2BE /* Transformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4D1B4F0F6F00F187EE /* Transformers.swift */; }; CC03145A24ACCFA0002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */; }; CC03145B24ACCFA0002AB2BE /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A441B4F0F6F00F187EE /* Extensions.swift */; }; CC03145C24ACCFA0002AB2BE /* ConditionedValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */; }; CC03145D24ACCFA0002AB2BE /* NetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */; }; CC03145E24ACCFA0002AB2BE /* SwitchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65461B7A66D90005023B /* SwitchCache.swift */; }; CC03145F24ACCFA0002AB2BE /* MemoryCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */; }; CC03146024ACCFA0002AB2BE /* Carlos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3E1B4F0F6F00F187EE /* Carlos.swift */; }; CC03146124ACCFA0002AB2BE /* CacheLevel+Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */; }; CC03146224ACCFA0002AB2BE /* BasicCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */; }; CC03146424ACCFA0002AB2BE /* ImageTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */; }; CC03146524ACCFA0002AB2BE /* ValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */; }; CC03146624ACCFA0002AB2BE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A451B4F0F6F00F187EE /* Logger.swift */; }; CC03146724ACCFA0002AB2BE /* ComposedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */; }; CC03146824ACCFA0002AB2BE /* KeyTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339741B52435C00074CB9 /* KeyTransformation.swift */; }; CC03146924ACCFA0002AB2BE /* CacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */; }; CC03146A24ACCFA0002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */; }; CC03146B24ACCFA0002AB2BE /* BatchAllCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */; }; CC03146E24ACCFA0002AB2BE /* ComposedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */; }; CC03146F24ACCFA0002AB2BE /* StringTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205729A1BB8716E0098DD09 /* StringTransformer.swift */; }; CC03147024ACCFA0002AB2BE /* FetcherValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */; }; CC03147124ACCFA0002AB2BE /* PoolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */; }; CC03147224ACCFA0002AB2BE /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; }; CC03147424ACCFA0002AB2BE /* OneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */; }; CC03147524ACCFA0002AB2BE /* ConditionedOutputProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */; }; CC03147624ACCFA0002AB2BE /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A431B4F0F6F00F187EE /* Errors.swift */; }; CC03147724ACCFA0002AB2BE /* ExpensiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */; }; CC03147824ACCFA0002AB2BE /* TwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */; }; CC03147924ACCFA0002AB2BE /* CacheProvider+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */; }; CC03147A24ACCFA0002AB2BE /* DiskCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */; }; CC03147B24ACCFA0002AB2BE /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */; }; CC03147C24ACCFA0002AB2BE /* Composed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3F1B4F0F6F00F187EE /* Composed.swift */; }; CC03147D24ACCFA0002AB2BE /* Conditioned.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A401B4F0F6F00F187EE /* Conditioned.swift */; }; CC03147E24ACCFA0002AB2BE /* JSONTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */; }; CC03147F24ACCFA0002AB2BE /* StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4B21B77670F00737B3B /* StringConvertible.swift */; }; CC03148024ACCFA0002AB2BE /* BasicFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */; }; CC03148124ACCFA0002AB2BE /* Normalize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E37F1BB1345700EB7388 /* Normalize.swift */; }; CC03148224ACCFA0002AB2BE /* ConditionedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */; }; CC0314C924ACCFAC002AB2BE /* ImageTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */; }; CC0314CA24ACCFAC002AB2BE /* MemoryWarningNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339801B52805000074CB9 /* MemoryWarningNotificationTests.swift */; }; CC0314CB24ACCFAC002AB2BE /* StringConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4BE1B77686300737B3B /* StringConvertibleTests.swift */; }; CC0314CC24ACCFAC002AB2BE /* DiskCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339881B52B51C00074CB9 /* DiskCacheTests.swift */; }; CC0314CD24ACCFAC002AB2BE /* ConditionedTwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3471C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift */; }; CC0314CE24ACCFAC002AB2BE /* MKDistanceFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064541B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift */; }; CC0314CF24ACCFAC002AB2BE /* StringTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520572A81BB8752B0098DD09 /* StringTransformerTests.swift */; }; CC0314D024ACCFAC002AB2BE /* ConditionedOutputProcessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7C1C18264B0034D032 /* ConditionedOutputProcessingTests.swift */; }; CC0314D224ACCFAC002AB2BE /* OneWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CE11BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift */; }; CC0314D324ACCFAC002AB2BE /* JSONTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */; }; CC0314D424ACCFAC002AB2BE /* OneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D31B519AB900A10A56 /* OneWayTransformationBoxTests.swift */; }; CC0314D524ACCFAC002AB2BE /* ConditionedOneWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108F1C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift */; }; CC0314D624ACCFAC002AB2BE /* SwitchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65521B7A89440005023B /* SwitchCacheTests.swift */; }; CC0314D824ACCFAC002AB2BE /* CacheLevelFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD551B51BA140000084A /* CacheLevelFake.swift */; }; CC0314DA24ACCFAC002AB2BE /* BatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDBB1C552854007E5328 /* BatchTests.swift */; }; CC0314DB24ACCFAC002AB2BE /* NSDateFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */; }; CC0314DC24ACCFAC002AB2BE /* FetcherValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9D81BC069E7006962E8 /* FetcherValueTransformationTests.swift */; }; CC0314DD24ACCFAC002AB2BE /* Base64EncodedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0313DF24ACBBC8002AB2BE /* Base64EncodedImage.swift */; }; CC0314DE24ACCFAC002AB2BE /* BasicCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339821B52866900074CB9 /* BasicCacheTests.swift */; }; CC0314DF24ACCFAC002AB2BE /* ValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397E1B52760F00074CB9 /* ValueTransformationTests.swift */; }; CC0314E024ACCFAC002AB2BE /* TwoWayTransformerCompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBC11BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift */; }; CC0314E124ACCFAC002AB2BE /* CacheProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524019BB1C0AF9ED00749957 /* CacheProviderTests.swift */; }; CC0314E224ACCFAC002AB2BE /* MemoryCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397C1B525D1600074CB9 /* MemoryCacheLevelTests.swift */; }; CC0314E324ACCFAC002AB2BE /* KeyTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263397A1B52536700074CB9 /* KeyTransformationTests.swift */; }; CC0314E424ACCFAC002AB2BE /* ConditionedTransformersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526078241BE2590E0022040B /* ConditionedTransformersTests.swift */; }; CC0314E524ACCFAC002AB2BE /* CompositionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339841B52A82800074CB9 /* CompositionTests.swift */; }; CC0314E624ACCFAC002AB2BE /* PoolCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BBFD571B51BAD20000084A /* PoolCacheTests.swift */; }; CC0314E724ACCFAC002AB2BE /* ConditionedCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339761B52450C00074CB9 /* ConditionedCacheTests.swift */; }; CC0314E824ACCFAC002AB2BE /* NSNumberFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F064501B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift */; }; CC0314E924ACCFAC002AB2BE /* NetworkFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71C59731B6A721C00AE5294 /* NetworkFetcherTests.swift */; }; CC0314EA24ACCFAC002AB2BE /* BasicFetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A9CC1BC06976006962E8 /* BasicFetcherTests.swift */; }; CC0314EB24ACCFAC002AB2BE /* ConditionedValueTransformationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D5F3531C52C32000BA3452 /* ConditionedValueTransformationTests.swift */; }; CC0314EC24ACCFAC002AB2BE /* PostProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */; }; CC0314ED24ACCFAC002AB2BE /* NSUserDefaultsCacheLevelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5235BAD61BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift */; }; CC0314EE24ACCFAC002AB2BE /* NormalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E3831BB137BF00EB7388 /* NormalizationTests.swift */; }; CC0314EF24ACCFAC002AB2BE /* TwoWayTransformationBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523257D11B51993000A10A56 /* TwoWayTransformationBoxTests.swift */; }; CC03150224ACCFB1002AB2BE /* Transformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4D1B4F0F6F00F187EE /* Transformers.swift */; }; CC03150324ACCFB1002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */; }; CC03150424ACCFB1002AB2BE /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A441B4F0F6F00F187EE /* Extensions.swift */; }; CC03150524ACCFB1002AB2BE /* ConditionedValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */; }; CC03150624ACCFB1002AB2BE /* NetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */; }; CC03150724ACCFB1002AB2BE /* SwitchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527C65461B7A66D90005023B /* SwitchCache.swift */; }; CC03150824ACCFB1002AB2BE /* MemoryCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */; }; CC03150924ACCFB1002AB2BE /* Carlos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3E1B4F0F6F00F187EE /* Carlos.swift */; }; CC03150A24ACCFB1002AB2BE /* CacheLevel+Batch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */; }; CC03150B24ACCFB1002AB2BE /* BasicCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */; }; CC03150D24ACCFB1002AB2BE /* ImageTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */; }; CC03150E24ACCFB1002AB2BE /* ValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */; }; CC03150F24ACCFB1002AB2BE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A451B4F0F6F00F187EE /* Logger.swift */; }; CC03151024ACCFB1002AB2BE /* ComposedTwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */; }; CC03151124ACCFB1002AB2BE /* KeyTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526339741B52435C00074CB9 /* KeyTransformation.swift */; }; CC03151224ACCFB1002AB2BE /* CacheProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */; }; CC03151324ACCFB1002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */; }; CC03151424ACCFB1002AB2BE /* BatchAllCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */; }; CC03151724ACCFB1002AB2BE /* ComposedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */; }; CC03151824ACCFB1002AB2BE /* StringTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205729A1BB8716E0098DD09 /* StringTransformer.swift */; }; CC03151924ACCFB1002AB2BE /* FetcherValueTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */; }; CC03151A24ACCFB1002AB2BE /* PoolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */; }; CC03151B24ACCFB1002AB2BE /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; }; CC03151D24ACCFB1002AB2BE /* OneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */; }; CC03151E24ACCFB1002AB2BE /* ConditionedOutputProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */; }; CC03151F24ACCFB1002AB2BE /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A431B4F0F6F00F187EE /* Errors.swift */; }; CC03152024ACCFB1002AB2BE /* ExpensiveObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */; }; CC03152124ACCFB1002AB2BE /* TwoWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */; }; CC03152224ACCFB1002AB2BE /* CacheProvider+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */; }; CC03152324ACCFB1002AB2BE /* DiskCacheLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */; }; CC03152424ACCFB1002AB2BE /* MemoryWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */; }; CC03152524ACCFB1002AB2BE /* Composed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A3F1B4F0F6F00F187EE /* Composed.swift */; }; CC03152624ACCFB1002AB2BE /* Conditioned.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52635A401B4F0F6F00F187EE /* Conditioned.swift */; }; CC03152724ACCFB1002AB2BE /* JSONTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */; }; CC03152824ACCFB1002AB2BE /* StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5243C4B21B77670F00737B3B /* StringConvertible.swift */; }; CC03152924ACCFB1002AB2BE /* BasicFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */; }; CC03152A24ACCFB1002AB2BE /* Normalize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5250E37F1BB1345700EB7388 /* Normalize.swift */; }; CC03152B24ACCFB1002AB2BE /* ConditionedOneWayTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */; }; CC5C369224EC164D004AD171 /* FunctionComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369024EC164C004AD171 /* FunctionComposition.swift */; }; CC5C369324EC164D004AD171 /* FunctionComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369024EC164C004AD171 /* FunctionComposition.swift */; }; CC5C369424EC164D004AD171 /* FunctionComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369024EC164C004AD171 /* FunctionComposition.swift */; }; CC5C369524EC164D004AD171 /* FunctionComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369024EC164C004AD171 /* FunctionComposition.swift */; }; CC5C369624EC164D004AD171 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369124EC164D004AD171 /* UnfairLock.swift */; }; CC5C369724EC164D004AD171 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369124EC164D004AD171 /* UnfairLock.swift */; }; CC5C369824EC164D004AD171 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369124EC164D004AD171 /* UnfairLock.swift */; }; CC5C369924EC164D004AD171 /* UnfairLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C369124EC164D004AD171 /* UnfairLock.swift */; }; CC5C36A124EC16BB004AD171 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36A024EC16BB004AD171 /* Quick */; }; CC5C36A324EC16BF004AD171 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36A224EC16BF004AD171 /* Nimble */; }; CC5C36A524EC16C6004AD171 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36A424EC16C6004AD171 /* Quick */; }; CC5C36A724EC16C9004AD171 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36A624EC16C9004AD171 /* Nimble */; }; CC5C36AF24EC16D3004AD171 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36AE24EC16D3004AD171 /* Quick */; }; CC5C36B124EC16D3004AD171 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = CC5C36B024EC16D3004AD171 /* Nimble */; }; CC5C36BD24EC1725004AD171 /* FetcherFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C36B824EC1717004AD171 /* FetcherFake.swift */; }; CC5C36BE24EC1726004AD171 /* FetcherFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C36B824EC1717004AD171 /* FetcherFake.swift */; }; CC5C36BF24EC1727004AD171 /* FetcherFake.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5C36B824EC1717004AD171 /* FetcherFake.swift */; }; CCC7061E24AC93EA00D16827 /* CacheProvider+ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */; }; CCC7062224AC93EA00D16827 /* ImageTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */; }; CCC88F7624ADD4F2008C4060 /* Carlos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC03141924ACCF94002AB2BE /* Carlos.framework */; }; CCC88F8424ADD56C008C4060 /* Carlos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC03148B24ACCFA0002AB2BE /* Carlos.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 52635A6D1B4F110500F187EE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD695CD71B46CD65004E998D /* Project object */; proxyType = 1; remoteGlobalIDString = 52635A1C1B4F0F3D00F187EE; remoteInfo = Carlos; }; CC0315A824ACCFFD002AB2BE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD695CD71B46CD65004E998D /* Project object */; proxyType = 1; remoteGlobalIDString = CC0313E424ACCF94002AB2BE; remoteInfo = "Carlos macOS"; }; CC0315B624ACD0A1002AB2BE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD695CD71B46CD65004E998D /* Project object */; proxyType = 1; remoteGlobalIDString = CC03145624ACCFA0002AB2BE; remoteInfo = "Carlos tvOS"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 52BED3511CE87C3C002C045A /* Copy Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Copy Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; CC03144D24ACCF99002AB2BE /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC0314F624ACCFAC002AB2BE /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CCC88F7524ADD4E0008C4060 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; CCC88F8324ADD564008C4060 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; CCC88F9124ADD5A1008C4060 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 5205729A1BB8716E0098DD09 /* StringTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTransformer.swift; sourceTree = ""; }; 520572A81BB8752B0098DD09 /* StringTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTransformerTests.swift; sourceTree = ""; }; 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpensiveObject.swift; sourceTree = ""; }; 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsCacheLevel.swift; sourceTree = ""; }; 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchAllCache.swift; sourceTree = ""; }; 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedOneWayTransformer.swift; sourceTree = ""; }; 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedTwoWayTransformer.swift; sourceTree = ""; }; 5222108F1C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedOneWayTransformationBoxTests.swift; sourceTree = ""; }; 523257D11B51993000A10A56 /* TwoWayTransformationBoxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoWayTransformationBoxTests.swift; sourceTree = ""; }; 523257D31B519AB900A10A56 /* OneWayTransformationBoxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneWayTransformationBoxTests.swift; sourceTree = ""; }; 5235BAD61BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaultsCacheLevelTests.swift; sourceTree = ""; }; 524019BB1C0AF9ED00749957 /* CacheProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheProviderTests.swift; sourceTree = ""; }; 5243C4B21B77670F00737B3B /* StringConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringConvertible.swift; sourceTree = ""; }; 5243C4BE1B77686300737B3B /* StringConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringConvertibleTests.swift; sourceTree = ""; }; 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedOutputProcessing.swift; sourceTree = ""; }; 52463F7C1C18264B0034D032 /* ConditionedOutputProcessingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedOutputProcessingTests.swift; sourceTree = ""; }; 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedValueTransformation.swift; sourceTree = ""; }; 5250E37F1BB1345700EB7388 /* Normalize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Normalize.swift; sourceTree = ""; }; 5250E3831BB137BF00EB7388 /* NormalizationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalizationTests.swift; sourceTree = ""; }; 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposedTwoWayTransformer.swift; sourceTree = ""; }; 5257BBC11BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoWayTransformerCompositionTests.swift; sourceTree = ""; }; 526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTransformerTests.swift; sourceTree = ""; }; 526005BA1BB84BBC00D96242 /* swift-og.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "swift-og.png"; sourceTree = ""; }; 526078241BE2590E0022040B /* ConditionedTransformersTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedTransformersTests.swift; sourceTree = ""; }; 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposedOneWayTransformer.swift; sourceTree = ""; }; 52615CE11BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneWayTransformerCompositionTests.swift; sourceTree = ""; }; 526339741B52435C00074CB9 /* KeyTransformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyTransformation.swift; sourceTree = ""; }; 526339761B52450C00074CB9 /* ConditionedCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedCacheTests.swift; sourceTree = ""; }; 5263397A1B52536700074CB9 /* KeyTransformationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyTransformationTests.swift; sourceTree = ""; }; 5263397C1B525D1600074CB9 /* MemoryCacheLevelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryCacheLevelTests.swift; sourceTree = ""; }; 5263397E1B52760F00074CB9 /* ValueTransformationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTransformationTests.swift; sourceTree = ""; }; 526339801B52805000074CB9 /* MemoryWarningNotificationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryWarningNotificationTests.swift; sourceTree = ""; }; 526339821B52866900074CB9 /* BasicCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicCacheTests.swift; sourceTree = ""; }; 526339841B52A82800074CB9 /* CompositionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositionTests.swift; sourceTree = ""; }; 526339881B52B51C00074CB9 /* DiskCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskCacheTests.swift; sourceTree = ""; }; 52635A1D1B4F0F3D00F187EE /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52635A201B4F0F3D00F187EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicCache.swift; sourceTree = ""; }; 52635A3E1B4F0F6F00F187EE /* Carlos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Carlos.swift; sourceTree = ""; }; 52635A3F1B4F0F6F00F187EE /* Composed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Composed.swift; sourceTree = ""; }; 52635A401B4F0F6F00F187EE /* Conditioned.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Conditioned.swift; sourceTree = ""; }; 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskCacheLevel.swift; sourceTree = ""; }; 52635A431B4F0F6F00F187EE /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 52635A441B4F0F6F00F187EE /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 52635A451B4F0F6F00F187EE /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryCacheLevel.swift; sourceTree = ""; }; 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryWarning.swift; sourceTree = ""; }; 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkFetcher.swift; sourceTree = ""; }; 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OneWayTransformer.swift; sourceTree = ""; }; 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolCache.swift; sourceTree = ""; }; 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueTransformation.swift; sourceTree = ""; }; 52635A4D1B4F0F6F00F187EE /* Transformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transformers.swift; sourceTree = ""; }; 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoWayTransformer.swift; sourceTree = ""; }; 52635A661B4F110500F187EE /* Carlos iOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Carlos iOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52635A691B4F110500F187EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetcherValueTransformation.swift; sourceTree = ""; }; 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicFetcher.swift; sourceTree = ""; }; 5274A9CC1BC06976006962E8 /* BasicFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicFetcherTests.swift; sourceTree = ""; }; 5274A9D81BC069E7006962E8 /* FetcherValueTransformationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetcherValueTransformationTests.swift; sourceTree = ""; }; 527C65461B7A66D90005023B /* SwitchCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCache.swift; sourceTree = ""; }; 527C65521B7A89440005023B /* SwitchCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCacheTests.swift; sourceTree = ""; }; 5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONTransformerTests.swift; sourceTree = ""; }; 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONTransformer.swift; sourceTree = ""; }; 52BBFD551B51BA140000084A /* CacheLevelFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheLevelFake.swift; sourceTree = ""; }; 52BBFD571B51BAD20000084A /* PoolCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolCacheTests.swift; sourceTree = ""; }; 52D5F3471C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedTwoWayTransformationBoxTests.swift; sourceTree = ""; }; 52D5F3531C52C32000BA3452 /* ConditionedValueTransformationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedValueTransformationTests.swift; sourceTree = ""; }; 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CacheLevel+Batch.swift"; sourceTree = ""; }; 52D7DDBB1C552854007E5328 /* BatchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchTests.swift; sourceTree = ""; }; 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostProcess.swift; sourceTree = ""; }; 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostProcessTests.swift; sourceTree = ""; }; 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateFormatterTransformerTests.swift; sourceTree = ""; }; 52F064501B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSNumberFormatterTransformerTests.swift; sourceTree = ""; }; 52F064541B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKDistanceFormatterTransformerTests.swift; sourceTree = ""; }; AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheProvider.swift; sourceTree = ""; }; C71C59731B6A721C00AE5294 /* NetworkFetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkFetcherTests.swift; sourceTree = ""; }; CC0313DF24ACBBC8002AB2BE /* Base64EncodedImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Base64EncodedImage.swift; sourceTree = ""; }; CC0313E024ACBBC8002AB2BE /* CacheLevelFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheLevelFake.swift; sourceTree = ""; }; CC03141924ACCF94002AB2BE /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC03145424ACCF99002AB2BE /* Carlos macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Carlos macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; CC03148B24ACCFA0002AB2BE /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC0314FD24ACCFAC002AB2BE /* Carlos tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Carlos tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; CC03153424ACCFB1002AB2BE /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC5C369024EC164C004AD171 /* FunctionComposition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionComposition.swift; sourceTree = ""; }; CC5C369124EC164D004AD171 /* UnfairLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnfairLock.swift; sourceTree = ""; }; CC5C36B824EC1717004AD171 /* FetcherFake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetcherFake.swift; sourceTree = ""; }; CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CacheProvider+ImageCache.swift"; sourceTree = ""; }; CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTransformer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 52635A191B4F0F3D00F187EE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 52635A631B4F110500F187EE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC5C36A324EC16BF004AD171 /* Nimble in Frameworks */, CC5C36A124EC16BB004AD171 /* Quick in Frameworks */, 52BED3501CE87C38002C045A /* Carlos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03141124ACCF94002AB2BE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03144724ACCF99002AB2BE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC5C36A724EC16C9004AD171 /* Nimble in Frameworks */, CC5C36A524EC16C6004AD171 /* Quick in Frameworks */, CCC88F7624ADD4F2008C4060 /* Carlos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03148324ACCFA0002AB2BE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC0314F024ACCFAC002AB2BE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC5C36B124EC16D3004AD171 /* Nimble in Frameworks */, CC5C36AF24EC16D3004AD171 /* Quick in Frameworks */, CCC88F8424ADD56C008C4060 /* Carlos.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03152C24ACCFB1002AB2BE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2677A0397774353D4B14F63F /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 52635A1E1B4F0F3D00F187EE /* Carlos */ = { isa = PBXGroup; children = ( CCC7061C24AC93EA00D16827 /* CacheProvider+ImageCache.swift */, CCC7061D24AC93EA00D16827 /* ImageTransformer.swift */, 520DBB4E1CF6307500F9ABE3 /* BatchAllCache.swift */, 52635A1F1B4F0F3D00F187EE /* Supporting Files */, 52635A3C1B4F0F6F00F187EE /* BasicCache.swift */, 52635A3E1B4F0F6F00F187EE /* Carlos.swift */, 52635A3F1B4F0F6F00F187EE /* Composed.swift */, 52635A401B4F0F6F00F187EE /* Conditioned.swift */, 52635A421B4F0F6F00F187EE /* DiskCacheLevel.swift */, 52635A431B4F0F6F00F187EE /* Errors.swift */, 52635A441B4F0F6F00F187EE /* Extensions.swift */, 52635A451B4F0F6F00F187EE /* Logger.swift */, 52635A461B4F0F6F00F187EE /* MemoryCacheLevel.swift */, 52635A471B4F0F6F00F187EE /* MemoryWarning.swift */, 52635A481B4F0F6F00F187EE /* NetworkFetcher.swift */, 52635A491B4F0F6F00F187EE /* OneWayTransformer.swift */, 52635A4A1B4F0F6F00F187EE /* PoolCache.swift */, 52635A4C1B4F0F6F00F187EE /* ValueTransformation.swift */, 5274A99A1BC05CB0006962E8 /* FetcherValueTransformation.swift */, CC5C369024EC164C004AD171 /* FunctionComposition.swift */, CC5C369124EC164D004AD171 /* UnfairLock.swift */, 52635A4D1B4F0F6F00F187EE /* Transformers.swift */, 52635A4E1B4F0F6F00F187EE /* TwoWayTransformer.swift */, 526339741B52435C00074CB9 /* KeyTransformation.swift */, AD22FB2C1B70BBF5007F319F /* CacheProvider.swift */, 5243C4B21B77670F00737B3B /* StringConvertible.swift */, 527C65461B7A66D90005023B /* SwitchCache.swift */, 5250E37F1BB1345700EB7388 /* Normalize.swift */, 52B7621B1BB84F160087CD91 /* JSONTransformer.swift */, 5205729A1BB8716E0098DD09 /* StringTransformer.swift */, 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */, 52615CD31BBC8123001DA9BE /* ComposedOneWayTransformer.swift */, 5257BBB31BBFC3FD00787819 /* ComposedTwoWayTransformer.swift */, 5274A99F1BC0613C006962E8 /* BasicFetcher.swift */, 5207E6981BE13D2E0062DEAC /* ExpensiveObject.swift */, 5207E6AC1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift */, 52463F6D1C181D1B0034D032 /* ConditionedOutputProcessing.swift */, 52463F7E1C1837CF0034D032 /* ConditionedValueTransformation.swift */, 5222107B1C501C5400E682D1 /* ConditionedOneWayTransformer.swift */, 5222108A1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift */, 52D7DDAC1C54F4E1007E5328 /* CacheLevel+Batch.swift */, ); name = Carlos; path = Sources/Carlos; sourceTree = SOURCE_ROOT; }; 52635A1F1B4F0F3D00F187EE /* Supporting Files */ = { isa = PBXGroup; children = ( 52635A201B4F0F3D00F187EE /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 52635A671B4F110500F187EE /* CarlosTests */ = { isa = PBXGroup; children = ( CC0313DE24ACBBC8002AB2BE /* Fakes */, 52635A681B4F110500F187EE /* Supporting Files */, 523257D11B51993000A10A56 /* TwoWayTransformationBoxTests.swift */, 523257D31B519AB900A10A56 /* OneWayTransformationBoxTests.swift */, 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */, 52F064501B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift */, 52F064541B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift */, C71C59731B6A721C00AE5294 /* NetworkFetcherTests.swift */, 52BBFD571B51BAD20000084A /* PoolCacheTests.swift */, 526339761B52450C00074CB9 /* ConditionedCacheTests.swift */, 5263397A1B52536700074CB9 /* KeyTransformationTests.swift */, 5263397C1B525D1600074CB9 /* MemoryCacheLevelTests.swift */, 5263397E1B52760F00074CB9 /* ValueTransformationTests.swift */, 526339801B52805000074CB9 /* MemoryWarningNotificationTests.swift */, 526339821B52866900074CB9 /* BasicCacheTests.swift */, 526339841B52A82800074CB9 /* CompositionTests.swift */, 526339881B52B51C00074CB9 /* DiskCacheTests.swift */, 5243C4BE1B77686300737B3B /* StringConvertibleTests.swift */, 527C65521B7A89440005023B /* SwitchCacheTests.swift */, 5250E3831BB137BF00EB7388 /* NormalizationTests.swift */, 5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */, 526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */, 520572A81BB8752B0098DD09 /* StringTransformerTests.swift */, 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */, 52615CE11BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift */, 5257BBC11BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift */, 5274A9CC1BC06976006962E8 /* BasicFetcherTests.swift */, 5274A9D81BC069E7006962E8 /* FetcherValueTransformationTests.swift */, 5235BAD61BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift */, 526078241BE2590E0022040B /* ConditionedTransformersTests.swift */, 524019BB1C0AF9ED00749957 /* CacheProviderTests.swift */, 52463F7C1C18264B0034D032 /* ConditionedOutputProcessingTests.swift */, 5222108F1C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift */, 52D5F3471C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift */, 52D5F3531C52C32000BA3452 /* ConditionedValueTransformationTests.swift */, 52D7DDBB1C552854007E5328 /* BatchTests.swift */, ); name = CarlosTests; path = Tests/CarlosTests; sourceTree = SOURCE_ROOT; }; 52635A681B4F110500F187EE /* Supporting Files */ = { isa = PBXGroup; children = ( 526005BA1BB84BBC00D96242 /* swift-og.png */, 52BBFD4A1B51BA070000084A /* Fakes */, 52635A691B4F110500F187EE /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 52BBFD4A1B51BA070000084A /* Fakes */ = { isa = PBXGroup; children = ( 52BBFD551B51BA140000084A /* CacheLevelFake.swift */, ); path = Fakes; sourceTree = ""; }; AD695CD61B46CD65004E998D = { isa = PBXGroup; children = ( 52635A1E1B4F0F3D00F187EE /* Carlos */, 52635A671B4F110500F187EE /* CarlosTests */, AD695CE01B46CD65004E998D /* Products */, 2677A0397774353D4B14F63F /* Frameworks */, ); sourceTree = ""; }; AD695CE01B46CD65004E998D /* Products */ = { isa = PBXGroup; children = ( 52635A1D1B4F0F3D00F187EE /* Carlos.framework */, 52635A661B4F110500F187EE /* Carlos iOS Tests.xctest */, CC03141924ACCF94002AB2BE /* Carlos.framework */, CC03145424ACCF99002AB2BE /* Carlos macOS Tests.xctest */, CC03148B24ACCFA0002AB2BE /* Carlos.framework */, CC0314FD24ACCFAC002AB2BE /* Carlos tvOS Tests.xctest */, CC03153424ACCFB1002AB2BE /* Carlos.framework */, ); name = Products; sourceTree = ""; }; CC0313DE24ACBBC8002AB2BE /* Fakes */ = { isa = PBXGroup; children = ( CC5C36B824EC1717004AD171 /* FetcherFake.swift */, CC0313DF24ACBBC8002AB2BE /* Base64EncodedImage.swift */, CC0313E024ACBBC8002AB2BE /* CacheLevelFake.swift */, ); path = Fakes; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 52635A1A1B4F0F3D00F187EE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03141324ACCF94002AB2BE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03148524ACCFA0002AB2BE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03152E24ACCFB1002AB2BE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 52635A1C1B4F0F3D00F187EE /* Carlos iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 52635A361B4F0F3E00F187EE /* Build configuration list for PBXNativeTarget "Carlos iOS" */; buildPhases = ( 52635A181B4F0F3D00F187EE /* Sources */, 52635A191B4F0F3D00F187EE /* Frameworks */, 52635A1A1B4F0F3D00F187EE /* Headers */, 52635A1B1B4F0F3D00F187EE /* Resources */, CCC88F7524ADD4E0008C4060 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Carlos iOS"; packageProductDependencies = ( ); productName = Carlos; productReference = 52635A1D1B4F0F3D00F187EE /* Carlos.framework */; productType = "com.apple.product-type.framework"; }; 52635A651B4F110500F187EE /* Carlos iOS Tests */ = { isa = PBXNativeTarget; buildConfigurationList = 52635A6F1B4F110500F187EE /* Build configuration list for PBXNativeTarget "Carlos iOS Tests" */; buildPhases = ( 52635A621B4F110500F187EE /* Sources */, 52635A631B4F110500F187EE /* Frameworks */, 52635A641B4F110500F187EE /* Resources */, 52BED3511CE87C3C002C045A /* Copy Frameworks */, ); buildRules = ( ); dependencies = ( 52635A6E1B4F110500F187EE /* PBXTargetDependency */, ); name = "Carlos iOS Tests"; packageProductDependencies = ( CC5C36A024EC16BB004AD171 /* Quick */, CC5C36A224EC16BF004AD171 /* Nimble */, ); productName = CarlosTests; productReference = 52635A661B4F110500F187EE /* Carlos iOS Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; CC0313E424ACCF94002AB2BE /* Carlos macOS */ = { isa = PBXNativeTarget; buildConfigurationList = CC03141624ACCF94002AB2BE /* Build configuration list for PBXNativeTarget "Carlos macOS" */; buildPhases = ( CC0313E524ACCF94002AB2BE /* Sources */, CC03141124ACCF94002AB2BE /* Frameworks */, CC03141324ACCF94002AB2BE /* Headers */, CC03141524ACCF94002AB2BE /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Carlos macOS"; packageProductDependencies = ( ); productName = Carlos; productReference = CC03141924ACCF94002AB2BE /* Carlos.framework */; productType = "com.apple.product-type.framework"; }; CC03141B24ACCF99002AB2BE /* Carlos macOS Tests */ = { isa = PBXNativeTarget; buildConfigurationList = CC03145124ACCF99002AB2BE /* Build configuration list for PBXNativeTarget "Carlos macOS Tests" */; buildPhases = ( CC03141E24ACCF99002AB2BE /* Sources */, CC03144724ACCF99002AB2BE /* Frameworks */, CC03144B24ACCF99002AB2BE /* Resources */, CC03144D24ACCF99002AB2BE /* CopyFiles */, ); buildRules = ( ); dependencies = ( CC0315A924ACCFFD002AB2BE /* PBXTargetDependency */, ); name = "Carlos macOS Tests"; packageProductDependencies = ( CC5C36A424EC16C6004AD171 /* Quick */, CC5C36A624EC16C9004AD171 /* Nimble */, ); productName = CarlosTests; productReference = CC03145424ACCF99002AB2BE /* Carlos macOS Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; CC03145624ACCFA0002AB2BE /* Carlos tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = CC03148824ACCFA0002AB2BE /* Build configuration list for PBXNativeTarget "Carlos tvOS" */; buildPhases = ( CC03145724ACCFA0002AB2BE /* Sources */, CC03148324ACCFA0002AB2BE /* Frameworks */, CC03148524ACCFA0002AB2BE /* Headers */, CC03148724ACCFA0002AB2BE /* Resources */, CCC88F8324ADD564008C4060 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Carlos tvOS"; packageProductDependencies = ( ); productName = Carlos; productReference = CC03148B24ACCFA0002AB2BE /* Carlos.framework */; productType = "com.apple.product-type.framework"; }; CC0314C424ACCFAC002AB2BE /* Carlos tvOS Tests */ = { isa = PBXNativeTarget; buildConfigurationList = CC0314FA24ACCFAC002AB2BE /* Build configuration list for PBXNativeTarget "Carlos tvOS Tests" */; buildPhases = ( CC0314C724ACCFAC002AB2BE /* Sources */, CC0314F024ACCFAC002AB2BE /* Frameworks */, CC0314F424ACCFAC002AB2BE /* Resources */, CC0314F624ACCFAC002AB2BE /* CopyFiles */, ); buildRules = ( ); dependencies = ( CC0315B724ACD0A1002AB2BE /* PBXTargetDependency */, ); name = "Carlos tvOS Tests"; packageProductDependencies = ( CC5C36AE24EC16D3004AD171 /* Quick */, CC5C36B024EC16D3004AD171 /* Nimble */, ); productName = CarlosTests; productReference = CC0314FD24ACCFAC002AB2BE /* Carlos tvOS Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; CC0314FF24ACCFB1002AB2BE /* Carlos watchOS */ = { isa = PBXNativeTarget; buildConfigurationList = CC03153124ACCFB1002AB2BE /* Build configuration list for PBXNativeTarget "Carlos watchOS" */; buildPhases = ( CC03150024ACCFB1002AB2BE /* Sources */, CC03152C24ACCFB1002AB2BE /* Frameworks */, CC03152E24ACCFB1002AB2BE /* Headers */, CC03153024ACCFB1002AB2BE /* Resources */, CCC88F9124ADD5A1008C4060 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "Carlos watchOS"; packageProductDependencies = ( ); productName = Carlos; productReference = CC03153424ACCFB1002AB2BE /* Carlos.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ AD695CD71B46CD65004E998D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = WeltN24; TargetAttributes = { 52635A1C1B4F0F3D00F187EE = { CreatedOnToolsVersion = 6.4; LastSwiftMigration = 1020; }; 52635A651B4F110500F187EE = { CreatedOnToolsVersion = 6.4; LastSwiftMigration = 1150; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = AD695CDA1B46CD65004E998D /* Build configuration list for PBXProject "Carlos" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = AD695CD61B46CD65004E998D; packageReferences = ( CC5C368A24EC1619004AD171 /* XCRemoteSwiftPackageReference "Quick" */, CC5C368D24EC1636004AD171 /* XCRemoteSwiftPackageReference "Nimble" */, ); productRefGroup = AD695CE01B46CD65004E998D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 52635A1C1B4F0F3D00F187EE /* Carlos iOS */, 52635A651B4F110500F187EE /* Carlos iOS Tests */, CC0313E424ACCF94002AB2BE /* Carlos macOS */, CC03141B24ACCF99002AB2BE /* Carlos macOS Tests */, CC03145624ACCFA0002AB2BE /* Carlos tvOS */, CC0314C424ACCFAC002AB2BE /* Carlos tvOS Tests */, CC0314FF24ACCFB1002AB2BE /* Carlos watchOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 52635A1B1B4F0F3D00F187EE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 52635A641B4F110500F187EE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03141524ACCF94002AB2BE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03144B24ACCF99002AB2BE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03148724ACCFA0002AB2BE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC0314F424ACCFAC002AB2BE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; CC03153024ACCFB1002AB2BE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 52635A181B4F0F3D00F187EE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 52635A601B4F0F6F00F187EE /* Transformers.swift in Sources */, 5222108B1C501F2100E682D1 /* ConditionedTwoWayTransformer.swift in Sources */, 52635A571B4F0F6F00F187EE /* Extensions.swift in Sources */, 52463F7F1C1837CF0034D032 /* ConditionedValueTransformation.swift in Sources */, 52635A5B1B4F0F6F00F187EE /* NetworkFetcher.swift in Sources */, 527C65471B7A66D90005023B /* SwitchCache.swift in Sources */, 52635A591B4F0F6F00F187EE /* MemoryCacheLevel.swift in Sources */, 52635A511B4F0F6F00F187EE /* Carlos.swift in Sources */, 52D7DDAD1C54F4E1007E5328 /* CacheLevel+Batch.swift in Sources */, 52635A4F1B4F0F6F00F187EE /* BasicCache.swift in Sources */, CCC7062224AC93EA00D16827 /* ImageTransformer.swift in Sources */, 52635A5F1B4F0F6F00F187EE /* ValueTransformation.swift in Sources */, 52635A581B4F0F6F00F187EE /* Logger.swift in Sources */, 5257BBB41BBFC3FD00787819 /* ComposedTwoWayTransformer.swift in Sources */, 526339751B52435C00074CB9 /* KeyTransformation.swift in Sources */, AD22FB2D1B70BBF6007F319F /* CacheProvider.swift in Sources */, 5207E6AD1BE15BCE0062DEAC /* NSUserDefaultsCacheLevel.swift in Sources */, 520DBB4F1CF6307500F9ABE3 /* BatchAllCache.swift in Sources */, 52615CD41BBC8123001DA9BE /* ComposedOneWayTransformer.swift in Sources */, 5205729B1BB8716E0098DD09 /* StringTransformer.swift in Sources */, 5274A99B1BC05CB0006962E8 /* FetcherValueTransformation.swift in Sources */, 52635A5D1B4F0F6F00F187EE /* PoolCache.swift in Sources */, 52E0CBF91BBC4E1800F20C22 /* PostProcess.swift in Sources */, 52635A5C1B4F0F6F00F187EE /* OneWayTransformer.swift in Sources */, 52463F6E1C181D1B0034D032 /* ConditionedOutputProcessing.swift in Sources */, 52635A561B4F0F6F00F187EE /* Errors.swift in Sources */, 5207E6991BE13D2E0062DEAC /* ExpensiveObject.swift in Sources */, 52635A611B4F0F6F00F187EE /* TwoWayTransformer.swift in Sources */, CC5C369224EC164D004AD171 /* FunctionComposition.swift in Sources */, CCC7061E24AC93EA00D16827 /* CacheProvider+ImageCache.swift in Sources */, 52635A551B4F0F6F00F187EE /* DiskCacheLevel.swift in Sources */, 52635A5A1B4F0F6F00F187EE /* MemoryWarning.swift in Sources */, 52635A521B4F0F6F00F187EE /* Composed.swift in Sources */, CC5C369624EC164D004AD171 /* UnfairLock.swift in Sources */, 52635A531B4F0F6F00F187EE /* Conditioned.swift in Sources */, 52B7621C1BB84F160087CD91 /* JSONTransformer.swift in Sources */, 5243C4B31B77670F00737B3B /* StringConvertible.swift in Sources */, 5274A9A01BC0613C006962E8 /* BasicFetcher.swift in Sources */, 5250E3801BB1345700EB7388 /* Normalize.swift in Sources */, 5222107C1C501C5400E682D1 /* ConditionedOneWayTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 52635A621B4F110500F187EE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 526005AF1BB84AB200D96242 /* ImageTransformerTests.swift in Sources */, 526339811B52805000074CB9 /* MemoryWarningNotificationTests.swift in Sources */, 5243C4BF1B77686300737B3B /* StringConvertibleTests.swift in Sources */, 526339891B52B51C00074CB9 /* DiskCacheTests.swift in Sources */, 52D5F3481C503C6700BA3452 /* ConditionedTwoWayTransformationBoxTests.swift in Sources */, 52F064551B51B26200145C98 /* MKDistanceFormatterTransformerTests.swift in Sources */, 520572A91BB8752B0098DD09 /* StringTransformerTests.swift in Sources */, 52463F7D1C18264B0034D032 /* ConditionedOutputProcessingTests.swift in Sources */, 52615CE21BBC87F4001DA9BE /* OneWayTransformerCompositionTests.swift in Sources */, 5291C8531BB7FAA800C4E15E /* JSONTransformerTests.swift in Sources */, 523257D41B519AB900A10A56 /* OneWayTransformationBoxTests.swift in Sources */, 522210901C50358700E682D1 /* ConditionedOneWayTransformationBoxTests.swift in Sources */, 527C65531B7A89440005023B /* SwitchCacheTests.swift in Sources */, 52BBFD561B51BA140000084A /* CacheLevelFake.swift in Sources */, 52D7DDBC1C552854007E5328 /* BatchTests.swift in Sources */, 52F0644F1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift in Sources */, 5274A9D91BC069E7006962E8 /* FetcherValueTransformationTests.swift in Sources */, CC0313E124ACBBC8002AB2BE /* Base64EncodedImage.swift in Sources */, 526339831B52866900074CB9 /* BasicCacheTests.swift in Sources */, 5263397F1B52760F00074CB9 /* ValueTransformationTests.swift in Sources */, 5257BBC21BBFC7A800787819 /* TwoWayTransformerCompositionTests.swift in Sources */, 524019BC1C0AF9ED00749957 /* CacheProviderTests.swift in Sources */, 5263397D1B525D1600074CB9 /* MemoryCacheLevelTests.swift in Sources */, 5263397B1B52536700074CB9 /* KeyTransformationTests.swift in Sources */, 526078251BE2590E0022040B /* ConditionedTransformersTests.swift in Sources */, 526339851B52A82800074CB9 /* CompositionTests.swift in Sources */, 52BBFD581B51BAD20000084A /* PoolCacheTests.swift in Sources */, 526339771B52450C00074CB9 /* ConditionedCacheTests.swift in Sources */, 52F064511B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift in Sources */, C71C59751B6A722800AE5294 /* NetworkFetcherTests.swift in Sources */, 5274A9CD1BC06976006962E8 /* BasicFetcherTests.swift in Sources */, 52D5F3541C52C32000BA3452 /* ConditionedValueTransformationTests.swift in Sources */, CC5C36BF24EC1727004AD171 /* FetcherFake.swift in Sources */, 52E197021BBC6B27004BF6C5 /* PostProcessTests.swift in Sources */, 5235BAD71BE175BE0049CFA6 /* NSUserDefaultsCacheLevelTests.swift in Sources */, 5250E3841BB137BF00EB7388 /* NormalizationTests.swift in Sources */, 523257D21B51993000A10A56 /* TwoWayTransformationBoxTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CC0313E524ACCF94002AB2BE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CC0313E724ACCF94002AB2BE /* Transformers.swift in Sources */, CC0313E824ACCF94002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */, CC0313E924ACCF94002AB2BE /* Extensions.swift in Sources */, CC0313EA24ACCF94002AB2BE /* ConditionedValueTransformation.swift in Sources */, CC0313EB24ACCF94002AB2BE /* NetworkFetcher.swift in Sources */, CC0313EC24ACCF94002AB2BE /* SwitchCache.swift in Sources */, CC0313ED24ACCF94002AB2BE /* MemoryCacheLevel.swift in Sources */, CC0313EE24ACCF94002AB2BE /* Carlos.swift in Sources */, CC0313EF24ACCF94002AB2BE /* CacheLevel+Batch.swift in Sources */, CC0313F024ACCF94002AB2BE /* BasicCache.swift in Sources */, CC0313F224ACCF94002AB2BE /* ImageTransformer.swift in Sources */, CC0313F324ACCF94002AB2BE /* ValueTransformation.swift in Sources */, CC0313F424ACCF94002AB2BE /* Logger.swift in Sources */, CC0313F524ACCF94002AB2BE /* ComposedTwoWayTransformer.swift in Sources */, CC0313F624ACCF94002AB2BE /* KeyTransformation.swift in Sources */, CC0313F724ACCF94002AB2BE /* CacheProvider.swift in Sources */, CC0313F824ACCF94002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */, CC0313F924ACCF94002AB2BE /* BatchAllCache.swift in Sources */, CC0313FC24ACCF94002AB2BE /* ComposedOneWayTransformer.swift in Sources */, CC0313FD24ACCF94002AB2BE /* StringTransformer.swift in Sources */, CC0313FE24ACCF94002AB2BE /* FetcherValueTransformation.swift in Sources */, CC0313FF24ACCF94002AB2BE /* PoolCache.swift in Sources */, CC03140024ACCF94002AB2BE /* PostProcess.swift in Sources */, CC03140224ACCF94002AB2BE /* OneWayTransformer.swift in Sources */, CC03140324ACCF94002AB2BE /* ConditionedOutputProcessing.swift in Sources */, CC03140424ACCF94002AB2BE /* Errors.swift in Sources */, CC03140524ACCF94002AB2BE /* ExpensiveObject.swift in Sources */, CC03140624ACCF94002AB2BE /* TwoWayTransformer.swift in Sources */, CC5C369324EC164D004AD171 /* FunctionComposition.swift in Sources */, CC03140724ACCF94002AB2BE /* CacheProvider+ImageCache.swift in Sources */, CC03140824ACCF94002AB2BE /* DiskCacheLevel.swift in Sources */, CC03140924ACCF94002AB2BE /* MemoryWarning.swift in Sources */, CC03140A24ACCF94002AB2BE /* Composed.swift in Sources */, CC5C369724EC164D004AD171 /* UnfairLock.swift in Sources */, CC03140B24ACCF94002AB2BE /* Conditioned.swift in Sources */, CC03140C24ACCF94002AB2BE /* JSONTransformer.swift in Sources */, CC03140D24ACCF94002AB2BE /* StringConvertible.swift in Sources */, CC03140E24ACCF94002AB2BE /* BasicFetcher.swift in Sources */, CC03140F24ACCF94002AB2BE /* Normalize.swift in Sources */, CC03141024ACCF94002AB2BE /* ConditionedOneWayTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03141E24ACCF99002AB2BE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CC03142024ACCF99002AB2BE /* ImageTransformerTests.swift in Sources */, CC03142124ACCF99002AB2BE /* MemoryWarningNotificationTests.swift in Sources */, CC03142224ACCF99002AB2BE /* StringConvertibleTests.swift in Sources */, CC03142324ACCF99002AB2BE /* DiskCacheTests.swift in Sources */, CC03142424ACCF99002AB2BE /* ConditionedTwoWayTransformationBoxTests.swift in Sources */, CC03142524ACCF99002AB2BE /* MKDistanceFormatterTransformerTests.swift in Sources */, CC03142624ACCF99002AB2BE /* StringTransformerTests.swift in Sources */, CC03142724ACCF99002AB2BE /* ConditionedOutputProcessingTests.swift in Sources */, CC03142924ACCF99002AB2BE /* OneWayTransformerCompositionTests.swift in Sources */, CC03142A24ACCF99002AB2BE /* JSONTransformerTests.swift in Sources */, CC03142B24ACCF99002AB2BE /* OneWayTransformationBoxTests.swift in Sources */, CC03142C24ACCF99002AB2BE /* ConditionedOneWayTransformationBoxTests.swift in Sources */, CC03142D24ACCF99002AB2BE /* SwitchCacheTests.swift in Sources */, CC03142F24ACCF99002AB2BE /* CacheLevelFake.swift in Sources */, CC03143124ACCF99002AB2BE /* BatchTests.swift in Sources */, CC03143224ACCF99002AB2BE /* NSDateFormatterTransformerTests.swift in Sources */, CC03143324ACCF99002AB2BE /* FetcherValueTransformationTests.swift in Sources */, CC03143424ACCF99002AB2BE /* Base64EncodedImage.swift in Sources */, CC03143524ACCF99002AB2BE /* BasicCacheTests.swift in Sources */, CC03143624ACCF99002AB2BE /* ValueTransformationTests.swift in Sources */, CC03143724ACCF99002AB2BE /* TwoWayTransformerCompositionTests.swift in Sources */, CC03143824ACCF99002AB2BE /* CacheProviderTests.swift in Sources */, CC03143924ACCF99002AB2BE /* MemoryCacheLevelTests.swift in Sources */, CC03143A24ACCF99002AB2BE /* KeyTransformationTests.swift in Sources */, CC03143B24ACCF99002AB2BE /* ConditionedTransformersTests.swift in Sources */, CC03143C24ACCF99002AB2BE /* CompositionTests.swift in Sources */, CC03143D24ACCF99002AB2BE /* PoolCacheTests.swift in Sources */, CC03143E24ACCF99002AB2BE /* ConditionedCacheTests.swift in Sources */, CC03143F24ACCF99002AB2BE /* NSNumberFormatterTransformerTests.swift in Sources */, CC03144024ACCF99002AB2BE /* NetworkFetcherTests.swift in Sources */, CC03144124ACCF99002AB2BE /* BasicFetcherTests.swift in Sources */, CC03144224ACCF99002AB2BE /* ConditionedValueTransformationTests.swift in Sources */, CC5C36BE24EC1726004AD171 /* FetcherFake.swift in Sources */, CC03144324ACCF99002AB2BE /* PostProcessTests.swift in Sources */, CC03144424ACCF99002AB2BE /* NSUserDefaultsCacheLevelTests.swift in Sources */, CC03144524ACCF99002AB2BE /* NormalizationTests.swift in Sources */, CC03144624ACCF99002AB2BE /* TwoWayTransformationBoxTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03145724ACCFA0002AB2BE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CC03145924ACCFA0002AB2BE /* Transformers.swift in Sources */, CC03145A24ACCFA0002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */, CC03145B24ACCFA0002AB2BE /* Extensions.swift in Sources */, CC03145C24ACCFA0002AB2BE /* ConditionedValueTransformation.swift in Sources */, CC03145D24ACCFA0002AB2BE /* NetworkFetcher.swift in Sources */, CC03145E24ACCFA0002AB2BE /* SwitchCache.swift in Sources */, CC03145F24ACCFA0002AB2BE /* MemoryCacheLevel.swift in Sources */, CC03146024ACCFA0002AB2BE /* Carlos.swift in Sources */, CC03146124ACCFA0002AB2BE /* CacheLevel+Batch.swift in Sources */, CC03146224ACCFA0002AB2BE /* BasicCache.swift in Sources */, CC03146424ACCFA0002AB2BE /* ImageTransformer.swift in Sources */, CC03146524ACCFA0002AB2BE /* ValueTransformation.swift in Sources */, CC03146624ACCFA0002AB2BE /* Logger.swift in Sources */, CC03146724ACCFA0002AB2BE /* ComposedTwoWayTransformer.swift in Sources */, CC03146824ACCFA0002AB2BE /* KeyTransformation.swift in Sources */, CC03146924ACCFA0002AB2BE /* CacheProvider.swift in Sources */, CC03146A24ACCFA0002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */, CC03146B24ACCFA0002AB2BE /* BatchAllCache.swift in Sources */, CC03146E24ACCFA0002AB2BE /* ComposedOneWayTransformer.swift in Sources */, CC03146F24ACCFA0002AB2BE /* StringTransformer.swift in Sources */, CC03147024ACCFA0002AB2BE /* FetcherValueTransformation.swift in Sources */, CC03147124ACCFA0002AB2BE /* PoolCache.swift in Sources */, CC03147224ACCFA0002AB2BE /* PostProcess.swift in Sources */, CC03147424ACCFA0002AB2BE /* OneWayTransformer.swift in Sources */, CC03147524ACCFA0002AB2BE /* ConditionedOutputProcessing.swift in Sources */, CC03147624ACCFA0002AB2BE /* Errors.swift in Sources */, CC03147724ACCFA0002AB2BE /* ExpensiveObject.swift in Sources */, CC03147824ACCFA0002AB2BE /* TwoWayTransformer.swift in Sources */, CC5C369424EC164D004AD171 /* FunctionComposition.swift in Sources */, CC03147924ACCFA0002AB2BE /* CacheProvider+ImageCache.swift in Sources */, CC03147A24ACCFA0002AB2BE /* DiskCacheLevel.swift in Sources */, CC03147B24ACCFA0002AB2BE /* MemoryWarning.swift in Sources */, CC03147C24ACCFA0002AB2BE /* Composed.swift in Sources */, CC5C369824EC164D004AD171 /* UnfairLock.swift in Sources */, CC03147D24ACCFA0002AB2BE /* Conditioned.swift in Sources */, CC03147E24ACCFA0002AB2BE /* JSONTransformer.swift in Sources */, CC03147F24ACCFA0002AB2BE /* StringConvertible.swift in Sources */, CC03148024ACCFA0002AB2BE /* BasicFetcher.swift in Sources */, CC03148124ACCFA0002AB2BE /* Normalize.swift in Sources */, CC03148224ACCFA0002AB2BE /* ConditionedOneWayTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CC0314C724ACCFAC002AB2BE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CC0314C924ACCFAC002AB2BE /* ImageTransformerTests.swift in Sources */, CC0314CA24ACCFAC002AB2BE /* MemoryWarningNotificationTests.swift in Sources */, CC0314CB24ACCFAC002AB2BE /* StringConvertibleTests.swift in Sources */, CC0314CC24ACCFAC002AB2BE /* DiskCacheTests.swift in Sources */, CC0314CD24ACCFAC002AB2BE /* ConditionedTwoWayTransformationBoxTests.swift in Sources */, CC0314CE24ACCFAC002AB2BE /* MKDistanceFormatterTransformerTests.swift in Sources */, CC0314CF24ACCFAC002AB2BE /* StringTransformerTests.swift in Sources */, CC0314D024ACCFAC002AB2BE /* ConditionedOutputProcessingTests.swift in Sources */, CC0314D224ACCFAC002AB2BE /* OneWayTransformerCompositionTests.swift in Sources */, CC0314D324ACCFAC002AB2BE /* JSONTransformerTests.swift in Sources */, CC0314D424ACCFAC002AB2BE /* OneWayTransformationBoxTests.swift in Sources */, CC0314D524ACCFAC002AB2BE /* ConditionedOneWayTransformationBoxTests.swift in Sources */, CC0314D624ACCFAC002AB2BE /* SwitchCacheTests.swift in Sources */, CC0314D824ACCFAC002AB2BE /* CacheLevelFake.swift in Sources */, CC0314DA24ACCFAC002AB2BE /* BatchTests.swift in Sources */, CC0314DB24ACCFAC002AB2BE /* NSDateFormatterTransformerTests.swift in Sources */, CC0314DC24ACCFAC002AB2BE /* FetcherValueTransformationTests.swift in Sources */, CC0314DD24ACCFAC002AB2BE /* Base64EncodedImage.swift in Sources */, CC0314DE24ACCFAC002AB2BE /* BasicCacheTests.swift in Sources */, CC0314DF24ACCFAC002AB2BE /* ValueTransformationTests.swift in Sources */, CC0314E024ACCFAC002AB2BE /* TwoWayTransformerCompositionTests.swift in Sources */, CC0314E124ACCFAC002AB2BE /* CacheProviderTests.swift in Sources */, CC0314E224ACCFAC002AB2BE /* MemoryCacheLevelTests.swift in Sources */, CC0314E324ACCFAC002AB2BE /* KeyTransformationTests.swift in Sources */, CC0314E424ACCFAC002AB2BE /* ConditionedTransformersTests.swift in Sources */, CC0314E524ACCFAC002AB2BE /* CompositionTests.swift in Sources */, CC0314E624ACCFAC002AB2BE /* PoolCacheTests.swift in Sources */, CC0314E724ACCFAC002AB2BE /* ConditionedCacheTests.swift in Sources */, CC0314E824ACCFAC002AB2BE /* NSNumberFormatterTransformerTests.swift in Sources */, CC0314E924ACCFAC002AB2BE /* NetworkFetcherTests.swift in Sources */, CC0314EA24ACCFAC002AB2BE /* BasicFetcherTests.swift in Sources */, CC0314EB24ACCFAC002AB2BE /* ConditionedValueTransformationTests.swift in Sources */, CC5C36BD24EC1725004AD171 /* FetcherFake.swift in Sources */, CC0314EC24ACCFAC002AB2BE /* PostProcessTests.swift in Sources */, CC0314ED24ACCFAC002AB2BE /* NSUserDefaultsCacheLevelTests.swift in Sources */, CC0314EE24ACCFAC002AB2BE /* NormalizationTests.swift in Sources */, CC0314EF24ACCFAC002AB2BE /* TwoWayTransformationBoxTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CC03150024ACCFB1002AB2BE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( CC03150224ACCFB1002AB2BE /* Transformers.swift in Sources */, CC03150324ACCFB1002AB2BE /* ConditionedTwoWayTransformer.swift in Sources */, CC03150424ACCFB1002AB2BE /* Extensions.swift in Sources */, CC03150524ACCFB1002AB2BE /* ConditionedValueTransformation.swift in Sources */, CC03150624ACCFB1002AB2BE /* NetworkFetcher.swift in Sources */, CC03150724ACCFB1002AB2BE /* SwitchCache.swift in Sources */, CC03150824ACCFB1002AB2BE /* MemoryCacheLevel.swift in Sources */, CC03150924ACCFB1002AB2BE /* Carlos.swift in Sources */, CC03150A24ACCFB1002AB2BE /* CacheLevel+Batch.swift in Sources */, CC03150B24ACCFB1002AB2BE /* BasicCache.swift in Sources */, CC03150D24ACCFB1002AB2BE /* ImageTransformer.swift in Sources */, CC03150E24ACCFB1002AB2BE /* ValueTransformation.swift in Sources */, CC03150F24ACCFB1002AB2BE /* Logger.swift in Sources */, CC03151024ACCFB1002AB2BE /* ComposedTwoWayTransformer.swift in Sources */, CC03151124ACCFB1002AB2BE /* KeyTransformation.swift in Sources */, CC03151224ACCFB1002AB2BE /* CacheProvider.swift in Sources */, CC03151324ACCFB1002AB2BE /* NSUserDefaultsCacheLevel.swift in Sources */, CC03151424ACCFB1002AB2BE /* BatchAllCache.swift in Sources */, CC03151724ACCFB1002AB2BE /* ComposedOneWayTransformer.swift in Sources */, CC03151824ACCFB1002AB2BE /* StringTransformer.swift in Sources */, CC03151924ACCFB1002AB2BE /* FetcherValueTransformation.swift in Sources */, CC03151A24ACCFB1002AB2BE /* PoolCache.swift in Sources */, CC03151B24ACCFB1002AB2BE /* PostProcess.swift in Sources */, CC03151D24ACCFB1002AB2BE /* OneWayTransformer.swift in Sources */, CC03151E24ACCFB1002AB2BE /* ConditionedOutputProcessing.swift in Sources */, CC03151F24ACCFB1002AB2BE /* Errors.swift in Sources */, CC03152024ACCFB1002AB2BE /* ExpensiveObject.swift in Sources */, CC03152124ACCFB1002AB2BE /* TwoWayTransformer.swift in Sources */, CC5C369524EC164D004AD171 /* FunctionComposition.swift in Sources */, CC03152224ACCFB1002AB2BE /* CacheProvider+ImageCache.swift in Sources */, CC03152324ACCFB1002AB2BE /* DiskCacheLevel.swift in Sources */, CC03152424ACCFB1002AB2BE /* MemoryWarning.swift in Sources */, CC03152524ACCFB1002AB2BE /* Composed.swift in Sources */, CC5C369924EC164D004AD171 /* UnfairLock.swift in Sources */, CC03152624ACCFB1002AB2BE /* Conditioned.swift in Sources */, CC03152724ACCFB1002AB2BE /* JSONTransformer.swift in Sources */, CC03152824ACCFB1002AB2BE /* StringConvertible.swift in Sources */, CC03152924ACCFB1002AB2BE /* BasicFetcher.swift in Sources */, CC03152A24ACCFB1002AB2BE /* Normalize.swift in Sources */, CC03152B24ACCFB1002AB2BE /* ConditionedOneWayTransformer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 52635A6E1B4F110500F187EE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 52635A1C1B4F0F3D00F187EE /* Carlos iOS */; targetProxy = 52635A6D1B4F110500F187EE /* PBXContainerItemProxy */; }; CC0315A924ACCFFD002AB2BE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CC0313E424ACCF94002AB2BE /* Carlos macOS */; targetProxy = CC0315A824ACCFFD002AB2BE /* PBXContainerItemProxy */; }; CC0315B724ACD0A1002AB2BE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = CC03145624ACCFA0002AB2BE /* Carlos tvOS */; targetProxy = CC0315B624ACD0A1002AB2BE /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 52635A371B4F0F3E00F187EE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 52635A381B4F0F3E00F187EE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; SDKROOT = iphoneos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTS_MACCATALYST = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 52635A701B4F110500F187EE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Debug; }; 52635A711B4F110500F187EE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Release; }; AD695CFC1B46CD65004E998D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/Carlos/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 0.10.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.carlos; PRODUCT_NAME = Carlos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos macosx watchos watchsimulator appletvsimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; AD695CFD1B46CD65004E998D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = Sources/Carlos/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 0.10.0; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.carlos; PRODUCT_NAME = Carlos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos macosx watchos watchsimulator appletvsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 10.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; CC03141724ACCF94002AB2BE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; CC03141824ACCF94002AB2BE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; SDKROOT = macosx; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; CC03145224ACCF99002AB2BE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/Mac", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Debug; }; CC03145324ACCF99002AB2BE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/Mac", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SUPPORTED_PLATFORMS = macosx; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Release; }; CC03148924ACCFA0002AB2BE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; CC03148A24ACCFA0002AB2BE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; SDKROOT = appletvos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; CC0314FB24ACCFAC002AB2BE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/tvOS", ); GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Debug; }; CC0314FC24ACCFAC002AB2BE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/tvOS", ); INFOPLIST_FILE = Tests/CarlosTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SUPPORTED_PLATFORMS = "appletvsimulator appletvos"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; }; name = Release; }; CC03153224ACCFB1002AB2BE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; CC03153324ACCFB1002AB2BE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0.0; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 52635A361B4F0F3E00F187EE /* Build configuration list for PBXNativeTarget "Carlos iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 52635A371B4F0F3E00F187EE /* Debug */, 52635A381B4F0F3E00F187EE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 52635A6F1B4F110500F187EE /* Build configuration list for PBXNativeTarget "Carlos iOS Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( 52635A701B4F110500F187EE /* Debug */, 52635A711B4F110500F187EE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD695CDA1B46CD65004E998D /* Build configuration list for PBXProject "Carlos" */ = { isa = XCConfigurationList; buildConfigurations = ( AD695CFC1B46CD65004E998D /* Debug */, AD695CFD1B46CD65004E998D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CC03141624ACCF94002AB2BE /* Build configuration list for PBXNativeTarget "Carlos macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( CC03141724ACCF94002AB2BE /* Debug */, CC03141824ACCF94002AB2BE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CC03145124ACCF99002AB2BE /* Build configuration list for PBXNativeTarget "Carlos macOS Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( CC03145224ACCF99002AB2BE /* Debug */, CC03145324ACCF99002AB2BE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CC03148824ACCFA0002AB2BE /* Build configuration list for PBXNativeTarget "Carlos tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( CC03148924ACCFA0002AB2BE /* Debug */, CC03148A24ACCFA0002AB2BE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CC0314FA24ACCFAC002AB2BE /* Build configuration list for PBXNativeTarget "Carlos tvOS Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( CC0314FB24ACCFAC002AB2BE /* Debug */, CC0314FC24ACCFAC002AB2BE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; CC03153124ACCFB1002AB2BE /* Build configuration list for PBXNativeTarget "Carlos watchOS" */ = { isa = XCConfigurationList; buildConfigurations = ( CC03153224ACCFB1002AB2BE /* Debug */, CC03153324ACCFB1002AB2BE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ CC5C368A24EC1619004AD171 /* XCRemoteSwiftPackageReference "Quick" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Quick.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 3.0.0; }; }; CC5C368D24EC1636004AD171 /* XCRemoteSwiftPackageReference "Nimble" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Quick/Nimble.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 8.1.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ CC5C36A024EC16BB004AD171 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = CC5C368A24EC1619004AD171 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; CC5C36A224EC16BF004AD171 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = CC5C368D24EC1636004AD171 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; CC5C36A424EC16C6004AD171 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = CC5C368A24EC1619004AD171 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; CC5C36A624EC16C9004AD171 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = CC5C368D24EC1636004AD171 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; CC5C36AE24EC16D3004AD171 /* Quick */ = { isa = XCSwiftPackageProductDependency; package = CC5C368A24EC1619004AD171 /* XCRemoteSwiftPackageReference "Quick" */; productName = Quick; }; CC5C36B024EC16D3004AD171 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = CC5C368D24EC1636004AD171 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = AD695CD71B46CD65004E998D /* Project object */; } ================================================ FILE: Carlos.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Carlos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Carlos.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "Nimble", "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, "revision": "7a46a5fc86cb917f69e3daf79fcb045283d8f008", "version": "8.1.2" } }, { "package": "Quick", "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, "revision": "0038bcbab4292f3b028632556507c124e5ba69f3", "version": "3.0.0" } } ] }, "version": 1 } ================================================ FILE: Carlos.xcodeproj/xcshareddata/xcschemes/Carlos iOS.xcscheme ================================================ ================================================ FILE: Carlos.xcodeproj/xcshareddata/xcschemes/Carlos watchOS.xcscheme ================================================ ================================================ FILE: Carlos.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Carlos.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Carlos.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "CwlCatchException", "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", "state": { "branch": null, "revision": "682841464136f8c66e04afe5dbd01ab51a3a56f2", "version": "2.1.0" } }, { "package": "CwlPreconditionTesting", "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state": { "branch": null, "revision": "02b7a39a99c4da27abe03cab2053a9034379639f", "version": "2.0.0" } }, { "package": "Nimble", "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, "revision": "af1730dde4e6c0d45bf01b99f8a41713ce536790", "version": "9.2.0" } }, { "package": "Quick", "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, "revision": "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6", "version": "4.0.0" } } ] }, "version": 1 } ================================================ FILE: Example/CarlosMacSample/AppDelegate.swift ================================================ import Carlos import Cocoa import Combine @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { let cache = CacheProvider.dataCache() private var cancellables = Set() func applicationDidFinishLaunching(_: Notification) { cache.get(URL(string: "https://github.com/WeltN24/Carlos")!) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { print("Got the following error from the data cache:") print(error) } }, receiveValue: { value in print("Got the following string from the data cache:") print(String(describing: String(data: value as Data, encoding: .utf8))) }) .store(in: &cancellables) } } ================================================ FILE: Example/CarlosMacSample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosMacSample/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/CarlosMacSample/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2015 WeltN24. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: Example/CarlosMacSample/ViewController.swift ================================================ import Cocoa class ViewController: NSViewController { override func viewDidLoad() { if #available(OSX 10.10, *) { super.viewDidLoad() } } } ================================================ FILE: Example/CarlosTvSample/AppDelegate.swift ================================================ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { true } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json ================================================ { "assets" : [ { "filename" : "App Icon - Large.imagestack", "idiom" : "tv", "role" : "primary-app-icon", "size" : "1280x768" }, { "filename" : "App Icon - Small.imagestack", "idiom" : "tv", "role" : "primary-app-icon", "size" : "400x240" }, { "filename" : "Top Shelf Image Wide.imageset", "idiom" : "tv", "role" : "top-shelf-image-wide", "size" : "2320x720" }, { "filename" : "Top Shelf Image.imageset", "idiom" : "tv", "role" : "top-shelf-image", "size" : "1920x720" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosTvSample/Assets.xcassets/LaunchImage.launchimage/Contents.json ================================================ { "images" : [ { "extent" : "full-screen", "idiom" : "tv", "minimum-system-version" : "11.0", "orientation" : "landscape", "scale" : "2x" }, { "extent" : "full-screen", "idiom" : "tv", "minimum-system-version" : "9.0", "orientation" : "landscape", "scale" : "1x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosTvSample/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/CarlosTvSample/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UIMainStoryboardFile Main UIRequiredDeviceCapabilities arm64 ================================================ FILE: Example/CarlosTvSample/ViewController.swift ================================================ import Carlos import Combine import UIKit class BitcoinResult { let USDValue: Float init(USDValue: Float) { self.USDValue = USDValue } } extension BitcoinResult: ExpensiveObject { var cost: Int { 1 } } enum SampleError: Error { case invalidJSON } class ViewController: UIViewController { private var cancellables = Set() override func viewDidLoad() { super.viewDidLoad() let JSONFetcher: BasicFetcher = NetworkFetcher().transformValues(JSONTransformer()) let cache = JSONFetcher.transformValues(BTCTransformer()) cache.get(URL(string: "http://coinabul.com/api.php")!) .sink(receiveCompletion: { completion in print(completion) }, receiveValue: { result in print("Bitcoin value is \(result.USDValue) USD") }).store(in: &cancellables) } } struct BTCTransformer: OneWayTransformer { func transform(_ val: AnyObject) -> AnyPublisher { Future { promise in if let JSON = val as? [String: AnyObject], let BTCDict = JSON["BTC"] as? [String: AnyObject], let USDStringValue = BTCDict["USD"] as? String, let USDFloatValue = Float(USDStringValue) { promise(.success(BitcoinResult(USDValue: USDFloatValue))) } else { promise(.failure(SampleError.invalidJSON)) } }.eraseToAnyPublisher() } } ================================================ FILE: Example/CarlosWatchSample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "watch", "role" : "notificationCenter", "scale" : "2x", "size" : "24x24", "subtype" : "38mm" }, { "idiom" : "watch", "role" : "notificationCenter", "scale" : "2x", "size" : "27.5x27.5", "subtype" : "42mm" }, { "idiom" : "watch", "role" : "companionSettings", "scale" : "2x", "size" : "29x29" }, { "idiom" : "watch", "role" : "companionSettings", "scale" : "3x", "size" : "29x29" }, { "idiom" : "watch", "role" : "appLauncher", "scale" : "2x", "size" : "40x40", "subtype" : "38mm" }, { "idiom" : "watch", "role" : "appLauncher", "scale" : "2x", "size" : "44x44", "subtype" : "40mm" }, { "idiom" : "watch", "role" : "appLauncher", "scale" : "2x", "size" : "50x50", "subtype" : "44mm" }, { "idiom" : "watch", "role" : "quickLook", "scale" : "2x", "size" : "86x86", "subtype" : "38mm" }, { "idiom" : "watch", "role" : "quickLook", "scale" : "2x", "size" : "98x98", "subtype" : "42mm" }, { "idiom" : "watch", "role" : "quickLook", "scale" : "2x", "size" : "108x108", "subtype" : "44mm" }, { "idiom" : "watch-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Example/CarlosWatchSample/Base.lproj/Interface.storyboard ================================================
================================================ FILE: Example/CarlosWatchSample/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName CarlosSample CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown WKCompanionAppBundleIdentifier de.axelspringer.CarlosSample WKWatchKitApp ================================================ FILE: Example/CarlosWatchSample Extension/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosWatchSample Extension/Assets.xcassets/placeholder.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "placeholder.jpg", "scale" : "1x" }, { "idiom" : "universal", "filename" : "placeholder-1.jpg", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/CarlosWatchSample Extension/CountryRow.swift ================================================ import Foundation import WatchKit class CountryRow: NSObject { @IBOutlet var countryName: WKInterfaceLabel! @IBOutlet var flagImage: WKInterfaceImage! } ================================================ FILE: Example/CarlosWatchSample Extension/ExtensionDelegate.swift ================================================ import WatchKit class ExtensionDelegate: NSObject, WKExtensionDelegate { func applicationDidFinishLaunching() {} } ================================================ FILE: Example/CarlosWatchSample Extension/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName CarlosWatchSample Extension CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 NSAppTransportSecurity NSAllowsArbitraryLoads NSExtension NSExtensionAttributes WKAppBundleIdentifier de.axelspringer.CarlosSample.watchkitapp NSExtensionPointIdentifier com.apple.watchkit RemoteInterfacePrincipalClass $(PRODUCT_MODULE_NAME).InterfaceController WKExtensionDelegateClassName $(PRODUCT_MODULE_NAME).ExtensionDelegate ================================================ FILE: Example/CarlosWatchSample Extension/InterfaceController.swift ================================================ import Foundation import WatchKit import Carlos import Combine private struct Country { let name: String let flagURL: URL } class InterfaceController: WKInterfaceController { @IBOutlet var tableView: WKInterfaceTable! let imageCache = CacheProvider.imageCache() private var cancellables = Set() private let countries = [ Country(name: "Italy", flagURL: URL(string: "http://2.bp.blogspot.com/-51ZhmfLCi9s/VBLNUQL-giI/AAAAAAAAAfA/LTayxh5K3C4/s1600/flag_italy_mini.gif")!), Country(name: "Germany", flagURL: URL(string: "http://www.weezerpedia.com/wiki/images/e/eb/Flag-germany.png")!), Country(name: "France", flagURL: URL(string: "http://www.worldflagsportal.com/pics/thumbnails/france-flag.png")!), Country(name: "Netherlands", flagURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Flag_of_the_Netherlands.svg/50px-Flag_of_the_Netherlands.svg.png")!), Country(name: "South Africa", flagURL: URL(string: "https://8b90b43d6bcfc09ee36c-3ad5470e7d4bb324e402ac2f90d6d0ba.ssl.cf3.rackcdn.com/soaf_1.gif")!), Country(name: "USA", flagURL: URL(string: "http://www.scramblestuff.us/images/us_flag.png")!), Country(name: "Australia", flagURL: URL(string: "http://dropdownaustralia.com/wp-content/uploads/2013/11/Australian-Flag.png")!), Country(name: "Spain", flagURL: URL(string: "http://www.romanhomes.com/vacation_rentals/images/navona-campo-fiori-turtles-dream/navona-campo-fiori-turtles-dream/flag-spain-small.jpg")!), Country(name: "Austria", flagURL: URL(string: "http://flagpedia.net/data/flags/mini/at.png")!), Country(name: "Congo", flagURL: URL(string: "https://www.usaid.gov/sites/default/files/styles/40x24_flag/public/missions/flags/congo-democratic-republic-of.gif?itok=xRT3uqRi")!), Country(name: "Cuba", flagURL: URL(string: "http://flagpedia.net/data/flags/mini/cu.png")!), Country(name: "UK", flagURL: URL(string: "http://images.smh.com.au/2012/07/18/3464759/Olympic-Flag-Icon_Great_Britain.gif")!) ] override func awake(withContext context: Any?) { super.awake(withContext: context) tableView.setNumberOfRows(countries.count, withRowType: "CountryRow") for (idx, country) in countries.enumerated() { if let row = tableView.rowController(at: idx) as? CountryRow { row.countryName.setText(country.name) row.flagImage.setImage(UIImage(named: "placeholder")) imageCache.get(country.flagURL).sink(receiveCompletion: { _ in }) { image in row.flagImage.setImage(image) }.store(in: &cancellables) } } } } ================================================ FILE: Example/Example/AppDelegate.swift ================================================ import Carlos import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { true } } func simpleCache() -> BasicCache { CacheProvider.dataCache() } func delayedNetworkCache() -> BasicCache { MemoryCacheLevel().compose(DiskCacheLevel()).compose(DelayedNetworkFetcher()) } ================================================ FILE: Example/Example/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/Example/BaseCacheViewController.swift ================================================ import Carlos import Foundation import UIKit class BaseCacheViewController: UIViewController { @IBOutlet var urlKeyField: UITextField? @IBOutlet var fetchButton: UIButton! @IBOutlet var eventsLogView: UITextView! override func viewDidLoad() { super.viewDidLoad() title = titleForScreen() setupCache() Logger.output = { message, _ in self.eventsLogView.text = "\(self.eventsLogView.text!)\(message)\n" } } func setupCache() {} func fetchRequested() {} func titleForScreen() -> String { "Carlos Sample" } @IBAction func fetchButtonTapped(_: AnyObject) { fetchRequested() urlKeyField?.resignFirstResponder() } } extension BaseCacheViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let text = textField.text { let newText = (text as NSString).replacingCharacters(in: range, with: string) let textIsURL = URL(string: newText) != nil fetchButton.isEnabled = textIsURL } return true } } ================================================ FILE: Example/Example/ComplexCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine struct ModelDomain { let name: String let identifier: Int let URL: Foundation.URL } extension ModelDomain: StringConvertible { func toString() -> String { "\(identifier)" } } enum IgnoreError: Error { case ignore } class CustomCacheLevel: Fetcher { typealias KeyType = Int typealias OutputType = String func get(_ key: KeyType) -> AnyPublisher { if key > 0 { Logger.log("Fetched \(key) on the custom cache", .info) return Just("\(key)") .setFailureType(to: Error.self) .eraseToAnyPublisher() } Logger.log("Failed fetching \(key) on the custom cache", .info) return Fail(error: IgnoreError.ignore).eraseToAnyPublisher() } } class ComplexCacheSampleViewController: BaseCacheViewController { @IBOutlet var nameField: UITextField! @IBOutlet var identifierField: UITextField! @IBOutlet var urlField: UITextField! private var cache: BasicCache! private var cancellables = Set() override func titleForScreen() -> String { "Complex cache" } override func setupCache() { super.setupCache() let modelDomainToString = OneWayTransformationBox(transform: { Just($0.name).setFailureType(to: Error.self).eraseToAnyPublisher() }) let modelDomainToInt = OneWayTransformationBox(transform: { Just($0.identifier).setFailureType(to: Error.self).eraseToAnyPublisher() }) let stringToData = StringTransformer().invert() let uppercaseTransformer = OneWayTransformationBox(transform: { Just($0.uppercased()).setFailureType(to: Error.self).eraseToAnyPublisher() }) let memoryAndDisk = MemoryCacheLevel() .compose(DiskCacheLevel()) .transformKeys(modelDomainToString) let customCache = CustomCacheLevel() .postProcess(uppercaseTransformer) .transformKeys(modelDomainToInt) .transformValues(stringToData) cache = memoryAndDisk .compose(customCache) .compose( BasicFetcher(getClosure: { (key: ModelDomain) in Logger.log("Fetched \(key.name) on the fetcher closure", .info) return Just(("Last level was hit!".data(using: .utf8, allowLossyConversion: false) as NSData?)!) .setFailureType(to: Error.self) .eraseToAnyPublisher() }) ) } override func fetchRequested() { super.fetchRequested() let key = ModelDomain(name: nameField.text ?? "", identifier: Int(identifierField.text ?? "") ?? 0, URL: URL(string: urlField.text ?? "")!) cache.get(key) .subscribe(on: DispatchQueue(label: "carlose test queue", qos: .userInitiated)) .sink(receiveCompletion: { _ in }) { data in print("Is Main Thread:", Thread.isMainThread) print(data) }.store(in: &cancellables) for field in [nameField, identifierField, urlField] { field?.resignFirstResponder() } } } ================================================ FILE: Example/Example/ConditionedCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine enum ConditionError: Error { case globalKillSwitch case urlScheme func toString() -> String { switch self { case .globalKillSwitch: return "Global kill switch is on" case .urlScheme: return "URL Scheme is not HTTP" } } } final class ConditionedCacheSampleViewController: BaseCacheViewController { private var cache: BasicCache! private var globalKillSwitch = false private var cancellables = Set() override func fetchRequested() { super.fetchRequested() cache.get(URL(string: urlKeyField?.text ?? "")!) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { self.eventsLogView.text = "\(self.eventsLogView.text!)Failed because of condition\n" print(error) } }, receiveValue: { _ in }) .store(in: &cancellables) } override func titleForScreen() -> String { "Conditioned cache" } @IBAction func killSwitchValueChanged(_ sender: UISwitch) { globalKillSwitch = sender.isOn } override func setupCache() { super.setupCache() cache = simpleCache().conditioned { key -> AnyPublisher in if self.globalKillSwitch { return Fail(error: ConditionError.globalKillSwitch).eraseToAnyPublisher() } else if key.scheme != "http" { return Fail(error: ConditionError.urlScheme).eraseToAnyPublisher() } return Just(true).setFailureType(to: Error.self).eraseToAnyPublisher() } } } ================================================ FILE: Example/Example/DataCacheSampleViewController.swift ================================================ import Carlos import Combine import Foundation import UIKit class DataCacheSampleViewController: BaseCacheViewController { fileprivate var cache: BasicCache! private var cancellable: AnyCancellable? override func fetchRequested() { super.fetchRequested() cancellable = cache.get(URL(string: urlKeyField?.text ?? "")!) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } override func titleForScreen() -> String { "Data cache" } override func setupCache() { super.setupCache() cache = CacheProvider.dataCache() } } ================================================ FILE: Example/Example/DelayedNetworkFetcher.swift ================================================ import Foundation import Carlos import Combine final class DelayedNetworkFetcher: NetworkFetcher { override func get(_ key: KeyType) -> AnyPublisher { super.get(key) .delay(for: 2, scheduler: DispatchQueue.global()) .eraseToAnyPublisher() } } ================================================ FILE: Example/Example/ExampleCell.swift ================================================ import Foundation import UIKit class ExampleCell: UITableViewCell { static let Identifier = "ExampleCell" func configureWithExample(_ example: Example) { textLabel?.text = example.name detailTextLabel?.text = example.shortDescription } } ================================================ FILE: Example/Example/ExamplesListViewController.swift ================================================ import Foundation import UIKit struct ExamplesListSection { let name: String let samples: [Example] } struct Example { let name: String let shortDescription: String let segueIdentifier: String } class ExamplesListViewController: UIViewController { fileprivate let sections = [ ExamplesListSection(name: "Simple", samples: [ Example(name: "Image cache", shortDescription: "Out-of-the-box image cache", segueIdentifier: "imageCache"), Example(name: "Data cache", shortDescription: "Out-of-the-box data cache", segueIdentifier: "dataCache"), Example(name: "JSON cache", shortDescription: "Out-of-the-box JSON cache", segueIdentifier: "jsonCache"), Example(name: "User defaults cache", shortDescription: "Out-of-the-box NSUserDefaults cache", segueIdentifier: "userDefaultsCache"), Example(name: "Memory warnings", shortDescription: "Simple stack with memory warnings listeners", segueIdentifier: "memoryWarning") ]), ExamplesListSection(name: "Advanced", samples: [ Example(name: "Complex cache", shortDescription: "Custom stack with key and value transformations", segueIdentifier: "complexCache"), Example(name: "Conditioned cache", shortDescription: "Simple stack with conditioned levels", segueIdentifier: "conditionedCache"), Example(name: "Pooled cache", shortDescription: "Simple stack with requests pooling", segueIdentifier: "pooledCache"), Example(name: "Switched cache", shortDescription: "2 Simple switched lanes", segueIdentifier: "switchedCache") ]) ] override func viewDidLoad() { super.viewDidLoad() title = "Carlos Samples" } } extension ExamplesListViewController: UITableViewDataSource { func numberOfSections(in _: UITableView) -> Int { sections.count } func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { sections[section].samples.count } func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { sections[section].name } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: ExampleCell.Identifier, for: indexPath) as! ExampleCell cell.configureWithExample(sections[(indexPath as NSIndexPath).section].samples[(indexPath as NSIndexPath).row]) return cell } } extension ExamplesListViewController: UITableViewDelegate { func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { let example = sections[(indexPath as NSIndexPath).section].samples[(indexPath as NSIndexPath).row] performSegue(withIdentifier: example.segueIdentifier, sender: self) } } ================================================ FILE: Example/Example/ImageCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine final class ImageCacheSampleViewController: BaseCacheViewController { private var cache: BasicCache! private var cancellables = Set() @IBOutlet var imageView: UIImageView? override func fetchRequested() { super.fetchRequested() cache.get(URL(string: urlKeyField?.text ?? "")!) .receive(on: DispatchQueue.main) .print() .sink(receiveCompletion: { completion in switch completion { case .failure: self.imageView?.image = self.imageWithColor(.darkGray, size: self.imageView?.frame.size ?? .zero) default: break } print(completion) }, receiveValue: { image in self.imageView?.image = image }) .store(in: &cancellables) } private func imageWithColor(_ color: UIColor, size: CGSize) -> UIImage { let rect = CGRect(origin: CGPoint.zero, size: size) UIGraphicsBeginImageContextWithOptions(size, false, 0) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } override func titleForScreen() -> String { "Image cache" } override func setupCache() { super.setupCache() cache = CacheProvider.imageCache() } } ================================================ FILE: Example/Example/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/Example/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UILaunchStoryboardName Main UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Example/Example/JSONCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine private var myContext = 0 final class JSONCacheSampleViewController: BaseCacheViewController { private var cache: BasicCache! private var cancellables = Set() override func fetchRequested() { super.fetchRequested() cache.get(URL(string: urlKeyField?.text ?? "")!) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { _ in }, receiveValue: { JSON in self.eventsLogView.text = "\(self.eventsLogView.text!)\nJSON Dictionary result: \(JSON as? NSDictionary)\n" }) .store(in: &cancellables) let progress = Progress.current() progress?.addObserver(self, forKeyPath: "fractionCompleted", options: .initial, context: &myContext) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { if context == &myContext { if let newValue = change?[NSKeyValueChangeKey.newKey] { print("Progress changed: \(newValue)") } } else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } override func titleForScreen() -> String { "JSON cache" } override func setupCache() { super.setupCache() cache = CacheProvider.JSONCache() } } ================================================ FILE: Example/Example/MemoryWarningSampleViewController.swift ================================================ import Carlos import Combine import Foundation import UIKit class MemoryWarningSampleViewController: BaseCacheViewController { private var cache: BasicCache! private var token: NSObjectProtocol? var cancellable: AnyCancellable? override func fetchRequested() { super.fetchRequested() cancellable = cache.get(URL(string: urlKeyField?.text ?? "")!) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } override func titleForScreen() -> String { "Memory warnings" } override func setupCache() { super.setupCache() cache = simpleCache() } @IBAction func memoryWarningSwitchValueChanged(_ sender: UISwitch) { if sender.isOn, token == nil { token = cache.listenToMemoryWarnings() } else if let token = token, !sender.isOn { unsubscribeToMemoryWarnings(token) self.token = nil } } @IBAction func simulateMemoryWarning(_: AnyObject) { NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } } ================================================ FILE: Example/Example/PooledCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine final class PooledCacheSampleViewController: BaseCacheViewController { private var cache: PoolCache>! private var cancellables = Set() override func fetchRequested() { super.fetchRequested() let timestamp = Date().timeIntervalSince1970 eventsLogView.text = "\(eventsLogView.text!)Request timestamp: \(timestamp)\n" cache.get(URL(string: urlKeyField?.text ?? "")!) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { _ in }, receiveValue: { _ in self.eventsLogView.text = "\(self.eventsLogView.text!)Request with timestamp \(timestamp) succeeded\n" }).store(in: &cancellables) } override func titleForScreen() -> String { "Pooled cache" } override func setupCache() { super.setupCache() cache = delayedNetworkCache().pooled() } } ================================================ FILE: Example/Example/SwitchCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine final class SwitchCacheSampleViewController: BaseCacheViewController { private var cache: BasicCache! private var cancellables = Set() override func fetchRequested() { super.fetchRequested() cache.get(URL(string: urlKeyField?.text ?? "")!) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } override func titleForScreen() -> String { "Switched caches" } override func setupCache() { super.setupCache() let lane1 = MemoryCacheLevel() lane1.set(("Yes, this is hitting the memory cache now".data(using: .utf8, allowLossyConversion: false) as NSData?)!, forKey: URL(string: "test")!) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) lane1.set(("Carlos lets you create quite complex cache infrastructures".data(using: .utf8, allowLossyConversion: false) as NSData?)!, forKey: URL(string: "carlos")!) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) let lane2 = CacheProvider.dataCache() cache = switchLevels(cacheA: lane1, cacheB: lane2, switchClosure: { key in if key.scheme == "http" { return .cacheB } else { return .cacheA } }) } } ================================================ FILE: Example/Example/UserDefaultsCacheSampleViewController.swift ================================================ import Foundation import UIKit import Carlos import Combine final class UserDefaultsCacheSampleViewController: BaseCacheViewController { private var cache: NSUserDefaultsCacheLevel! private var cancellables = Set() override func fetchRequested() { super.fetchRequested() cache.get(urlKeyField?.text ?? "") .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } override func titleForScreen() -> String { "User defaults cache" } @IBAction func clearCache(_: AnyObject) { cache.clear() } override func setupCache() { super.setupCache() cache = NSUserDefaultsCacheLevel() let values = [ "test": "value".data(using: String.Encoding.utf8)!, "key": "another value".data(using: String.Encoding.utf8)! ] for (key, value) in values { cache.set(value as NSData, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } let prepopulatingMessage = values.reduce("") { accumulator, value in "\(accumulator)\n\(value.0): \(value.1)" } eventsLogView.text = "\(eventsLogView.text!)Prepopulating the cache:\n\(prepopulatingMessage)\n" } } ================================================ FILE: Example/Example.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ 5208636F1B64268000F740C7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863661B64268000F740C7 /* AppDelegate.swift */; }; 520863701B64268000F740C7 /* BaseCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863671B64268000F740C7 /* BaseCacheViewController.swift */; }; 520863711B64268000F740C7 /* ComplexCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863681B64268000F740C7 /* ComplexCacheSampleViewController.swift */; }; 520863721B64268000F740C7 /* ConditionedCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863691B64268000F740C7 /* ConditionedCacheSampleViewController.swift */; }; 520863731B64268000F740C7 /* ExampleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5208636A1B64268000F740C7 /* ExampleCell.swift */; }; 520863741B64268000F740C7 /* ExamplesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5208636B1B64268000F740C7 /* ExamplesListViewController.swift */; }; 520863751B64268000F740C7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5208636C1B64268000F740C7 /* Images.xcassets */; }; 520863761B64268000F740C7 /* MemoryWarningSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5208636D1B64268000F740C7 /* MemoryWarningSampleViewController.swift */; }; 520863771B64268000F740C7 /* DataCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5208636E1B64268000F740C7 /* DataCacheSampleViewController.swift */; }; 5208637F1B64268E00F740C7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5208637C1B64268E00F740C7 /* Main.storyboard */; }; 520863811B642D4A00F740C7 /* PooledCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863801B642D4A00F740C7 /* PooledCacheSampleViewController.swift */; }; 520863851B64314400F740C7 /* DelayedNetworkFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520863841B64314400F740C7 /* DelayedNetworkFetcher.swift */; }; 5216CD0F1BC04B43005B729F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216CD0E1BC04B43005B729F /* AppDelegate.swift */; }; 5216CD111BC04B43005B729F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216CD101BC04B43005B729F /* ViewController.swift */; }; 5216CD141BC04B43005B729F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5216CD121BC04B43005B729F /* Main.storyboard */; }; 5216CD161BC04B43005B729F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5216CD151BC04B43005B729F /* Assets.xcassets */; }; 522A4E6B1B7BDACB00646D52 /* ImageCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522A4E6A1B7BDACB00646D52 /* ImageCacheSampleViewController.swift */; }; 5235BAD51BE16B010049CFA6 /* UserDefaultsCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5235BAD41BE16B010049CFA6 /* UserDefaultsCacheSampleViewController.swift */; }; 523BBF891BC07C410040DA1B /* JSONCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523BBF881BC07C410040DA1B /* JSONCacheSampleViewController.swift */; }; 528FAD481B7B0DCB0020A70E /* SwitchCacheSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528FAD471B7B0DCB0020A70E /* SwitchCacheSampleViewController.swift */; }; 52CF0A8E1B91F5EE0061022D /* CountryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CF0A8D1B91F5EE0061022D /* CountryRow.swift */; }; 52CF0AD11B920DC20061022D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CF0AD01B920DC20061022D /* AppDelegate.swift */; }; 52CF0AD31B920DC20061022D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CF0AD21B920DC20061022D /* ViewController.swift */; }; 52CF0AD51B920DC20061022D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52CF0AD41B920DC20061022D /* Assets.xcassets */; }; 52CF0AD81B920DC20061022D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52CF0AD61B920DC20061022D /* Main.storyboard */; }; AD64D97E1B8DE69200C783D3 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD64D97C1B8DE69200C783D3 /* Interface.storyboard */; }; AD64D9801B8DE69200C783D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD64D97F1B8DE69200C783D3 /* Assets.xcassets */; }; AD64D9871B8DE69200C783D3 /* ExampleWatchSample Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = AD64D9861B8DE69200C783D3 /* ExampleWatchSample Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; AD64D98C1B8DE69200C783D3 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD64D98B1B8DE69200C783D3 /* InterfaceController.swift */; }; AD64D98E1B8DE69200C783D3 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD64D98D1B8DE69200C783D3 /* ExtensionDelegate.swift */; }; AD64D9901B8DE69200C783D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD64D98F1B8DE69200C783D3 /* Assets.xcassets */; }; AD64D9941B8DE69200C783D3 /* ExampleWatchSample.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = AD64D97A1B8DE69200C783D3 /* ExampleWatchSample.app */; }; CC6E521924EC192700DF1D80 /* Carlos in Frameworks */ = {isa = PBXBuildFile; productRef = CC6E521824EC192700DF1D80 /* Carlos */; }; CC6E521B24EC192F00DF1D80 /* Carlos in Frameworks */ = {isa = PBXBuildFile; productRef = CC6E521A24EC192F00DF1D80 /* Carlos */; }; CC6E521D24EC193500DF1D80 /* Carlos in Frameworks */ = {isa = PBXBuildFile; productRef = CC6E521C24EC193500DF1D80 /* Carlos */; }; CC6E521F24EC194E00DF1D80 /* Carlos in Frameworks */ = {isa = PBXBuildFile; productRef = CC6E521E24EC194E00DF1D80 /* Carlos */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ AD64D9881B8DE69200C783D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD695CD71B46CD65004E998D /* Project object */; proxyType = 1; remoteGlobalIDString = AD64D9851B8DE69200C783D3; remoteInfo = "CarlosWatchSample Extension"; }; AD64D9921B8DE69200C783D3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = AD695CD71B46CD65004E998D /* Project object */; proxyType = 1; remoteGlobalIDString = AD64D9791B8DE69200C783D3; remoteInfo = CarlosWatchSample; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ AD64D9981B8DE69200C783D3 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( AD64D9871B8DE69200C783D3 /* ExampleWatchSample Extension.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; }; ADBC8F4C1B8DE4AB0080EF0A /* Embed Watch Content */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; dstSubfolderSpec = 16; files = ( AD64D9941B8DE69200C783D3 /* ExampleWatchSample.app in Embed Watch Content */, ); name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 520863661B64268000F740C7 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 520863671B64268000F740C7 /* BaseCacheViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCacheViewController.swift; sourceTree = ""; }; 520863681B64268000F740C7 /* ComplexCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplexCacheSampleViewController.swift; sourceTree = ""; }; 520863691B64268000F740C7 /* ConditionedCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConditionedCacheSampleViewController.swift; sourceTree = ""; }; 5208636A1B64268000F740C7 /* ExampleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleCell.swift; sourceTree = ""; }; 5208636B1B64268000F740C7 /* ExamplesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplesListViewController.swift; sourceTree = ""; }; 5208636C1B64268000F740C7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 5208636D1B64268000F740C7 /* MemoryWarningSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryWarningSampleViewController.swift; sourceTree = ""; }; 5208636E1B64268000F740C7 /* DataCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataCacheSampleViewController.swift; sourceTree = ""; }; 520863781B64268700F740C7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5208637D1B64268E00F740C7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 520863801B642D4A00F740C7 /* PooledCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PooledCacheSampleViewController.swift; sourceTree = ""; }; 520863841B64314400F740C7 /* DelayedNetworkFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DelayedNetworkFetcher.swift; sourceTree = ""; }; 5216CD0C1BC04B43005B729F /* ExampleTvSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleTvSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5216CD0E1BC04B43005B729F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5216CD101BC04B43005B729F /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 5216CD131BC04B43005B729F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 5216CD151BC04B43005B729F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 5216CD171BC04B43005B729F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 522A4E6A1B7BDACB00646D52 /* ImageCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCacheSampleViewController.swift; sourceTree = ""; }; 5235BAD41BE16B010049CFA6 /* UserDefaultsCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsCacheSampleViewController.swift; sourceTree = ""; }; 523BBF881BC07C410040DA1B /* JSONCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONCacheSampleViewController.swift; sourceTree = ""; }; 528FAD471B7B0DCB0020A70E /* SwitchCacheSampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCacheSampleViewController.swift; sourceTree = ""; }; 52CF0A8D1B91F5EE0061022D /* CountryRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryRow.swift; sourceTree = ""; }; 52CF0ACE1B920DC20061022D /* ExampleMacSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleMacSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 52CF0AD01B920DC20061022D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 52CF0AD21B920DC20061022D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 52CF0AD41B920DC20061022D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52CF0AD71B920DC20061022D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52CF0AD91B920DC20061022D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AD64D97A1B8DE69200C783D3 /* ExampleWatchSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleWatchSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; AD64D97D1B8DE69200C783D3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; AD64D97F1B8DE69200C783D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AD64D9811B8DE69200C783D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AD64D9861B8DE69200C783D3 /* ExampleWatchSample Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "ExampleWatchSample Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; AD64D98B1B8DE69200C783D3 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = ""; }; AD64D98D1B8DE69200C783D3 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; AD64D98F1B8DE69200C783D3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; AD64D9911B8DE69200C783D3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AD695CDF1B46CD65004E998D /* ExampleSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; CCC88FA724ADDB60008C4060 /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CCC88FAA24ADDB78008C4060 /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CCC88FAD24ADDB7F008C4060 /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CCC88FB124ADDB83008C4060 /* Carlos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Carlos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 5216CD091BC04B43005B729F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC6E521D24EC193500DF1D80 /* Carlos in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 52CF0ACB1B920DC20061022D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC6E521B24EC192F00DF1D80 /* Carlos in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; AD64D9831B8DE69200C783D3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC6E521F24EC194E00DF1D80 /* Carlos in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; AD695CDC1B46CD65004E998D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( CC6E521924EC192700DF1D80 /* Carlos in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2677A0397774353D4B14F63F /* Frameworks */ = { isa = PBXGroup; children = ( CCC88FB124ADDB83008C4060 /* Carlos.framework */, CCC88FAD24ADDB7F008C4060 /* Carlos.framework */, CCC88FAA24ADDB78008C4060 /* Carlos.framework */, CCC88FA724ADDB60008C4060 /* Carlos.framework */, ); name = Frameworks; sourceTree = ""; }; 5216CD0D1BC04B43005B729F /* CarlosTvSample */ = { isa = PBXGroup; children = ( 5216CD0E1BC04B43005B729F /* AppDelegate.swift */, 5216CD101BC04B43005B729F /* ViewController.swift */, 5216CD121BC04B43005B729F /* Main.storyboard */, 5216CD151BC04B43005B729F /* Assets.xcassets */, 5216CD171BC04B43005B729F /* Info.plist */, ); path = CarlosTvSample; sourceTree = ""; }; 52CF0ACF1B920DC20061022D /* CarlosMacSample */ = { isa = PBXGroup; children = ( 52CF0AD01B920DC20061022D /* AppDelegate.swift */, 52CF0AD21B920DC20061022D /* ViewController.swift */, 52CF0AD41B920DC20061022D /* Assets.xcassets */, 52CF0AD61B920DC20061022D /* Main.storyboard */, 52CF0AD91B920DC20061022D /* Info.plist */, ); path = CarlosMacSample; sourceTree = ""; }; AD64D97B1B8DE69200C783D3 /* CarlosWatchSample */ = { isa = PBXGroup; children = ( AD64D97C1B8DE69200C783D3 /* Interface.storyboard */, AD64D97F1B8DE69200C783D3 /* Assets.xcassets */, AD64D9811B8DE69200C783D3 /* Info.plist */, ); path = CarlosWatchSample; sourceTree = ""; }; AD64D98A1B8DE69200C783D3 /* CarlosWatchSample Extension */ = { isa = PBXGroup; children = ( AD64D98B1B8DE69200C783D3 /* InterfaceController.swift */, AD64D98D1B8DE69200C783D3 /* ExtensionDelegate.swift */, AD64D98F1B8DE69200C783D3 /* Assets.xcassets */, AD64D9911B8DE69200C783D3 /* Info.plist */, 52CF0A8D1B91F5EE0061022D /* CountryRow.swift */, ); path = "CarlosWatchSample Extension"; sourceTree = ""; }; AD695CD61B46CD65004E998D = { isa = PBXGroup; children = ( AD695CE11B46CD65004E998D /* Example */, AD64D97B1B8DE69200C783D3 /* CarlosWatchSample */, AD64D98A1B8DE69200C783D3 /* CarlosWatchSample Extension */, 52CF0ACF1B920DC20061022D /* CarlosMacSample */, 5216CD0D1BC04B43005B729F /* CarlosTvSample */, AD695CE01B46CD65004E998D /* Products */, 2677A0397774353D4B14F63F /* Frameworks */, ); sourceTree = ""; }; AD695CE01B46CD65004E998D /* Products */ = { isa = PBXGroup; children = ( AD695CDF1B46CD65004E998D /* ExampleSample.app */, AD64D97A1B8DE69200C783D3 /* ExampleWatchSample.app */, AD64D9861B8DE69200C783D3 /* ExampleWatchSample Extension.appex */, 52CF0ACE1B920DC20061022D /* ExampleMacSample.app */, 5216CD0C1BC04B43005B729F /* ExampleTvSample.app */, ); name = Products; sourceTree = ""; }; AD695CE11B46CD65004E998D /* Example */ = { isa = PBXGroup; children = ( AD695CE21B46CD65004E998D /* Supporting Files */, 5208636C1B64268000F740C7 /* Images.xcassets */, 5208637C1B64268E00F740C7 /* Main.storyboard */, 520863661B64268000F740C7 /* AppDelegate.swift */, 520863671B64268000F740C7 /* BaseCacheViewController.swift */, 520863681B64268000F740C7 /* ComplexCacheSampleViewController.swift */, 520863691B64268000F740C7 /* ConditionedCacheSampleViewController.swift */, 5208636A1B64268000F740C7 /* ExampleCell.swift */, 5208636B1B64268000F740C7 /* ExamplesListViewController.swift */, 5208636D1B64268000F740C7 /* MemoryWarningSampleViewController.swift */, 5208636E1B64268000F740C7 /* DataCacheSampleViewController.swift */, 520863801B642D4A00F740C7 /* PooledCacheSampleViewController.swift */, 520863841B64314400F740C7 /* DelayedNetworkFetcher.swift */, 528FAD471B7B0DCB0020A70E /* SwitchCacheSampleViewController.swift */, 522A4E6A1B7BDACB00646D52 /* ImageCacheSampleViewController.swift */, 523BBF881BC07C410040DA1B /* JSONCacheSampleViewController.swift */, 5235BAD41BE16B010049CFA6 /* UserDefaultsCacheSampleViewController.swift */, ); path = Example; sourceTree = SOURCE_ROOT; }; AD695CE21B46CD65004E998D /* Supporting Files */ = { isa = PBXGroup; children = ( 520863781B64268700F740C7 /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 5216CD0B1BC04B43005B729F /* ExampleTvSample */ = { isa = PBXNativeTarget; buildConfigurationList = 5216CD241BC04B43005B729F /* Build configuration list for PBXNativeTarget "ExampleTvSample" */; buildPhases = ( 5216CD081BC04B43005B729F /* Sources */, 5216CD091BC04B43005B729F /* Frameworks */, 5216CD0A1BC04B43005B729F /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ExampleTvSample; packageProductDependencies = ( CC6E521C24EC193500DF1D80 /* Carlos */, ); productName = CarlosTvSample; productReference = 5216CD0C1BC04B43005B729F /* ExampleTvSample.app */; productType = "com.apple.product-type.application"; }; 52CF0ACD1B920DC20061022D /* ExampleMacSample */ = { isa = PBXNativeTarget; buildConfigurationList = 52CF0ADA1B920DC20061022D /* Build configuration list for PBXNativeTarget "ExampleMacSample" */; buildPhases = ( 52CF0ACA1B920DC20061022D /* Sources */, 52CF0ACB1B920DC20061022D /* Frameworks */, 52CF0ACC1B920DC20061022D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ExampleMacSample; packageProductDependencies = ( CC6E521A24EC192F00DF1D80 /* Carlos */, ); productName = CarlosMacSample; productReference = 52CF0ACE1B920DC20061022D /* ExampleMacSample.app */; productType = "com.apple.product-type.application"; }; AD64D9791B8DE69200C783D3 /* ExampleWatchSample */ = { isa = PBXNativeTarget; buildConfigurationList = AD64D9991B8DE69200C783D3 /* Build configuration list for PBXNativeTarget "ExampleWatchSample" */; buildPhases = ( AD64D9781B8DE69200C783D3 /* Resources */, AD64D9981B8DE69200C783D3 /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( AD64D9891B8DE69200C783D3 /* PBXTargetDependency */, ); name = ExampleWatchSample; productName = CarlosWatchSample; productReference = AD64D97A1B8DE69200C783D3 /* ExampleWatchSample.app */; productType = "com.apple.product-type.application.watchapp2"; }; AD64D9851B8DE69200C783D3 /* ExampleWatchSample Extension */ = { isa = PBXNativeTarget; buildConfigurationList = AD64D9951B8DE69200C783D3 /* Build configuration list for PBXNativeTarget "ExampleWatchSample Extension" */; buildPhases = ( AD64D9821B8DE69200C783D3 /* Sources */, AD64D9831B8DE69200C783D3 /* Frameworks */, AD64D9841B8DE69200C783D3 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "ExampleWatchSample Extension"; packageProductDependencies = ( CC6E521E24EC194E00DF1D80 /* Carlos */, ); productName = "CarlosWatchSample Extension"; productReference = AD64D9861B8DE69200C783D3 /* ExampleWatchSample Extension.appex */; productType = "com.apple.product-type.watchkit2-extension"; }; AD695CDE1B46CD65004E998D /* ExampleSample */ = { isa = PBXNativeTarget; buildConfigurationList = AD695CFE1B46CD65004E998D /* Build configuration list for PBXNativeTarget "ExampleSample" */; buildPhases = ( AD695CDB1B46CD65004E998D /* Sources */, AD695CDC1B46CD65004E998D /* Frameworks */, AD695CDD1B46CD65004E998D /* Resources */, ADBC8F4C1B8DE4AB0080EF0A /* Embed Watch Content */, ); buildRules = ( ); dependencies = ( AD64D9931B8DE69200C783D3 /* PBXTargetDependency */, ); name = ExampleSample; packageProductDependencies = ( CC6E521824EC192700DF1D80 /* Carlos */, ); productName = CarlosSample; productReference = AD695CDF1B46CD65004E998D /* ExampleSample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ AD695CD71B46CD65004E998D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = WeltN24; TargetAttributes = { 5216CD0B1BC04B43005B729F = { CreatedOnToolsVersion = 7.1; LastSwiftMigration = 0800; }; 52CF0ACD1B920DC20061022D = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1150; }; AD64D9791B8DE69200C783D3 = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 0920; }; AD64D9851B8DE69200C783D3 = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; }; AD695CDE1B46CD65004E998D = { CreatedOnToolsVersion = 6.3.2; LastSwiftMigration = 1020; }; }; }; buildConfigurationList = AD695CDA1B46CD65004E998D /* Build configuration list for PBXProject "Example" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = AD695CD61B46CD65004E998D; productRefGroup = AD695CE01B46CD65004E998D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( AD695CDE1B46CD65004E998D /* ExampleSample */, AD64D9791B8DE69200C783D3 /* ExampleWatchSample */, AD64D9851B8DE69200C783D3 /* ExampleWatchSample Extension */, 52CF0ACD1B920DC20061022D /* ExampleMacSample */, 5216CD0B1BC04B43005B729F /* ExampleTvSample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 5216CD0A1BC04B43005B729F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 5216CD161BC04B43005B729F /* Assets.xcassets in Resources */, 5216CD141BC04B43005B729F /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 52CF0ACC1B920DC20061022D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 52CF0AD51B920DC20061022D /* Assets.xcassets in Resources */, 52CF0AD81B920DC20061022D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD64D9781B8DE69200C783D3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AD64D9801B8DE69200C783D3 /* Assets.xcassets in Resources */, AD64D97E1B8DE69200C783D3 /* Interface.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD64D9841B8DE69200C783D3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( AD64D9901B8DE69200C783D3 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD695CDD1B46CD65004E998D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 520863751B64268000F740C7 /* Images.xcassets in Resources */, 5208637F1B64268E00F740C7 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 5216CD081BC04B43005B729F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 5216CD111BC04B43005B729F /* ViewController.swift in Sources */, 5216CD0F1BC04B43005B729F /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 52CF0ACA1B920DC20061022D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 52CF0AD31B920DC20061022D /* ViewController.swift in Sources */, 52CF0AD11B920DC20061022D /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD64D9821B8DE69200C783D3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 52CF0A8E1B91F5EE0061022D /* CountryRow.swift in Sources */, AD64D98E1B8DE69200C783D3 /* ExtensionDelegate.swift in Sources */, AD64D98C1B8DE69200C783D3 /* InterfaceController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; AD695CDB1B46CD65004E998D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 520863731B64268000F740C7 /* ExampleCell.swift in Sources */, 5235BAD51BE16B010049CFA6 /* UserDefaultsCacheSampleViewController.swift in Sources */, 520863701B64268000F740C7 /* BaseCacheViewController.swift in Sources */, 520863851B64314400F740C7 /* DelayedNetworkFetcher.swift in Sources */, 528FAD481B7B0DCB0020A70E /* SwitchCacheSampleViewController.swift in Sources */, 522A4E6B1B7BDACB00646D52 /* ImageCacheSampleViewController.swift in Sources */, 5208636F1B64268000F740C7 /* AppDelegate.swift in Sources */, 520863721B64268000F740C7 /* ConditionedCacheSampleViewController.swift in Sources */, 520863741B64268000F740C7 /* ExamplesListViewController.swift in Sources */, 520863711B64268000F740C7 /* ComplexCacheSampleViewController.swift in Sources */, 520863811B642D4A00F740C7 /* PooledCacheSampleViewController.swift in Sources */, 520863761B64268000F740C7 /* MemoryWarningSampleViewController.swift in Sources */, 520863771B64268000F740C7 /* DataCacheSampleViewController.swift in Sources */, 523BBF891BC07C410040DA1B /* JSONCacheSampleViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ AD64D9891B8DE69200C783D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AD64D9851B8DE69200C783D3 /* ExampleWatchSample Extension */; targetProxy = AD64D9881B8DE69200C783D3 /* PBXContainerItemProxy */; }; AD64D9931B8DE69200C783D3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AD64D9791B8DE69200C783D3 /* ExampleWatchSample */; targetProxy = AD64D9921B8DE69200C783D3 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 5208637C1B64268E00F740C7 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 5208637D1B64268E00F740C7 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 5216CD121BC04B43005B729F /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 5216CD131BC04B43005B729F /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 52CF0AD61B920DC20061022D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 52CF0AD71B920DC20061022D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; AD64D97C1B8DE69200C783D3 /* Interface.storyboard */ = { isa = PBXVariantGroup; children = ( AD64D97D1B8DE69200C783D3 /* Base */, ); name = Interface.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 5216CD181BC04B43005B729F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; DEBUG_INFORMATION_FORMAT = dwarf; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = CarlosTvSample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosTvSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 4.2; TVOS_DEPLOYMENT_TARGET = 13.0; }; name = Debug; }; 5216CD191BC04B43005B729F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = CarlosTvSample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosTvSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 4.2; TVOS_DEPLOYMENT_TARGET = 13.0; }; name = Release; }; 52CF0ADB1B920DC20061022D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; DEBUG_INFORMATION_FORMAT = dwarf; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = CarlosMacSample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosMacSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Debug; }; 52CF0ADC1B920DC20061022D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = CarlosMacSample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosMacSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_VERSION = 5.0; }; name = Release; }; AD64D9961B8DE69200C783D3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { DEBUG_INFORMATION_FORMAT = dwarf; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "CarlosWatchSample Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample.watchkitapp.watchkitextension; PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; SWIFT_VERSION = 5.0; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; AD64D9971B8DE69200C783D3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "CarlosWatchSample Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample.watchkitapp.watchkitextension; PRODUCT_NAME = "${TARGET_NAME}"; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; AD64D99A1B8DE69200C783D3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEBUG_INFORMATION_FORMAT = dwarf; FRAMEWORK_SEARCH_PATHS = ""; IBSC_MODULE = CarlosWatchSample_Extension; INFOPLIST_FILE = CarlosWatchSample/Info.plist; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Debug; }; AD64D99B1B8DE69200C783D3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; FRAMEWORK_SEARCH_PATHS = ""; IBSC_MODULE = CarlosWatchSample_Extension; INFOPLIST_FILE = CarlosWatchSample/Info.plist; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample.watchkitapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos"; WATCHOS_DEPLOYMENT_TARGET = 6.0; }; name = Release; }; AD695CFC1B46CD65004E998D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 0.10.0; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.carlos; PRODUCT_NAME = Carlos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos macosx watchos watchsimulator appletvsimulator"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Debug; }; AD695CFD1B46CD65004E998D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 10.15; MARKETING_VERSION = 0.10.0; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.carlos; PRODUCT_NAME = Carlos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvos macosx watchos watchsimulator appletvsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; WATCHOS_DEPLOYMENT_TARGET = 3.0; }; name = Release; }; AD695CFF1B46CD65004E998D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; INFOPLIST_FILE = Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; AD695D001B46CD65004E998D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = Example/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = de.axelspringer.CarlosSample; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 5216CD241BC04B43005B729F /* Build configuration list for PBXNativeTarget "ExampleTvSample" */ = { isa = XCConfigurationList; buildConfigurations = ( 5216CD181BC04B43005B729F /* Debug */, 5216CD191BC04B43005B729F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 52CF0ADA1B920DC20061022D /* Build configuration list for PBXNativeTarget "ExampleMacSample" */ = { isa = XCConfigurationList; buildConfigurations = ( 52CF0ADB1B920DC20061022D /* Debug */, 52CF0ADC1B920DC20061022D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD64D9951B8DE69200C783D3 /* Build configuration list for PBXNativeTarget "ExampleWatchSample Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( AD64D9961B8DE69200C783D3 /* Debug */, AD64D9971B8DE69200C783D3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD64D9991B8DE69200C783D3 /* Build configuration list for PBXNativeTarget "ExampleWatchSample" */ = { isa = XCConfigurationList; buildConfigurations = ( AD64D99A1B8DE69200C783D3 /* Debug */, AD64D99B1B8DE69200C783D3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD695CDA1B46CD65004E998D /* Build configuration list for PBXProject "Example" */ = { isa = XCConfigurationList; buildConfigurations = ( AD695CFC1B46CD65004E998D /* Debug */, AD695CFD1B46CD65004E998D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AD695CFE1B46CD65004E998D /* Build configuration list for PBXNativeTarget "ExampleSample" */ = { isa = XCConfigurationList; buildConfigurations = ( AD695CFF1B46CD65004E998D /* Debug */, AD695D001B46CD65004E998D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ CC6E521824EC192700DF1D80 /* Carlos */ = { isa = XCSwiftPackageProductDependency; productName = Carlos; }; CC6E521A24EC192F00DF1D80 /* Carlos */ = { isa = XCSwiftPackageProductDependency; productName = Carlos; }; CC6E521C24EC193500DF1D80 /* Carlos */ = { isa = XCSwiftPackageProductDependency; productName = Carlos; }; CC6E521E24EC194E00DF1D80 /* Carlos */ = { isa = XCSwiftPackageProductDependency; productName = Carlos; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = AD695CD71B46CD65004E998D /* Project object */; } ================================================ FILE: Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'fastlane', '~> 2.140' gem 'rake' ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 SPRING AS Digital News Media GmbH 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: MIGRATING.md ================================================ ## Migrating from 0.8 to 0.9 ### Swift 3 support Please make sure to convert all usages of `Carlos` APIs to the Swift 3 counterparts. In most cases there won't be a big syntactic difference. ### Deprecated functions All deprecated functions have been removed. This were of 2 types: - Global functions - Functions taking fetch closures or transformation closures For the first type, just use the corresponding protocol extension function. For the second type, please use `BasicFetcher` instead of fetch closures, and explicit `OneWayTransformationBox` instances instead of transformation closures. ### Custom operators All custom operators have been removed. Please use the corresponding protocol extension functions instead. This may require some reordering of the objects. ## Migrating from 0.7 to 0.8 ### `Promise` now has only an empty `init`. If you used one of the convenience `init` (with `value:`, with `error:` or with `value:error:`), they now moved to `Future`. ```swift // Before let future = Promise(value: 10).future // Now let future = Future(10) ``` ```swift // Before let future = Promise(error: MyError.SomeError).future // Now let future = Future(MyError.SomeError) ``` ```swift // Before let future = Promise(value: someOptionalInt, error: MyError.InvalidConversion).future // Now let future = Future(value: someOptionalInt, error: MyError.InvalidConversion) ``` ## Migrating from 0.6 to 0.7 ##### - Please note that with `Carlos` 0.7 the `Future` and `Promise`s code has been moved to a new framework. - If you use `CocoaPods` or `Carthage`, you will just have to add a `import PiedPiper` line everywhere you make use of Carlos' `Future`s. - If you did a submodule integration, please add `PiedPiper` as `Embeded binary` to your target. - If you did a manual integration, please make sure that all the files missing from your target are re-added from the `Futures` folder. ##### - Check all your usages of `onCompletion` and replace the tuple `(value, error)` with the value `result`. Code will look like the following: *Before* ```swift future.onCompletion { (value, error) in if let value = value { //handle success case } else if let error = error { //handle error case } else { //handle cancelation case } } ``` *Now* ```swift future.onCompletion { result in switch result { case .Success(let value): //handle success case case .Error(let error): //handle error case case .Cancelled: //handle cancelation case } } ``` ##### - Check all your usages of closures in the API. Methods taking closures instead of `Fetcher`, `CacheLevel` or `OneWayTransformer` values have been deprecated. ## Migrating from 0.4 to 0.5 ##### - Rename all your usages of `CacheRequest` to `Future` ##### - If you created come custom `CacheLevel` or `Fetcher`, you should internally use `Promise` instead of `Future` so that you can control when the request can `fail` or `succeed`. `Future` is in fact a read-only version of `Promise` *Before* ```swift class MyCustomLevel: Fetcher { typealias KeyType = String typealias OutputType = String func get(key: String) -> CacheRequest { let request = CacheRequest() //Do stuff... request.succeed("Yay!") return request } } ``` *Now* ```swift class MyCustomLevel: Fetcher { typealias KeyType = String typealias OutputType = String func get(key: String) -> Future { let request = Promise() //Do stuff... request.succeed("Yay!") return request.future } } ``` ##### - If you created some custom `OneWayTransformer` or `TwoWayTransformer`, you should return `Future` now. This means that you have to wrap simple return values into `Future` instances *Before* ```swift let transformer = OneWayTransformationBox(transform: { $0.uppercaseString }) ``` *Now* ```swift let transformer = OneWayTransformationBox(transform: { Promise(value: $0.uppercaseString).future }) ``` ##### - If you used the `conditioned` API, the same async changes apply *Before* ```swift let conditionedCache = myCache.conditioned { key in //whatever return true } ``` *Now* ```swift let conditionedCache = myCache.conditioned { key in return Promise(value: true).future } ``` ##### - If you use global functions, please consider using protocol extensions instead. Global functions are now **deprecated** and will be discontinued in `Carlos 1.0` For example: *Before* ```swift let cache = compose(firstLevel, secondLevel) ``` *Now* ```swift let cache = firstLevel.compose(secondLevel) ``` ================================================ FILE: Package.resolved ================================================ { "object": { "pins": [ { "package": "CwlCatchException", "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", "state": { "branch": null, "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", "version": "2.1.1" } }, { "package": "CwlPreconditionTesting", "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", "state": { "branch": null, "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", "version": "2.1.0" } }, { "package": "Nimble", "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, "revision": "c93f16c25af5770f0d3e6af27c9634640946b068", "version": "9.2.1" } }, { "package": "Quick", "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, "revision": "bd86ca0141e3cfb333546de5a11ede63f0c4a0e6", "version": "4.0.0" } } ] }, "version": 1 } ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.2 import PackageDescription let package = Package( name: "Carlos", platforms: [ .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6) ], products: [ .library( name: "Carlos", targets: ["Carlos"] ) ], dependencies: [ .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "9.2.0")) ], targets: [ .target( name: "Carlos", dependencies: [] ), .testTarget( name: "CarlosTests", dependencies: [ "Carlos", "Quick", "Nimble" ] ) ], swiftLanguageVersions: [.v5] ) ================================================ FILE: README.md ================================================ # Carlos [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) > A simple but flexible cache, written in Swift for `iOS 13+` and `WatchOS 6` apps. # Breaking Changes Carlos 1.0.0 has been migrated from PiedPiper dependency to Combine hence the minimum supported platforms versions are equal to the Combine's minimum supported platforms versions. See the releases page for more information. # Contents of this Readme - [Carlos](#carlos) - [Breaking Changes](#breaking-changes) - [Contents of this Readme](#contents-of-this-readme) - [What is Carlos?](#what-is-carlos) - [Installation](#installation) - [Swift Package Manager (Preferred)](#swift-package-manager-preferred) - [CocoaPods](#cocoapods) - [Carthage](#carthage) - [Requirements](#requirements) - [Usage](#usage) - [Usage examples](#usage-examples) - [Creating requests](#creating-requests) - [Key transformations](#key-transformations) - [Value transformations](#value-transformations) - [Post-processing output](#post-processing-output) - [Conditioned output post-processing](#conditioned-output-post-processing) - [Conditioned value transformation](#conditioned-value-transformation) - [Composing transformers](#composing-transformers) - [Pooling requests](#pooling-requests) - [Batching get requests](#batching-get-requests) - [Conditioning caches](#conditioning-caches) - [Multiple cache lanes](#multiple-cache-lanes) - [Listening to memory warnings](#listening-to-memory-warnings) - [Normalization](#normalization) - [Creating custom levels](#creating-custom-levels) - [Creating custom fetchers](#creating-custom-fetchers) - [Built-in levels](#built-in-levels) - [Logging](#logging) - [Tests](#tests) - [Future development](#future-development) - [Apps using Carlos](#apps-using-carlos) - [Authors](#authors) - [Contributors:](#contributors) - [License](#license) - [Acknowledgements](#acknowledgements) ## What is Carlos? `Carlos` is a small set of classes and functions to **realize custom, flexible and powerful cache layers** in your application. With a Functional Programming vocabulary, Carlos makes for a monoidal cache system. You can check the best explanation of how that is realized [here](https://bkase.github.io/slides/composable-caching-swift/) or in [this video](https://www.youtube.com/watch?v=8uqXuEZLyUU), thanks to [@bkase](https://github.com/bkase) for the slides. By default, **`Carlos` ships with an in-memory cache, a disk cache, a simple network fetcher and a `NSUserDefaults` cache** (the disk cache is inspired by [HanekeSwift](https://github.com/Haneke/HanekeSwift)). With `Carlos` you can: - **[create levels and fetchers](#creating-custom-levels)** depending on your needs - [combine levels](#usage-examples) - Cancel pending requests - [transform the key](#key-transformations) each level will get, [or the values](#value-transformations) each level will output (this means you're free to implement every level independing on how it will be used later on). Some common value transformers are already provided with `Carlos` - Apply [post-processing steps](#post-processing-output) to a cache level, for example sanitizing the output or resizing images - [Post-processing steps](#conditioned-output-post-processing) and [value transformations](#conditioned-value-transformation) can also be applied conditionally on the key used to fetch the value - [react to memory pressure events](#listening-to-memory-warnings) in your app - **automatically populate upper levels when one of the lower levels fetches a value** for a key, so the next time the first level will already have it cached - enable or disable specific levels of your composed cache depending on [boolean conditions](#conditioning-caches) - easily [**pool requests**](#pooling-requests) so you don't have to care whether 5 requests with the same key have to be executed by an expensive cache level before even only 1 of them is done. `Carlos` can take care of that for you - [batch get requests](#batching-get-requests) to only get notified when all of them are done - setup [multiple lanes](#multiple-cache-lanes) for complex scenarios where, depending on certain keys or conditions, different caches should be used - have a type-safe complex cache that won't even compile if the code doesn't satisfy the type requirements ## Installation ### Swift Package Manager (Preferred) Add `Carlos` to your project through the Xcode or add the following line to your package dependencies: ```swift .package("https://github.com/spring-media/Carlos", from: "1.0.0") ``` ### CocoaPods `Carlos` is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: ``` pod "Carlos", :git => "https://github.com/spring-media/Carlos" ``` ### Carthage `Carthage` is also supported. ## Requirements - iOS 13.0+ - WatchOS 6+ - Xcode 12+ ## Usage To run the example project, clone the repo. ### Usage examples ```swift let cache = MemoryCacheLevel().compose(DiskCacheLevel()) ``` This line will generate a cache that takes `String` keys and returns `NSData` values. Setting a value for a given key on this cache will set it for both the levels. Getting a value for a given key on this cache will first try getting it on the memory level, and if it cannot find one, will ask the disk level. In case both levels don't have a value, the request will fail. In case the disk level can fetch a value, this will also be set on the memory level so that the next fetch will be faster. `Carlos` comes with a `CacheProvider` class so that standard caches are easily accessible. - `CacheProvider.dataCache()` to create a cache that takes `URL` keys and returns `NSData` values - `CacheProvider.imageCache()` to create a cache that takes `URL` keys and returns `UIImage` values - `CacheProvider.JSONCache()` to create a cache that takes `URL` keys and returns `AnyObject` values (that should be then safely casted to arrays or dictionaries depending on your application) The above methods always create new instances (so calling `CacheProvider.imageCache()` twice doesn't return the same instance, even though the disk level will be effectively shared because it will use the same folder on disk, but this is a side-effect and should not be relied upon) and you should take care of retaining the result in your application layer. If you want to always get the same instance, you can use the following accessors instead: - `CacheProvider.sharedDataCache` to retrieve a shared instance of a data cache - `CacheProvider.sharedImageCache` to retrieve a shared instance of an image cache - `CacheProvider.sharedJSONCache` to retrieve a shared instance of a JSON cache ### Creating requests To fetch a value from a cache, use the `get` method. ```swift cache.get("key") .sink( receiveCompletion: { completion in if case let .failure(error) = completion { print("An error occurred :( \(error)") } }, receiveValue: { value in print("I found \(value)!") } ) ``` A request can also be canceled with the `cancel()` method, and you can be notified of this event by calling `onCancel` on a given request: ```swift let cancellable = cache.get(key) .handleEvents(receiveCancel: { print("Looks like somebody canceled this request!") }) .sink(...) [... somewhere else] cancellable.cancel() ``` This cache is not very useful, though. It will never *actively* fetch values, just store them for later use. Let's try to make it more interesting: ```swift let cache = MemoryCacheLevel() .compose(DiskCacheLevel()) .compose(NetworkFetcher()) ``` This will create a cache level that takes `URL` keys and stores `NSData` values (the type is inferred from the `NetworkFetcher` hard-requirement of `URL` keys and `NSData` values, while `MemoryCacheLevel` and `DiskCacheLevel` are much more flexible as described later). ### Key transformations Key transformations are meant to make it possible to plug cache levels in whatever cache you're building. Let's see how they work: ```swift // Define your custom ErrorType values enum URLTransformationError: Error { case invalidURLString } let transformedCache = NetworkFetcher().transformKeys( OneWayTransformationBox( transform: { Future { promise in let url = URL(string: $0) { promise(.success(url)) } else { promise(.failure(URLTransformationError.invalidURLString)) } } } ) ) ``` With the line above, we're saying that all the keys coming into the NetworkFetcher level have to be transformed to `URL` values first. We can now plug this cache into a previously defined cache level that takes `String` keys: ```swift let cache = MemoryCacheLevel().compose(transformedCache) ``` If this doesn't look very safe (one could always pass string garbage as a key and it won't magically translate to a `URL`, thus causing the `NetworkFetcher` to silently fail), we can still use a domain specific structure as a key, assuming it contains both `String` and `URL` values: ```swift struct Image { let identifier: String let URL: Foundation.URL } let imageToString = OneWayTransformationBox(transform: { (image: Image) -> AnyPublisher in Just(image.identifier).eraseToAnyPublisher() }) let imageToURL = OneWayTransformationBox(transform: { (image: Image) -> AnyPublisher in Just(image.URL).eraseToAnyPublisher() }) let memoryLevel = MemoryCacheLevel().transformKeys(imageToString) let diskLevel = DiskCacheLevel().transformKeys(imageToString) let networkLevel = NetworkFetcher().transformKeys(imageToURL) let cache = memoryLevel.compose(diskLevel).compose(networkLevel) ``` Now we can perform safe requests like this: ```swift let image = Image(identifier: "550e8400-e29b-41d4-a716-446655440000", URL: URL(string: "http://goo.gl/KcGz8T")!) cache.get(image).sink { print("Found \(value)!") } ``` Since `Carlos 0.5` you can also apply conditions to `OneWayTransformers` used for key transformations. Just call the `conditioned` function on the transformer and pass your condition. The condition can also be asynchronous and has to return a `AnyPublisher`, having the chance to return a specific error for the failure of the transformation. ```swift let transformer = OneWayTransformationBox(transform: { key in Future { promise in if let value = URL(string: key) { promise(.success(value)) } else { promise(.failure(MyError.stringIsNotURL)) } }.eraseToAnyPublisher() }).conditioned { key in Just(key) .filter { $0.rangeOfString("http") != nil } .eraseToAnyPublisher() } let cache = CacheProvider.imageCache().transformKeys(transformer) ``` That's not all, though. What if our disk cache only stores `Data`, but we want our memory cache to conveniently store `UIImage` instances instead? ### Value transformations Value transformers let you have a cache that (let's say) stores `Data` and mutate it to a cache that stores `UIImage` values. Let's see how: ```swift let dataTransformer = TwoWayTransformationBox(transform: { (image: UIImage) -> AnyPublisher in Just(UIImagePNGRepresentation(image)).eraseToAnyPublisher() }, inverseTransform: { (data: Data) -> AnyPublisher in Just(UIImage(data: data)!).eraseToAnyPublisher() }) let memoryLevel = MemoryCacheLevel().transformKeys(imageToString).transformValues(dataTransformer) ``` This memory level can now replace the one we had before, with the difference that it will internally store `UIImage` values! Keep in mind that, as with key transformations, if your transformation closure fails (either the forward transformation or the inverse transformation), the cache level will be skipped, as if the fetch would fail. Same considerations apply for `set` calls. `Carlos` comes with some value transformers out of the box, for example: - `JSONTransformer` to serialize `NSData` instances into JSON - `ImageTransformer` to serialize `NSData` instances into `UIImage` values (not available on the Mac OS X framework) - `StringTransformer` to serialize `NSData` instances into `String` values with a given encoding - Extensions for some Cocoa classes (`DateFormatter`, `NumberFormatter`, `MKDistanceFormatter`) so that you can use customized instances depending on your needs. As of `Carlos 0.4`, it's possible to transform values coming out of `Fetcher` instances with just a `OneWayTransformer` (as opposed to the required `TwoWayTransformer` for normal `CacheLevel` instancess. This is because the `Fetcher` protocol doesn't require `set`). This means you can easily chain `Fetcher`s that get a JSON from the internet and transform their output to a model object (for example a `struct`) into a complex cache pipeline without having to create a dummy inverse transformation just to satisfy the requirements of the `TwoWayTransformer` protocol. As of `Carlos 0.5`, all transformers natively support asynchronous computation, so you can have expensive transformations in your custom transformers without blocking other operations. In fact, the `ImageTransformer` that comes out of the box processes image transformations on a background queue. As of `Carlos 0.5` you can also apply conditions to `TwoWayTransformers` used for value transformations. Just call the `conditioned` function on the transformer and pass your conditions (one for the forward transformation, one for the inverse transformation). The conditions can also be asynchronous and have to return a `AnyPublisher`, having the chance to return a specific error for the failure of the transformation. ```swift let transformer = JSONTransformer().conditioned({ input in Just(myCondition).eraseToAnyPublisher() }, inverseCondition: { input in Just(myCondition)eraseToAnyPublisher() }) let cache = CacheProvider.dataCache().transformValues(transformer) ``` ### Post-processing output In some cases your cache level could return the right value, but in a sub-optimal format. For example, you would like to sanitize the output you're getting from the Cache as a whole, independently of the exact layer that returned it. For these cases, the `postProcess` function introduced with `Carlos 0.4` could come helpful. The function is available as a protocol extension of the `CacheLevel` protocol. The `postProcess` function takes a `CacheLevel` and a `OneWayTransformer` with `TypeIn == TypeOut` as parameters and outputs a decorated `BasicCache` with the post-processing step embedded in. ```swift // Let's create a simple "to uppercase" transformer let transformer = OneWayTransformationBox(transform: { Just($0.uppercased() as String).eraseToAnyPublisher() }) // Our memory cache let memoryCache = MemoryCacheLevel() // Our decorated cache let transformedCache = memoryCache.postProcess(transformer) // Lowercase value set on the memory layer memoryCache.set("test String", forKey: "key") // We get the lowercase value from the undecorated memory layer memoryCache.get("key").sink { value in let x = value } // We get the uppercase value from the decorated cache, though transformedCache.get("key").sink { value in let x = value } ``` Since `Carlos 0.5` you can also apply conditions to `OneWayTransformers` used for post processing transformations. Just call the `conditioned` function on the transformer and pass your condition. The condition can also be asynchronous and has to return a `AnyPublisher`, having the chance to return a specific error for the failure of the transformation. Keep in mind that the condition will actually take the output of the cache as the input, not the key used to fetch this value! If you want to apply conditions based on the key, use `conditionedPostProcess` instead, but keep in mind this doesn't support using `OneWayTransformer` instances yet. ```swift let processer = OneWayTransformationBox(transform: { value in Future { promise in if let value = String(data: value as Data, encoding: .utf8)?.uppercased().data(using: .utf8) as NSData? { promise(.success(value)) } else { promise(.failure(FetchError.conditionNotSatisfied)) } } }).conditioned { value in Just(value.length < 1000).eraseToAnyPublisher() } let cache = CacheProvider.dataCache().postProcess(processer) ``` ### Conditioned output post-processing Extending the case for simple [output post-processing](#post-processing-output), you can also apply conditional transformations based on the key used to fetch the value. For these cases, the `conditionedPostProcess` function introduced with `Carlos 0.6` could come helpful. The function is available as a protocol extension of the `CacheLevel` protocol. The `conditionedPostProcess` function takes a `CacheLevel` and a conditioned transformer conforming to `ConditionedOneWayTransformer` as parameters and outputs a decorated `CacheLevel` with the conditional post-processing step embedded in. ```swift // Our memory cache let memoryCache = MemoryCacheLevel() // Our decorated cache let transformedCache = memoryCache.conditionedPostProcess(ConditionedOneWayTransformationBox(conditionalTransformClosure: { (key, value) in if key == "some sentinel value" { return Just(value.uppercased()).eraseToAnyPublisher() } else { return Just(value).eraseToAnyPublisher() } }) // Lowercase value set on the memory layer memoryCache.set("test String", forKey: "some sentinel value") // We get the lowercase value from the undecorated memory layer memoryCache.get("some sentinel value").sink { value in let x = value } // We get the uppercase value from the decorated cache, though transformedCache.get("some sentinel value").sink { value in let x = value } ``` ### Conditioned value transformation Extending the case for simple [value transformation](#value-transformations), you can also apply conditional transformations based on the key used to fetch or set the value. For these cases, the `conditionedValueTransformation` function introduced with `Carlos 0.6` could come helpful. The function is available as a protocol extension of the `CacheLevel` protocol. The `conditionedValueTransformation` function takes a `CacheLevel` and a conditioned transformer conforming to `ConditionedTwoWayTransformer` as parameters and outputs a decorated `CacheLevel` with a modified `OutputType` (equal to the transformer's `TypeOut`, as in the normal value transformation case) with the conditional value transformation step embedded in. ```swift // Our memory cache let memoryCache = MemoryCacheLevel() // Our decorated cache let transformedCache = memoryCache.conditionedValueTransformation(ConditionedTwoWayTransformationBox(conditionalTransformClosure: { (key, value) in if key == "some sentinel value" { return Just(1).eraseToAnyPublisher() } else { return Just(0).eraseToAnyPublisher() } }, conditionalInverseTransformClosure: { (key, value) in if key > 0 { return Just("Positive").eraseToAnyPublisher() } else { return Just("Null or negative").eraseToAnyPublisher() } }) // Value set on the memory layer memoryCache.set("test String", forKey: "some sentinel value") // We get the same value from the undecorated memory layer memoryCache.get("some sentinel value").sink { value in let x = value } // We get 1 from the decorated cache, though transformedCache.get("some sentinel value").sink { value in let x = value } // We set "Positive" on the decorated cache transformedCache.set(5, forKey: "test") ``` ### Composing transformers As of `Carlos 0.4`, it's possible to compose multiple `OneWayTransformer` objects. This way, one can create several transformer modules to build a small library and then combine them as more convenient depending on the application. You can compose the transformers in the same way you do with normal `CacheLevel`s: with the `compose` protocol extension: ```swift let firstTransformer = ImageTransformer() // NSData -> UIImage let secondTransformer = ImageTransformer().invert() // Trivial UIImage -> NSData let identityTransformer = firstTransformer.compose(secondTransformer) ``` The same approach can be applied to `TwoWayTransformer` objects (that by the way are already `OneWayTransformer` as well). Many transformer modules will be provided by default with `Carlos`. ### Pooling requests When you have a working cache, but some of your levels are expensive (say a Network fetcher or a database fetcher), **you may want to pool requests in a way that multiple requests for the same key, coming together before one of them completes, are grouped so that when one completes all of the other complete as well without having to actually perform the expensive operation multiple times**. This functionality comes with `Carlos`. ```swift let cache = (memoryLevel.compose(diskLevel).compose(networkLevel)).pooled() ``` Keep in mind that the key must conform to the `Hashable` protocol for the `pooled` function to work: ```swift extension Image: Hashable { var hashValue: Int { return identifier.hashValue } } extension Image: Equatable {} func ==(lhs: Image, rhs: Image) -> Bool { return lhs.identifier == rhs.identifier && lhs.URL == rhs.URL } ``` Now we can execute multiple fetches for the same `Image` value and be sure that only one network request will be started. ### Batching get requests Since `Carlos 0.7` you can pass a list of keys to your `CacheLevel` through `batchGetSome`. This returns a `AnyPublisher` that succeeds when all the requests for the specified keys *complete*, not necessarily succeeding. You will only get the successful values in the success callback, though. Since `Carlos 0.9` you can transform your `CacheLevel` into one that takes a list of keys through `allBatch`. Calling `get` on such a `CacheLevel` returns a `AnyPublisher` that succeeds only when the requests for **all** of the specified keys succeed, and fails **as soon as one** of the requests for the specified keys fails. If you cancel the `AnyPublisher` returned by this `CacheLevel`, all of the pending requests are canceled, too. An example of the usage: ```swift let cache = MemoryCacheLevel() for iter in 0..<99 { cache.set(iter, forKey: "key_\(iter)") } let keysToBatch = (0..<100).map { "key_\($0)" } cache.batchGetSome(keysToBatch).sink( receiveCompletion: { completion in print("Failed because \($0)") }, receiveValue: { values in print("Got \(values.count) values in total") } ) ``` In this case the `allBatch().get` call would fail because there are only 99 keys set and the last request will make the whole batch fail, with a `valueNotInCache` error. The `batchGetSome().get` will succeed instead, printing `Got 99 values in total`. Since `allBatch` returns a new `CacheLevel` instance, it can be composed or transformed just like any other cache: In this case `cache` is a cache that takes a sequence of `String` keys and returns a `AnyPublisher` of a list of `Int` values, but is limited to 3 concurrent requests (see the next paragraph for more information on limiting concurrent requests). ### Conditioning caches Sometimes we may have levels that should only be queried under some conditions. Let's say we have a `DatabaseLevel` that should only be triggered when users enable a given setting in the app that actually starts storing data in the database. We may want to avoid accessing the database if the setting is disabled in the first place. ```swift let conditionedCache = cache.conditioned { key in Just(appSettingIsEnabled).eraseToAnyPublisher() } ``` The closure gets the key the cache was asked to fetch and has to return a `AnyPublisher` object indicating whether the request can proceed or should skip the level, with the possibility to fail with a specific `Error` to communicate the error to the caller. At runtime, if the variable `appSettingIsEnabled` is `false`, the `get` request will skip the level (or fail if this was the only or last level in the cache). If `true`, the `get` request will be executed. ### Multiple cache lanes If you have a complex scenario where, depending on the key or some other external condition, either one or another cache should be used, then the `switchLevels` function could turn useful. Usage: ```swift let lane1 = MemoryCacheLevel() // The two lanes have to be equivalent (same key type, same value type). let lane2 = CacheProvider.dataCache() // Keep in mind that you can always use key transformation or value transformations if two lanes don't match by default let switched = switchLevels(lane1, lane2) { key in if key.scheme == "http" { return .cacheA } else { return .cacheB // The example is just meant to show how to return different lanes } } ``` Now depending on the scheme of the key URL, either the first lane or the second will be used. ### Listening to memory warnings If we store big objects in memory in our cache levels, we may want to be notified of memory warning events. This is where the `listenToMemoryWarnings` and `unsubscribeToMemoryWarnings` functions come handy: ```swift let token = cache.listenToMemoryWarnings() ``` and later ```swift unsubscribeToMemoryWarnings(token) ``` With the first call, the cache level and all its composing levels will get a call to `onMemoryWarning` when a memory warning comes. With the second call, the behavior will stop. Keep in mind that this functionality is not yet supported by the WatchOS 2 framework `CarlosWatch.framework`. ### Normalization In case you need to store the result of multiple `Carlos` composition calls in a property, it may be troublesome to set the type of the property to `BasicCache` as some calls return different types (e.g. `PoolCache`). In this case, you can `normalize` the cache level before assigning it to the property and it will be converted to a `BasicCache` value. ```swift import Carlos class CacheManager { let cache: BasicCache init(injectedCache: BasicCache) { self.cache = injectedCache } } [...] let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled()) // This won't compile let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled().normalize()) // This will ``` As a tip, always use `normalize` if you need to assign the result of multiple composition calls to a property. The call is a no-op if the value is already a `BasicCache`, so there will be no performance loss in that case. ### Creating custom levels Creating custom levels is easy and encouraged (after all, there are multiple cache libraries already available if you only need memory, disk and network functionalities!). Let's see how to do it: ```swift class MyLevel: CacheLevel { typealias KeyType = Int typealias OutputType = Float func get(_ key: KeyType) -> AnyPublisher { Future { // Perform the fetch and either succeed or fail }.eraseToAnyPublisher() } func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher { Future { // Store the value (db, memory, file, etc) and call this on completion: }.eraseToAnyPublisher() } func clear() { // Clear the stored values } func onMemoryWarning() { // A memory warning event came. React appropriately } } ``` The above class conforms to the `CacheLevel` protocol. First thing we need is to declare what key types we accept and what output types we return. In this example case, we have `Int` keys and `Float` output values. The required methods to implement are 4: `get`, `set`, `clear` and `onMemoryWarning`. This sample cache can now be pipelined to a list of other caches, transforming its keys or values if needed as we saw in the earlier paragraphs. ### Creating custom fetchers With `Carlos 0.4`, the `Fetcher` protocol was introduced to make it easier for users of the library to create custom fetchers that can be used as read-only levels in the cache. An example of a "`Fetcher` in disguise" that has always been included in `Carlos` is `NetworkFetcher`: you can only use it to read from the network, not to write (`set`, `clear` and `onMemoryWarning` were **no-ops**). This is how easy it is now to implement your custom fetcher: ```swift class CustomFetcher: Fetcher { typealias KeyType = String typealias OutputType = String func get(_ key: KeyType) -> Anypublisher { return Just("Found an hardcoded value :)").eraseToAnyPublisher() } } ``` You still need to declare what `KeyType` and `OutputType` your `CacheLevel` deals with, of course, but then you're only required to implement `get`. Less boilerplate for you! ### Built-in levels `Carlos` comes with 3 cache levels out of the box: - `MemoryCacheLevel` - `DiskCacheLevel` - `NetworkFetcher` - Since the `0.5` release, a `UserDefaultsCacheLevel` **MemoryCacheLevel** is a volatile cache that internally stores its values in an `NSCache` instance. The capacity can be specified through the initializer, and it supports clearing under memory pressure (if the level is [subscribed to memory warning notifications](#listening-to-memory-warnings)). It accepts keys of any given type that conforms to the `StringConvertible` protocol and can store values of any given type that conforms to the `ExpensiveObject` protocol. `Data`, `NSData`, `String`, `NSString` `UIImage`, `URL` already conform to the latter protocol out of the box, while `String`, `NSString` and `URL` conform to the `StringConvertible` protocol. This cache level is thread-safe. **DiskCacheLevel** is a persistent cache that asynchronously stores its values on disk. The capacity can be specified through the initializer, so that the disk size will never get too big. It accepts keys of any given type that conforms to the `StringConvertible` protocol and can store values of any given type that conforms to the `NSCoding` protocol. This cache level is thread-safe, and currently the only `CacheLevel` that can fail when calling `set`, with a `DiskCacheLevelError.diskArchiveWriteFailed` error. **NetworkFetcher** is a cache level that asynchronously fetches values over the network. It accepts `URL` keys and returns `NSData` values. This cache level is thread-safe. **NSUserDefaultsCacheLevel** is a persistent cache that stores its values on a `UserDefaults` persistent domain with a specific name. It accepts keys of any given type that conforms to the `StringConvertible` protocol and can store values of any given type that conforms to the `NSCoding` protocol. It has an internal soft cache used to avoid hitting the persistent storage too often, and can be cleared without affecting other values saved on the `standardUserDefaults` or on other persistent domains. This cache level is thread-safe. ### Logging When we decided how to handle logging in Carlos, we went for the most flexible approach that didn't require us to code a complete logging framework, that is the ability to plug-in your own logging library. If you want the output of Carlos to only be printed if exceeding a given level, if you want to completely silent it for release builds, or if you want to route it to a file, or whatever else: just assign your logging handling closure to `Carlos.Logger.output`: ```swift Carlos.Logger.output = { message, level in myLibrary.log(message) //Plug here your logging library } ``` ## Tests `Carlos` is thouroughly tested so that the features it's designed to provide are safe for refactoring and as much as possible bug-free. We use [Quick](https://github.com/Quick/Quick) and [Nimble](https://github.com/Quick/Nimble) instead of `XCTest` in order to have a good BDD test layout. As of today, there are around **1000 tests** for `Carlos` (see the folder `Tests`), and overall the tests codebase is *double the size* of the production codebase. ## Future development `Carlos` is under development and [here](https://github.com/WeltN24/Carlos/issues) you can see all the open issues. They are assigned to milestones so that you can have an idea of when a given feature will be shipped. If you want to contribute to this repo, please: - Create an issue explaining your problem and your solution - Clone the repo on your local machine - Create a branch with the issue number and a short abstract of the feature name - Implement your solution - Write tests (untested features won't be merged) - When all the tests are written and green, create a pull request, with a short description of the approach taken ## Apps using Carlos - [Die Welt Edition](https://itunes.apple.com/de/app/welt-edition-digitale-zeitung/id372746348?mt=8) - [Welt news](https://itunes.apple.com/de/app/welt-news-aktuelle-nachrichten/id340021100?mt=8) Using Carlos? Please let us know through a Pull request, we'll be happy to mention your app! ## Authors `Carlos` was made in-house by WeltN24 ### Contributors: Vittorio Monaco, [vittorio.monaco@weltn24.de](mailto:vittorio.monaco@weltn24.de), [@vittoriom](https://github.com/vittoriom) on Github, [@Vittorio_Monaco](https://twitter.com/Vittorio_Monaco) on Twitter Esad Hajdarevic, @esad ## License `Carlos` is available under the MIT license. See the LICENSE file for more info. ## Acknowledgements `Carlos` internally uses: The **DiskCacheLevel** class is inspired by [Haneke](https://github.com/Haneke/HanekeSwift). The source code has been heavily modified, but adapting the original file has proven valuable for `Carlos` development. ================================================ FILE: Sources/Carlos/CacheLevels/BatchAllCache.swift ================================================ import Combine /// A reified batchGetAll public final class BatchAllCache: CacheLevel where KeySeq.Iterator.Element == Cache.KeyType { /// A sequence of keys for the wrapped cache public typealias KeyType = KeySeq /// An array of output elements public typealias OutputType = [Cache.OutputType] private let cache: Cache public init(cache: Cache) { self.cache = cache } /** Dispatch each key in the sequence in parallel Merge the results -- if any key fails, it all fails */ public func get(_ key: KeyType) -> AnyPublisher { let all = key.map(cache.get) return all.publisher .setFailureType(to: Error.self) .flatMap { $0 } .collect(all.count) .eraseToAnyPublisher() } /** Zip the keys with the values and set them all */ public func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher { let initial = Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() return zip(value, key) .map(cache.set) .reduce(initial) { previous, current in previous.flatMap { current } .eraseToAnyPublisher() } } public func remove(_ key: KeyType) -> AnyPublisher { let all = key.map(cache.remove) return all.publisher .setFailureType(to: Error.self) .collect(all.count) .map { _ in () } .eraseToAnyPublisher() } public func clear() { cache.clear() } public func onMemoryWarning() { cache.onMemoryWarning() } } extension CacheLevel { /** Wrap a cache into a , [V]> cache where each k in Sequence is dispatched in parallel and if any K fails, it all fails */ public func allBatch() -> BatchAllCache { BatchAllCache(cache: self) } } ================================================ FILE: Sources/Carlos/CacheLevels/Composed.swift ================================================ import Combine import Foundation extension CacheLevel { /** Composes two cache levels - parameter cache: The second cache level - returns: A new cache level that is the result of the composition of the two cache levels */ public func compose(_ cache: A) -> BasicCache where A.KeyType == KeyType, A.OutputType == OutputType { BasicCache( getClosure: { key in self.get(key) .catch { _ -> AnyPublisher in Logger.log("Composed| error on getting value for key \(key) on cache \(String(describing: self)).", .info) return cache.get(key) .flatMap { [weak self] value -> AnyPublisher<(OutputType, Void), Error> in guard let self = self else { return Empty(completeImmediately: true).eraseToAnyPublisher() } let get = Just(value).setFailureType(to: Error.self) let set = self.set(value, forKey: key) return Publishers.Zip(get, set) .eraseToAnyPublisher() } .map(\.0) .eraseToAnyPublisher() }.eraseToAnyPublisher() }, setClosure: { value, key in Publishers.Zip( self.set(value, forKey: key), cache.set(value, forKey: key) ) .map { _ in () } .eraseToAnyPublisher() }, removeClosure: { key in Publishers.Zip(self.remove(key), cache.remove(key)) .map { _ in () } .eraseToAnyPublisher() }, clearClosure: { self.clear() cache.clear() }, memoryClosure: { self.onMemoryWarning() cache.onMemoryWarning() } ) } } ================================================ FILE: Sources/Carlos/CacheLevels/Conditioned.swift ================================================ import Combine import Foundation extension CacheLevel { /** Wraps the CacheLevel with a boolean condition on the key that controls when a get call should fail unconditionally - parameter condition: The condition closure that takes a key and returns true if the key can be fetched, or false if the request should fail unconditionally. The closure can also pass a specific error in case it wants to explicitly communicate why it failed. The condition can be asynchronous and has to return a Future - returns: A new BasicCache that will check for the condition before every get is dispatched to the decorated cache level The condition doesn't apply to the set, clear, onMemoryWarning calls */ public func conditioned(_ condition: @escaping (KeyType) -> AnyPublisher) -> BasicCache { BasicCache( getClosure: conditionedClosure(get, condition: condition), setClosure: set, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } private func conditionedClosure(_ closure: @escaping (A) -> AnyPublisher, condition: @escaping (A) -> AnyPublisher) -> ((A) -> AnyPublisher) { return { input in condition(input).flatMap { (passesCondition: Bool) -> AnyPublisher in if passesCondition { return closure(input) } else { return Fail(error: FetchError.conditionNotSatisfied).eraseToAnyPublisher() } }.eraseToAnyPublisher() } } extension OneWayTransformer { /** Wraps the transformer with a boolean condition on the input that controls when a transformation should fail unconditionally. - parameter condition: The condition closure that takes an input and returns true if the input can be transformed, or false if the request should fail unconditionally. The closure can also pass a specific error in case it wants to explicitly communicate why it failed. The condition can be asynchronous and has to return a Future - returns: A new OneWayTransformer that will check for the condition before every transformation is dispatched to the decorated transformer */ public func conditioned(_ condition: @escaping (TypeIn) -> AnyPublisher) -> OneWayTransformationBox { OneWayTransformationBox(transform: conditionedClosure(transform, condition: condition)) } } extension TwoWayTransformer { /** Wraps the transformer with a boolean condition on the input and a boolean condition on the "inverse input" that controls when a transformation on either side should fail unconditionally. - parameter condition: The condition closure used for normal transformations that takes an input and returns true if the input can be transformed, or false if the request should fail unconditionally. The closure can also pass a specific error in case it wants to explicitly communicate why it failed. The condition can be asynchronous and has to return a Future - parameter inverseCondition: The condition closure used for inverse transformations that takes a TypeOut argument and returns true if the input can be transformed, or false if the request should fail unconditionally. The closure can also pass a specific error in case it wants to explicitly communicate why it failed. The condition can be asynchronous and has to return a Future. - returns: A new TwoWayTransformer that will check for the conditions before every transformation is dispatched to the decorated transformer */ public func conditioned(_ condition: @escaping (TypeIn) -> AnyPublisher, inverseCondition: @escaping (TypeOut) -> AnyPublisher) -> TwoWayTransformationBox { TwoWayTransformationBox( transform: conditionedClosure(transform, condition: condition), inverseTransform: conditionedClosure(inverseTransform, condition: inverseCondition) ) } } ================================================ FILE: Sources/Carlos/CacheLevels/DiskCacheLevel.swift ================================================ import Combine import Foundation public enum DiskCacheLevelError: Error { case diskArchiveWriteFailed } /// This class is a disk cache level. It has a configurable total size that defaults to 100 MB. public final class DiskCacheLevel: CacheLevel { /// At the moment the disk cache level only accepts keys that can be converted to string values public typealias KeyType = K /// The output type of the cache, should conform to NSCoding public typealias OutputType = T private let path: String private var size: UInt64 = 0 private let fileManager: FileManager /// The capacity of the cache public var capacity: UInt64 = 0 { didSet { cacheQueue.async { self.controlCapacity() } } } private lazy var cacheQueue: DispatchQueue = { DispatchQueue(label: "\(CarlosGlobals.queueNamePrefix)\((self.path as NSString).lastPathComponent)") }() /** This method is a no-op since all the contents of the cache are stored on disk, so removing them would have no benefit for memory pressure */ public func onMemoryWarning() {} /** Initializes a new disk cache level - parameter path: The path to the disk storage. Defaults to a Carlos specific folder in the Caches sandbox folder. - parameter capacity: The total capacity in bytes for the disk cache. Defaults to 100 MB - parameter fileManager: The file manager to use. Defaults to the default NSFileManager. It's here mainly for dependency injection testing purposes. */ public init( path: String = (CarlosGlobals.caches as NSString).appendingPathComponent(CarlosGlobals.queueNamePrefix + "default"), capacity: UInt64 = 100 * 1024 * 1024, fileManager: FileManager = FileManager.default ) { self.path = path self.fileManager = fileManager self.capacity = capacity _ = try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: [:]) cacheQueue.async { self.calculateSize() self.controlCapacity() } } /** Asynchronously sets a value for the given key - parameter value: The value to save on disk - parameter key: The key for the value */ public func set(_ value: T, forKey key: K) -> AnyPublisher { Logger.log("DiskCacheLevel| Setting a value for the key \(key.toString()) on the disk cache \(self)", .info) return Just((value, key)) .setFailureType(to: Error.self) .subscribe(on: cacheQueue) .flatMap { [weak self] payload -> AnyPublisher in guard let self = self else { return Empty(completeImmediately: true).eraseToAnyPublisher() } return self.setDataSync(payload.0, key: payload.1) } .eraseToAnyPublisher() } /** Asynchronously gets the value for the given key - parameter key: The key for the value - returns: A Future where you can call onSuccess and onFailure to be notified of the result of the fetch */ public func get(_ key: KeyType) -> AnyPublisher { AnyPublisher.create { [weak self] promise in guard let self = self else { return } let path = self.pathForKey(key) if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let obj = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T { Logger.log("DiskCacheLevel| Fetched \(key.toString()) on disk level", .info) promise(.success(obj)) _ = self.updateDiskAccessDateAtPath(path) } else { // Remove the file (maybe corrupted) Logger.log("DiskCacheLevel| Failed fetching \(key.toString()) in path: \(path) on the disk cache", .info) _ = try? self.fileManager.removeItem(atPath: path) promise(.failure(FetchError.valueNotInCache)) } } .subscribe(on: cacheQueue) .eraseToAnyPublisher() } public func remove(_ key: K) -> AnyPublisher { AnyPublisher.create { [weak self] promise in guard let path = self?.pathForKey(key) else { return } do { Logger.log("DiskCacheLevel| Removing \(key.toString()) at path: \(path) on the disk cache", .info) try self?.fileManager.removeItem(atPath: path) self?.calculateSize() promise(.success(())) } catch { Logger.log("DiskCacheLevel| Failed to remove \(key.toString()) at path: \(path) on the disk cache", .error) promise(.failure(error)) } } .subscribe(on: cacheQueue) .eraseToAnyPublisher() } /** Asynchronously clears the contents of the cache All the cached files will be removed from the disk storage */ public func clear() { cacheQueue.async { self.itemsInDirectory(self.path).forEach { filePath in _ = try? self.fileManager.removeItem(atPath: filePath) } self.calculateSize() } } // MARK: Private private func removeData(_ key: K) { cacheQueue.async { self.removeFileAtPath(self.pathForKey(key)) } } private func pathForKey(_ key: K) -> String { let md5PathComponent = key.toString().MD5String() let strippedMd5PathComponent = stripSpecialCharactersForPath(from: md5PathComponent) return (path as NSString).appendingPathComponent(strippedMd5PathComponent) } private func stripSpecialCharactersForPath(from string: String) -> String { let okayChars = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ1234567890") return string.filter { okayChars.contains($0) } } private func sizeForFileAtPath(_ filePath: String) -> UInt64 { var size: UInt64 = 0 do { let attributes: NSDictionary = try fileManager.attributesOfItem(atPath: filePath) as NSDictionary size = attributes.fileSize() } catch {} return size } private func calculateSize() { size = itemsInDirectory(path).reduce(0) { accumulator, filePath in accumulator + sizeForFileAtPath(filePath) } } private func controlCapacity() { if size > capacity { enumerateContentsOfDirectorySortedByAscendingModificationDateAtPath(path) { (URL, stop: inout Bool) in removeFileAtPath(URL.path) stop = size <= capacity } } } private func setDataSync(_ data: T, key: K) -> AnyPublisher { let path = pathForKey(key) let previousSize = sizeForFileAtPath(path) do { let data = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false) try data.write(to: URL(fileURLWithPath: path), options: .atomicWrite) _ = updateDiskAccessDateAtPath(path) let newSize = sizeForFileAtPath(path) if newSize > previousSize { size += newSize - previousSize controlCapacity() } else { size -= previousSize - newSize } return Just(()) .setFailureType(to: Error.self) .eraseToAnyPublisher() } catch { Logger.log("DiskCacheLevel| Failed to write key \(key.toString()) on the disk cache", .error) return Fail(error: DiskCacheLevelError.diskArchiveWriteFailed).eraseToAnyPublisher() } } private func updateDiskAccessDateAtPath(_ path: String) -> Bool { var result = false do { try fileManager.setAttributes([ FileAttributeKey.modificationDate: Date() ], ofItemAtPath: path) result = true } catch _ {} return result } private func removeFileAtPath(_ path: String) { do { if let attributes: NSDictionary = try fileManager.attributesOfItem(atPath: path) as NSDictionary? { try fileManager.removeItem(atPath: path) size -= attributes.fileSize() } } catch _ {} } private func itemsInDirectory(_ directory: String) -> [String] { var items: [String] = [] do { items = try fileManager.contentsOfDirectory(atPath: directory).map { (directory as NSString).appendingPathComponent($0) } } catch _ {} return items } private func enumerateContentsOfDirectorySortedByAscendingModificationDateAtPath(_ path: String, usingBlock block: (URL, inout Bool) -> Void) { let property = URLResourceKey.contentModificationDateKey do { let directoryURL = URL(fileURLWithPath: path) let contents = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [property], options: []) let sortedContents = contents.sorted(by: { URL1, URL2 in var value1: AnyObject? do { try (URL1 as NSURL).getResourceValue(&value1, forKey: property) } catch _ { return true } var value2: AnyObject? do { try (URL2 as NSURL).getResourceValue(&value2, forKey: property) } catch _ { return false } if let date1 = value1 as? Date, let date2 = value2 as? Date { return date1.compare(date2) == .orderedAscending } return false }) for value in sortedContents { var stop = false block(value, &stop) if stop { break } } } catch _ {} } } ================================================ FILE: Sources/Carlos/CacheLevels/Fetcher.swift ================================================ // // Fetcher.swift // // // Created by Lisovyi, Ivan on 24.10.20. // import Combine import Foundation /// An abstraction for a generic cache level that can only fetch values but not store them public protocol Fetcher: CacheLevel {} /// Extending the Fetcher protocol to have a default no-op implementation for clear, onMemoryWarning and set extension Fetcher { /// No-op public func remove(_ key: KeyType) -> AnyPublisher { Empty(completeImmediately: true).eraseToAnyPublisher() } /// No-op public func clear() {} /// No-op public func onMemoryWarning() {} /// No-op public func set(_: OutputType, forKey _: KeyType) -> AnyPublisher { Empty(completeImmediately: true).eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/CacheLevels/MemoryCacheLevel.swift ================================================ import Combine import Foundation /// This class is a memory cache level. It internally uses NSCache, and has a configurable total cost limit that defaults to 50 MB. public final class MemoryCacheLevel: CacheLevel where T: ExpensiveObject { /// At the moment the memory cache level only accepts String keys public typealias KeyType = K public typealias OutputType = T private let internalCache: NSCache /** Initializes a new memory cache level - parameter cost: The total cost limit for the memory cache. Defaults to 50 MB */ public init(capacity: Int = 50 * 1024 * 1024) { internalCache = NSCache() internalCache.totalCostLimit = capacity } /** Synchronously gets a value for the given key - parameter key: The key for the value - returns: A Future where you can call onSuccess and onFailure to be notified of the result of the fetch */ public func get(_ key: KeyType) -> AnyPublisher { AnyPublisher.create { [weak self] promise in if let result = self?.internalCache.object(forKey: key.toString() as NSString) as? T { Logger.log("MemoryCacheLevel| Fetched \(key.toString()) on memory level.", .info) promise(.success(result)) } else { Logger.log("MemoryCacheLevel| Failed fetching \(key.toString()) on the memory cache.", .info) promise(.failure(FetchError.valueNotInCache)) } } .eraseToAnyPublisher() } public func remove(_ key: K) -> AnyPublisher { AnyPublisher.create { [weak self] promise in Logger.log("MemoryCacheLevel| Removing \(key.toString()) on memory level.", .info) self?.internalCache.removeObject(forKey: key.toString() as NSString) promise(.success(())) } } /** Clears the contents of the cache */ public func onMemoryWarning() { clear() } /** Sets a value for the given key - parameter value: The value to set - parameter key: The key for the value */ public func set(_ value: T, forKey key: K) -> AnyPublisher { AnyPublisher.create { [weak self] promise in self?.internalCache.setObject(value, forKey: key.toString() as NSString, cost: value.cost) promise(.success(())) } } /** Clears the contents of the cache */ public func clear() { internalCache.removeAllObjects() } } ================================================ FILE: Sources/Carlos/CacheLevels/NSUserDefaultsCacheLevel.swift ================================================ import Combine import Foundation /** Default name for the persistent domain used by the NSUserDefaultsCacheLevel Keep in mind that using this domain for multiple cache levels at the same time could lead to undesired results! For example, if one of the cache levels get cleared, also the other will be affected unless they save something before leaving the app. The behavior is not 100% certain and this possibility is discouraged. */ public let DefaultUserDefaultsDomainName = "CarlosPersistentDomain" /// This class is a NSUserDefaults cache level. It has a configurable domain name so that multiple levels can be included in the same sandboxed app. public final class NSUserDefaultsCacheLevel: CacheLevel { /// The key type of the cache, should be convertible to String values public typealias KeyType = K /// The output type of the cache, should conform to NSCoding public typealias OutputType = T private let domainName: String private let lock: UnfairLock private let userDefaults: UserDefaults private var internalDomain: [String: Data]? private var safeInternalDomain: [String: Data] { if let internalDomain = internalDomain { return internalDomain } else { let fetchedDomain = (userDefaults.persistentDomain(forName: domainName) as? [String: Data]) ?? [:] internalDomain = fetchedDomain return fetchedDomain } } /** Creates a new instance of this NSUserDefaults-based cache level. - parameter name: The name to use for the persistent domain on NSUserDefaults. Should be unique in your sandboxed app */ public init(name: String = DefaultUserDefaultsDomainName) { domainName = name lock = UnfairLock() userDefaults = UserDefaults.standard internalDomain = safeInternalDomain } /** Sets a new value for the given key - parameter value: The value to set for the given key - parameter key: The key you want to set This method will convert the value to NSData by using NSCoding and save the data on the persistent domain. A soft-cache is used to avoid hitting the persistent domain everytime you are going to fetch values from this cache. The operation is thread-safe */ public func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher { AnyPublisher.create { [weak self] promise in guard let self = self else { return } var softCache = self.safeInternalDomain Logger.log("Setting a value for the key \(key.toString()) on the user defaults cache \(self)") if let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false) { softCache[key.toString()] = data self.internalDomain = softCache self.userDefaults.setPersistentDomain(softCache, forName: self.domainName) promise(.success(())) } else { Logger.log("Failed setting a value for the key \(key.toString()) on the user defaults cache \(self)") promise(.failure(FetchError.invalidCachedData)) } } .eraseToAnyPublisher() } /** Fetches a value on the persistent domain for the given key - parameter key: The key you want to fetch - returns: The result of this fetch on the cache A soft-cache is used to avoid hitting the persistent domain everytime. This operation is thread-safe */ public func get(_ key: KeyType) -> AnyPublisher { AnyPublisher.create { [weak self] promise in guard let self = self else { return } if let cachedValue = self.safeInternalDomain[key.toString()] { if let unencodedObject = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(cachedValue) as? T { Logger.log("Fetched \(key.toString()) on user defaults level (domain \(self.domainName)") promise(.success(unencodedObject)) } else { Logger.log("Failed fetching \(key.toString()) on the user defaults cache (domain \(self.domainName), corrupted data") promise(.failure(FetchError.invalidCachedData)) } } else { Logger.log("Failed fetching \(key.toString()) on the user defaults cache (domain \(self.domainName), no data") promise(.failure(FetchError.valueNotInCache)) } } .eraseToAnyPublisher() } public func remove(_ key: K) -> AnyPublisher { AnyPublisher.create { [weak self] promise in self?.userDefaults.removeObject(forKey: key.toString()) promise(.success(())) } } /** Completely clears the contents of this cache Please keep in mind that if the same name is used for multiple cache levels, the contents of these caches will also be cleared, at least from a persistence point of view. The soft caches of the other levels will still contain consistent values, though, so setting a value on one of these levels will result in the whole previous content of the cache to be persisted on NSUserDefaults, even this may or may not be the desired behavior. The conclusion is that you should only use the same name for multiple cache levels if you are aware of the consequences. In general the behavior may not be the expected one. The operation is thread-safe */ public func clear() { lock.locked { userDefaults.removePersistentDomain(forName: domainName) internalDomain = [:] } } /** Clears the contents of the soft cache for this cache level. Fetching or setting a value after this call is safe, since the content will be pre-fetched from the disk immediately before. The operation is thread-safe */ public func onMemoryWarning() { lock.locked { internalDomain = nil } } } ================================================ FILE: Sources/Carlos/CacheLevels/NetworkFetcher.swift ================================================ import Foundation import Combine public enum NetworkFetcherError: Error { /// Used when the status code of the network response is not included in the range 200..<300 case statusCodeNotOk /// Used when the network response had an invalid size case invalidNetworkResponse /// Used when the network request didn't manage to retrieve data case noDataRetrieved } /// This class is a network cache level, mostly acting as a fetcher (meaning that calls to the set method won't have any effect). It internally uses NSURLSession to retrieve values from the internet open class NetworkFetcher: Fetcher { /// The network cache accepts only NSURL keys public typealias KeyType = URL /// The network cache returns only NSData values public typealias OutputType = NSData /** Initializes a new instance of a NetworkFetcher */ public init() {} /** Asks the cache to get a value for the given key - parameter key: The key for the value. It represents the URL to fetch the value - returns: A Future that you can use to get the asynchronous results of the network fetch */ open func get(_ key: KeyType) -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: key) .tryMap { [weak self] data, response -> NSData in guard let response = response as? HTTPURLResponse else { throw NetworkFetcherError.invalidNetworkResponse } guard 200..<300 ~= response.statusCode else { throw NetworkFetcherError.statusCodeNotOk } if self?.validate(response, withData: data) == true { return data as NSData } throw NetworkFetcherError.invalidNetworkResponse } .eraseToAnyPublisher() } private func validate(_ response: HTTPURLResponse, withData data: Data) -> Bool { var responseIsValid = true let expectedContentLength = response.expectedContentLength if expectedContentLength > -1 { responseIsValid = Int64(data.count) >= expectedContentLength } return responseIsValid } } ================================================ FILE: Sources/Carlos/CacheLevels/PoolCache.swift ================================================ import Combine import Foundation extension CacheLevel where KeyType: Hashable { /// Wraps the CacheLevel with a requests pool /// /// - Returns: A `PoolCache` that will pool requests coming to the decorated cache. /// This means that multiple requests for the same key will be pooled and only one will be actually done /// (so that expensive operations like network or file system fetches will only be done once). public func pooled() -> PoolCache { PoolCache(internalCache: self) } } /// A CacheLevel that pools incoming get requests. /// /// This means that multiple requests for the same key will be pooled and only one will be actually executed /// (so that expensive operations like network or file system fetches will only be done once). public final class PoolCache: CacheLevel where C.KeyType: Hashable { public typealias KeyType = C.KeyType public typealias OutputType = C.OutputType private let internalCache: C private let lock: UnfairLock private var requestsPool: [C.KeyType: AnyPublisher] = [:] /// Creates a new instance of a pooled cache /// /// - Parameter internalCache: The CacheLevel instance that this pooled cache will manage public init(internalCache: C) { self.internalCache = internalCache lock = UnfairLock() } /// Asks the cache to get the value for the given key /// /// - Parameter key: The key for the value /// /// - Returns: A `Publisher` that could either have been just created or it could have been reused from a pool of pending Publishers /// if there is a Publisher for the same key going on at the moment. public func get(_ key: KeyType) -> AnyPublisher { if let pooledRequest = lock.locked({ self.requestsPool[key] }) { Logger.log("Using pooled request \(pooledRequest) for key \(key)") return pooledRequest } let request = internalCache.get(key) lock.locked { self.requestsPool[key] = request } Logger.log("Creating a new request \(request) for key \(key)") return request .handleEvents(receiveCompletion: { [weak self] _ in self?.lock.locked { self?.requestsPool[key] = nil } }) .eraseToAnyPublisher() } /// Sets a value for the given key on the managed cache /// /// - Parameter value: The value to set /// - Parameter key: The key for the value public func set(_ value: C.OutputType, forKey key: C.KeyType) -> AnyPublisher { internalCache.set(value, forKey: key) } public func remove(_ key: C.KeyType) -> AnyPublisher { internalCache.remove(key) } /// Clears the managed cache public func clear() { internalCache.clear() } /// Notifies the managed cache that a memory warning event was thrown public func onMemoryWarning() { internalCache.onMemoryWarning() } } ================================================ FILE: Sources/Carlos/CacheProvider.swift ================================================ import Foundation /// A provider for the built-int caches. public enum CacheProvider { /// A shared data cache instance public static let sharedDataCache: BasicCache = CacheProvider.dataCache() /// A shared JSON cache instance public static let sharedJSONCache: BasicCache = CacheProvider.JSONCache() /// A shared image cache instance public static let sharedImageCache: BasicCache = CacheProvider.imageCache() /// - Returns: An initialized and configured CacheLevel that takes URL keys and stores NSData values. /// Network requests are pooled for efficiency. Keep in mind that calling this method twice returns two different instances. /// You should take care of retaining the result or use `sharedDataCache` instead. public static func dataCache() -> BasicCache { MemoryCacheLevel() .compose( DiskCacheLevel().compose(NetworkFetcher()) .pooled() ) } /// - Returns: An initialized and configured CacheLevel that takes URL keys and stores JSON values in the form of AnyObject. /// Network requests are pooled for efficiency. Keep in mind that calling this method twice returns two different instances. /// You should take care of retaining the result or use `sharedJSONCache` instead public static func JSONCache() -> BasicCache { dataCache().transformValues(JSONTransformer()) } /// - Returns: An initialized and configured `CacheLevel` that takes URL keys and stores `CarlosImage` (UIImage | NSImage) values. /// Network requests are pooled for efficiency. Keep in mind that calling this method twice returns two different instances. /// You should take care of retaining the result or use `sharedImageCache` instead public static func imageCache() -> BasicCache { MemoryCacheLevel() .compose(DiskCacheLevel()) .compose( NetworkFetcher() .pooled() .transformValues(ImageTransformer()) ) } } ================================================ FILE: Sources/Carlos/Carlos.swift ================================================ import Combine import Foundation public struct CarlosGlobals { public static let queueNamePrefix = "com.carlos." public static let caches = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] } /// An abstraction for a generic cache level public protocol CacheLevel: AnyObject { /// A typealias for the key the cache level accepts associatedtype KeyType /// A typealias for the data the cache returns in the success closure associatedtype OutputType /// Tries to get a value from the cache level /// /// - Parameter key: The key of the value you would like to get /// /// - Returns: A `Publisher` that you can attach success and failure closures to func get(_ key: KeyType) -> AnyPublisher /// Tries to set a value on the cache level /// /// - Parameter value: The bytes to set on the cache level /// - Parameter key: The key of the value you're trying to set /// /// - Returns: A `Publisher` that will reflect the status of the set operation func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher /// Remove value from cache for a given key. /// /// - Parameter key: They key of the value to be removed /// /// - Returns: A `Publisher` that reflects a status of the remove operation. func remove(_ key: KeyType) -> AnyPublisher /// Asks to clear the cache level func clear() /// Notifies the cache level that a memory warning was thrown, and asks it to do its best to clean some memory func onMemoryWarning() } ================================================ FILE: Sources/Carlos/Core/BasicCache.swift ================================================ import Combine import Foundation /// A wrapper cache that explicitly takes get, set, clear and memory warning closures public final class BasicCache: CacheLevel { public typealias KeyType = A public typealias OutputType = B private let getClosure: (_ key: A) -> AnyPublisher private let setClosure: (_ value: B, _ key: A) -> AnyPublisher private let removeClosure: (_ key: A) -> AnyPublisher private let clearClosure: () -> Void private let memoryClosure: () -> Void /** Initializes a new instance of a BasicCache specifying closures for get, set, clear and onMemoryWarning, thus determining the behavior of the cache level as a whole - parameter getClosure: The closure to execute when you call get(key) on this instance - parameter setClosure: The closure to execute when you call set(value, key) on this instance - parameter clearClosure: The closure to execute when you call clear() on this instance - parameter memoryClosure: The closure to execute when you call onMemoryWarning() on this instance, or when a memory warning is thrown by the system and the cache level is listening for memory pressure events */ public init( getClosure: @escaping (_ key: A) -> AnyPublisher, setClosure: @escaping (_ value: B, _ key: A) -> AnyPublisher, removeClosure: @escaping (_ key: A) -> AnyPublisher, clearClosure: @escaping () -> Void, memoryClosure: @escaping () -> Void ) { self.getClosure = getClosure self.setClosure = setClosure self.removeClosure = removeClosure self.clearClosure = clearClosure self.memoryClosure = memoryClosure } /** Asks the cache to get the value for a given key - parameter key: The key you want to get the value for - returns: The result of the getClosure specified when initializing the instance */ public func get(_ key: KeyType) -> AnyPublisher { getClosure(key) } /** Asks the cache to set a value for the given key - parameter value: The value to set on the cache - parameter key: The key to use for the given value This call executes the setClosure specified when initializing the instance */ public func set(_ value: B, forKey key: A) -> AnyPublisher { setClosure(value, key) } public func remove(_ key: A) -> AnyPublisher { removeClosure(key) } /** Asks the cache to clear its contents This call executes the clearClosure specified when initializing the instance */ public func clear() { clearClosure() } /** Tells the cache that a memory warning event was received This call executes the memoryClosure specified when initializing the instance */ public func onMemoryWarning() { memoryClosure() } } ================================================ FILE: Sources/Carlos/Core/BasicFetcher.swift ================================================ import Combine import Foundation /// A wrapper fetcher that explicitly takes a get closure public final class BasicFetcher: Fetcher { public typealias KeyType = A public typealias OutputType = B private let getClosure: (_ key: A) -> AnyPublisher /** Initializes a new instance of a BasicFetcher specifying a get closure, thus determining the behavior of the fetcher as a whole - parameter getClosure: The closure to execute when you call get(key) on this instance */ public init(getClosure: @escaping (_ key: A) -> AnyPublisher) { self.getClosure = getClosure } /** Asks the fetcher to get the value for a given key - parameter key: The key you want to get the value for - returns: The result of the getClosure specified when initializing the instance */ public func get(_ key: KeyType) -> AnyPublisher { getClosure(key) } } ================================================ FILE: Sources/Carlos/Core/Errors.swift ================================================ import Foundation public enum FetchError: Error { /// Used when a cache level doesn't have a value in the cache case valueNotInCache /// Used when no cache level did find the key case noCacheLevelsRemaining /// Used when the specified key was invalid case invalidKey /// Used when some cached data was found but was likely corrupted case invalidCachedData /// Used when the key doesn't satisfy the cache condition case conditionNotSatisfied /// Used when a key transformation failed and the cache level had to skip a get operation case keyTransformationFailed /// Used when a value transformation failed and the cache level had to skip a get operation case valueTransformationFailed } ================================================ FILE: Sources/Carlos/Core/ExpensiveObject.swift ================================================ import Foundation /// Abstracts objects that have a cost (useful for the MemoryCacheLevel) public protocol ExpensiveObject { /// The cost of the object var cost: Int { get } } extension Data: ExpensiveObject { /// The number of bytes of the data block public var cost: Int { count } } extension NSData: ExpensiveObject { /// The number of bytes of the data block public var cost: Int { length } } extension String: ExpensiveObject { /// The number of characters of the string public var cost: Int { count } } extension NSString: ExpensiveObject { /// The number of characters of the NSString public var cost: Int { length } } extension URL: ExpensiveObject { /// The size of the URL public var cost: Int { String(absoluteString).cost } } extension Int: ExpensiveObject { /// Integers have a unit cost public var cost: Int { 1 } } extension Float: ExpensiveObject { /// Floats have a unit cost public var cost: Int { 1 } } extension Double: ExpensiveObject { /// Doubles have a unit cost public var cost: Int { 1 } } extension Character: ExpensiveObject { /// Characters have a unit cost public var cost: Int { 1 } } extension CarlosImage: ExpensiveObject { /// The size of the image in pixels (W x H) public var cost: Int { Int(size.width * size.height) } } ================================================ FILE: Sources/Carlos/Core/Extensions.swift ================================================ import Combine import CryptoKit import Foundation public extension String { func MD5String() -> String { guard let data = data(using: .utf8) else { return self } return Insecure.MD5.hash(data: data).map { String(format: "%02hhx", $0) }.joined() } } extension AnyPublisher { static func create(_ block: @escaping ((Result) -> Void) -> Void) -> AnyPublisher { Deferred { Future { promise in block(promise) } } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Core/FetcherValueTransformation.swift ================================================ import Combine import Foundation extension Fetcher { /** Applies a transformation to the fetcher The transformation works by changing the type of the value the fetcher returns when succeeding Use this transformation when you store a value type but want to mount the fetcher in a pipeline that works with other value types - parameter transformer: The transformation you want to apply - returns: A new fetcher result of the transformation of the original fetcher */ public func transformValues(_ transformer: A) -> BasicFetcher where OutputType == A.TypeIn { BasicFetcher( getClosure: { key in self.get(key) .flatMap(transformer.transform) .eraseToAnyPublisher() } ) } } ================================================ FILE: Sources/Carlos/Core/FunctionComposition.swift ================================================ import Combine import Foundation infix operator >>>: CompositionPrecedence precedencegroup CompositionPrecedence { associativity: left higherThan: AssignmentPrecedence } /** Composes two sync closures - parameter f: A closure taking an A parameter and returning an Optional - parameter g: A closure taking a B parameter and returning an Optional - returns: A closure taking an A parameter and returning an Optional obtained by combining f and g in a way similar to g(f(x)) */ func >>> (f: @escaping (A) -> B?, g: @escaping (B) -> C?) -> (A) -> C? { { x in if let fx = f(x) { return g(fx) } else { return nil } } } /** Composes two sync closures - parameter f: A closure taking an A parameter and returning a value of type B - parameter g: A closure taking a B parameter and returning a value of type C - returns: A closure taking an A parameter and returning a value of type C obtained by combining f and g through g(f(x)) */ func >>> (f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C { { x in g(f(x)) } } /** Composes two async (Future) closures - parameter f: A closure taking an A parameter and returning a Future (basically a future for a B return type) - parameter g: A closure taking a B parameter and returning a Future (basically a future for a C return type) - returns: A closure taking an A parameter and returning a Future (basically a future for a C return type) obtained by combining f and g in a way similar to g(f(x)) (if the closures were sync) */ func >>> (f: @escaping (A) -> AnyPublisher, g: @escaping (B) -> AnyPublisher) -> (A) -> AnyPublisher { { param in f(param).flatMap(g).eraseToAnyPublisher() } } // Expose later if it makes sense to /** Composes two async closures - parameter f: A closure taking an A parameter and a completion callback taking an Optional and returning Void - parameter g: A closure taking a B parameter and a completion callback taking an Optional and returning Void - returns: A closure taking an A parameter and a completion callback taking an Optional and returning Void obtained by combining f and g in a way similar to g(f(x)) (if the closures were sync) */ internal func >>> (f: @escaping (A, (B?) -> Void) -> Void, g: @escaping (B, (C?) -> Void) -> Void) -> (A, (C?) -> Void) -> Void { { x, completion in f(x) { fx in if let fx = fx { g(fx) { result in completion(result) } } else { completion(nil) } } } } ================================================ FILE: Sources/Carlos/Core/Logger.swift ================================================ import Foundation /// A simple logger to use instead of println with configurable output closure public final class Logger { /// The level of the logged message public enum Level: String { case debug = "Debug" case info = "Info" case warning = "Warning" case error = "Error" } private static let queue = DispatchQueue(label: CarlosGlobals.queueNamePrefix + "logger") /** Called to output the log message. Override for custom logging. */ public static var output: (String, Level) -> Void = { msg, level in queue.async { print("[Carlos][\(level.rawValue)]: \(msg)") } } /** Logs a message on the console - parameter message: The message to log This method uses the output closure internally to output the message. The closure is always dispatched on the main queue */ public static func log(_ message: String, _ level: Level = Level.debug) { DispatchQueue.main.async { self.output(message, level) } } } ================================================ FILE: Sources/Carlos/Core/MemoryWarning.swift ================================================ import Foundation #if os(iOS) || os(tvOS) import UIKit #endif #if !os(watchOS) extension CacheLevel where Self: AnyObject { /** Adds a memory warning listener on the given cache - returns: The token that you should use later on to unsubscribe */ public func listenToMemoryWarnings() -> NSObjectProtocol { #if os(macOS) let source = DispatchSource.makeMemoryPressureSource(eventMask: [.warning, .critical]) let workItem = DispatchWorkItem(block: { [weak self] in self?.onMemoryWarning() }) source.setEventHandler(handler: workItem) return source #else return NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: OperationQueue.main, using: { [weak self] _ in self?.onMemoryWarning() }) #endif } } /** Removes the memory warning listener - parameter token: The token you got from the call to listenToMemoryWarning: previously */ public func unsubscribeToMemoryWarnings(_ token: NSObjectProtocol) { #if os(macOS) if let source = token as? DispatchSource { source.cancel() } #else NotificationCenter.default.removeObserver(token, name: UIApplication.didReceiveMemoryWarningNotification, object: nil) #endif } #endif ================================================ FILE: Sources/Carlos/Core/StringConvertible.swift ================================================ import Foundation /// Represents a type that can be converted to a string public protocol StringConvertible { /** - returns: the String representation of the value */ func toString() -> String } extension StringConvertible { public func toString() -> String { "Nan" } } extension String: StringConvertible { /** - returns: The value itself */ public func toString() -> String { self } } extension NSString: StringConvertible { /** - returns: The value itself */ public func toString() -> String { self as String } } extension URL: StringConvertible { /** - returns: The absolute string or an empty string if the absolute string is nil */ public func toString() -> String { absoluteString } } ================================================ FILE: Sources/Carlos/Core/UnfairLock.swift ================================================ // // UnfairLock.swift // // // Created by Lisovyi, Ivan on 16.08.20. // import Foundation final class UnfairLock { private var _lock: UnsafeMutablePointer init() { _lock = UnsafeMutablePointer.allocate(capacity: 1) _lock.initialize(to: os_unfair_lock()) } deinit { _lock.deallocate() } func locked(_ f: () throws -> ReturnValue) rethrows -> ReturnValue { os_unfair_lock_lock(_lock) defer { os_unfair_lock_unlock(_lock) } return try f() } func assertOwned() { os_unfair_lock_assert_owner(_lock) } func assertNotOwned() { os_unfair_lock_assert_not_owner(_lock) } } ================================================ FILE: Sources/Carlos/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Sources/Carlos/Operations/CacheLevel+Batch.swift ================================================ import Combine import Foundation extension CacheLevel { /** Performs a batch of get requests on this CacheLevel - parameter keys: The list of keys to batch - returns: A Future that will call the success callback when all the keys will be either fetched or failed, passing a list containing just the successful results */ public func batchGetSome(_ keys: [KeyType]) -> AnyPublisher<[OutputType], Error> { let allResults: [AnyPublisher, Error>] = keys.map { key in get(key) .map { Result.success($0) } .catch { Just(Result.failure($0)) .setFailureType(to: Error.self) } .eraseToAnyPublisher() } return allResults.publisher .setFailureType(to: Error.self) .flatMap { $0 } .collect(allResults.count) .map { result in result.compactMap { try? $0.get() } } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Operations/KeyTransformation.swift ================================================ import Combine import Foundation extension CacheLevel { /** Applies a transformation to the cache level The transformation works by changing the type of the key the cache accepts Use this transformation when you use a domain specific key or a wrapper key that contains several values every cache level can choose from - parameter transformer: The transformation you want to apply - returns: A new cache level result of the transformation of the original cache level */ public func transformKeys(_ transformer: A) -> BasicCache where KeyType == A.TypeOut { BasicCache( getClosure: { key in transformer.transform(key) .flatMap(self.get) .eraseToAnyPublisher() }, setClosure: { value, key in transformer.transform(key) .flatMap { transformedKey in self.set(value, forKey: transformedKey) } .eraseToAnyPublisher() }, removeClosure: { transformer.transform($0) .flatMap(self.remove) .eraseToAnyPublisher() }, clearClosure: clear, memoryClosure: onMemoryWarning ) } } ================================================ FILE: Sources/Carlos/Operations/Normalize.swift ================================================ import Foundation extension CacheLevel { /** Normalizes the CacheLevel into a BasicCache. Use this function when you want to have a value of type BasicCache (e.g. to store as a instance property) and you don't care about the specific class of the CacheLevel you're going to obtain from the sequence of Carlos composition calls - returns: The CacheLevel normalized to appear as a BasicCache. */ public func normalize() -> BasicCache { if let normalized = self as? BasicCache { return normalized } else { return BasicCache( getClosure: get, setClosure: set, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } } ================================================ FILE: Sources/Carlos/Operations/PostProcess.swift ================================================ import Foundation extension CacheLevel { /** Adds a post-processing step to the get results of this CacheLevel As usual, if the transformation fails, the get request will also fail - parameter transformer: The OneWayTransformer that will be applied to every successful result of the method get called on the cache level. The transformer has to return the same type of the input type, and the transformation won't be applied when setting values on the cache level. - returns: A transformed CacheLevel that incorporates the post-processing step */ public func postProcess(_ transformer: A) -> BasicCache where OutputType == A.TypeIn, A.TypeIn == A.TypeOut { BasicCache( getClosure: { key in self.get(key) .flatMap(transformer.transform) .eraseToAnyPublisher() }, setClosure: set, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } ================================================ FILE: Sources/Carlos/Operations/SwitchCache.swift ================================================ import Foundation import Combine /// Convenience enumeration to specify which of two switched cache levels should be used for a get or set operation public enum CacheLevelSwitchResult { /// The first CacheLevel of the switch case cacheA /// The second CacheLevel of the switch case cacheB } /** Switches two existing cache levels and returns a new cache level with the switching logic inside. This enables you to have multiple cache "lanes" and switch between them depending on the key that is currently being fetcher or set, or some other external condition. - parameter cacheA: The first cache level you want to switch - parameter cacheB: The second cache level you want to switch - parameter switchClosure: The closure where you return which of the two cache levels should be used for get or set calls depending on the key or some other external condition - returns: A new cache level that includes the specified switching logic. clear and onMemoryWarning calls are dispatched to both lanes. */ public func switchLevels(cacheA: A, cacheB: B, switchClosure: @escaping (_ key: A.KeyType) -> CacheLevelSwitchResult) -> BasicCache where A.KeyType == B.KeyType, A.OutputType == B.OutputType { BasicCache( getClosure: { key in switch switchClosure(key) { case .cacheA: return cacheA.get(key) case .cacheB: return cacheB.get(key) } }, setClosure: { value, key in switch switchClosure(key) { case .cacheA: return cacheA.set(value, forKey: key) case .cacheB: return cacheB.set(value, forKey: key) } }, removeClosure: { Publishers.Zip(cacheA.remove($0), cacheB.remove($0)) .map { _ in () } .eraseToAnyPublisher() }, clearClosure: { cacheA.clear() cacheB.clear() }, memoryClosure: { cacheA.onMemoryWarning() cacheB.onMemoryWarning() } ) } ================================================ FILE: Sources/Carlos/Operations/ValueTransformation.swift ================================================ import Combine import Foundation extension CacheLevel { /** Applies a transformation to the cache level The transformation works by changing the type of the value the cache returns when succeeding Use this transformation when you store a value type but want to mount the cache in a pipeline that works with other value types - parameter transformer: The transformation you want to apply - returns: A new cache result of the transformation of the original cache */ public func transformValues(_ transformer: A) -> BasicCache where OutputType == A.TypeIn { BasicCache( getClosure: { key in self.get(key) .flatMap(transformer.transform) .eraseToAnyPublisher() }, setClosure: { value, key in transformer.inverseTransform(value) .flatMap { transformedValue in self.set(transformedValue, forKey: key) } .eraseToAnyPublisher() }, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } ================================================ FILE: Sources/Carlos/Transformers/ComposedOneWayTransformer.swift ================================================ import Foundation extension OneWayTransformer { /** Composes the transformer with another OneWayTransformer - parameter transformer: The second OneWayTransformer to apply - returns: A new OneWayTransformer that is the result of the composition of the two OneWayTransformers */ public func compose(_ transformer: A) -> OneWayTransformationBox where A.TypeIn == TypeOut { OneWayTransformationBox(transform: transform >>> transformer.transform) } } ================================================ FILE: Sources/Carlos/Transformers/ComposedTwoWayTransformer.swift ================================================ import Combine import Foundation extension TwoWayTransformer { /** Composes the transformer with another TwoWayTransformer - parameter transformer: The second TwoWayTransformer to apply - returns: A new TwoWayTransformer that is the result of the composition of the two TwoWayTransformers */ public func compose(_ transformer: A) -> TwoWayTransformationBox where A.TypeIn == TypeOut { TwoWayTransformationBox( transform: transform >>> transformer.transform, inverseTransform: transformer.inverseTransform >>> inverseTransform ) } } ================================================ FILE: Sources/Carlos/Transformers/ConditionedOneWayTransformer.swift ================================================ import Combine import Foundation /// Abstract an object that can conditionally transform values to another type public protocol ConditionedOneWayTransformer { /// The input type of the transformer associatedtype TypeIn /// The output type of the transformer associatedtype TypeOut /// The type of the key used to evaluate the condition associatedtype KeyType /** Apply the conditional transformation from A to B - parameter key: The key to use to evaluate the condition - parameter val: The value to transform - returns: A Future that will contain the transformed value, or fail if the transformation failed */ func conditionalTransform(key: KeyType, value: TypeIn) -> AnyPublisher } /// Simple implementation of the ConditionedOneWayTransformer protocol public final class ConditionedOneWayTransformationBox: ConditionedOneWayTransformer { /// The input type of the transformation box public typealias TypeIn = InputType /// The output type of the transformation box public typealias TypeOut = OutputType /// The key type used by the transformation box public typealias KeyType = Key private let conditionalTransformClosure: (_ key: Key, _ value: InputType) -> AnyPublisher /** Initializes a conditioned 1-way transformation box with the given closure - parameter conditionalTransformClosure: The conditional transformation closure to convert a value of type TypeIn into a value of type TypeOut given a key of type KeyType */ public init(conditionalTransformClosure: @escaping (_ key: Key, _ value: InputType) -> AnyPublisher) { self.conditionalTransformClosure = conditionalTransformClosure } /** Convenience initializer to create a conditioned 1-way transformation box through a normal 1-way transformer - parameter transformer: The normal OneWayTransformer with matching input and output type This initializer will basically ignore the key */ public convenience init(transformer: T) where T.TypeIn == TypeIn, T.TypeOut == TypeOut { self.init(conditionalTransformClosure: { _, value in transformer.transform(value) }) } /** Conditionally transforms a value of type TypeIn into a value of type TypeOut, based on the given key - parameter key: The key to use to evaluate the condition - parameter val: The value to convert - returns: A Future that will contain the converted value or fail if the transformation fails */ public func conditionalTransform(key: KeyType, value: TypeIn) -> AnyPublisher { conditionalTransformClosure(key, value) } } ================================================ FILE: Sources/Carlos/Transformers/ConditionedOutputProcessing.swift ================================================ import Combine import Foundation extension CacheLevel { /** Adds a conditioned post-processing step to the get results of this CacheLevel As usual, if the transformation fails, the get request will also fail - parameter conditionedTransformer: The transformer that will be applied to every successful result of the method get called on the cache level. The object gets the key used for the get request (where it can apply its condition on) and the fetched value, and has to return the same type of the value. The transformation won't be applied when setting values on the cache level. - returns: A transformed CacheLevel that incorporates the post-processing step */ public func conditionedPostProcess(_ conditionedTransformer: T) -> BasicCache where T.KeyType == KeyType, T.TypeIn == OutputType, T.TypeOut == OutputType { BasicCache( getClosure: { key in self.get(key) .flatMap { conditionedTransformer.conditionalTransform(key: key, value: $0) } .eraseToAnyPublisher() }, setClosure: set, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } ================================================ FILE: Sources/Carlos/Transformers/ConditionedTwoWayTransformer.swift ================================================ import Combine import Foundation /// Abstract an object that can conditionally transform values to another type and back to the original one, based on a given key public protocol ConditionedTwoWayTransformer: ConditionedOneWayTransformer { /** Conditionally apply the inverse transformation from B to A - parameter key: The key to use to evaluate the condition - parameter val: The value to inverse transform - returns: A Future that will contain the original value, or fail if the transformation failed */ func conditionalInverseTransform(key: KeyType, value: TypeOut) -> AnyPublisher } extension ConditionedTwoWayTransformer { /** Inverts a ConditionedTwoWayTransformer - returns: A ConditionedTwoWayTransformationBox that takes the output type of the original transformer and returns the input type of the original transformer, maintaining the condition unmodified */ public func invert() -> ConditionedTwoWayTransformationBox { ConditionedTwoWayTransformationBox(conditionalTransformClosure: conditionalInverseTransform, conditionalInverseTransformClosure: conditionalTransform) } } /// Simple implementation of the ConditionedTwoWayTransformer protocol public final class ConditionedTwoWayTransformationBox: ConditionedTwoWayTransformer { /// The input type of the transformation box public typealias TypeIn = InputType /// The output type of the transformation box public typealias TypeOut = OutputType /// The key type to use for the condition public typealias KeyType = Key private let conditionalTransformClosure: (_ key: Key, _ value: InputType) -> AnyPublisher private let conditionalInverseTransformClosure: (_ key: Key, _ value: OutputType) -> AnyPublisher /** Initializes a new instance of a conditioned 2-way transformation box - parameter conditionalTransformClosure: The conditional transformation closure to convert a value of type TypeIn to a value of type TypeOut - parameter conditionalInverseTransformClosure: The conditional transformation closure to convert a value of type TypeOut to a value of type TypeIn */ public init(conditionalTransformClosure: @escaping (_ key: Key, _ value: InputType) -> AnyPublisher, conditionalInverseTransformClosure: @escaping (_ key: Key, _ value: OutputType) -> AnyPublisher) { self.conditionalTransformClosure = conditionalTransformClosure self.conditionalInverseTransformClosure = conditionalInverseTransformClosure } /** Convenience initializer to create a conditioned 2-way transformation box through a normal 2-way transformer - parameter transformer: The normal TwoWayTransformer with matching input and output type This initializer will basically ignore the key */ public convenience init(transformer: T) where T.TypeIn == TypeIn, T.TypeOut == TypeOut { self.init(conditionalTransformClosure: { _, value in transformer.transform(value) }, conditionalInverseTransformClosure: { _, value in transformer.inverseTransform(value) }) } /** Conditionally converts a value of type TypeIn to a value of type TypeOut, based on the given key - parameter key: The key to use to evaluate the condition - parameter val: The value to convert - returns: A Future that will contain the converted value, or fail if the transformation fails */ public func conditionalTransform(key: KeyType, value: TypeIn) -> AnyPublisher { conditionalTransformClosure(key, value) } /** Conditionally converts a value of type TypeOut to a value of type TypeIn, based on the given key - parameter key: The key to use to evaluate the condition - parameter val: The value to convert - returns: A Future that will contain the converted value, or fail if the inverse transformation fails */ public func conditionalInverseTransform(key: KeyType, value: TypeOut) -> AnyPublisher { conditionalInverseTransformClosure(key, value) } } ================================================ FILE: Sources/Carlos/Transformers/ConditionedValueTransformation.swift ================================================ import Combine import Foundation extension CacheLevel { /** Applies a conditional transformation to the cache level The transformation works by changing the type of the value the cache returns when succeeding Use this transformation when you store a value type but want to mount the cache in a pipeline that works with other value types - parameter conditionedTransformer: The conditioned transformer that will be applied to every successful result of the method get or (inverse transform) set called on the cache level. The object gets the key used for the get request (where it can apply its condition on) and the fetched value, and returns the transformed value. - returns: A new cache result of the transformation of the original cache */ public func conditionedValueTransformation(transformer: A) -> BasicCache where OutputType == A.TypeIn, A.KeyType == KeyType { BasicCache( getClosure: { key -> AnyPublisher in self.get(key) .flatMap { transformer.conditionalTransform(key: key, value: $0) } .eraseToAnyPublisher() }, setClosure: { value, key in return transformer.conditionalInverseTransform(key: key, value: value) .flatMap { transformedValue in self.set(transformedValue, forKey: key) } .eraseToAnyPublisher() }, removeClosure: remove, clearClosure: clear, memoryClosure: onMemoryWarning ) } } ================================================ FILE: Sources/Carlos/Transformers/ImageTransformer.swift ================================================ import Foundation import Combine #if os(macOS) import Cocoa public typealias CarlosImage = NSImage #else import UIKit public typealias CarlosImage = UIImage #endif /** This class takes care of transforming NSData instances into UIImage (or NSImage objects on macOS). Keep in mind that at the moment this class always deserializes images through UIImagePNGRepresentation, so there may be a data usage bigger than actually required. */ public final class ImageTransformer: TwoWayTransformer { public enum TransformationError: Error { case invalidData case cannotConvertImage } public typealias TypeIn = NSData public typealias TypeOut = CarlosImage /// Initializes a new instance of ImageTransformer public init() {} /** Serializes an NSData instance into a UIImage - parameter val: The NSData you want to serialize - returns: A Future object */ public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { promise in if let image = CarlosImage(data: val as Data) { promise(.success(image)) } else { promise(.failure(TransformationError.invalidData)) } } .eraseToAnyPublisher() } /** Deserializes an UIImage instance into NSData - parameter val: The UIImage you want to deserialize - returns: A Future instance obtained with UIImagePNGRepresentation */ public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { promise in #if os(macOS) if let rep = val.tiffRepresentation, let bitmapImageRep = NSBitmapImageRep(data: rep) { promise(.success(bitmapImageRep.representation(using: .png, properties: [:]))) } promise(.success(nil)) #else /* This is a waste of bytes, we should probably use a lower-level framework */ promise(.success(val.pngData())) #endif }.flatMap { (data: Data?) -> AnyPublisher in guard let data = data else { return Fail(error: TransformationError.cannotConvertImage).eraseToAnyPublisher() } return Just(data as NSData) .setFailureType(to: Error.self) .eraseToAnyPublisher() } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Transformers/JSONTransformer.swift ================================================ import Combine import Foundation /** This class takes care of transforming NSData instances into JSON objects in the form of AnyObject instances. Depending on your usage, the AnyObject could contain an Array, a Dictionary, or nil if the NSData is not a valid JSON */ public final class JSONTransformer: TwoWayTransformer { public typealias TypeIn = NSData public typealias TypeOut = AnyObject /// Initializes a new instance of JSONTransformer public init() {} /** Parses JSON from an NSData instance into an AnyObject instance - parameter val: The NSData representing the received JSON - returns: A Future value, with the parsed JSON if the input data was valid */ public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { promise in do { let transformed = try JSONSerialization.jsonObject(with: val as Data, options: [.allowFragments]) as AnyObject promise(.success(transformed)) } catch { promise(.failure(error)) } } } /** Deserializes a JSON object into an NSData instance - parameter val: The JSON object you want to deserialize - returns: A Future value, with the deserialized JSON if the input was valid */ public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { promise in do { let transformed = try JSONSerialization.data(withJSONObject: val, options: []) promise(.success(transformed as NSData)) } catch { promise(.failure(error)) } } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Transformers/OneWayTransformer.swift ================================================ import Combine import Foundation /// Abstract an object that can transform values to another type public protocol OneWayTransformer { /// The input type of the transformer associatedtype TypeIn /// The output type of the transformer associatedtype TypeOut /** Apply the transformation from A to B - parameter val: The value to transform - returns: A Future that will contain the transformed value, or fail if the transformation failed */ func transform(_ val: TypeIn) -> AnyPublisher } /// Simple implementation of the OneWayTransformer protocol public final class OneWayTransformationBox: OneWayTransformer { /// The input type of the transformation box public typealias TypeIn = I /// The output type of the transformation box public typealias TypeOut = O private let transformClosure: (I) -> AnyPublisher /** Initializes a 1-way transformation box with the given closure - parameter transform: The transformation closure to convert a value of type TypeIn into a value of type TypeOut */ public init(transform: @escaping ((I) -> AnyPublisher)) { transformClosure = transform } /** Transforms a value of type TypeIn into a value of type TypeOut - parameter val: The value to convert - returns: A Future that will contain the converted value or fail if the transformation fails */ public func transform(_ val: TypeIn) -> AnyPublisher { transformClosure(val) } } ================================================ FILE: Sources/Carlos/Transformers/StringTransformer.swift ================================================ import Combine import Foundation /** This class takes care of transforming NSData instances into String values. */ public final class StringTransformer: TwoWayTransformer { public enum TransformationError: Error { case invalidData case dataConversionToStringFailed } public typealias TypeIn = NSData public typealias TypeOut = String private let encoding: String.Encoding /** Initializes a new instance of StringTransformer - parameter encoding: The encoding the transformer will use when serializing and deserializing NSData instances. By default it's NSUTF8StringEncoding */ public init(encoding: String.Encoding = String.Encoding.utf8) { self.encoding = encoding } /** Serializes a NSData instance into a String with the configured encoding - parameter val: The NSData instance to serialize - returns: A Future containing the serialized String with the given encoding if the input is valid */ public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { [encoding] promise in guard let string = String(data: val as Data, encoding: encoding) else { promise(.failure(TransformationError.invalidData)) return } promise(.success(string)) } .eraseToAnyPublisher() } /** Deserializes a String into a NSData instance - parameter val: The String to deserialize - returns: A Future instance containing the bytes representation of the given string */ public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { [encoding] promise in guard let data = val.data(using: encoding, allowLossyConversion: false) as NSData? else { promise(.failure(TransformationError.dataConversionToStringFailed)) return } promise(.success(data)) } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Transformers/Transformers.swift ================================================ import Combine import Foundation import MapKit public enum NSDateFormatterError: Error { case invalidInputString } /** NSDateFormatter extension to conform to the TwoWayTransformer protocol This class transforms from NSDate to String (transform) and viceversa (inverseTransform) */ extension DateFormatter: TwoWayTransformer { public typealias TypeIn = Date public typealias TypeOut = String public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { promise in promise(.success(self.string(from: val))) } .eraseToAnyPublisher() } public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { promise in guard let date = self.date(from: val) else { promise(.failure(NSDateFormatterError.invalidInputString)) return } promise(.success(date)) } .eraseToAnyPublisher() } } public enum NSNumberFormatterError: Error { case cannotConvertToString case invalidString } /** NSNumberFormatter extension to conform to the TwoWayTransformer protocol This class transforms from NSNumber to String (transform) and viceversa (inverseTransform) */ extension NumberFormatter: TwoWayTransformer { public typealias TypeIn = NSNumber public typealias TypeOut = String public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { promise in guard let string = self.string(from: val) else { promise(.failure(NSNumberFormatterError.cannotConvertToString)) return } promise(.success(string)) } .eraseToAnyPublisher() } public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { promise in guard let number = self.number(from: val) else { promise(.failure(NSNumberFormatterError.invalidString)) return } promise(.success(number)) } .eraseToAnyPublisher() } } /** MKDistanceFormatter extension to conform to the TwoWayTransformer protocol This class transforms from CLLocationDistance to String (transform) and viceversa (inverseTransform) */ extension MKDistanceFormatter: TwoWayTransformer { public typealias TypeIn = CLLocationDistance public typealias TypeOut = String public func transform(_ val: TypeIn) -> AnyPublisher { AnyPublisher.create { $0(.success(self.string(fromDistance: val))) } .eraseToAnyPublisher() } public func inverseTransform(_ val: TypeOut) -> AnyPublisher { AnyPublisher.create { $0(.success(self.distance(from: val))) } .eraseToAnyPublisher() } } ================================================ FILE: Sources/Carlos/Transformers/TwoWayTransformer.swift ================================================ import Combine import Foundation /// Abstract an object that can transform values to another type and back to the original one public protocol TwoWayTransformer: OneWayTransformer { /** Apply the inverse transformation from B to A - parameter val: The value to inverse transform - returns: A Future that will contain the original value, or fail if the transformation failed */ func inverseTransform(_ val: TypeOut) -> AnyPublisher } extension TwoWayTransformer { /** Inverts a TwoWayTransformer - returns: A TwoWayTransformationBox that takes the output type of the original transformer and returns the input type of the original transformer */ public func invert() -> TwoWayTransformationBox { TwoWayTransformationBox( transform: inverseTransform, inverseTransform: transform ) } } /// Simple implementation of the TwoWayTransformer protocol public final class TwoWayTransformationBox: TwoWayTransformer { /// The input type of the transformation box public typealias TypeIn = I /// The output type of the transformation box public typealias TypeOut = O private let transformClosure: (I) -> AnyPublisher private let inverseTransformClosure: (O) -> AnyPublisher /** Initializes a new instance of a 2-way transformation box - parameter transform: The transformation closure to convert a value of type TypeIn to a value of type TypeOut - parameter inverseTransform: The transformation closure to convert a value of type TypeOut to a value of type TypeIn */ public init(transform: @escaping ((I) -> AnyPublisher), inverseTransform: @escaping ((O) -> AnyPublisher)) { transformClosure = transform inverseTransformClosure = inverseTransform } /** Converts a value of type TypeIn to a value of type TypeOut - parameter val: The value to convert - returns: A Future that will contain the converted value, or fail if the transformation fails */ public func transform(_ val: I) -> AnyPublisher { transformClosure(val) } /** Converts a value of type TypeOut to a value of type TypeIn - parameter val: The value to convert - returns: A Future that will contain the converted value, or fail if the inverse transformation fails */ public func inverseTransform(_ val: O) -> AnyPublisher { inverseTransformClosure(val) } } ================================================ FILE: Tests/CarlosTests/BasicCacheTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine enum TestError: Error { case anotherError case simpleError } final class BasicCacheTests: QuickSpec { override func spec() { describe("BasicCache") { var cache: BasicCache! var numberOfTimesCalledClear = 0 var numberOfTimesCalledOnMemoryWarning = 0 var numberOfTimesCalledGet = 0 var numberOfTimesCalledSet = 0 var numberOfTimesCalledRemove = 0 var didSetKey: String? var didSetValue: Int? var didGetKey: String? var removeKey: String? var getSubject: PassthroughSubject! var setSubject: PassthroughSubject! var removeSubject: PassthroughSubject! var cancellable: AnyCancellable? beforeEach { numberOfTimesCalledClear = 0 numberOfTimesCalledGet = 0 numberOfTimesCalledOnMemoryWarning = 0 numberOfTimesCalledSet = 0 getSubject = PassthroughSubject() setSubject = PassthroughSubject() cache = BasicCache( getClosure: { key in didGetKey = key numberOfTimesCalledGet += 1 return getSubject.eraseToAnyPublisher() }, setClosure: { value, key in didSetKey = key didSetValue = value numberOfTimesCalledSet += 1 return setSubject.eraseToAnyPublisher() }, removeClosure: { key in numberOfTimesCalledRemove += 1 removeKey = key return removeSubject.eraseToAnyPublisher() }, clearClosure: { numberOfTimesCalledClear += 1 }, memoryClosure: { numberOfTimesCalledOnMemoryWarning += 1 } ) } afterEach { cancellable?.cancel() cancellable = nil } context("when calling get") { let key = "key to test" var succeeded: Int? var failed: Error? var canceled: Bool! beforeEach { canceled = false failed = nil succeeded = nil cancellable = cache.get(key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { succeeded = $0 }) } it("should call the closure") { expect(numberOfTimesCalledGet) == 1 } it("should pass the right key") { expect(didGetKey) == key } context("when the get closure succeeds") { let value = 3 beforeEach { getSubject.send(value) } it("should succeed the future") { expect(succeeded) == value } } context("when the get clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled) == true } } context("when the get closure fails") { let error = TestError.anotherError beforeEach { getSubject.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError) == error } } } context("when calling set") { let key = "test key" let value = 101 var succeeded: Bool! var failed: Error? var canceled: Bool! beforeEach { succeeded = false failed = nil canceled = false cancellable = cache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { succeeded = true }) } it("should call the closure") { expect(numberOfTimesCalledSet) == 1 } it("should pass the right key") { expect(didSetKey) == key } it("should pass the right value") { expect(didSetValue) == value } context("when the set closure succeeds") { beforeEach { setSubject.send() } it("should succeed the future") { expect(succeeded) == true } } context("when the set clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled) == true } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { setSubject.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError) == error } } } context("when calling clear") { beforeEach { cache.clear() } it("should call the closure") { expect(numberOfTimesCalledClear) == 1 } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should call the closure") { expect(numberOfTimesCalledOnMemoryWarning) == 1 } } } } } ================================================ FILE: Tests/CarlosTests/BasicFetcherTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class BasicFetcherTests: QuickSpec { override func spec() { describe("BasicFetcher") { var fetcher: BasicFetcher! var numberOfTimesCalledGet = 0 var didGetKey: String? var getSubject: PassthroughSubject! var cancellable: AnyCancellable? beforeEach { numberOfTimesCalledGet = 0 getSubject = PassthroughSubject() fetcher = BasicFetcher( getClosure: { key in didGetKey = key numberOfTimesCalledGet += 1 return getSubject.eraseToAnyPublisher() } ) } afterEach { cancellable?.cancel() cancellable = nil } context("when calling get") { let key = "key to test" var succeeded: Int? var failed: Error? var canceled: Bool! beforeEach { canceled = false failed = nil succeeded = nil cancellable = fetcher.get(key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { succeeded = $0 }) } it("should call the closure") { expect(numberOfTimesCalledGet) == 1 } it("should pass the right key") { expect(didGetKey) == key } context("when the get closure succeeds") { let value = 3 beforeEach { getSubject.send(value) } it("should succeed the future") { expect(succeeded) == value } } context("when the get clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled) == true } } context("when the get closure fails") { let error = TestError.anotherError beforeEach { getSubject.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError) == error } } } context("when calling set") { var succeeded: Bool! beforeEach { succeeded = false _ = fetcher.set(0, forKey: "") .sink(receiveCompletion: { _ in succeeded = true }, receiveValue: {}) } it("should immediately succeed the future") { expect(succeeded) == true } } } } } ================================================ FILE: Tests/CarlosTests/BatchTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class BatchAllCacheTests: QuickSpec { override func spec() { describe("allBatch") { let requestsCount = 5 var internalCache: CacheLevelFake! var cache: BatchAllCache<[Int], CacheLevelFake>! var cancellable: AnyCancellable? beforeEach { internalCache = CacheLevelFake() cache = internalCache.allBatch() } afterEach { cancellable?.cancel() cancellable = nil } context("when calling clear") { beforeEach { cache.clear() } it("should call clear on the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should call onMemoryWarning on the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } context("when calling set") { var succeeded: Bool! var failed: Error? let keys = [1, 2, 3] let values = ["", "", ""] beforeEach { cancellable = cache.set(values, forKey: keys) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in succeeded = true }) } it("should call set on the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(values.count)) } context("when one of the set calls fails") { let error = TestError.anotherError beforeEach { internalCache.setPublishers[0]?.send() internalCache.setPublishers[1]?.send(completion: .failure(error)) } it("should fail the whole future") { expect(failed as? TestError).toEventually(equal(error)) } } context("when all the set calls succeed") { beforeEach { internalCache.setPublishers[1]?.send() internalCache.setPublishers[2]?.send() internalCache.setPublishers[3]?.send() } it("should succeed the whole future") { expect(succeeded).toEventually(beTrue()) } } } context("when calling get") { var result: [String]? var errors: [Error]! beforeEach { errors = [] result = nil cancellable = cache.get(Array(0..! var cancellable: AnyCancellable? beforeEach { cache = CacheLevelFake() } afterEach { cancellable?.cancel() } describe("batchGetSome") { var result: [String]! var errors: [Error]! beforeEach { errors = [] result = nil cancellable = cache.batchGetSome(Array(0..! beforeEach { cache = CacheProvider.imageCache() } it("should always return new instances") { expect(CacheProvider.imageCache()).notTo(beIdenticalTo(cache)) } } context("when calling dataCache") { var cache: BasicCache! beforeEach { cache = CacheProvider.dataCache() } it("should always return new instances") { expect(CacheProvider.dataCache()).notTo(beIdenticalTo(cache)) } } context("when calling JSONCache") { var cache: BasicCache! beforeEach { cache = CacheProvider.JSONCache() } it("should always return new instances") { expect(CacheProvider.JSONCache()).notTo(beIdenticalTo(cache)) } } context("when calling sharedImageCache") { var cache: BasicCache! beforeEach { cache = CacheProvider.sharedImageCache } it("should always return the same instance") { expect(CacheProvider.sharedImageCache) === cache } } context("when calling sharedDataCache") { var cache: BasicCache! beforeEach { cache = CacheProvider.sharedDataCache } it("should always return the same instance") { expect(CacheProvider.sharedDataCache) === cache } } context("when calling sharedJSONCache") { var cache: BasicCache! beforeEach { cache = CacheProvider.sharedJSONCache } it("should always return the same instance") { expect(CacheProvider.sharedJSONCache) === cache } } } } } ================================================ FILE: Tests/CarlosTests/CompositionTests.swift ================================================ import Foundation import XCTest import Nimble import Quick import Carlos import Combine struct ComposedCacheSharedExamplesContext { static let CacheToTest = "composedCache" static let FirstComposedCache = "cache1" static let SecondComposedCache = "cache2" } final class CompositionSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("get without considering set calls") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } context("when calling get") { let key = "test key" var cancellable: AnyCancellable? var cache1Subject: PassthroughSubject! var cache2Subject: PassthroughSubject! var successSentinel: Bool? var failureSentinel: Bool? var cancelSentinel: Bool! var successValue: Int? beforeEach { cancelSentinel = false successSentinel = nil successValue = nil failureSentinel = nil cache1Subject = PassthroughSubject() cache1.getSubject = cache1Subject cache1.setSubject = PassthroughSubject() cache2Subject = PassthroughSubject() cache2.getSubject = cache2Subject cache2.setSubject = PassthroughSubject() for cache in [cache1, cache2] { cache?.numberOfTimesCalledGet = 0 cache?.numberOfTimesCalledSet = 0 } cancellable = composedCache.get(key) .handleEvents(receiveCancel: { cancelSentinel = true }) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { value in successSentinel = true successValue = value }) } afterEach { cancellable?.cancel() cancellable = nil } it("should not call any success closure") { expect(successSentinel).toEventually(beNil()) } it("should not call any failure closure") { expect(failureSentinel).toEventually(beNil()) } it("should not call any cancel closure") { expect(cancelSentinel).toEventually(beFalse()) } it("should call get on the first cache") { expect(cache1.numberOfTimesCalledGet).toEventually(equal(1)) } it("should not call get on the second cache") { expect(cache2.numberOfTimesCalledGet).toEventually(equal(0)) } context("when the first request succeeds") { let value = 1022 beforeEach { cache1Subject.send(value) } it("should call the success closure") { expect(successSentinel).toEventuallyNot(beNil()) } it("should pass the right value") { expect(successValue).toEventually(equal(value)) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beFalse()) } it("should not call get on the second cache") { expect(cache2.numberOfTimesCalledGet).toEventually(equal(0)) } } context("when the request is canceled") { beforeEach { cancellable?.cancel() } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } it("should call the cancel closure") { expect(cancelSentinel).toEventually(beTrue()) } } context("when the first request fails") { beforeEach { cache1Subject.send(completion: .failure(TestError.simpleError)) } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beFalse()) } it("should call get on the second cache") { expect(cache2.numberOfTimesCalledGet).toEventually(equal(1)) } it("should not do other get calls on the first cache") { expect(cache1.numberOfTimesCalledGet).toEventually(equal(1)) } context("when the second request succeeds") { let value = -122 beforeEach { cache2Subject.send(value) cache1.setSubject?.send(()) } it("should call the success closure") { expect(successSentinel).toEventuallyNot(beNil()) } it("should pass the right value") { expect(successValue).toEventually(equal(value)) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beFalse()) } } context("when the second request fails") { beforeEach { cache2Subject.send(completion: .failure(TestError.simpleError)) } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } it("should call the failure closure") { expect(failureSentinel).toEventuallyNot(beNil()) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beFalse()) } it("should not do other get calls on the first cache") { expect(cache1.numberOfTimesCalledGet).toEventually(equal(1)) } it("should not do other get calls on the second cache") { expect(cache2.numberOfTimesCalledGet).toEventually(equal(1)) } } } } } sharedExamples("get on caches") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } context("when calling get") { let key = "test key" var cancellable: AnyCancellable? var cache1Subject: PassthroughSubject! var cache2Subject: PassthroughSubject! beforeEach { cache1Subject = PassthroughSubject() cache1.getSubject = cache1Subject cache2Subject = PassthroughSubject() cache2.getSubject = cache2Subject for cache in [cache1, cache2] { cache?.numberOfTimesCalledGet = 0 cache?.numberOfTimesCalledSet = 0 } cancellable = composedCache.get(key).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } afterEach { cancellable?.cancel() cancellable = nil } itBehavesLike("get without considering set calls") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } context("when the first request fails") { beforeEach { cache1Subject.send(completion: .failure(TestError.simpleError)) } context("when the second request succeeds") { let value = -122 beforeEach { cache2Subject.send(value) } it("should set the value on the first cache") { expect(cache1.numberOfTimesCalledSet).toEventually(equal(1)) } it("should set the value on the first cache with the right key") { expect(cache1.didSetKey).toEventually(equal(key)) } it("should set the right value on the first cache") { expect(cache1.didSetValue).toEventually(equal(value)) } it("should not set the same value again on the second cache") { expect(cache2.numberOfTimesCalledSet).toEventually(equal(0)) } } } } } sharedExamples("both caches are caches") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("first cache is a cache") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } itBehavesLike("second cache is a cache") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } context("when calling set") { let key = "this key" let value = 102 var succeeded: Bool! var failed: Error? var canceled: Bool! var cancellable: AnyCancellable? beforeEach { succeeded = false failed = nil canceled = false cancellable = composedCache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in succeeded = true }) } afterEach { cancellable?.cancel() cancellable = nil } it("should call set on the first cache") { expect(cache1.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key on the first cache") { expect(cache1.didSetKey).toEventually(equal(key)) } it("should pass the right value on the first cache") { expect(cache1.didSetValue).toEventually(equal(value)) } context("when the set closure succeeds") { beforeEach { cache1.setPublishers[key]?.send() } it("should call set on the second cache") { expect(cache2.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key on the second cache") { expect(cache2.didSetKey).toEventually(equal(key)) } it("should pass the right value on the second cache") { expect(cache2.didSetValue).toEventually(equal(value)) } context("when the set closure succeeds") { beforeEach { cache2.setPublishers[key]?.send() } it("should succeed the future") { expect(succeeded).toEventually(beTrue()) } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { cache2.setPublishers[key]?.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError).toEventually(equal(error)) } } } context("when the set clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled).toEventually(beTrue()) } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { cache1.setPublishers[key]?.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError).toEventually(equal(error)) } } } } sharedExamples("first cache is a cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } context("when calling set") { let key = "this key" let value = 102 var failed: Error? var canceled: Bool! var cancellable: AnyCancellable? beforeEach { failed = nil canceled = false cancellable = composedCache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in }) } afterEach { cancellable?.cancel() cancellable = nil } it("should call set on the first cache") { expect(cache1.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key on the first cache") { expect(cache1.didSetKey).toEventually(equal(key)) } it("should pass the right value on the first cache") { expect(cache1.didSetValue).toEventually(equal(value)) } context("when the set clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled).toEventually(beTrue()) } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { cache1.setPublishers[key]?.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError).toEventually(equal(error)) } } } context("when calling clear") { beforeEach { composedCache.clear() } it("should call clear on the first cache") { expect(cache1.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { composedCache.onMemoryWarning() } it("should call onMemoryWarning on the first cache") { expect(cache1.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } sharedExamples("second cache is a cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache2: CacheLevelFake! var cache1: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } context("when calling set") { let key = "this key" let value = 102 var succeeded: Bool! var failed: Error? var canceled: Bool! var cancellable: AnyCancellable? beforeEach { succeeded = false failed = nil canceled = false cancellable = composedCache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in succeeded = true }) } afterEach { cancellable?.cancel() cancellable = nil } it("should call set on the second cache") { expect(cache2.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key on the second cache") { expect(cache2.didSetKey).toEventually(equal(key)) } it("should pass the right value on the second cache") { expect(cache2.didSetValue).toEventually(equal(value)) } context("when the set closure succeeds") { beforeEach { cache1.setPublishers[key]?.send() cache2.setPublishers[key]?.send() } it("should succeed the future") { expect(succeeded).toEventually(beTrue()) } } context("when the set clousure is canceled") { beforeEach { cancellable?.cancel() } it("should cancel the future") { expect(canceled).toEventually(beTrue()) } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { cache1.setPublishers[key]?.send(completion: .failure(error)) cache2.setPublishers[key]?.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError).toEventually(equal(error)) } } } context("when calling clear") { beforeEach { composedCache.clear() } it("should call clear on the second cache") { expect(cache2.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { composedCache.onMemoryWarning() } it("should call onMemoryWarning on the second cache") { expect(cache2.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } sharedExamples("a composition of two fetch closures") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("get without considering set calls") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } } sharedExamples("a composition of a fetch closure and a cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("get without considering set calls") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } itBehavesLike("second cache is a cache") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } } sharedExamples("a composition of a cache and a fetch closure") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("get on caches") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } itBehavesLike("first cache is a cache") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } } sharedExamples("a composed cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! beforeEach { cache1 = sharedExampleContext()[ComposedCacheSharedExamplesContext.FirstComposedCache] as? CacheLevelFake cache2 = sharedExampleContext()[ComposedCacheSharedExamplesContext.SecondComposedCache] as? CacheLevelFake composedCache = sharedExampleContext()[ComposedCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("get on caches") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } itBehavesLike("both caches are caches") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } } } } final class CacheLevelCompositionTests: QuickSpec { override func spec() { var cache1: CacheLevelFake! var cache2: CacheLevelFake! var composedCache: BasicCache! describe("Cache composition using two cache levels with the instance function") { beforeEach { cache1 = CacheLevelFake() cache2 = CacheLevelFake() composedCache = cache1.compose(cache2) } itBehavesLike("a composed cache") { [ ComposedCacheSharedExamplesContext.FirstComposedCache: cache1 as Any, ComposedCacheSharedExamplesContext.SecondComposedCache: cache2 as Any, ComposedCacheSharedExamplesContext.CacheToTest: composedCache as Any ] } } } } final class ComposedCacheTests: XCTestCase { var cancellables: Set! var cache: BasicCache! override func setUp() { super.setUp() cancellables = Set() cache = MemoryCacheLevel().compose(DiskCacheLevel()) } override func tearDown() { cache.clear() super.tearDown() } func testGet_whenKeyDoesNotExistInCache_shallCompleteWithError() { let expectation = self.expectation(description: #function) cache.get("does_not_exist") .sink(receiveCompletion: { completion in switch completion { case let .failure(error): XCTAssertEqual(error as! FetchError, FetchError.valueNotInCache) expectation.fulfill() case .finished: XCTFail("Shall not finish") } }, receiveValue: { _ in XCTFail("Shall not receive any value") }) .store(in: &cancellables) wait(for: [expectation], timeout: 1.0) } func testGet_whenKeyDoesExistInCache_shallReturnCachedValue() { let expectation = self.expectation(description: #function) let key = "does_exist" let expectedValue = "value" cache.set(expectedValue.data(using: .utf8)! as NSData, forKey: key) .flatMap { self.cache.get(key) } .sink(receiveCompletion: { completion in switch completion { case let .failure(error): XCTFail("Shall not fail with error \(error)") case .finished: expectation.fulfill() } }, receiveValue: { value in let resultValue = String(data: value as Data, encoding: .utf8) XCTAssertEqual(resultValue, expectedValue) }) .store(in: &cancellables) wait(for: [expectation], timeout: 1.0) } } ================================================ FILE: Tests/CarlosTests/ConditionedCacheTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ConditionedCacheSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" } final class ConditionedCacheSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a conditioned fetch closure") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var cancellable: AnyCancellable? beforeEach { cache = sharedExampleContext()[ConditionedCacheSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ConditionedCacheSharedExamplesContext.InternalCache] as? CacheLevelFake } context("when calling get") { let value = 221 var fakeRequest: PassthroughSubject! var successSentinel: Bool? var successValue: Int? var failureSentinel: Bool? var failureValue: Error? var cancelSentinel: Bool? beforeEach { failureSentinel = nil failureValue = nil successSentinel = nil successValue = nil cancelSentinel = nil fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest } afterEach { cancellable?.cancel() cancellable = nil } context("when the condition is satisfied") { let key = "this key works" beforeEach { cancellable = cache.get(key) .handleEvents(receiveCancel: { cancelSentinel = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureSentinel = true failureValue = error } }, receiveValue: { value in successSentinel = true successValue = value }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { beforeEach { fakeRequest.send(value) } it("should call the original closure") { expect(successSentinel).toEventuallyNot(beNil()) } it("should pass the right value") { expect(successValue).toEventually(equal(value)) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beNil()) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } } context("when the request is canceled") { beforeEach { cancellable?.cancel() } it("should call the original closure") { expect(cancelSentinel).toEventually(beTrue()) } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } it("should not call the failure closure") { expect(failureSentinel).toEventually(beNil()) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original closure") { expect(failureSentinel).toEventuallyNot(beNil()) } it("should pass the right error") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beNil()) } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } } } context("when the condition is not satisfied") { let key = ":(" beforeEach { cancellable = cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureSentinel = true failureValue = error } }, receiveValue: { value in successSentinel = true successValue = value }) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(0)) } it("should call the failure closure") { expect(failureSentinel).toEventuallyNot(beNil()) } it("should pass the provided error") { expect(failureValue as? ConditionError).toEventually(equal(ConditionError.MyError)) } it("should not call the cancel closure") { expect(cancelSentinel).toEventually(beNil()) } it("should not call the success closure") { expect(successSentinel).toEventually(beNil()) } } } } sharedExamples("a conditioned cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! beforeEach { cache = sharedExampleContext()[ConditionedCacheSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ConditionedCacheSharedExamplesContext.InternalCache] as? CacheLevelFake } itBehavesLike("a conditioned fetch closure") { [ ConditionedCacheSharedExamplesContext.CacheToTest: cache as Any, ConditionedCacheSharedExamplesContext.InternalCache: internalCache as Any ] } context("when calling set") { let key = "test-key" let value = 201 beforeEach { _ = cache.set(value, forKey: key).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key") { expect(internalCache.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(internalCache.didSetValue).toEventually(equal(value)) } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } private enum ConditionError: Error { case MyError case AnotherError } final class ConditionedCacheTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! let closure: ((String) -> AnyPublisher) = { key in if key.count >= 5 { return Just(true).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: ConditionError.MyError).eraseToAnyPublisher() } } describe("The conditioned instance function, applied to a cache level") { beforeEach { internalCache = CacheLevelFake() cache = internalCache.conditioned(closure) } itBehavesLike("a conditioned cache") { [ ConditionedCacheSharedExamplesContext.CacheToTest: cache as Any, ConditionedCacheSharedExamplesContext.InternalCache: internalCache as Any ] } } } } ================================================ FILE: Tests/CarlosTests/ConditionedOneWayTransformationBoxTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class ConditionedOneWayTransformationBoxTests: QuickSpec { override func spec() { describe("Conditioned one-way transformation box") { var box: ConditionedOneWayTransformationBox<[String: Bool], String, Int>! var cancellable: AnyCancellable? afterEach { cancellable?.cancel() cancellable = nil } context("when created through a closure") { beforeEach { box = ConditionedOneWayTransformationBox(conditionalTransformClosure: { key, value in if let _ = key["value"] { guard let intValue = Int(value) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } }) } context("when calling conditionalTransform") { var result: Int! var error: Error! beforeEach { result = nil error = nil } context("if the transformation is possible") { let originString = "102" beforeEach { cancellable = box.conditionalTransform(key: ["value": true], value: originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(Int(originString))) } } context("if the transformation is not possible") { let originString = "10asd2" beforeEach { cancellable = box.conditionalTransform(key: ["value": true], value: originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } context("if the key doesn't satisfy the condition") { beforeEach { cancellable = box.conditionalTransform(key: [:], value: "whatever") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } } context("when created through a one way transformer") { beforeEach { let transformer = OneWayTransformationBox(transform: { value in guard let intValue = Int(value) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() }) box = ConditionedOneWayTransformationBox(transformer: transformer) } context("when calling conditionalTransform") { var result: Int! var error: Error! beforeEach { result = nil error = nil } context("if the transformation is possible") { let originString = "102" beforeEach { cancellable = box.conditionalTransform(key: ["value": true], value: originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(Int(originString))) } } context("if the transformation is not possible") { let originString = "10asd2" beforeEach { cancellable = box.conditionalTransform(key: [:], value: originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } } } } } } ================================================ FILE: Tests/CarlosTests/ConditionedOutputProcessingTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ConditionedPostProcessSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" static let Transformer = "transformer" } final class ConditionedPostProcessSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a fetch closure with conditioned post-processing") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: ConditionedOneWayTransformationBox! var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.Transformer] as? ConditionedOneWayTransformationBox } afterEach { cancellables = nil } context("when calling get with a key that triggers some post-processing") { let key = "do" var successValue: Int? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = 101 beforeEach { getSubject.send(value) } it("should call the transformation closure with the right value") { var expected: Int! transformer.conditionalTransform(key: key, value: value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } context("when calling get with a key that triggers failure on the post-processing") { let key = "don't" var successValue: Int? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = -101 beforeEach { getSubject.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.anotherError)) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } context("when calling get") { let key = "12" var successValue: Int? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { context("when the transformation closure returns a value") { let value = 101 beforeEach { getSubject.send(value) } it("should call the transformation closure with the success value") { var expected: Int! transformer.conditionalTransform(key: key, value: value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the transformation closure returns nil") { let value = -101 beforeEach { getSubject.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } } sharedExamples("a cache with conditioned post-processing") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: ConditionedOneWayTransformationBox! beforeEach { cache = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[ConditionedPostProcessSharedExamplesContext.Transformer] as? ConditionedOneWayTransformationBox } itBehavesLike("a fetch closure with conditioned post-processing") { [ ConditionedPostProcessSharedExamplesContext.CacheToTest: cache as Any, ConditionedPostProcessSharedExamplesContext.InternalCache: internalCache as Any, ConditionedPostProcessSharedExamplesContext.Transformer: transformer as Any ] } context("when calling set") { let key = "10" let value = 222 beforeEach { _ = cache.set(value, forKey: key) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(internalCache.didSetValue).toEventually(equal(value)) } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class ConditionedOutputPostProcessingTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! let transformer: ConditionedOneWayTransformationBox = ConditionedOneWayTransformationBox(conditionalTransformClosure: { key, value in if key == "do" { return Just(value * 2).setFailureType(to: Error.self).eraseToAnyPublisher() } else if key == "don't" { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } if value > 0 { return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() }) describe("Conditioned post processing on a CacheLevel with the protocol extension") { beforeEach { internalCache = CacheLevelFake() cache = internalCache.conditionedPostProcess(transformer) } itBehavesLike("a cache with conditioned post-processing") { [ ConditionedPostProcessSharedExamplesContext.CacheToTest: cache as Any, ConditionedPostProcessSharedExamplesContext.InternalCache: internalCache as Any, ConditionedPostProcessSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/ConditionedTransformersTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ConditionedTransformerSharedExamplesContext { static let TransformerToTest = "transformer" } final class ConditionedTransformerSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a conditioned one-way transformer") { (sharedExampleContext: @escaping SharedExampleContext) in var transformer: OneWayTransformationBox! var cancellable: AnyCancellable? beforeEach { transformer = sharedExampleContext()[ConditionedTransformerSharedExamplesContext.TransformerToTest] as? OneWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil } context("when calling transform") { var result: Int? var failure: Error? beforeEach { result = nil failure = nil } context("when the condition is satisfied") { context("when the transformation is successful") { beforeEach { cancellable = transformer.transform("15") .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(15)) } it("should not fail") { expect(failure).toEventually(beNil()) } } context("when the transformation fails") { beforeEach { cancellable = transformer.transform("not a number") .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failure).toEventuallyNot(beNil()) } it("should return the right error") { expect(failure as? TransformerError).toEventually(equal(TransformerError.transformationError)) } } } context("when the condition is not satisfied") { beforeEach { cancellable = transformer.transform("fail now") .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failure).toEventuallyNot(beNil()) } it("should return the right error") { expect(failure as? FetchError).toEventually(equal(FetchError.conditionNotSatisfied)) } } context("when the condition fails") { beforeEach { cancellable = transformer.transform("fail with custom error") .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failure).toEventuallyNot(beNil()) } it("should return the right error") { expect(failure as? ConditionError).toEventually(equal(ConditionError.customError)) } } } } sharedExamples("a conditioned two-way transformer") { (sharedExampleContext: @escaping SharedExampleContext) in var transformer: TwoWayTransformationBox! var cancellable: AnyCancellable? beforeEach { transformer = sharedExampleContext()[ConditionedTransformerSharedExamplesContext.TransformerToTest] as? TwoWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil } itBehavesLike("a conditioned one-way transformer") { [ ConditionedTransformerSharedExamplesContext.TransformerToTest: OneWayTransformationBox(transform: transformer.transform) ] } context("when calling inverseTransform") { var result: String? var failure: Error? beforeEach { result = nil failure = nil } context("when the condition is satisfied") { beforeEach { cancellable = transformer.inverseTransform(15) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal("15")) } it("should not fail") { expect(failure).toEventually(beNil()) } } context("when the condition is not satisfied") { beforeEach { cancellable = transformer.inverseTransform(-14) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failure).toEventuallyNot(beNil()) } it("should return the right error") { expect(failure as? FetchError).toEventually(equal(FetchError.conditionNotSatisfied)) } } context("when the condition fails") { beforeEach { cancellable = transformer.inverseTransform(0) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failure = error } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failure).toEventuallyNot(beNil()) } it("should return the right error") { expect(failure as? ConditionError).toEventually(equal(ConditionError.customError)) } } } } } } private enum ConditionError: Error { case customError } private enum TransformerError: Error { case transformationError } final class ConditionedTransformersTests: QuickSpec { override func spec() { describe("Conditioned one way transformers") { var transformer: OneWayTransformationBox! let condition: (String) -> AnyPublisher = { input in if (input as NSString).range(of: "fail").location != NSNotFound { if (input as NSString).range(of: "custom").location != NSNotFound { return Fail(error: ConditionError.customError).eraseToAnyPublisher() } else { return Just(false).setFailureType(to: Error.self).eraseToAnyPublisher() } } else { return Just(true).setFailureType(to: Error.self).eraseToAnyPublisher() } } beforeEach { transformer = OneWayTransformationBox(transform: { guard let intValue = Int($0) else { return Fail(error: TransformerError.transformationError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() }).conditioned(condition) } itBehavesLike("a conditioned one-way transformer") { [ ConditionedTransformerSharedExamplesContext.TransformerToTest: transformer as Any ] } } describe("Conditioned two way transformers") { var transformer: TwoWayTransformationBox! let condition: (String) -> AnyPublisher = { input in if (input as NSString).range(of: "fail").location != NSNotFound { if (input as NSString).range(of: "custom").location != NSNotFound { return Fail(error: ConditionError.customError).eraseToAnyPublisher() } else { return Just(false).setFailureType(to: Error.self).eraseToAnyPublisher() } } else { return Just(true).setFailureType(to: Error.self).eraseToAnyPublisher() } } let inverseCondition: (Int) -> AnyPublisher = { input in if input >= 0 { if input == 0 { return Fail(error: ConditionError.customError).eraseToAnyPublisher() } else { return Just(true).setFailureType(to: Error.self).eraseToAnyPublisher() } } else { return Just(false).setFailureType(to: Error.self).eraseToAnyPublisher() } } beforeEach { transformer = TwoWayTransformationBox(transform: { guard let intValue = Int($0) else { return Fail(error: TransformerError.transformationError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() }, inverseTransform: { Just("\($0)").setFailureType(to: Error.self).eraseToAnyPublisher() }).conditioned(condition, inverseCondition: inverseCondition) } itBehavesLike("a conditioned two-way transformer") { [ ConditionedTransformerSharedExamplesContext.TransformerToTest: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/ConditionedTwoWayTransformationBoxTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class ConditionedTwoWayTransformationBoxTests: QuickSpec { override func spec() { describe("Conditioned two-way transformation box") { var box: ConditionedTwoWayTransformationBox! var error: Error! var cancellable: AnyCancellable? afterEach { cancellable?.cancel() cancellable = nil } context("when created through closures") { beforeEach { error = nil box = ConditionedTwoWayTransformationBox(conditionalTransformClosure: { key, value in if key > 0 { guard value.scheme == "http", let value = value.absoluteString else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } }, conditionalInverseTransformClosure: { key, value in if key > 0 { guard let value = NSURL(string: value) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } }) } context("when calling the conditional transformation") { var result: String! beforeEach { result = nil } context("if the transformation is possible") { let expectedResult = "http://www.google.de?test=1" beforeEach { cancellable = box.conditionalTransform(key: 1, value: NSURL(string: expectedResult)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(expectedResult)) } } context("if the transformation is not possible") { beforeEach { cancellable = box.conditionalTransform(key: 1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } context("if the key doesn't satisfy the condition") { beforeEach { cancellable = box.conditionalTransform(key: -1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when calling the conditional inverse transformation") { var result: NSURL! beforeEach { result = nil } context("if the transformation is possible") { let usedString = "http://www.google.de?test=1" beforeEach { cancellable = box.conditionalInverseTransform(key: 1, value: usedString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(NSURL(string: usedString)!)) } } context("if the transformation is not possible") { beforeEach { cancellable = box.conditionalInverseTransform(key: 1, value: "this is not a valid URL :'(") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } context("if the key doesn't satisfy the condition") { beforeEach { cancellable = box.conditionalInverseTransform(key: -1, value: "http://validurl.de") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when inverting the transformer") { var invertedBox: ConditionedTwoWayTransformationBox! beforeEach { invertedBox = box.invert() } context("when calling the inverse transformation") { var result: String! beforeEach { result = nil } context("if the transformation is possible") { let expectedResult = "http://www.google.de?test=1" beforeEach { cancellable = invertedBox.conditionalInverseTransform(key: 1, value: NSURL(string: expectedResult)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(expectedResult)) } } context("if the transformation is not possible") { beforeEach { cancellable = invertedBox.conditionalInverseTransform(key: 1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } context("if the key doesn't satisfy the condition") { beforeEach { cancellable = invertedBox.conditionalInverseTransform(key: -1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when calling the conditional transformation") { var result: NSURL! beforeEach { result = nil } context("if the transformation is possible") { let usedString = "http://www.google.de?test=1" beforeEach { cancellable = invertedBox.conditionalTransform(key: 1, value: usedString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(NSURL(string: usedString)!)) } } context("if the transformation is not possible") { beforeEach { cancellable = invertedBox.conditionalTransform(key: 1, value: "this is not a valid URL :'(") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } context("if the key doesn't satisfy the condition") { beforeEach { cancellable = invertedBox.conditionalTransform(key: -1, value: "http://validurl.de") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } } } context("when created through a 2-way transformer") { var originalTransformer: TwoWayTransformationBox! beforeEach { error = nil originalTransformer = TwoWayTransformationBox(transform: { value in guard value.scheme == "http", let value = value.absoluteString else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() }, inverseTransform: { value in guard let value = NSURL(string: value) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() }) box = ConditionedTwoWayTransformationBox(transformer: originalTransformer) } context("when calling the conditional transformation") { var result: String! beforeEach { result = nil } context("if the transformation is possible") { let expectedResult = "http://www.google.de?test=1" beforeEach { cancellable = box.conditionalTransform(key: -1, value: NSURL(string: expectedResult)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(expectedResult)) } } context("if the transformation is not possible") { beforeEach { cancellable = box.conditionalTransform(key: 1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when calling the conditional inverse transformation") { var result: NSURL! beforeEach { result = nil } context("if the transformation is possible") { let usedString = "http://www.google.de?test=1" beforeEach { cancellable = box.conditionalInverseTransform(key: -1, value: usedString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(NSURL(string: usedString)!)) } } context("if the transformation is not possible") { beforeEach { cancellable = box.conditionalInverseTransform(key: 1, value: "this is not a valid URL :'(") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when inverting the transformer") { var invertedBox: ConditionedTwoWayTransformationBox! beforeEach { invertedBox = box.invert() } context("when calling the inverse transformation") { var result: String! beforeEach { result = nil } context("if the transformation is possible") { let expectedResult = "http://www.google.de?test=1" beforeEach { cancellable = invertedBox.conditionalInverseTransform(key: 1, value: NSURL(string: expectedResult)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(expectedResult)) } } context("if the transformation is not possible") { beforeEach { cancellable = invertedBox.conditionalInverseTransform(key: 1, value: NSURL(string: "ftp://google.de/robots.txt")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when calling the conditional transformation") { var result: NSURL! beforeEach { result = nil } context("if the transformation is possible") { let usedString = "http://www.google.de?test=1" beforeEach { cancellable = invertedBox.conditionalTransform(key: 1, value: usedString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(NSURL(string: usedString)!)) } } context("if the transformation is not possible") { beforeEach { cancellable = invertedBox.conditionalTransform(key: 1, value: "this is not a valid URL :'(") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.simpleError)) } } } } } } } } ================================================ FILE: Tests/CarlosTests/ConditionedValueTransformationTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ConditionedValueTransformationSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" static let Transformer = "transformer" } final class ConditionedValueTransformationSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a cache with conditioned value transformation") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: ConditionedTwoWayTransformationBox! var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[ConditionedValueTransformationSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ConditionedValueTransformationSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[ConditionedValueTransformationSharedExamplesContext.Transformer] as? ConditionedTwoWayTransformationBox } afterEach { cancellables = nil } context("when calling get with a key that meets the condition") { let key = "do" var successValue: Float? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = 101 beforeEach { getSubject.send(value) } it("should call the transformation closure with the right value") { var expected: Float! transformer.conditionalTransform(key: key, value: value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } context("when calling get with a key that doesn't meet the condition") { let key = "don't" var successValue: Float? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = -101 beforeEach { getSubject.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.anotherError)) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } context("when calling get") { let key = "12" var successValue: Float? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalCache.getSubject = getSubject successValue = nil failureValue = nil cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { context("when the transformation closure returns a value") { let value = 101 beforeEach { getSubject.send(value) } it("should call the transformation closure with the success value") { var expected: Float! transformer.conditionalTransform(key: key, value: value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the transformation closure returns nil") { let value = -101 beforeEach { getSubject.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } context("when calling set") { var failed: Error? var succeeded: Bool! var canceled: Bool! beforeEach { canceled = false succeeded = false failed = nil } context("when the condition is met") { let key = "10" let value: Float = 222 beforeEach { cache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in succeeded = true }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(internalCache.didSetValue).toEventually(equal(Int(value))) } context("when the set closure succeeds") { beforeEach { internalCache.setPublishers[key]?.send() } it("should succeed the future") { expect(succeeded).toEventually(beTrue()) } } context("when the set clousure is canceled") { beforeEach { cancellables.first?.cancel() } it("should cancel the future") { expect(canceled).toEventually(beTrue()) } } context("when the set closure fails") { let error = TestError.anotherError beforeEach { internalCache.setPublishers[key]?.send(completion: .failure(error)) } it("should fail the future") { expect(failed as? TestError).toEventually(equal(error)) } } } context("when the condition is not met") { let key = "Test" let value: Float = -222 beforeEach { cache.set(value, forKey: key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failed = error } }, receiveValue: { _ in succeeded = true }) .store(in: &cancellables) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(0)) } it("should fail the future") { expect(failed).toEventuallyNot(beNil()) } } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class ConditionedValueTransformationTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! let transformer: ConditionedTwoWayTransformationBox = ConditionedTwoWayTransformationBox(conditionalTransformClosure: { key, value in if key == "do" { return Just(Float(value * 2)).setFailureType(to: Error.self).eraseToAnyPublisher() } else if key == "don't" { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } if value > 0 { return Just(Float(value)).setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() }, conditionalInverseTransformClosure: { key, value in if key == "do" { return Just(Int(value / 2)).setFailureType(to: Error.self).eraseToAnyPublisher() } else if key == "don't" { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } if value > 0 { return Just(Int(value)).setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() }) describe("Conditioned post processing on a CacheLevel with the protocol extension") { beforeEach { internalCache = CacheLevelFake() cache = internalCache.conditionedValueTransformation(transformer: transformer) } itBehavesLike("a cache with conditioned value transformation") { [ ConditionedValueTransformationSharedExamplesContext.CacheToTest: cache as Any, ConditionedValueTransformationSharedExamplesContext.InternalCache: internalCache as Any, ConditionedValueTransformationSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/DiskCacheTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine private func filesInDirectory(directory: String) -> [String] { let result = (try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? [] return result } final class DiskCacheTests: QuickSpec { override func spec() { describe("DiskCacheLevel") { var cache: DiskCacheLevel! let path = (NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0] as NSString).appendingPathComponent("com.carlos.default") var fileManager: FileManager! var cancellables: Set! beforeEach { cancellables = Set() fileManager = FileManager.default _ = try? fileManager.removeItem(atPath: path) cache = DiskCacheLevel(path: path, capacity: 400) } afterEach { cancellables = nil } context("when calling get") { var result: NSData? let key = "test-key" var failureSentinel: Bool? beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } context("when setting a value for that key") { let value = "value to set".data(using: .utf8, allowLossyConversion: false)! beforeEach { failureSentinel = nil cache.set(value as NSData, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: {}) .store(in: &cancellables) } context("when getting the value for another key") { let anotherKey = "test_key_2" beforeEach { cache.get(anotherKey) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failureSentinel).toEventuallyNot(beNil()) } } } } context("when calling set") { let key = "key" let value = "value".data(using: .utf8, allowLossyConversion: false)! var result: NSData? var failureSentinel: Bool? var writeSucceeded: Bool! beforeEach { writeSucceeded = false cache.set(value as NSData, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: { _ in writeSucceeded = true }).store(in: &cancellables) } afterEach { failureSentinel = nil result = nil } it("should save the key on disk") { expect(fileManager.fileExists(atPath: (path as NSString).appendingPathComponent(key.MD5String()))).toEventually(beTrue()) } it("should save the data on disk") { expect(NSKeyedUnarchiver.unarchiveObject(withFile: (path as NSString).appendingPathComponent(key.MD5String())) as? NSData).toEventually(equal(value as NSData)) } // TODO: How to simulate failure during writing in order to test it? it("should eventually succeed") { expect(writeSucceeded).toEventually(beTrue()) } context("when calling get") { beforeEach { result = nil failureSentinel = nil cache.set(value as NSData, forKey: key) .flatMap { cache.get(key) } .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should succeed") { expect(result).toEventually(equal(value as NSData)) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } } context("when setting a different value for the same key") { let newValue = "another value".data(using: .utf8, allowLossyConversion: false)! beforeEach { cache.set(newValue as NSData, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: {}) .store(in: &cancellables) } it("should keep the key on disk") { expect(fileManager.fileExists(atPath: (path as NSString).appendingPathComponent(key.MD5String()))).toEventually(beTrue()) } it("should overwrite the data on disk") { expect(NSKeyedUnarchiver.unarchiveObject(withFile: (path as NSString).appendingPathComponent(key.MD5String())) as? NSData).toEventually(equal(newValue as NSData)) } context("when calling get") { beforeEach { cache.set(newValue as NSData, forKey: key) .flatMap { cache.get(key) } .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should succeed with the overwritten value") { expect(result).toEventually(equal(newValue as NSData)) } } } context("when setting more than its capacity") { let otherKeys = ["key1", "key2", "key3"] let otherValues = [ "long string value", "even longer string value but should still fit the cache", "longest string value that should fill the cache capacity and force it to evict some values" ] beforeEach { for (key, value) in zip(otherKeys, otherValues) { cache.set(value.data(using: .utf8, allowLossyConversion: false)! as NSData, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: {}) .store(in: &cancellables) } } it("should evict at least one value") { var evictedAtLeastOne = false for key in otherKeys { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { evictedAtLeastOne = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } expect(evictedAtLeastOne).toEventually(beTrue()) } } context("when calling remove") { var result = false let key = "test-key" beforeEach { result = false cache.clear() } it("shall remove object from cache for given key") { cache.set("value".data(using: .utf8)! as NSData, forKey: key) .flatMap { cache.remove(key) } .sink( receiveCompletion: { _ in }, receiveValue: { _ in result = true } ) .store(in: &cancellables) expect(result).toEventually(beTrue()) } } context("when calling clear") { beforeEach { result = nil cache.clear() } it("should remove all the files on disk") { expect(filesInDirectory(directory: path)).toEventually(beEmpty()) } context("when calling get") { beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } } } context("when calling onMemoryWarning") { beforeEach { result = nil cache.onMemoryWarning() } context("when calling get") { beforeEach { beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } it("should succeed") { expect(result).toEventually(equal(value as NSData)) } } } } } } } } ================================================ FILE: Tests/CarlosTests/Fakes/Base64EncodedImage.swift ================================================ // // Base64EncodedImage.swift // // // Created by Lisovyi, Ivan on 01.07.20. // import Foundation let base64EncodedImage = "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" ================================================ FILE: Tests/CarlosTests/Fakes/CacheLevelFake.swift ================================================ import Foundation import Carlos import Combine class CacheLevelFake: CacheLevel { typealias KeyType = A typealias OutputType = B var numberOfTimesRemoveCalled = 0 var removeKey: KeyType? var removeSubject: PassthroughSubject! init() {} // MARK: Get var numberOfTimesCalledGet = 0 var didGetKey: KeyType? var getSubject: PassthroughSubject? var getPublishers: [KeyType: PassthroughSubject] = [:] func get(_ key: KeyType) -> AnyPublisher { numberOfTimesCalledGet += 1 didGetKey = key if let getSubject = getSubject { return getSubject.eraseToAnyPublisher() } if let subject = getPublishers[key] { return subject.eraseToAnyPublisher() } let newSubject = PassthroughSubject() getPublishers[key] = newSubject return newSubject.eraseToAnyPublisher() } // MARK: Set var numberOfTimesCalledSet = 0 var didSetValue: OutputType? var didSetKey: KeyType? var setSubject: PassthroughSubject? var setPublishers: [KeyType: PassthroughSubject] = [:] func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher { numberOfTimesCalledSet += 1 didSetKey = key didSetValue = value if let setSubject = setSubject { return setSubject.eraseToAnyPublisher() } if let subject = setPublishers[key] { return subject.eraseToAnyPublisher() } let newSubject = PassthroughSubject() setPublishers[key] = newSubject return newSubject.eraseToAnyPublisher() } // MARK: - Remove func remove(_ key: A) -> AnyPublisher { numberOfTimesRemoveCalled += 1 removeKey = key return removeSubject.eraseToAnyPublisher() } // MARK: - Clear var numberOfTimesCalledClear = 0 func clear() { numberOfTimesCalledClear += 1 } // MARK: - On Memory Warning var numberOfTimesCalledOnMemoryWarning = 0 func onMemoryWarning() { numberOfTimesCalledOnMemoryWarning += 1 } } ================================================ FILE: Tests/CarlosTests/Fakes/FetcherFake.swift ================================================ // // FetcherFake.swift // // // Created by Lisovyi, Ivan on 17.08.20. // import Foundation import Carlos import Combine class FetcherFake: Fetcher { typealias KeyType = A typealias OutputType = B var queueUsedForTheLastCall: UnsafeMutableRawPointer! init() {} var numberOfTimesCalledGet = 0 var didGetKey: KeyType? var getSubject: PassthroughSubject! func get(_ key: KeyType) -> AnyPublisher { numberOfTimesCalledGet += 1 didGetKey = key return getSubject.eraseToAnyPublisher() } } ================================================ FILE: Tests/CarlosTests/FetcherValueTransformationTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct FetcherValueTransformationsSharedExamplesContext { static let FetcherToTest = "fetcher" static let InternalFetcher = "internalFetcher" static let Transformer = "transformer" } final class FetcherValueTransformationSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a fetch closure with transformed values") { (sharedExampleContext: @escaping SharedExampleContext) in var fetcher: BasicFetcher! var internalFetcher: FetcherFake! var transformer: OneWayTransformationBox! var cancellables: Set! beforeEach { cancellables = Set() fetcher = sharedExampleContext()[FetcherValueTransformationsSharedExamplesContext.FetcherToTest] as? BasicFetcher internalFetcher = sharedExampleContext()[FetcherValueTransformationsSharedExamplesContext.InternalFetcher] as? FetcherFake transformer = sharedExampleContext()[FetcherValueTransformationsSharedExamplesContext.Transformer] as? OneWayTransformationBox } context("when calling get") { let key = "12" var successValue: String? var failureValue: Error? var getSubject: PassthroughSubject! beforeEach { getSubject = PassthroughSubject() internalFetcher.getSubject = getSubject fetcher.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalFetcher.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(internalFetcher.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { context("when the value can be successfully transformed") { let value = 101 beforeEach { getSubject.send(value) } it("should call the original success closure") { expect(successValue).toEventuallyNot(beNil()) } it("should transform the value") { var expected: String! transformer.transform(value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the value transformation returns nil") { let value = -101 beforeEach { successValue = nil getSubject.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should fail with the right code") { expect(failureValue as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { getSubject.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should fail with the right code") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } } } } final class FetcherValueTransformationTests: QuickSpec { override func spec() { var fetcher: BasicFetcher! var internalFetcher: FetcherFake! var transformer: OneWayTransformationBox! let forwardTransformationClosure: (Int) -> AnyPublisher = { if $0 > 0 { return Just("\($0 + 1)").setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() } describe("Value transformation using a transformer and a fetcher, with the instance function") { beforeEach { internalFetcher = FetcherFake() transformer = OneWayTransformationBox(transform: forwardTransformationClosure) fetcher = internalFetcher.transformValues(transformer) } itBehavesLike("a fetch closure with transformed values") { [ FetcherValueTransformationsSharedExamplesContext.FetcherToTest: fetcher as Any, FetcherValueTransformationsSharedExamplesContext.InternalFetcher: internalFetcher as Any, FetcherValueTransformationsSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/ImageTransformerTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine #if !os(macOS) import UIKit final class ImageTransformerTests: QuickSpec { override func spec() { describe("Image transformer") { var transformer: ImageTransformer! var error: Error! let sampleData = Data(base64Encoded: base64EncodedImage, options: .ignoreUnknownCharacters)! var cancellable: AnyCancellable? beforeEach { error = nil transformer = ImageTransformer() } afterEach { cancellable?.cancel() cancellable = nil } context("when transforming NSData to UIImage") { var result: UIImage! beforeEach { result = nil } context("when the NSData is a valid image") { var imageSample: UIImage! beforeEach { imageSample = UIImage(data: sampleData) cancellable = transformer.transform(imageSample.pngData()! as NSData) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the error closure") { expect(error).toEventually(beNil()) } it("should return the expected data") { expect(result?.pngData()).toEventually(equal(imageSample.pngData())) } } context("when the NSData is not a valid image") { beforeEach { cancellable = transformer.transform(("test for an invalid image".data(using: .utf8) as NSData?)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the error closure") { expect(error).toEventuallyNot(beNil()) } } } context("when transforming UIImage to NSData") { var imageSample: UIImage! var result: NSData! beforeEach { imageSample = UIImage(data: sampleData) cancellable = transformer.inverseTransform(imageSample) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected data") { expect(result).toEventually(equal(imageSample.pngData() as NSData?)) } } } } } #endif ================================================ FILE: Tests/CarlosTests/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: Tests/CarlosTests/JSONTransformerTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class JSONTransformerTests: QuickSpec { override func spec() { describe("JSONTransformer") { var transformer: JSONTransformer! var cancellable: AnyCancellable? beforeEach { transformer = JSONTransformer() } afterEach { cancellable?.cancel() cancellable = nil } context("when transforming NSData to JSON") { var result: AnyObject! var error: Error! afterEach { result = nil error = nil } context("when the NSData is a valid JSON") { context("when it's an array") { let testObject = [ "list", "of", "strings" ] beforeEach { let data = try! JSONSerialization.data(withJSONObject: testObject, options: []) cancellable = transformer.transform(data as NSData) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return an array") { expect(result as? [String]).toEventuallyNot(beNil()) } it("should contain the right number of items") { expect((result as? [String])?.count).toEventually(equal(testObject.count)) } it("should contain the right objects") { expect(result as? [String]).toEventually(equal(testObject)) } } context("when it's a dictionary") { let testObject: [String: Any] = [ "id": 2, "value": "test", "anotherKey": [ "1", "2", "3" ] ] beforeEach { let data = try! JSONSerialization.data(withJSONObject: testObject, options: []) cancellable = transformer.transform(data as NSData) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return a dictionary") { expect(result as? [String: AnyObject]).toEventuallyNot(beNil()) } it("should contain the right number of items") { expect((result as? [String: AnyObject])?.keys.count).toEventually(equal(testObject.keys.count)) } } } context("when the NSData is not a valid JSON") { beforeEach { cancellable = transformer.transform(("test for an invalid JSON".data(using: .utf8) as NSData?)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the failure closure") { expect(error).toEventuallyNot(beNil()) } } } context("when transforming JSON to NSData") { var result: NSData! var error: Error! context("when the JSON is valid") { var expectedResult: NSData! context("when it's an array") { let testObject = [ "1", "two", "3", "some other thing" ] beforeEach { expectedResult = try! JSONSerialization.data(withJSONObject: testObject, options: []) as NSData cancellable = transformer.inverseTransform(testObject as AnyObject) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should pass the expected result") { expect(result).toEventually(equal(expectedResult)) } } context("when it's a dictionary") { let testObject: [String: Any] = [ "id": 1, "key": "value", "anotherKey": [ 1, 2, 3, 4, 5 ] ] beforeEach { expectedResult = try! JSONSerialization.data(withJSONObject: testObject, options: []) as NSData cancellable = transformer.inverseTransform(testObject as AnyObject) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should pass the expected result") { expect(result).toEventually(equal(expectedResult)) } } } } } } } ================================================ FILE: Tests/CarlosTests/KeyTransformationTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct KeyTransformationsSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" static let Transformer = "transformer" } final class KeyTransformationSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a fetch closure with transformed keys") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! var cancellable: AnyCancellable? beforeEach { cache = sharedExampleContext()[KeyTransformationsSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[KeyTransformationsSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[KeyTransformationsSharedExamplesContext.Transformer] as? OneWayTransformationBox } context("when calling get") { var successValue: Int? var failureValue: Error? var fakeRequest: PassthroughSubject! var canceled: Bool! beforeEach { canceled = false failureValue = nil successValue = nil } afterEach { cancellable?.cancel() cancellable = nil } context("when the transformation closure returns a value") { let key = 12 beforeEach { fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest cancellable = cache.get(key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should transform the key first") { var expected: String! _ = transformer.transform(key) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) expect(internalCache.didGetKey).toEventually(equal(expected)) } context("when the request succeeds") { let value = 101 beforeEach { fakeRequest.send(value) } it("should call the original success closure") { expect(successValue).toEventually(equal(value)) } it("should not call the original failure closure") { expect(failureValue).toEventually(beNil()) } it("should not call the original cancel closure") { expect(canceled).toEventually(beFalse()) } } context("when the request is canceled") { beforeEach { cancellable?.cancel() } it("should not call the original failure closure") { expect(failureValue).toEventually(beNil()) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original cancel closure") { expect(canceled).toEventually(beTrue()) } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should not call the original cancel closure") { expect(canceled).toEventually(beFalse()) } } } context("when the transformation closure returns nil") { let key = -12 beforeEach { fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest _ = cache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(0)) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should not call the original cancel closure") { expect(canceled).toEventually(beFalse()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.simpleError)) } } } } sharedExamples("a cache with transformed keys") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! beforeEach { cache = sharedExampleContext()[KeyTransformationsSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[KeyTransformationsSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[KeyTransformationsSharedExamplesContext.Transformer] as? OneWayTransformationBox } itBehavesLike("a fetch closure with transformed keys") { [ KeyTransformationsSharedExamplesContext.CacheToTest: cache as Any, KeyTransformationsSharedExamplesContext.InternalCache: internalCache as Any, KeyTransformationsSharedExamplesContext.Transformer: transformer as Any ] } context("when calling set") { var setSucceeded: Bool! var setError: Error? var cancellable: AnyCancellable? beforeEach { setSucceeded = false setError = nil } afterEach { cancellable?.cancel() cancellable = nil } context("when the transformation closure returns a value") { let key = 10 let value = 222 beforeEach { cancellable = cache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should transform the key first") { var expected: String! _ = transformer.transform(key) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) expect(internalCache.didSetKey).toEventually(equal(expected)) } it("should pass the right value") { expect(internalCache.didSetValue).toEventually(equal(value)) } context("when the set succeeds") { beforeEach { internalCache.setPublishers["\(key + 1)"]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when the set fails") { beforeEach { internalCache.setPublishers["\(key + 1)"]?.send(completion: .failure(TestError.anotherError)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when the transformation closure fails") { let key = -10 let value = 222 beforeEach { cancellable = cache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(0)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the transformation error") { expect(setError as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class KeyTransformationTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! let transformationClosure: (Int) -> AnyPublisher = { if $0 > 0 { return Just("\($0 + 1)").setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() } describe("Key transformation using a transformer and a cache, with the instance function") { beforeEach { internalCache = CacheLevelFake() transformer = OneWayTransformationBox(transform: transformationClosure) cache = internalCache.transformKeys(transformer) } itBehavesLike("a cache with transformed keys") { [ KeyTransformationsSharedExamplesContext.CacheToTest: cache as Any, KeyTransformationsSharedExamplesContext.InternalCache: internalCache as Any, KeyTransformationsSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/MKDistanceFormatterTransformerTests.swift ================================================ import Foundation import MapKit import Nimble import Quick import Carlos import Combine final class MKDistanceFormatterTransformerTests: QuickSpec { override func spec() { describe("MKDistanceFormatter") { var formatter: MKDistanceFormatter! var cancellable: AnyCancellable? beforeEach { formatter = MKDistanceFormatter() formatter.units = .metric } afterEach { cancellable?.cancel() cancellable = nil } context("when used as a transformer") { context("when transforming") { let originDistance: CLLocationDistance = 10293.12 var resultString: String! beforeEach { cancellable = formatter.transform(originDistance) .sink(receiveCompletion: { _ in }, receiveValue: { resultString = $0 }) } it("should return the expected string") { expect(resultString).toEventually(equal("10 km")) } } context("when inverse transforming") { let originString = "10 km" var resultDistance: CLLocationDistance! beforeEach { cancellable = formatter.inverseTransform(originString) .sink(receiveCompletion: { _ in }, receiveValue: { resultDistance = $0 }) } it("should return the expected number") { expect(resultDistance).toEventually(equal(10000.0)) } } } } } } ================================================ FILE: Tests/CarlosTests/MemoryCacheLevelTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class MemoryCacheLevelTests: QuickSpec { override func spec() { describe("Memory cache level") { var cache: MemoryCacheLevel! var cancellable: AnyCancellable? beforeEach { cache = MemoryCacheLevel(capacity: 100) } afterEach { cancellable?.cancel() cancellable = nil cache = nil } context("when calling get") { var result: NSString? let key = "test-key" var failureSentinel: Bool? beforeEach { cancellable = cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) } afterEach { failureSentinel = nil result = nil } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } context("when setting a value for that key") { let value = "value to set" beforeEach { failureSentinel = nil _ = cache.set(value as NSString, forKey: key) } context("when getting the value for another key") { let anotherKey = "test_key_2" beforeEach { cancellable = cache.get(anotherKey).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failureSentinel).toEventuallyNot(beNil()) } } } } context("when calling set") { let key = "key" let value = "value" var result: NSString? var failureSentinel: Bool? var didWrite: Bool! beforeEach { cancellable = cache.set(value as NSString, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: { didWrite = true }) } afterEach { didWrite = false result = nil failureSentinel = nil } it("should immediately succeed the future") { expect(didWrite).toEventually(beTrue()) } context("when calling get") { beforeEach { cancellable = cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } } context("when setting a different value for the same key") { let newValue = "another value" it("should succeed with the overwritten value") { cancellable = cache.set(newValue as NSString, forKey: key) .flatMap { _ in cache.get(key) } .sink( receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 } ) expect(result).toEventually(equal(newValue as NSString)) } } context("when setting more than its capacity") { let otherKeys = ["key1", "key2", "key3"] let otherValues = [ "long string value", "even longer string value but should still fit the cache", "longest string value that should fill the cache capacity and force it to evict some values and we will hope it happends" ] beforeEach { for (key, value) in zip(otherKeys, otherValues) { _ = cache.set(value as NSString, forKey: key) } } it("should evict at least one value") { var evictedAtLeastOne = false for key in otherKeys { cancellable = cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { evictedAtLeastOne = true } }, receiveValue: { _ in }) } expect(evictedAtLeastOne).toEventually(beTrue(), timeout: .seconds(5)) } } context("when calling clear") { beforeEach { result = nil cache.clear() } context("when calling get") { beforeEach { cancellable = cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } } } context("when calling onMemoryWarning") { beforeEach { result = nil cache.onMemoryWarning() } context("when calling get") { beforeEach { beforeEach { cancellable = cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } } } } } context("when calling remove") { var result = false let key = "test-key" beforeEach { result = false } it("shall remove object from cache for given key") { cancellable = cache.set("value", forKey: key) .flatMap { cache.remove(key) } .sink( receiveCompletion: { _ in }, receiveValue: { _ in result = true } ) expect(result).toEventually(beTrue()) } } } } } ================================================ FILE: Tests/CarlosTests/MemoryWarningNotificationTests.swift ================================================ import Foundation import Nimble import Quick import Carlos #if !os(macOS) import UIKit class MemoryWarningNotificationTests: QuickSpec { override func spec() { describe("Memory warning listener") { var cache: CacheLevelFake! var token: NSObjectProtocol! beforeEach { cache = CacheLevelFake() } context("when listening to memory warnings") { beforeEach { token = cache.listenToMemoryWarnings() } context("when posting memory warnings") { beforeEach { NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } it("should call onMemoryWarning on the cache") { expect(cache.numberOfTimesCalledOnMemoryWarning) == 1 } it("should not directly call clear") { expect(cache.numberOfTimesCalledClear) == 0 } } context("when unsubscribing later") { beforeEach { unsubscribeToMemoryWarnings(token) } context("when posting memory warnings") { beforeEach { NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } it("should not call onMemoryWarning on the cache") { expect(cache.numberOfTimesCalledOnMemoryWarning) == 0 } it("should not call clear") { expect(cache.numberOfTimesCalledClear) == 0 } } } } context("by default, posting memory warning notifications") { beforeEach { NotificationCenter.default.post(name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } it("should not call clear") { expect(cache.numberOfTimesCalledClear) == 0 } it("should not call onMemoryWarning") { expect(cache.numberOfTimesCalledOnMemoryWarning) == 0 } } } } } #endif ================================================ FILE: Tests/CarlosTests/NSDateFormatterTransformerTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class NSDateFormatterTransformerTests: QuickSpec { override func spec() { describe("DateFormatter") { var formatter: DateFormatter! var cancellable: AnyCancellable? beforeEach { formatter = DateFormatter() formatter.dateFormat = "YYYY-MM-dd" } afterEach { cancellable?.cancel() cancellable = nil } context("when used as a transformer") { var error: Error? afterEach { error = nil } context("when transforming") { let originDate = Date(timeIntervalSince1970: 1_436_644_623) var result: String! beforeEach { cancellable = formatter.transform(originDate) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } afterEach { result = nil } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected string") { expect(result).toEventually(equal("2015-07-11")) } } context("when inverse transforming") { var result: Date! var formattedResult: String? afterEach { result = nil formattedResult = nil } context("when the string is valid") { let originString = "2015-07-11" beforeEach { cancellable = formatter.inverseTransform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 formattedResult = formatter.string(from: $0) }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected date") { expect(formattedResult).toEventually(equal(originString)) } } context("when the string is invalid") { beforeEach { cancellable = formatter.inverseTransform("this is not a valid date") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the error closure") { expect(error).toEventuallyNot(beNil()) } } } } } } } ================================================ FILE: Tests/CarlosTests/NSNumberFormatterTransformerTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class NSNumberFormatterTransformerTests: QuickSpec { override func spec() { describe("NumberFormatter") { var formatter: NumberFormatter! var cancellable: AnyCancellable? beforeEach { formatter = NumberFormatter() formatter.locale = Locale(identifier: "us_US") formatter.numberStyle = .decimal formatter.maximumFractionDigits = 5 formatter.minimumFractionDigits = 3 } afterEach { cancellable?.cancel() cancellable = nil } context("when used as a transformer") { var error: Error! context("when transforming") { var result: String! afterEach { result = nil error = nil } context("when the number contains a valid number of fraction digits") { let originNumber = 10.1203 beforeEach { cancellable = formatter.transform(NSNumber(value: originNumber)) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected string") { expect(result).toEventually(equal("10.1203")) } } context("when the number contains less fractions digits") { let originNumber = 10.12 beforeEach { cancellable = formatter.transform(NSNumber(value: originNumber)) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected string") { expect(result).toEventually(equal("10.120")) } } context("when the number contains more fraction digits") { let originNumber = 10.120312 beforeEach { cancellable = formatter.transform(NSNumber(value: originNumber)) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected string") { expect(result).toEventually(equal("10.12031")) } } } context("when inverse transforming") { var result: NSNumber! context("when the string is valid") { let originString = "10.1203" beforeEach { cancellable = formatter.inverseTransform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } afterEach { result = nil error = nil } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected number") { expect(result.map { "\($0)" }).toEventually(equal(originString)) } } context("when the string is invalid") { beforeEach { cancellable = formatter.inverseTransform("not a number!") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e result = nil } }, receiveValue: { result = $0 }) } afterEach { result = nil error = nil } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the error closure") { expect(error).toEventuallyNot(beNil()) } } } } } } } ================================================ FILE: Tests/CarlosTests/NSUserDefaultsCacheLevelTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class NSUserDefaultsCacheLevelTests: QuickSpec { override func spec() { describe("User defaults cache level") { var cache: NSUserDefaultsCacheLevel! var secondCache: NSUserDefaultsCacheLevel! var standardCache: UserDefaults! var cancellables: Set! beforeEach { cancellables = Set() standardCache = UserDefaults.standard cache = NSUserDefaultsCacheLevel(name: "tests") secondCache = NSUserDefaultsCacheLevel(name: "fallback") } afterEach { cancellables = nil } afterSuite { cache.clear() secondCache.clear() } context("when calling get") { var result: NSString? let key = "test-key" var failureSentinel: Bool? beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } afterEach { result = nil failureSentinel = nil } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } context("when setting a value for that key") { context("when getting the value for another key") { let value = "value to set" let anotherKey = "test_key_2" beforeEach { cache.set(value as NSString, forKey: key) .flatMap { cache.get(anotherKey) } .sink(receiveCompletion: { completion in if case .failure = completion { result = nil failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not succeed") { expect(result).toEventually(beNil()) } it("should fail") { expect(failureSentinel).toEventuallyNot(beNil()) } } } } context("when calling set") { let key = "key" let value = "value" var result: NSString? var failureSentinel: Bool? var didWrite: Bool! beforeEach { didWrite = false cache.set(value as NSString, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: { didWrite = true }) .store(in: &cancellables) secondCache.set(value as NSString, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: {}) .store(in: &cancellables) standardCache.set(value, forKey: key) } it("should eventually succeed the set future") { expect(didWrite).toEventually(beTrue()) } context("when calling get") { beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } } context("when setting a different value for the same key") { let newValue = "another value" context("when calling get") { beforeEach { cache.set(newValue as NSString, forKey: key) .flatMap { cache.get(key) } .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should succeed with the overwritten value") { expect(result).toEventually(equal(newValue as NSString), timeout: .seconds(5)) } } } context("when calling remove") { var result = false let key = "test-key" beforeEach { result = false cache.clear() } it("shall remove object from cache for given key") { cache.set("value", forKey: key) .flatMap { cache.remove(key) } .sink( receiveCompletion: { _ in }, receiveValue: { _ in result = true } ) .store(in: &cancellables) expect(result).toEventually(beTrue()) } } context("when calling clear") { beforeEach { failureSentinel = nil result = nil cache.clear() } context("when calling get") { beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should fail") { expect(failureSentinel).toEventually(beTrue()) } it("should not succeed") { expect(result).toEventually(beNil()) } } context("when calling get on the other cache") { beforeEach { secondCache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } } context("when calling get on the standard user defaults") { beforeEach { result = standardCache.object(forKey: key) as? NSString } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } } } context("when calling onMemoryWarning") { beforeEach { result = nil cache.onMemoryWarning() } context("when calling get") { beforeEach { beforeEach { cache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } } } context("when calling get on the other cache") { beforeEach { secondCache.get(key) .sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { result = $0 }) .store(in: &cancellables) } it("should not fail") { expect(failureSentinel).toEventually(beNil()) } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } } context("when calling get on the standard user defaults") { beforeEach { result = standardCache.object(forKey: key) as? NSString } it("should succeed") { expect(result).toEventuallyNot(beNil()) } it("should return the right value") { expect(result).toEventually(equal(value as NSString)) } } } } } } } ================================================ FILE: Tests/CarlosTests/NetworkFetcherTests.swift ================================================ // // NetworkFetcherTests.swift // Carlos // // Created by Esad Hajdarevic on 30/07/15. // Copyright (c) 2015 WeltN24. All rights reserved. // import Foundation import Nimble import Quick import Carlos import Combine final class NetworkFetcherTests: QuickSpec { override func spec() { describe("NetworkFetcher") { var sut: NetworkFetcher! var cancellables: Set! beforeEach { cancellables = Set() sut = NetworkFetcher() } afterEach { cancellables = nil } context("simultaneous requests") { var finished = 0 let simultaneousRequests = 3 beforeEach { let url = URL(string: "http://www.google.com/images/logos/google_logo_41.png")! let lockQueue = DispatchQueue(label: "com.carlos.test") for _ in 0..! var originalCache: BasicCache! beforeEach { cacheToTest = sharedExampleContext()[NormalizedCacheSharedExamplesContext.CacheToTest] as? BasicCache originalCache = sharedExampleContext()[NormalizedCacheSharedExamplesContext.OriginalCache] as? BasicCache } it("should have a valid cache to test") { expect(cacheToTest).notTo(beNil()) } it("should return the same value for the normalized cache") { expect(cacheToTest) === originalCache } } sharedExamples("wrap the original cache into a BasicCache") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheToTest: BasicCache! var originalCache: CacheLevelFake! var cancellable: AnyCancellable? beforeEach { cacheToTest = sharedExampleContext()[NormalizedCacheSharedExamplesContext.CacheToTest] as? BasicCache originalCache = sharedExampleContext()[NormalizedCacheSharedExamplesContext.OriginalCache] as? CacheLevelFake } afterEach { cancellable?.cancel() cancellable = nil } context("when calling get") { let key = "key to test" var expectedRequest: PassthroughSubject! beforeEach { expectedRequest = PassthroughSubject() originalCache.getSubject = expectedRequest cancellable = cacheToTest.get(key).sink(receiveCompletion: { _ in }, receiveValue: { _ in }) } it("should call the closure") { expect(originalCache.numberOfTimesCalledGet) == 1 } it("should pass the right key") { expect(originalCache.didGetKey) == key } } context("when calling set") { let key = "test key" let value = 101 beforeEach { _ = cacheToTest.set(value, forKey: key) } it("should call the closure") { expect(originalCache.numberOfTimesCalledSet) == 1 } it("should pass the right key") { expect(originalCache.didSetKey) == key } it("should pass the right value") { expect(originalCache.didSetValue) == value } } context("when calling clear") { beforeEach { cacheToTest.clear() } it("should call the closure") { expect(originalCache.numberOfTimesCalledClear) == 1 } } context("when calling onMemoryWarning") { beforeEach { cacheToTest.onMemoryWarning() } it("should call the closure") { expect(originalCache.numberOfTimesCalledOnMemoryWarning) == 1 } } } } } final class NormalizationTests: QuickSpec { override func spec() { var cacheToTest: BasicCache! describe("Normalization through the protocol extension") { context("when normalizing a BasicCache") { var originalCache: BasicCache! var keyTransformer: OneWayTransformationBox! beforeEach { keyTransformer = OneWayTransformationBox(transform: { Just($0).setFailureType(to: Error.self).eraseToAnyPublisher() }) originalCache = CacheLevelFake().transformKeys(keyTransformer) cacheToTest = originalCache.normalize() } itBehavesLike("no-op if the original cache is a BasicCache") { [ NormalizedCacheSharedExamplesContext.OriginalCache: originalCache as Any, NormalizedCacheSharedExamplesContext.CacheToTest: cacheToTest as Any ] } } context("when normalizing another type of cache") { var originalCache: CacheLevelFake! beforeEach { originalCache = CacheLevelFake() cacheToTest = originalCache.normalize() } itBehavesLike("wrap the original cache into a BasicCache") { [ NormalizedCacheSharedExamplesContext.OriginalCache: originalCache as Any, NormalizedCacheSharedExamplesContext.CacheToTest: cacheToTest as Any ] } } } } } ================================================ FILE: Tests/CarlosTests/OneWayTransformationBoxTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class OneWayTransformationBoxTests: QuickSpec { override func spec() { describe("OneWayTransformationBox") { var box: OneWayTransformationBox! var cancellable: AnyCancellable? beforeEach { box = OneWayTransformationBox(transform: { value in guard let intValue = Int(value) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() }) } afterEach { cancellable?.cancel() cancellable = nil } context("when using the transformation") { var result: Int! var error: Error! beforeEach { result = nil error = nil } context("if the transformation is possible") { let originString = "102" beforeEach { cancellable = box.transform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).notTo(beNil()) } it("should not call the failure closure") { expect(error).to(beNil()) } it("should return the expected result") { expect(result) == Int(originString) } } context("if the transformation is not possible") { let originString = "10asd2" beforeEach { cancellable = box.transform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).to(beNil()) } it("should call the failure closure") { expect(error).notTo(beNil()) } it("should pass the right error") { expect(error as? TestError) == TestError.simpleError } } } } } } ================================================ FILE: Tests/CarlosTests/OneWayTransformerCompositionTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ComposedOneWayTransformerSharedExamplesContext { static let TransformerToTest = "composedTransformer" } final class OneWayTransformerCompositionSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a composed one-way transformer") { (sharedExampleContext: @escaping SharedExampleContext) in var composedTransformer: OneWayTransformationBox! var cancellable: AnyCancellable? beforeEach { composedTransformer = sharedExampleContext()[ComposedOneWayTransformerSharedExamplesContext.TransformerToTest] as? OneWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil } context("when transforming a value") { var result: Int! beforeEach { result = nil } context("if the transformation is possible") { beforeEach { cancellable = composedTransformer.transform("13.2") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should not return nil") { expect(result).toEventuallyNot(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(13)) } } context("if the transformation fails in the first transformer") { beforeEach { cancellable = composedTransformer.transform("13hallo") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } context("if the transformation fails in the second transformer") { beforeEach { cancellable = composedTransformer.transform("-13") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } } } } } final class OneWayTransformerCompositionTests: QuickSpec { override func spec() { var transformer1: OneWayTransformationBox! var transformer2: OneWayTransformationBox! var composedTransformer: OneWayTransformationBox! beforeEach { transformer1 = OneWayTransformationBox(transform: { guard let value = Float($0) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() }) transformer2 = OneWayTransformationBox(transform: { guard $0 > 0 else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(Int($0)).setFailureType(to: Error.self).eraseToAnyPublisher() }) } describe("Transformer composition using two transformers with the instance function") { beforeEach { composedTransformer = transformer1.compose(transformer2) } itBehavesLike("a composed one-way transformer") { [ ComposedOneWayTransformerSharedExamplesContext.TransformerToTest: composedTransformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/PoolCacheTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct PoolCacheSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" } final class PoolCacheSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a pooled cache") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: PoolCache>! var internalCache: CacheLevelFake! var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[PoolCacheSharedExamplesContext.CacheToTest] as? PoolCache> internalCache = sharedExampleContext()[PoolCacheSharedExamplesContext.InternalCache] as? CacheLevelFake } afterEach { cancellables = nil } context("when calling get") { var fakeRequest: PassthroughSubject! let key = "key_test" var successSentinel: Bool? var failureSentinel: Bool? var successValue: Int? beforeEach { successSentinel = nil failureSentinel = nil successValue = nil fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest cache.get(key).sink(receiveCompletion: { completion in if case .failure = completion { failureSentinel = true } }, receiveValue: { value in successSentinel = true successValue = value }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("as long as the request doesn't succeed or fail, when other requests with different keys are made") { var fakeRequest2: PassthroughSubject! let otherKey = "key_test_2" beforeEach { fakeRequest2 = PassthroughSubject() internalCache.getSubject = fakeRequest2 cache.get(otherKey) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(2)) } it("should pass the right key") { expect(internalCache.didGetKey).toEventually(equal(otherKey)) } context("as long as the request doesn't succeed or fail, when other requests with the same key are made") { beforeEach { cache.get(otherKey) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(2)) } } } context("as long as the request doesn't succeed or fail, when other requests with the same key are made") { var otherSuccessSentinels: [Bool?]! var otherFailureSentinels: [Bool?]! var otherSuccessValues: [Int?]! let numberOfOtherRequests = 2 beforeEach { otherSuccessSentinels = [] otherFailureSentinels = [] otherSuccessValues = [] for _ in 0..>! var internalCache: CacheLevelFake! describe("PoolCache") { beforeEach { internalCache = CacheLevelFake() cache = PoolCache>(internalCache: internalCache) } itBehavesLike("a pooled cache") { [ PoolCacheSharedExamplesContext.CacheToTest: cache as Any, PoolCacheSharedExamplesContext.InternalCache: internalCache as Any ] } } describe("The pooled instance function, applied to a cache level") { beforeEach { internalCache = CacheLevelFake() cache = internalCache.pooled() } itBehavesLike("a pooled cache") { [ PoolCacheSharedExamplesContext.CacheToTest: cache as Any, PoolCacheSharedExamplesContext.InternalCache: internalCache as Any ] } } } } ================================================ FILE: Tests/CarlosTests/PostProcessTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct PostProcessSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" static let Transformer = "transformer" } final class PostProcessSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a fetch closure with post-processing step") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[PostProcessSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[PostProcessSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[PostProcessSharedExamplesContext.Transformer] as? OneWayTransformationBox } afterEach { cancellables = nil } context("when calling get") { let key = "12" var successValue: Int? var failureValue: Error? var fakeRequest: PassthroughSubject! beforeEach { fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest successValue = nil failureValue = nil cache.get(key).sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { context("when the transformation closure returns a value") { let value = 101 beforeEach { fakeRequest.send(value) } it("should call the transformer with the success value") { var expected: Int! transformer.transform(value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } } context("when the transformation closure returns nil") { let value = -101 beforeEach { fakeRequest.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should pass the right error code") { expect(failureValue as? TestError).toEventually(equal(TestError.simpleError)) } } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } } } } sharedExamples("a cache with post-processing step") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[PostProcessSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[PostProcessSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[PostProcessSharedExamplesContext.Transformer] as? OneWayTransformationBox } afterEach { cancellables = nil } itBehavesLike("a fetch closure with post-processing step") { [ PostProcessSharedExamplesContext.CacheToTest: cache as Any, PostProcessSharedExamplesContext.InternalCache: internalCache as Any, PostProcessSharedExamplesContext.Transformer: transformer as Any ] } context("when calling set") { let key = "10" let value = 222 beforeEach { cache.set(value, forKey: key) .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) .store(in: &cancellables) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should forward the key") { expect(internalCache.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(internalCache.didSetValue).toEventually(equal(value)) } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class PostProcessTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: OneWayTransformationBox! let transformationClosure: (Int) -> AnyPublisher = { if $0 > 0 { return Just($0 + 1).setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.simpleError).eraseToAnyPublisher() } describe("Post processing using a transformer and a cache, with the instance function") { beforeEach { internalCache = CacheLevelFake() transformer = OneWayTransformationBox(transform: transformationClosure) cache = internalCache.postProcess(transformer) } itBehavesLike("a cache with post-processing step") { [ PostProcessSharedExamplesContext.CacheToTest: cache as Any, PostProcessSharedExamplesContext.InternalCache: internalCache as Any, PostProcessSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/StringConvertibleTests.swift ================================================ import Foundation import Nimble import Quick import Carlos final class StringConvertibleTests: QuickSpec { override func spec() { describe("String values") { let value = "this is the value" it("should return self") { expect(value.toString()) == value } } describe("NSString values") { let value = "this is the value" it("should return self") { expect(value.toString()) == value } } } } ================================================ FILE: Tests/CarlosTests/StringTransformerTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine final class StringTransformerTests: QuickSpec { override func spec() { describe("String transformer") { var transformer: StringTransformer! var error: Error! var cancellable: AnyCancellable? beforeEach { transformer = StringTransformer(encoding: .utf8) } afterEach { cancellable?.cancel() cancellable = nil } context("when transforming NSData to String") { var result: String! context("when the NSData is a valid string") { let stringSample = "this is a sample string" beforeEach { cancellable = transformer.transform((stringSample.data(using: .utf8) as NSData?)!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not return nil") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected String") { expect(result).toEventually(equal(stringSample)) } } } context("when transforming String to NSData") { var result: NSData? let expectedString = "this is the expected string value" beforeEach { cancellable = transformer.inverseTransform(expectedString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should return the expected data") { expect(result).toEventually(equal(expectedString.data(using: .utf8) as NSData?)) } } } } } ================================================ FILE: Tests/CarlosTests/SwitchCacheTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine let switchClosure: (String) -> CacheLevelSwitchResult = { str in if str.count > 5 { return .cacheA } else { return .cacheB } } struct SwitchCacheSharedExamplesContext { static let CacheA = "cacheA" static let CacheB = "cacheB" static let CacheToTest = "sutCache" } final class SwitchCacheSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("should correctly get") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! var cancellables: Set! beforeEach { cancellables = Set() cacheA = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheA] as? CacheLevelFake cacheB = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheB] as? CacheLevelFake finalCache = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheToTest] as? BasicCache } afterEach { cancellables = nil } context("when calling get") { var fakeRequest: PassthroughSubject! var successValue: Int? var errorValue: Error? beforeEach { fakeRequest = PassthroughSubject() cacheA.getSubject = fakeRequest cacheB.getSubject = fakeRequest successValue = nil errorValue = nil } context("when the switch closure returns cacheA") { let key = "quite long key" beforeEach { finalCache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { errorValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledGet).toEventually(equal(0)) } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheA.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = 2010 beforeEach { fakeRequest.send(value) } it("should call the original success closure") { expect(successValue).toEventually(equal(value)) } it("should not call the original failure closure") { expect(errorValue).toEventually(beNil()) } } context("when the request fails") { let errorCode = TestError.simpleError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(errorValue as? TestError).toEventually(equal(errorCode)) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } } } context("when the switch closure returns cacheB") { let key = "short" beforeEach { finalCache.get(key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { errorValue = error } }, receiveValue: { successValue = $0 }) .store(in: &cancellables) } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledGet).toEventually(equal(0)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheB.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { let value = 2010 beforeEach { fakeRequest.send(value) } it("should call the original success closure") { expect(successValue).toEventually(equal(value)) } it("should not call the original failure closure") { expect(errorValue).toEventually(beNil()) } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(errorValue as? TestError).toEventually(equal(errorCode)) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } } } } } sharedExamples("a switched cache with 2 fetch closures") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! beforeEach { cacheA = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheA] as? CacheLevelFake cacheB = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheB] as? CacheLevelFake finalCache = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheToTest] as? BasicCache } itBehavesLike("should correctly get") { [ SwitchCacheSharedExamplesContext.CacheA: cacheA as Any, SwitchCacheSharedExamplesContext.CacheB: cacheB as Any, SwitchCacheSharedExamplesContext.CacheToTest: finalCache as Any ] } } sharedExamples("a switched cache with 2 cache levels") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! var cancellables: Set! beforeEach { cancellables = Set() cacheA = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheA] as? CacheLevelFake cacheB = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheB] as? CacheLevelFake finalCache = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheToTest] as? BasicCache } afterEach { cancellables = nil } itBehavesLike("should correctly get") { [ SwitchCacheSharedExamplesContext.CacheA: cacheA as Any, SwitchCacheSharedExamplesContext.CacheB: cacheB as Any, SwitchCacheSharedExamplesContext.CacheToTest: finalCache as Any ] } context("when calling set") { let value = 30 var setSucceeded: Bool! var setError: Error? beforeEach { setSucceeded = false setError = nil } context("when the switch closure returns cacheA") { let key = "quite long key" beforeEach { finalCache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) .store(in: &cancellables) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(0)) } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheA.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(cacheA.didSetValue).toEventually(equal(value)) } context("when set succeeds") { beforeEach { cacheA.setPublishers[key]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when set fails") { let setFailure = TestError.anotherError beforeEach { cacheA.setPublishers[key]?.send(completion: .failure(setFailure)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(setFailure)) } } } context("when the switch closure returns cacheB") { let key = "short" beforeEach { finalCache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) .store(in: &cancellables) } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(0)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheB.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(cacheB.didSetValue).toEventually(equal(value)) } context("when set succeeds") { beforeEach { cacheB.setPublishers[key]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when set fails") { let setFailure = TestError.anotherError beforeEach { cacheB.setPublishers[key]?.send(completion: .failure(setFailure)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(setFailure)) } } } } context("when calling clear") { beforeEach { finalCache.clear() } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledClear).toEventually(equal(1)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { finalCache.onMemoryWarning() } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } sharedExamples("a switched cache with a cache level and a fetch closure") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! var cancellables: Set! beforeEach { cancellables = Set() cacheA = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheA] as? CacheLevelFake cacheB = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheB] as? CacheLevelFake finalCache = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheToTest] as? BasicCache } afterEach { cancellables = nil } itBehavesLike("should correctly get") { [ SwitchCacheSharedExamplesContext.CacheA: cacheA as Any, SwitchCacheSharedExamplesContext.CacheB: cacheB as Any, SwitchCacheSharedExamplesContext.CacheToTest: finalCache as Any ] } context("when calling set") { let value = 30 var setSucceeded: Bool! var setError: Error? beforeEach { setSucceeded = false setError = nil } context("when the switch closure returns cacheA") { let key = "quite long key" beforeEach { finalCache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) .store(in: &cancellables) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(0)) } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheA.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(cacheA.didSetValue).toEventually(equal(value)) } context("when set succeeds") { beforeEach { cacheA.setPublishers[key]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when set fails") { let setFailure = TestError.anotherError beforeEach { cacheA.setPublishers[key]?.send(completion: .failure(setFailure)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(setFailure)) } } } context("when the switch closure returns cacheB") { let key = "short" beforeEach { _ = finalCache.set(value, forKey: key) } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(0)) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(0)) } } } context("when calling clear") { beforeEach { finalCache.clear() } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledClear).toEventually(equal(1)) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledClear).toEventually(equal(0)) } } context("when calling onMemoryWarning") { beforeEach { finalCache.onMemoryWarning() } it("should dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledOnMemoryWarning).toEventually(equal(0)) } } } sharedExamples("a switched cache with a fetch closure and a cache level") { (sharedExampleContext: @escaping SharedExampleContext) in var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! var cancellables: Set! beforeEach { cancellables = Set() cacheA = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheA] as? CacheLevelFake cacheB = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheB] as? CacheLevelFake finalCache = sharedExampleContext()[SwitchCacheSharedExamplesContext.CacheToTest] as? BasicCache } afterEach { cancellables = nil } itBehavesLike("should correctly get") { [ SwitchCacheSharedExamplesContext.CacheA: cacheA as Any, SwitchCacheSharedExamplesContext.CacheB: cacheB as Any, SwitchCacheSharedExamplesContext.CacheToTest: finalCache as Any ] } context("when calling set") { let value = 30 var setSucceeded: Bool! var setError: Error? beforeEach { setSucceeded = false setError = nil } context("when the switch closure returns cacheA") { let key = "quite long key" beforeEach { _ = finalCache.set(value, forKey: key) } it("should not dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(0)) } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(0)) } } context("when the switch closure returns cacheB") { let key = "short" beforeEach { finalCache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) .store(in: &cancellables) } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledSet).toEventually(equal(0)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the right key") { expect(cacheB.didSetKey).toEventually(equal(key)) } it("should pass the right value") { expect(cacheB.didSetValue).toEventually(equal(value)) } context("when set succeeds") { beforeEach { cacheB.setPublishers[key]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when set fails") { let setFailure = TestError.anotherError beforeEach { cacheB.setPublishers[key]?.send(completion: .failure(setFailure)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(setFailure)) } } } } context("when calling clear") { beforeEach { finalCache.clear() } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledClear).toEventually(equal(0)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { finalCache.onMemoryWarning() } it("should not dispatch the call to the first cache") { expect(cacheA.numberOfTimesCalledOnMemoryWarning).toEventually(equal(0)) } it("should dispatch the call to the second cache") { expect(cacheB.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class SwitchCacheTests: QuickSpec { override func spec() { var cacheA: CacheLevelFake! var cacheB: CacheLevelFake! var finalCache: BasicCache! describe("Switching two cache levels") { beforeEach { cacheA = CacheLevelFake() cacheB = CacheLevelFake() finalCache = switchLevels(cacheA: cacheA, cacheB: cacheB, switchClosure: switchClosure) } itBehavesLike("a switched cache with 2 cache levels") { [ SwitchCacheSharedExamplesContext.CacheA: cacheA as Any, SwitchCacheSharedExamplesContext.CacheB: cacheB as Any, SwitchCacheSharedExamplesContext.CacheToTest: finalCache as Any ] } } } } ================================================ FILE: Tests/CarlosTests/TwoWayTransformationBoxTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct TwoWayTransformationBoxSharedExamplesContext { static let TransformerToTest = "transformer" } final class TwoWayTransformationBoxSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("an inverted two-way transformation box") { (sharedExampleContext: @escaping SharedExampleContext) in var invertedBox: TwoWayTransformationBox! var error: Error! var cancellable: AnyCancellable? beforeEach { error = nil invertedBox = sharedExampleContext()[TwoWayTransformationBoxSharedExamplesContext.TransformerToTest] as? TwoWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil } context("when using the transformation") { var result: NSURL! beforeEach { result = nil } context("if the transformation is possible") { let originString = "http://github.com/WeltN24/Carlos" beforeEach { cancellable = invertedBox.transform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).notTo(beNil()) } it("should not call the failure closure") { expect(error).to(beNil()) } it("should return the expected result") { expect(result?.absoluteString) == originString } } context("if the transformation is not possible") { beforeEach { cancellable = invertedBox.transform("not an URL") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the error closure") { expect(error).notTo(beNil()) } it("should not call the success closure") { expect(result).to(beNil()) } it("should pass the right error") { expect(error as? TestError) == TestError.anotherError } } } context("when using the inverse transformation") { var result: String! beforeEach { result = nil } context("when the transformation is possible") { let originURL = NSURL(string: "http://github.com/WeltN24/Carlos")! beforeEach { cancellable = invertedBox.inverseTransform(originURL) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).notTo(beNil()) } it("should not call the failure closure") { expect(error).to(beNil()) } it("should return the expected result") { expect(result) == originURL.absoluteString } } context("when the transformation is not possible") { beforeEach { cancellable = invertedBox.inverseTransform(NSURL(string: "ftp://test")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the error closure") { expect(error).notTo(beNil()) } it("should not call the success closure") { expect(result).to(beNil()) } it("should pass the right error") { expect(error as? TestError) == TestError.anotherError } } } } } } final class TwoWayTransformationBoxTests: QuickSpec { override func spec() { describe("TwoWayTransformationBox") { var box: TwoWayTransformationBox! var error: Error! var cancellable: AnyCancellable? beforeEach { error = nil box = TwoWayTransformationBox(transform: { guard $0.scheme == "http", let stringValue = $0.absoluteString else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } return Just(stringValue).setFailureType(to: Error.self).eraseToAnyPublisher() }, inverseTransform: { guard let value = NSURL(string: $0) else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() }) } afterEach { cancellable?.cancel() cancellable = nil } context("when using the transformation") { var result: String! beforeEach { result = nil } context("when the transformation is possible") { let originURL = NSURL(string: "http://github.com/WeltN24/Carlos")! beforeEach { cancellable = box.transform(originURL) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(originURL.absoluteString)) } } context("when the transformation is not possible") { beforeEach { cancellable = box.transform(NSURL(string: "ftp://whatever")!) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the error closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when using the inverse transformation") { var result: NSURL! beforeEach { result = nil } context("when the transformation is possible") { let originString = "http://github.com/WeltN24/Carlos" beforeEach { cancellable = box.inverseTransform(originString) .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should call the success closure") { expect(result).toEventuallyNot(beNil()) } it("should not call the failure closure") { expect(error).toEventually(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(NSURL(string: originString)!)) } } context("when the transformation is not possible") { beforeEach { cancellable = box.inverseTransform("not an URL") .sink(receiveCompletion: { completion in if case let .failure(e) = completion { error = e } }, receiveValue: { result = $0 }) } it("should not call the success closure") { expect(result).toEventually(beNil()) } it("should call the error closure") { expect(error).toEventuallyNot(beNil()) } it("should pass the right error") { expect(error as? TestError) == TestError.anotherError } } } context("when inverting the transformer") { var invertedBox: TwoWayTransformationBox! beforeEach { invertedBox = box.invert() } itBehavesLike("an inverted two-way transformation box") { [ TwoWayTransformationBoxSharedExamplesContext.TransformerToTest: invertedBox as Any ] } } } } } ================================================ FILE: Tests/CarlosTests/TwoWayTransformerCompositionTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ComposedTwoWayTransformerSharedExamplesContext { static let TransformerToTest = "composedTransformer" } final class TwoWayTransformerCompositionSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a composed two-way transformer") { (sharedExampleContext: @escaping SharedExampleContext) in var composedTransformer: TwoWayTransformationBox! var cancellable: AnyCancellable? beforeEach { composedTransformer = sharedExampleContext()[ComposedTwoWayTransformerSharedExamplesContext.TransformerToTest] as? TwoWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil } context("when transforming a value") { var result: Int? beforeEach { result = nil } context("if the transformation is possible") { beforeEach { cancellable = composedTransformer.transform("12.15") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should not return nil") { expect(result).toEventuallyNot(beNil()) } it("should return the expected result") { expect(result).toEventually(equal(12)) } } context("if the transformation fails in the first transformer") { beforeEach { cancellable = composedTransformer.transform("hallo world") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } context("if the transformation fails in the second transformer") { beforeEach { cancellable = composedTransformer.transform("-13.2") .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } } context("when doing the inverse transform") { var result: String? beforeEach { result = nil } context("when the transformation is possible") { beforeEach { cancellable = composedTransformer.inverseTransform(31) .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should not return nil") { expect(result).toEventuallyNot(beNil()) } it("should return the expected result") { expect(result).toEventually(equal("31.0")) } } context("if the transformation fails in the first transformer") { beforeEach { cancellable = composedTransformer.inverseTransform(-4) .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } context("if the transformation fails in the second transformer") { beforeEach { cancellable = composedTransformer.inverseTransform(105) .sink(receiveCompletion: { _ in }, receiveValue: { result = $0 }) } it("should return nil") { expect(result).toEventually(beNil()) } } } } } } final class TwoWayTransformerCompositionTests: QuickSpec { override func spec() { var transformer1: TwoWayTransformationBox! var transformer2: TwoWayTransformationBox! var composedTransformer: TwoWayTransformationBox! beforeEach { transformer1 = TwoWayTransformationBox(transform: { guard let value = Float($0) else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(value).setFailureType(to: Error.self).eraseToAnyPublisher() }, inverseTransform: { guard $0 < 100 else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just("\($0)").setFailureType(to: Error.self).eraseToAnyPublisher() }) transformer2 = TwoWayTransformationBox(transform: { guard $0 > 0 else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(Int($0)).setFailureType(to: Error.self).eraseToAnyPublisher() }, inverseTransform: { guard $0 > 0 else { return Fail(error: TestError.simpleError).eraseToAnyPublisher() } return Just(Float($0)).setFailureType(to: Error.self).eraseToAnyPublisher() }) } describe("Transformer composition using the instance function") { beforeEach { composedTransformer = transformer1.compose(transformer2) } itBehavesLike("a composed two-way transformer") { [ ComposedTwoWayTransformerSharedExamplesContext.TransformerToTest: composedTransformer as Any ] } } } } ================================================ FILE: Tests/CarlosTests/ValueTransformationTests.swift ================================================ import Foundation import Nimble import Quick import Carlos import Combine struct ValueTransformationsSharedExamplesContext { static let CacheToTest = "cache" static let InternalCache = "internalCache" static let Transformer = "transformer" } final class ValueTransformationSharedExamplesConfiguration: QuickConfiguration { override class func configure(_: Configuration) { sharedExamples("a cache with transformed values") { (sharedExampleContext: @escaping SharedExampleContext) in var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: TwoWayTransformationBox! var cancellable: AnyCancellable? var cancellables: Set! beforeEach { cancellables = Set() cache = sharedExampleContext()[ValueTransformationsSharedExamplesContext.CacheToTest] as? BasicCache internalCache = sharedExampleContext()[ValueTransformationsSharedExamplesContext.InternalCache] as? CacheLevelFake transformer = sharedExampleContext()[ValueTransformationsSharedExamplesContext.Transformer] as? TwoWayTransformationBox } afterEach { cancellable?.cancel() cancellable = nil cancellables = nil } context("when calling get") { let key = "12" var successValue: String? var failureValue: Error? var fakeRequest: PassthroughSubject! var canceled: Bool! beforeEach { canceled = false failureValue = nil successValue = nil fakeRequest = PassthroughSubject() internalCache.getSubject = fakeRequest cancellable = cache.get(key) .handleEvents(receiveCancel: { canceled = true }) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { failureValue = error } }, receiveValue: { successValue = $0 }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledGet).toEventually(equal(1)) } it("should pass the right key") { expect(internalCache.didGetKey).toEventually(equal(key)) } context("when the request succeeds") { context("when the value can be successfully transformed") { let value = 101 beforeEach { fakeRequest.send(value) } it("should call the original success closure") { expect(successValue).toEventuallyNot(beNil()) } it("should transform the value") { var expected: String! transformer.transform(value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(successValue).toEventually(equal(expected)) } it("should not call the original cancel closure") { expect(canceled).toEventually(beFalse()) } it("should not call the original failure closure") { expect(failureValue).toEventually(beNil()) } } context("when the value transformation returns nil") { let value = -101 beforeEach { successValue = nil fakeRequest.send(value) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should fail with the right code") { expect(failureValue as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when the request fails") { let errorCode = TestError.anotherError beforeEach { fakeRequest.send(completion: .failure(errorCode)) } it("should call the original failure closure") { expect(failureValue).toEventuallyNot(beNil()) } it("should fail with the right code") { expect(failureValue as? TestError).toEventually(equal(errorCode)) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } it("should not call the original cancel closure") { expect(canceled).toEventually(beFalse()) } } context("when the request is canceled") { beforeEach { cancellable?.cancel() } it("should call the original cancel closure") { expect(canceled).toEventually(beTrue()) } it("should not call the original failure closure") { expect(failureValue).toEventually(beNil()) } it("should not call the original success closure") { expect(successValue).toEventually(beNil()) } } } context("when calling set") { var setSucceeded: Bool! var setError: Error? beforeEach { setSucceeded = false setError = nil } context("when the inverse transformation succeeds") { let key = "test key to set" let value = "199" beforeEach { cancellable = cache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(1)) } it("should pass the key") { expect(internalCache.didSetKey).toEventually(equal(key)) } it("should transform the value first") { var expected: Int! transformer.inverseTransform(value) .sink(receiveCompletion: { _ in }, receiveValue: { expected = $0 }) .store(in: &cancellables) expect(internalCache.didSetValue).toEventually(equal(expected)) } context("when the set succeeds") { beforeEach { internalCache.setPublishers[key]?.send() } it("should succeed") { expect(setSucceeded).toEventually(beTrue()) } } context("when the set fails") { beforeEach { internalCache.setPublishers[key]?.send(completion: .failure(TestError.anotherError)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the error through") { expect(setError as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when the inverse transformation fails") { let key = "test key to set" let value = "will fail" beforeEach { cancellable = cache.set(value, forKey: key) .sink(receiveCompletion: { completion in if case let .failure(error) = completion { setError = error } }, receiveValue: { setSucceeded = true }) } it("should not forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledSet).toEventually(equal(0)) } it("should fail") { expect(setError).toEventuallyNot(beNil()) } it("should pass the transformation error") { expect(setError as? TestError).toEventually(equal(TestError.anotherError)) } } } context("when calling clear") { beforeEach { cache.clear() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledClear).toEventually(equal(1)) } } context("when calling onMemoryWarning") { beforeEach { cache.onMemoryWarning() } it("should forward the call to the internal cache") { expect(internalCache.numberOfTimesCalledOnMemoryWarning).toEventually(equal(1)) } } } } } final class ValueTransformationTests: QuickSpec { override func spec() { var cache: BasicCache! var internalCache: CacheLevelFake! var transformer: TwoWayTransformationBox! let forwardTransformationClosure: (Int) -> AnyPublisher = { if $0 > 0 { return Just("\($0 + 1)").setFailureType(to: Error.self).eraseToAnyPublisher() } return Fail(error: TestError.anotherError).eraseToAnyPublisher() } let inverseTransformationClosure: (String) -> AnyPublisher = { guard let intValue = Int($0) else { return Fail(error: TestError.anotherError).eraseToAnyPublisher() } return Just(intValue).setFailureType(to: Error.self).eraseToAnyPublisher() } describe("Value transformation using a transformer and a cache, with the instance function") { beforeEach { internalCache = CacheLevelFake() transformer = TwoWayTransformationBox(transform: forwardTransformationClosure, inverseTransform: inverseTransformationClosure) cache = internalCache.transformValues(transformer) } itBehavesLike("a cache with transformed values") { [ ValueTransformationsSharedExamplesContext.CacheToTest: cache as Any, ValueTransformationsSharedExamplesContext.InternalCache: internalCache as Any, ValueTransformationsSharedExamplesContext.Transformer: transformer as Any ] } } } } ================================================ FILE: fastlane/Fastfile ================================================ # https://github.com/KrauseFx/fastlane/tree/master/docs # All available actions: https://github.com/KrauseFx/fastlane/blob/master/docs/Actions.md fastlane_version "2.140" desc "Runs all the tests" lane :test do ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "30" clear_derived_data spm(command: "test", configuration: "release") end ================================================ FILE: fastlane/README.md ================================================ fastlane documentation ================ # Installation Make sure you have the latest version of the Xcode command line tools installed: ``` xcode-select --install ``` Install _fastlane_ using ``` [sudo] gem install fastlane -NV ``` or alternatively using `brew install fastlane` # Available Actions ### test ``` fastlane test ``` Runs all the tests ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). ================================================ FILE: fastlane/Scanfile ================================================ # https://github.com/fastlane/scan#scanfile clean true