Skip to content

Commit 9d04bc6

Browse files
authored
Provide more control over encoders and decoders used (#21)
# Provide more control over encoders and decoders used ## ♻️ Current situation & Problem Currently, the `LocalStorage` module automatically uses `JSONEncoder` and `JSONDecoder` instances that created and managed internally. This provides no flexibility to a) configure the encoders and decoders used (e.g., passing custom user data used while decoding) and b) doesn't allow different storage formats which might be more fitting for some scenarios. This PR adds a new optional parameter to both `store` and `load` calls that allows to pass in encoders or decoders instances from the outside. It also allows to use different encoders like for example the `PropertyListEncoder` and `PropertyListDecoder`. Lastly, this PR makes the package compatible with Swift 6. ## ⚙️ Release Notes * Control encoders and decoders with the LocalStorage module. * Swift 6 compatibility. ## 📚 Documentation New parameters were documented. ## ✅ Testing -- ## 📝 Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
1 parent b958df9 commit 9d04bc6

File tree

10 files changed

+79
-55
lines changed

10 files changed

+79
-55
lines changed

.github/workflows/build-and-test.yml

+4-2
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ jobs:
107107
runsonlabels: '["macOS", "self-hosted"]'
108108
path: 'Tests/UITests'
109109
scheme: TestApp
110-
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
110+
destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)'
111111
buildConfig: ${{ matrix.buildConfig }}
112112
resultBundle: ${{ matrix.resultBundle }}
113113
artifactname: ${{ matrix.artifactname }}
@@ -136,4 +136,6 @@ jobs:
136136
needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos]
137137
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
138138
with:
139-
coveragereports: 'SpeziStorage-Package-iOS.xcresult SpeziStorage-Package-visionOS.xcresult SpeziStorage-Package-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
139+
coveragereports: 'SpeziStorage-Package-iOS.xcresult SpeziStorage-Package-visionOS.xcresult SpeziStorage-Package-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
140+
secrets:
141+
token: ${{ secrets.CODECOV_TOKEN }}

Package.swift

+44-5
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,17 @@
88
// SPDX-License-Identifier: MIT
99
//
1010

11+
import class Foundation.ProcessInfo
1112
import PackageDescription
1213

1314

15+
#if swift(<6)
16+
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
17+
#else
18+
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
19+
#endif
20+
21+
1422
let package = Package(
1523
name: "SpeziStorage",
1624
platforms: [
@@ -25,27 +33,58 @@ let package = Package(
2533
dependencies: [
2634
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
2735
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.1")
28-
],
36+
] + swiftLintPackage(),
2937
targets: [
3038
.target(
3139
name: "SpeziLocalStorage",
3240
dependencies: [
3341
.product(name: "Spezi", package: "Spezi"),
3442
.target(name: "SpeziSecureStorage")
35-
]
43+
],
44+
swiftSettings: [
45+
swiftConcurrency
46+
],
47+
plugins: [] + swiftLintPlugin()
3648
),
3749
.testTarget(
3850
name: "SpeziLocalStorageTests",
3951
dependencies: [
40-
.target(name: "SpeziLocalStorage")
41-
]
52+
.target(name: "SpeziLocalStorage"),
53+
.product(name: "XCTSpezi", package: "Spezi")
54+
],
55+
swiftSettings: [
56+
swiftConcurrency
57+
],
58+
plugins: [] + swiftLintPlugin()
4259
),
4360
.target(
4461
name: "SpeziSecureStorage",
4562
dependencies: [
4663
.product(name: "Spezi", package: "Spezi"),
4764
.product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions")
48-
]
65+
],
66+
swiftSettings: [
67+
swiftConcurrency
68+
],
69+
plugins: [] + swiftLintPlugin()
4970
)
5071
]
5172
)
73+
74+
75+
func swiftLintPlugin() -> [Target.PluginUsage] {
76+
// Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app`
77+
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
78+
[.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]
79+
} else {
80+
[]
81+
}
82+
}
83+
84+
func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
85+
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
86+
[.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))]
87+
} else {
88+
[]
89+
}
90+
}

Sources/SpeziLocalStorage/LocalStorage.swift

+13-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// SPDX-License-Identifier: MIT
77
//
88

9+
import Combine
910
import Foundation
1011
import Security
1112
import Spezi
@@ -64,13 +65,15 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
6465
///
6566
/// - Parameters:
6667
/// - element: The element that should be stored conforming to `Encodable`
68+
/// - encoder: The `Encoder` to use for encoding the `element`.
6769
/// - storageKey: An optional storage key to identify the file.
6870
/// - settings: The ``LocalStorageSetting``s applied to the file on disk.
69-
public func store<C: Encodable>(
71+
public func store<C: Encodable, D: TopLevelEncoder>(
7072
_ element: C,
73+
encoder: D = JSONEncoder(),
7174
storageKey: String? = nil,
7275
settings: LocalStorageSetting = .encryptedUsingKeyChain()
73-
) throws {
76+
) throws where D.Output == Data {
7477
var fileURL = fileURL(from: storageKey, type: C.self)
7578
let fileExistsAlready = FileManager.default.fileExists(atPath: fileURL.path)
7679

@@ -91,9 +94,9 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
9194
throw LocalStorageError.couldNotExcludedFromBackup
9295
}
9396
}
94-
95-
let data = try JSONEncoder().encode(element)
96-
97+
98+
let data = try encoder.encode(element)
99+
97100

