Skip to content

Commit 151ab3f

Browse files
authored
Merge pull request #42 from dfed/dfed--objective-c-wrapper
Create CADCacheAdvance: a Objective-C compatibility wrapper around a CacheAdvance<Data>
2 parents eb8744a + ea758dd commit 151ab3f

17 files changed

+487
-120
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ matrix:
1111
after_success:
1212
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/iOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
1313
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/iOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
14+
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/iOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
15+
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/iOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
1416

1517
- osx_image: xcode11.2
1618
env: ACTION="swift-package";PLATFORMS="tvOS_12,tvOS_13";
1719
after_success:
1820
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/tvOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
1921
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/tvOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
22+
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/tvOS_12 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
23+
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/tvOS_13 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
2024

2125
- osx_image: xcode11.2
2226
env: ACTION="swift-package";PLATFORMS="macOS_10_15";
2327
after_success:
2428
- bash <(curl -s https://codecov.io/bash) -J '^CacheAdvance$' -D .build/derivedData/macOS_10_15 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
29+
- bash <(curl -s https://codecov.io/bash) -J '^CADCacheAdvance$' -D .build/derivedData/macOS_10_15 -t 8344b011-6b2a-4b3d-a573-eaf49684318e
2530

2631
- osx_image: xcode11.2
2732
env: ACTION="swift-package";PLATFORMS="watchOS_5,watchOS_6";

CacheAdvance.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Pod::Spec.new do |s|
22
s.name = 'CacheAdvance'
3-
s.version = '1.0.1'
3+
s.version = '1.1.0'
44
s.license = 'Apache License, Version 2.0'
55
s.summary = 'A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite.'
66
s.homepage = 'https://github.com/dfed/CacheAdvance'
77
s.authors = 'Dan Federman'
88
s.source = { :git => 'https://github.com/dfed/CacheAdvance.git', :tag => s.version }
99
s.swift_version = '5.1'
10-
s.source_files = 'Sources/CacheAdvance/**/*.{swift}', 'Sources/SwiftTryCatch/**/*.{h,m}'
10+
s.source_files = 'Sources/**/*.{swift}', 'Sources/**/*.{h,m}'
1111
s.ios.deployment_target = '12.0'
1212
s.tvos.deployment_target = '12.0'
1313
s.watchos.deployment_target = '5.0'

Package.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ let package = Package(
1414
products: [
1515
.library(
1616
name: "CacheAdvance",
17-
targets: ["CacheAdvance"]),
17+
targets: ["CacheAdvance"]
18+
),
19+
.library(
20+
name: "CADCacheAdvance",
21+
targets: ["CADCacheAdvance"]
22+
)
1823
],
1924
targets: [
2025
.target(
@@ -24,7 +29,21 @@ let package = Package(
2429
),
2530
.testTarget(
2631
name: "CacheAdvanceTests",
27-
dependencies: ["CacheAdvance"]),
32+
dependencies: ["CacheAdvance", "LorumIpsum"]
33+
),
34+
.target(
35+
name: "CADCacheAdvance",
36+
dependencies: ["CacheAdvance"],
37+
swiftSettings: [.define("SWIFT_PACKAGE_MANAGER")]
38+
),
39+
.target(
40+
name: "LorumIpsum",
41+
dependencies: []
42+
),
43+
.testTarget(
44+
name: "CADCacheAdvanceTests",
45+
dependencies: ["CADCacheAdvance", "LorumIpsum"]
46+
),
2847
.target(
2948
name: "SwiftTryCatch",
3049
dependencies: [],
@@ -35,7 +54,8 @@ let package = Package(
3554
),
3655
.testTarget(
3756
name: "SwiftTryCatchTests",
38-
dependencies: ["SwiftTryCatch"])
57+
dependencies: ["SwiftTryCatch"]
58+
)
3959
],
4060
swiftLanguageVersions: [.v5]
4161
)

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ let myCache = try CacheAdvance<MyMessageType>(
1919
maximumBytes: 5000,
2020
shouldOverwriteOldMessages: false)
2121
```
22+
23+
```objc
24+
CADCacheAdvance *const cache = [[CADCacheAdvance alloc]
25+
initWithFileURL:[NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"MyCache"]
26+
maximumBytes:5000
27+
shouldOverwriteOldMessages:YES
28+
error:nil];
29+
```
2230
To begin caching messages, you need to create a CacheAdvance instance with:
2331
2432
* A file URL – this URL must represent a file that has already been created. You can create a file by using `FileManager`'s [createFile(atPath:contents:attributes:)](https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile) API.
@@ -28,7 +36,11 @@ To begin caching messages, you need to create a CacheAdvance instance with:
2836
### Appending messages to disk
2937
3038
```swift
31-
try myCache.append(message: aMessageInstance)
39+
try myCache.append(message: someMessage)
40+
```
41+
42+
```objc
43+
[cache appendMessage:someData error:nil];
3244
```
3345
3446
By the time the above method exits, the message will have been persisted to disk. A CacheAdvance keeps no in-memory buffer. Appending a new message is cheap, as a CacheAdvance needs to encode and persist only the new message and associated metadata.
@@ -43,6 +55,10 @@ To ensure that caches can be read from 32bit devices, messages should not be lar
4355
let cachedMessages = try myCache.messages()
4456
```
4557

58+
```objc
59+
NSArray<NSData> *const cachedMessages = [cache messagesAndReturnError:nil];
60+
```
61+
4662
This method reads all cached messages from disk into memory.
4763
4864
### Thread safety
@@ -57,7 +73,7 @@ If a `CacheAdvanceError.fileCorrupted` error is thrown, the cache file is corrup
5773
5874
## How it works
5975
60-
CacheAdvance immediately persists each appended messages to disk using `FileHandle`s. Messages are encoded using a `JSONEncoder`. Messages are written to disk as an encoded data blob that is prefixed with the length of the message. The length of a message is stored using a `UInt32` to ensure that the size of the data on disk that stores a message's length is consistent between devices.
76+
CacheAdvance immediately persists each appended messages to disk using `FileHandle`s. Messages are encoded into `Data` using a `JSONEncoder` by default, though the encoding/decoding mechanism can be customized. Messages are written to disk as an encoded data blob that is prefixed with the length of the message. The length of a message is stored using a `UInt32` to ensure that the size of the data on disk that stores a message's length is consistent between devices.
6177
6278
The first 64bytes of a CacheAdvance is reserved for storing metadata about the file. Any configuration data that must be static between cache opens should be stored in this header. It is also reasonable to store mutable information in the header, if doing so speeds up reads or writes to the file. The header format is managed by [FileHeader.swift](Sources/CacheAdvance/FileHeader.swift).
6379

Scripts/build.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ for rawPlatform in rawPlatforms {
117117
"-configuration", "Release",
118118
"-derivedDataPath", platform.derivedDataPath,
119119
"-PBXBuildsContinueAfterErrors=0",
120+
"OTHER_CFLAGS='-DGENERATED_XCODE_PROJECT'",
120121
]
121122
if !platform.destination.isEmpty {
122123
xcodeBuildArguments.append("-destination")
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// Created by Dan Federman on 4/24/20.
3+
// Copyright © 2020 Dan Federman.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
//    http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
18+
#if SWIFT_PACKAGE_MANAGER
19+
// Swift Package Manager defines multiple modules, while other distribution mechanisms do not.
20+
// We only need to import CacheAdvance if this project is being built with Swift Package Manager.
21+
import CacheAdvance
22+
#endif
23+
import Foundation
24+
25+
/// A cache that enables the performant persistence of individual messages to disk.
26+
/// This cache is intended to be written to and read from using the same serial queue.
27+
/// - Attention: This type is meant to be used by Objective-C code, and is not exposed to Swift. Swift code should use CacheAdvance<T>.
28+
@objc(CADCacheAdvance)
29+
@available(swift, obsoleted: 1.0)
30+
public final class __ObjectiveCCompatibleCacheAdvanceWithGenericData: NSObject {
31+
32+
// MARK: Initialization
33+
34+
/// Creates a new instance of the receiver.
35+
///
36+
/// - Parameters:
37+
/// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist.
38+
/// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store.
39+
/// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry.
40+
///
41+
/// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
42+
/// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
43+
@objc
44+
public init(
45+
fileURL: URL,
46+
maximumBytes: Bytes,
47+
shouldOverwriteOldMessages: Bool)
48+
throws
49+
{
50+
cache = try CacheAdvance<Data>(
51+
fileURL: fileURL,
52+
maximumBytes: maximumBytes,
53+
shouldOverwriteOldMessages: shouldOverwriteOldMessages,
54+
decoder: PassthroughDataDecoder(),
55+
encoder: PassthroughDataEncoder())
56+
}
57+
58+
// MARK: Public
59+
60+
@objc
61+
public var fileURL: URL {
62+
cache.fileURL
63+
}
64+
65+
/// Checks if the all the header metadata provided at initialization matches the persisted header. If not, the cache is not writable.
66+
/// - Returns: `true` if the cache is writable.
67+
@objc
68+
public var isWritable: Bool {
69+
(try? cache.isWritable()) ?? false
70+
}
71+
72+
/// Appends a message to the cache.
73+
/// - Parameter message: A message to write to disk. Must be smaller than both `maximumBytes - FileHeader.expectedEndOfHeaderInFile` and `MessageSpan.max`.
74+
@objc
75+
public func appendMessage(_ message: Data) throws {
76+
try cache.append(message: message)
77+
}
78+
79+
/// - Returns: `true` when there are no messages written to the file, or when the file can not be read.
80+
@objc
81+
public var isEmpty: Bool {
82+
(try? cache.isEmpty()) ?? true
83+
}
84+
85+
/// Fetches all messages from the cache.
86+
@objc
87+
public func messages() throws -> [Data] {
88+
try cache.messages()
89+
}
90+
91+
// MARK: Private
92+
93+
private let cache: CacheAdvance<Data>
94+
}
95+
96+
// MARK: - PassthroughDataDecoder
97+
98+
/// A decoder that treats all messages as if they are `Data`.
99+
final class PassthroughDataDecoder: MessageDecoder {
100+
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
101+
if let data = data as? T {
102+
return data
103+
} else {
104+
throw DecodingError.dataCorrupted(
105+
DecodingError.Context(
106+
codingPath: [],
107+
debugDescription: "Type was not Data"))
108+
}
109+
}
110+
}
111+
112+
// MARK: - PassthroughDataDecoder
113+
114+
/// A encoder that treats all messages as if they are `Data`.
115+
final class PassthroughDataEncoder: MessageEncoder {
116+
func encode<T>(_ value: T) throws -> Data where T : Encodable {
117+
if let value = value as? Data {
118+
return value
119+
} else {
120+
throw EncodingError.invalidValue(
121+
value,
122+
EncodingError.Context(
123+
codingPath: [],
124+
debugDescription: "Value was not Data"))
125+
}
126+
}
127+
}

Sources/CacheAdvance/CacheAdvance.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,29 @@ public final class CacheAdvance<T: Codable> {
2929
/// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist.
3030
/// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store.
3131
/// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry.
32+
/// - decoder: The decoder that will be used to decode already-persisted messages.
33+
/// - encoder: The encoder that will be used to encode messages prior to persistence.
3234
///
3335
/// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
3436
/// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache.
37+
/// - Warning: `decoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache.
38+
/// - Warning: `encoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache.
3539
public init(
3640
fileURL: URL,
3741
maximumBytes: Bytes,
38-
shouldOverwriteOldMessages: Bool)
42+
shouldOverwriteOldMessages: Bool,
43+
decoder: MessageDecoder = JSONDecoder(),
44+
encoder: MessageEncoder = JSONEncoder())
3945
throws
4046
{
4147
self.fileURL = fileURL
4248

4349
writer = try FileHandle(forWritingTo: fileURL)
4450
reader = try CacheReader(forReadingFrom: fileURL)
4551
header = try CacheHeaderHandle(forReadingFrom: fileURL, maximumBytes: maximumBytes, overwritesOldMessages: shouldOverwriteOldMessages)
52+
53+
self.decoder = decoder
54+
self.encoder = encoder
4655
}
4756

4857
deinit {
@@ -219,6 +228,6 @@ public final class CacheAdvance<T: Codable> {
219228

220229
private var hasSetUpFileHandles = false
221230

222-
private let decoder = JSONDecoder()
223-
private let encoder = JSONEncoder()
231+
private let decoder: MessageDecoder
232+
private let encoder: MessageEncoder
224233
}

Sources/CacheAdvance/EncodableMessage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ struct EncodableMessage<T: Codable> {
3030
/// - Parameters:
3131
/// - message: The messages to encode.
3232
/// - encoder: The encoder to use.
33-
init(message: T, encoder: JSONEncoder) {
33+
init(message: T, encoder: MessageEncoder) {
3434
self.message = message
3535
self.encoder = encoder
3636
}
@@ -51,6 +51,6 @@ struct EncodableMessage<T: Codable> {
5151
// MARK: Private
5252

5353
private let message: T
54-
private let encoder: JSONEncoder
54+
private let encoder: MessageEncoder
5555

5656
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Created by Dan Federman on 4/26/20.
3+
// Copyright © 2020 Dan Federman.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
//    http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
18+
import Foundation
19+
20+
/// An object capable of decoding a message of type `T` from `Data`.
21+
public protocol MessageDecoder {
22+
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
23+
}
24+
25+
extension JSONDecoder: MessageDecoder {}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Created by Dan Federman on 4/26/20.
3+
// Copyright © 2020 Dan Federman.
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
//    http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
//
17+
18+
import Foundation
19+
20+
/// An object capable of encoding a message of type `T` to `Data`.
21+
public protocol MessageEncoder {
22+
func encode<T>(_ value: T) throws -> Data where T : Encodable
23+
}
24+
25+
extension JSONEncoder: MessageEncoder {}

0 commit comments

Comments
 (0)