Skip to content

Commit

Permalink
Closes #55 by adding unconditioned output transformers
Browse files Browse the repository at this point in the history
  • Loading branch information
vittoriom committed Sep 30, 2015
1 parent fa02596 commit 5092564
Show file tree
Hide file tree
Showing 4 changed files with 538 additions and 3 deletions.
12 changes: 12 additions & 0 deletions Carlos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@
52CF0AD81B920DC20061022D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 52CF0AD61B920DC20061022D /* Main.storyboard */; };
52CF0ADD1B920DD60061022D /* CarlosMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52CF0A9E1B9203730061022D /* CarlosMac.framework */; };
52CF0ADE1B920DD60061022D /* CarlosMac.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 52CF0A9E1B9203730061022D /* CarlosMac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
52E0CBF91BBC4E1800F20C22 /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; settings = {ASSET_TAGS = (); }; };
52E0CBFA1BBC4E1800F20C22 /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; settings = {ASSET_TAGS = (); }; };
52E0CBFB1BBC4E1800F20C22 /* PostProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */; settings = {ASSET_TAGS = (); }; };
52E197021BBC6B27004BF6C5 /* PostProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */; settings = {ASSET_TAGS = (); }; };
52E6E2461B5BFB5E00C3DF6C /* GenericOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E6E2451B5BFB5E00C3DF6C /* GenericOperation.swift */; };
52E6E24C1B5C02EA00C3DF6C /* DeferredCacheRequestOperationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E6E24B1B5C02EA00C3DF6C /* DeferredCacheRequestOperationTests.swift */; };
52F0644F1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */; };
Expand Down Expand Up @@ -435,6 +439,8 @@
52CF0AD41B920DC20061022D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
52CF0AD71B920DC20061022D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
52CF0AD91B920DC20061022D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostProcess.swift; sourceTree = "<group>"; };
52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostProcessTests.swift; sourceTree = "<group>"; };
52E6E2451B5BFB5E00C3DF6C /* GenericOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericOperation.swift; sourceTree = "<group>"; };
52E6E24B1B5C02EA00C3DF6C /* DeferredCacheRequestOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeferredCacheRequestOperationTests.swift; sourceTree = "<group>"; };
52F0644E1B51AC0D00145C98 /* NSDateFormatterTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateFormatterTransformerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -570,6 +576,7 @@
52B7620E1BB84EFF0087CD91 /* ImageTransformer+iOS.swift */,
52B7621B1BB84F160087CD91 /* JSONTransformer.swift */,
5205729A1BB8716E0098DD09 /* StringTransformer.swift */,
52E0CBF81BBC4E1800F20C22 /* PostProcess.swift */,
);
path = Carlos;
sourceTree = SOURCE_ROOT;
Expand Down Expand Up @@ -610,6 +617,7 @@
5291C8521BB7FAA800C4E15E /* JSONTransformerTests.swift */,
526005AE1BB84AB200D96242 /* ImageTransformerTests.swift */,
520572A81BB8752B0098DD09 /* StringTransformerTests.swift */,
52E197011BBC6B27004BF6C5 /* PostProcessTests.swift */,
);
name = CarlosTests;
path = Tests;
Expand Down Expand Up @@ -1206,6 +1214,7 @@
52E6E2461B5BFB5E00C3DF6C /* GenericOperation.swift in Sources */,
5205729B1BB8716E0098DD09 /* StringTransformer.swift in Sources */,
52635A5D1B4F0F6F00F187EE /* PoolCache.swift in Sources */,
52E0CBF91BBC4E1800F20C22 /* PostProcess.swift in Sources */,
AD8557F81BA94B690054FB5F /* NSKeyedUnarchiver+SwiftUtilities.m in Sources */,
52635A5C1B4F0F6F00F187EE /* OneWayTransformer.swift in Sources */,
52635A561B4F0F6F00F187EE /* Errors.swift in Sources */,
Expand Down Expand Up @@ -1249,6 +1258,7 @@
526339771B52450C00074CB9 /* ConditionedCacheTests.swift in Sources */,
52F064511B51AE4300145C98 /* NSNumberFormatterTransformerTests.swift in Sources */,
C71C59751B6A722800AE5294 /* NetworkFetcherTests.swift in Sources */,
52E197021BBC6B27004BF6C5 /* PostProcessTests.swift in Sources */,
5250E3841BB137BF00EB7388 /* NormalizationTests.swift in Sources */,
523257D21B51993000A10A56 /* TwoWayTransformationBoxTests.swift in Sources */,
);
Expand All @@ -1272,6 +1282,7 @@
52CF0AAF1B9203B30061022D /* Logger.swift in Sources */,
52CF0AB01B9203B30061022D /* MemoryCacheLevel.swift in Sources */,
5250E3821BB1345700EB7388 /* Normalize.swift in Sources */,
52E0CBFB1BBC4E1800F20C22 /* PostProcess.swift in Sources */,
52CF0AB21B9203B30061022D /* NetworkFetcher.swift in Sources */,
52CF0AB31B9203B30061022D /* OneWayTransformer.swift in Sources */,
52CF0AB41B9203B30061022D /* PoolCache.swift in Sources */,
Expand Down Expand Up @@ -1322,6 +1333,7 @@
AD64D9591B8DE62400C783D3 /* ConcurrentOperation.swift in Sources */,
AD64D95B1B8DE62400C783D3 /* GenericOperation.swift in Sources */,
52CF0AC71B9204C20061022D /* CacheProvider+iOS.swift in Sources */,
52E0CBFA1BBC4E1800F20C22 /* PostProcess.swift in Sources */,
AD64D9541B8DE62400C783D3 /* ValueTransformation.swift in Sources */,
AD64D95C1B8DE62400C783D3 /* CacheProvider.swift in Sources */,
AD64D9531B8DE62400C783D3 /* PoolCache.swift in Sources */,
Expand Down
151 changes: 151 additions & 0 deletions Carlos/PostProcess.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Foundation