98101
// Determine if the data should be encrypted or not:
99102
guard let keys = try settings.keys(from: secureStorage) else {
@@ -131,20 +134,22 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
131134
///
132135
/// - Parameters:
133136
/// - type: The `Decodable` type that is used to decode the data from disk.
137+
/// - decoder: The `Decoder` to use to decode the stored data into the provided `type`.
134138
/// - storageKey: An optional storage key to identify the file.
135139
/// - settings: The ``LocalStorageSetting``s used to retrieve the file on disk.
136140
/// - Returns: The element conforming to `Decodable`.
137-
public func read<C: Decodable>(
141+
public func read<C: Decodable, D: TopLevelDecoder>(
138142
_ type: C.Type = C.self,
143+
decoder: D = JSONDecoder(),
139144
storageKey: String? = nil,
140145
settings: LocalStorageSetting = .encryptedUsingKeyChain()
141-
) throws -> C {
146+
) throws -> C where D.Input == Data {
142147
let fileURL = fileURL(from: storageKey, type: C.self)
143148
let data = try Data(contentsOf: fileURL)
144149

145150
// Determine if the data should be decrypted or not:
146151
guard let keys = try settings.keys(from: secureStorage) else {
147-
return try JSONDecoder().decode(C.self, from: data)
152+
return try decoder.decode(C.self, from: data)
148153
}
149154

150155
guard SecKeyIsAlgorithmSupported(keys.privateKey, .decrypt, encryptionAlgorithm) else {

Sources/SpeziSecureStorage/Credentials.swift

+3
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ public struct Credentials: Equatable, Identifiable {
3030
self.password = password
3131
}
3232
}
33+
34+
35+
extension Credentials: Sendable {}

Sources/SpeziSecureStorage/SecureStorageItemTypes.swift

+3
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ public struct SecureStorageItemTypes: OptionSet {
4646
self.rawValue = rawValue
4747
}
4848
}
49+
50+
51+
extension SecureStorageItemTypes: Sendable {}

Sources/SpeziSecureStorage/SecureStorageScope.swift

+3
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ public enum SecureStorageScope: Equatable, Identifiable {
102102
}
103103
}
104104
}
105+
106+
107+
extension SecureStorageScope: Sendable {}

Tests/SpeziLocalStorageTests/LocalStorageTests.swift

+7-18
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,22 @@
66
// SPDX-License-Identifier: MIT
77
//
88

9-
@testable import Spezi
109
@testable import SpeziLocalStorage
1110
import XCTest
11+
import XCTSpezi
1212

1313

1414
final class LocalStorageTests: XCTestCase {
1515
struct Letter: Codable, Equatable {
1616
let greeting: String
1717
}
18-
19-
class LocalStorageTestsAppDelegate: SpeziAppDelegate {
20-
override var configuration: Configuration {
21-
Configuration {
22-
LocalStorage()
23-
}
24-
}
25-
}
26-
27-
18+
19+
@MainActor
2820
func testLocalStorage() async throws {
29-
#if !os(macOS)
30-
let spezi = await LocalStorageTestsAppDelegate().spezi
31-
#else
32-
let spezi = LocalStorageTestsAppDelegate().spezi
33-
#endif
34-
35-
let localStorage = try XCTUnwrap(spezi.storage[LocalStorage.self])
21+
let localStorage = LocalStorage()
22+
withDependencyResolution {
23+
localStorage
24+
}
3625

3726
let letter = Letter(greeting: "Hello Paul 👋\(String(repeating: "🚀", count: Int.random(in: 0...10)))")
3827
try localStorage.store(letter, settings: .unencrypted())

Tests/UITests/TestAppUITests/LocalStorageTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import XCTest
1010

1111

1212
final class LocalStorageTests: XCTestCase {
13+
@MainActor
1314
func testLocalStorage() throws {
1415
let app = XCUIApplication()
1516
app.launch()

Tests/UITests/TestAppUITests/SecureStorageTests.swift

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import XCTest
1010

1111

1212
final class SecureStorageTests: XCTestCase {
13+
@MainActor
1314
func testSecureStorage() throws {
1415
let app = XCUIApplication()
1516
app.launch()

Tests/UITests/UITests.xcodeproj/project.pbxproj

-22
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,6 @@
150150
2F6D138E28F5F384007C25D6 /* Sources */,
151151
2F6D138F28F5F384007C25D6 /* Frameworks */,
152152
2F6D139028F5F384007C25D6 /* Resources */,
153-
2F7CC6072A79D80300F42D90 /* ShellScript */,
154153
);
155154
buildRules = (
156155
);
@@ -246,27 +245,6 @@
246245
};
247246
/* End PBXResourcesBuildPhase section */
248247

249-
/* Begin PBXShellScriptBuildPhase section */
250-
2F7CC6072A79D80300F42D90 /* ShellScript */ = {
251-
isa = PBXShellScriptBuildPhase;
252-
alwaysOutOfDate = 1;
253-
buildActionMask = 2147483647;
254-
files = (
255-
);
256-
inputFileListPaths = (
257-
);
258-
inputPaths = (
259-
);
260-
outputFileListPaths = (
261-
);
262-
outputPaths = (
263-
);
264-
runOnlyForDeploymentPostprocessing = 0;
265-
shellPath = /bin/sh;
266-
shellScript = "if [ \"${CONFIGURATION}\" = \"Debug\" ]; then\n export PATH=\"$PATH:/opt/homebrew/bin\"\n if which swiftlint > /dev/null; then\n cd ../../ && swiftlint\n else\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\n fi\nfi\n";
267-
};
268-
/* End PBXShellScriptBuildPhase section */
269-
270248
/* Begin PBXSourcesBuildPhase section */
271249
2F6D138E28F5F384007C25D6 /* Sources */ = {
272250
isa = PBXSourcesBuildPhase;

0 commit comments

Comments
 (0)