Skip to content

Commit

Permalink
Provide more control over encoders and decoders used (#21)
Browse files Browse the repository at this point in the history
# 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).
  • Loading branch information
Supereg authored Jul 29, 2024
1 parent b958df9 commit 9d04bc6
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 55 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
runsonlabels: '["macOS", "self-hosted"]'
path: 'Tests/UITests'
scheme: TestApp
destination: 'platform=iOS Simulator,name=iPad Air (5th generation)'
destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)'
buildConfig: ${{ matrix.buildConfig }}
resultBundle: ${{ matrix.resultBundle }}
artifactname: ${{ matrix.artifactname }}
Expand Down Expand Up @@ -136,4 +136,6 @@ jobs:
needs: [buildandtest_ios, buildandtest_visionos, buildandtest_macos, buildandtestuitests_ios, buildandtestuitests_ipad, buildandtestuitests_visionos]
uses: StanfordSpezi/.github/.github/workflows/create-and-upload-coverage-report.yml@v2
with:
coveragereports: 'SpeziStorage-Package-iOS.xcresult SpeziStorage-Package-visionOS.xcresult SpeziStorage-Package-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
coveragereports: 'SpeziStorage-Package-iOS.xcresult SpeziStorage-Package-visionOS.xcresult SpeziStorage-Package-macOS.xcresult TestApp-iOS.xcresult TestApp-iPad.xcresult TestApp-visionOS.xcresult'
secrets:
token: ${{ secrets.CODECOV_TOKEN }}
49 changes: 44 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
// SPDX-License-Identifier: MIT
//

import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


let package = Package(
name: "SpeziStorage",
platforms: [
Expand All @@ -25,27 +33,58 @@ let package = Package(
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", from: "1.2.1"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.0.1")
],
] + swiftLintPackage(),
targets: [
.target(
name: "SpeziLocalStorage",
dependencies: [
.product(name: "Spezi", package: "Spezi"),
.target(name: "SpeziSecureStorage")
]
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziLocalStorageTests",
dependencies: [
.target(name: "SpeziLocalStorage")
]
.target(name: "SpeziLocalStorage"),
.product(name: "XCTSpezi", package: "Spezi")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "SpeziSecureStorage",
dependencies: [
.product(name: "Spezi", package: "Spezi"),
.product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions")
]
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
)
]
)


func swiftLintPlugin() -> [Target.PluginUsage] {
// Fully quit Xcode and open again with `open --env SPEZI_DEVELOPMENT_SWIFTLINT /Applications/Xcode.app`
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLint")]
} else {
[]
}
}

func swiftLintPackage() -> [PackageDescription.Package.Dependency] {
if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil {
[.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))]
} else {
[]
}
}
21 changes: 13 additions & 8 deletions Sources/SpeziLocalStorage/LocalStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// SPDX-License-Identifier: MIT
//

import Combine
import Foundation
import Security
import Spezi
Expand Down Expand Up @@ -64,13 +65,15 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
///
/// - Parameters:
/// - element: The element that should be stored conforming to `Encodable`
/// - encoder: The `Encoder` to use for encoding the `element`.
/// - storageKey: An optional storage key to identify the file.
/// - settings: The ``LocalStorageSetting``s applied to the file on disk.
public func store<C: Encodable>(
public func store<C: Encodable, D: TopLevelEncoder>(
_ element: C,
encoder: D = JSONEncoder(),
storageKey: String? = nil,
settings: LocalStorageSetting = .encryptedUsingKeyChain()
) throws {
) throws where D.Output == Data {
var fileURL = fileURL(from: storageKey, type: C.self)
let fileExistsAlready = FileManager.default.fileExists(atPath: fileURL.path)

Expand All @@ -91,9 +94,9 @@ public final class LocalStorage: Module, DefaultInitializable, EnvironmentAccess
throw LocalStorageError.couldNotExcludedFromBackup
}
}
let data = try JSONEncoder().encode(element)

let data = try encoder.encode(element)


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

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

guard SecKeyIsAlgorithmSupported(keys.privateKey, .decrypt, encryptionAlgorithm) else {
Expand Down
3 changes: 3 additions & 0 deletions Sources/SpeziSecureStorage/Credentials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ public struct Credentials: Equatable, Identifiable {
self.password = password
}
}


extension Credentials: Sendable {}
3 changes: 3 additions & 0 deletions Sources/SpeziSecureStorage/SecureStorageItemTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ public struct SecureStorageItemTypes: OptionSet {
self.rawValue = rawValue
}
}


extension SecureStorageItemTypes: Sendable {}
3 changes: 3 additions & 0 deletions Sources/SpeziSecureStorage/SecureStorageScope.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ public enum SecureStorageScope: Equatable, Identifiable {
}
}
}


extension SecureStorageScope: Sendable {}
25 changes: 7 additions & 18 deletions Tests/SpeziLocalStorageTests/LocalStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,22 @@
// SPDX-License-Identifier: MIT
//

@testable import Spezi
@testable import SpeziLocalStorage
import XCTest
import XCTSpezi


final class LocalStorageTests: XCTestCase {
struct Letter: Codable, Equatable {
let greeting: String
}

class LocalStorageTestsAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
LocalStorage()
}
}
}



@MainActor
func testLocalStorage() async throws {
#if !os(macOS)
let spezi = await LocalStorageTestsAppDelegate().spezi
#else
let spezi = LocalStorageTestsAppDelegate().spezi
#endif

let localStorage = try XCTUnwrap(spezi.storage[LocalStorage.self])
let localStorage = LocalStorage()
withDependencyResolution {
localStorage
}

let letter = Letter(greeting: "Hello Paul 👋\(String(repeating: "🚀", count: Int.random(in: 0...10)))")
try localStorage.store(letter, settings: .unencrypted())
Expand Down
1 change: 1 addition & 0 deletions Tests/UITests/TestAppUITests/LocalStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import XCTest


final class LocalStorageTests: XCTestCase {
@MainActor
func testLocalStorage() throws {
let app = XCUIApplication()
app.launch()
Expand Down
1 change: 1 addition & 0 deletions Tests/UITests/TestAppUITests/SecureStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import XCTest


final class SecureStorageTests: XCTestCase {
@MainActor
func testSecureStorage() throws {
let app = XCUIApplication()
app.launch()
Expand Down
22 changes: 0 additions & 22 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@
2F6D138E28F5F384007C25D6 /* Sources */,
2F6D138F28F5F384007C25D6 /* Frameworks */,
2F6D139028F5F384007C25D6 /* Resources */,
2F7CC6072A79D80300F42D90 /* ShellScript */,
);
buildRules = (
);
Expand Down Expand Up @@ -246,27 +245,6 @@
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
2F7CC6072A79D80300F42D90 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
2F6D138E28F5F384007C25D6 /* Sources */ = {
isa = PBXSourcesBuildPhase;
Expand Down

0 comments on commit 9d04bc6

Please sign in to comment.