infix operator ~>> { associativity left }

extension CacheLevel {

/**
Adds a post-processing step to the get results of this CacheLevel
:param: 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess<A: OneWayTransformer where OutputType == A.TypeIn, A.TypeIn == A.TypeOut>(transformer: A) -> BasicCache<KeyType, OutputType> {
return BasicCache(
getClosure: { key in
return self.get(key).mutate(transformer)
},
setClosure: self.set,
clearClosure: self.clear,
memoryClosure: self.onMemoryWarning
)
}

/**
Adds a post-processing step to the get results of this CacheLevel
:param: transformerClosure The transformation closure that will be applied to every successful result of the method get called on the cache level. The closure 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess(transformerClosure: OutputType -> OutputType?) -> BasicCache<KeyType, OutputType> {
return self.postProcess(wrapClosureIntoOneWayTransformer(transformerClosure))
}
}

/**
Adds a post-processing step to the get results of a fetch closure
:param: fetchClosure The fetch closure you want to decorate
:param: transformer The OneWayTransformer that will be applied to every successful result of the fetch closure. The transformer has to return the same type of the input type
:returns: A CacheLevel that incorporates the post-processing step
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess<A, B: OneWayTransformer where B.TypeIn == B.TypeOut>(fetchClosure: (key: A) -> CacheRequest<B.TypeIn>, transformer: B) -> BasicCache<A, B.TypeOut> {
return wrapClosureIntoCacheLevel(fetchClosure).postProcess(transformer)
}

/**
Adds a post-processing step to the get results of a fetch closure
:param: fetchClosure The fetch closure you want to decorate
:param: transformerClosure The transformation closure that will be applied to every successful result of the fetch closure. The transformation closure has to return the same type of the input type
:returns: A CacheLevel that incorporates the post-processing step
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess<A, B>(fetchClosure: (key: A) -> CacheRequest<B>, transformerClosure: B -> B?) -> BasicCache<A, B> {
return wrapClosureIntoCacheLevel(fetchClosure).postProcess(wrapClosureIntoOneWayTransformer(transformerClosure))
}

/**
Adds a post-processing step to the get results of a CacheLevel
:param: cache The CacheLevel you want to decorate
:param: transformer The OneWayTransformer that will be applied to every successful result of the CacheLevel. 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess<A: CacheLevel, B: OneWayTransformer where A.OutputType == B.TypeIn, B.TypeIn == B.TypeOut>(cache: A, transformer: B) -> BasicCache<A.KeyType, A.OutputType> {
return cache.postProcess(transformer)
}

/**
Adds a post-processing step to the get results of a CacheLevel
:param: cache The CacheLevel you want to decorate
:param: transformerClosure The transformation closure that will be applied to every successful result of the method get called on the cache level. The closure 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func postProcess<A: CacheLevel>(cache: A, transformerClosure: A.OutputType -> A.OutputType?) -> BasicCache<A.KeyType, A.OutputType> {
return cache.postProcess(wrapClosureIntoOneWayTransformer(transformerClosure))
}

/**
Adds a post-processing step to the get results of a fetch closure
:param: fetchClosure The fetch closure you want to decorate
:param: transformer The OneWayTransformer that will be applied to every successful result of the fetch closure. The transformer has to return the same type of the input type
:returns: A CacheLevel that incorporates the post-processing step
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func ~>><A, B: OneWayTransformer where B.TypeIn == B.TypeOut>(fetchClosure: (key: A) -> CacheRequest<B.TypeIn>, transformer: B) -> BasicCache<A, B.TypeOut> {
return postProcess(fetchClosure, transformer: transformer)
}

/**
Adds a post-processing step to the get results of a fetch closure
:param: fetchClosure The fetch closure you want to decorate
:param: transformerClosure The transformation closure that will be applied to every successful result of the fetch closure. The transformation closure has to return the same type of the input type
:returns: A CacheLevel that incorporates the post-processing step
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func ~>><A, B>(fetchClosure: (key: A) -> CacheRequest<B>, transformerClosure: B -> B?) -> BasicCache<A, B> {
return postProcess(fetchClosure, transformerClosure: transformerClosure)
}

/**
Adds a post-processing step to the get results of a CacheLevel
:param: cache The CacheLevel you want to decorate
:param: transformer The OneWayTransformer that will be applied to every successful result of the CacheLevel. 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func ~>><A: CacheLevel, B: OneWayTransformer where A.OutputType == B.TypeIn, B.TypeIn == B.TypeOut>(cache: A, transformer: B) -> BasicCache<A.KeyType, A.OutputType> {
return postProcess(cache, transformer: transformer)
}

/**
Adds a post-processing step to the get results of a CacheLevel
:param: cache The CacheLevel you want to decorate
:param: transformerClosure The transformation closure that will be applied to every successful result of the method get called on the cache level. The closure 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
:discussion: As usual, if the transformation fails, the get request will also fail
*/
public func ~>><A: CacheLevel>(cache: A, transformerClosure: A.OutputType -> A.OutputType?) -> BasicCache<A.KeyType, A.OutputType> {
return postProcess(cache, transformerClosure: transformerClosure)
}
41 changes: 38 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
- [Usage examples](#usage-examples)
- [Creating requests](#creating-requests)
- [Key transformations](#key-transformations)
- [Value transformations](#value-transformations)
- [Value transformations](#value-transformations)
- [Output post-processing](#post-processing-output)
- [Pooling requests](#pooling-requests)
- [Limiting concurrent requests](#limiting-concurrent-requests)
- [Conditioning caches](#conditioning-caches)
Expand All @@ -45,7 +46,8 @@ With Carlos you can:

- **create levels and fetchers** depending on your needs, either [through classes](#creating-custom-levels) or with [simple closures](#composing-with-closures)
- [combine levels](#usage-examples)
- [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)
- [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
- [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)
Expand Down Expand Up @@ -281,6 +283,39 @@ Carlos comes with some value transformers out of the box, for example:
- `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 (`NSDateFormatter`, `NSNumberFormatter`, `MKDistanceFormatter`) so that you can use customized instances depending on your needs

### 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 could come helpful.
The function is available as a protocol extension of the `CacheLevel` protocol, as a global function and as an operator in the form of `~>>`.

The `postProcess` function takes a `CacheLevel` (or a fetch closure) and a `OneWayTransformer` (or a transformation closure) 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<NSString, NSString>(transform: { $0.uppercaseString })

// Our memory cache
let memoryCache = MemoryCacheLevel<String, NSString>()

// 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").onSuccess { value in
let x = value
}

// We get the uppercase value from the decorated cache, though
transformedCache.get("key").onSuccess { value in
let x = value
}
```

### Pooling requests

Expand Down Expand Up @@ -550,7 +585,7 @@ Carlos is thouroughly tested so that the features it's designed to provide are s

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 **900 tests** for Carlos (see the folder `Tests`), and overall the tests codebase is *double the size* of the production codebase.
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

Expand Down
Loading

0 comments on commit 5092564

Please sign in to comment.