Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize encoders and decoders for UserStorage Properties #50

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/SpeziScheduler/SpeziScheduler.docc/SpeziScheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ class MySchedulerModule: Module {
- ``Task/Category-swift.struct``
- ``Event``
- ``Outcome``
- ``Property()``
- ``Property(coding:)``
- ``AllowedCompletionPolicy``

### Notifications

- ``SchedulerNotifications``
- ``SchedulerNotificationsConstraint``
- ``NotificationTime``
- ``NotificationThread``

### Date Extensions

Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziScheduler/Task/Outcome.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import SwiftData
///
/// An outcome supports storing additional metadata information (e.g., the measurement value or medication).
///
/// - Tip: Refer to the ``Property()`` macro on how to create new data types that can be stored alongside an outcome.
/// - Tip: Refer to the ``Property(coding:)`` macro on how to create new data types that can be stored alongside an outcome.
///
/// You provide the additional outcome values upon completion of an event (see ``Event/complete(with:)``.
/// Below is a short code example that sets a custom `measurement` property to the weight measurement that was received
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziScheduler/Task/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import SwiftData
///
/// Tasks support storing additional metadata information.
///
/// - Tip: Refer to the ``Property()`` macro on how to create new data types that can be stored alongside a task.
/// - Tip: Refer to the ``Property(coding:)`` macro on how to create new data types that can be stored alongside a task.
///
/// You can set additional information by supplying an additional closure that modifies the ``Context`` when creating or updating a task.
/// The code example below assume that the `measurementType` exists to store the type of measurement the user should record to complete the task.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// SPDX-License-Identifier: MIT
//

import SpeziFoundation


/// Add additional properties to an `Outcome` or `Task`.
///
Expand Down Expand Up @@ -34,6 +36,14 @@
/// @Property var measurement: WeightMeasurement?
/// }
/// ```
///
///
/// ## Topics
///
/// ### Customize Encoding and Decoding
/// - ``UserStorageCoding``
@attached(accessor, names: named(get), named(set))
@attached(peer, names: prefixed(__Key_))
public macro Property() = #externalMacro(module: "SpeziSchedulerMacros", type: "UserStorageEntryMacro")
public macro Property<Encoder: TopLevelEncoder, Decoder: TopLevelDecoder>(
coding: UserStorageCoding<Encoder, Decoder> = .propertyList
) = #externalMacro(module: "SpeziSchedulerMacros", type: "UserStorageEntryMacro")
6 changes: 6 additions & 0 deletions Sources/SpeziScheduler/UserInfo/UserInfoKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
// SPDX-License-Identifier: MIT
//

import Foundation
import SpeziFoundation


/// A typed-key for entries in the user info store of scheduler components.
///
/// Refer to the documentation of ``TaskStorageKey`` and ``OutcomeStorageKey`` on how to create userInfo entries.
public protocol _UserInfoKey<Anchor>: KnowledgeSource where Value: Codable { // swiftlint:disable:this type_name
associatedtype Encoder: TopLevelEncoder, Sendable where Encoder.Output == Data
associatedtype Decoder: TopLevelDecoder, Sendable where Decoder.Input == Data

/// The persistent identifier of the user info key.
static var identifier: String { get }
/// The encoder and decoder used with the user storage.
static var coding: UserStorageCoding<Encoder, Decoder> { get }
}


Expand Down
5 changes: 3 additions & 2 deletions Sources/SpeziScheduler/UserInfo/UserInfoStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension UserInfoStorage {
}

do {
let decoder = PropertyListDecoder()
let decoder = source.coding.decoder
let value = try decoder.decode(SingleValueWrapper<Source.Value>.self, from: data)

cache.repository.set(source, value: value.value)
Expand All @@ -69,7 +69,8 @@ extension UserInfoStorage {

if let newValue {
do {
userInfo[source.identifier] = try PropertyListEncoder().encode(SingleValueWrapper(value: newValue))
let encoder = source.coding.encoder
userInfo[source.identifier] = try encoder.encode(SingleValueWrapper(value: newValue))
} catch {
logger.error("Failed to encode userInfo value \(String(describing: newValue)) for \(source): \(error)")
}
Expand Down
64 changes: 64 additions & 0 deletions Sources/SpeziScheduler/UserInfo/UserStorageCoding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import SpeziFoundation


/// Defining the coding strategy for a user storage property.
///
/// This type defines the encoders and decoders for a user storage ``Property(coding:)``.
///
/// Below is a code example that specifies ``json`` encoding for the `measurementType` property.
///
/// ```swift
/// extension Task.Context {
/// @Property(coding: .json)
/// var measurementType: MeasurementType?
/// }
/// ```
///
/// ## Topics
///
/// ### Builtin Strategies
/// - ``json``
/// - ``propertyList``
///
/// ### Custom Strategies
/// - ``custom(encoder:decoder:)``
public struct UserStorageCoding<Encoder: TopLevelEncoder & Sendable, Decoder: TopLevelDecoder & Sendable>: Sendable
where Encoder.Output == Data, Decoder.Input == Data {
let encoder: Encoder
let decoder: Decoder

init(encoder: Encoder, decoder: Decoder) {
self.encoder = encoder
self.decoder = decoder
}

/// Create a custom user storage coding strategy.
/// - Parameters:
/// - encoder: The encoder.
/// - decoder: The decoder.
/// - Returns: The coding strategy.
public static func custom(encoder: Encoder, decoder: Decoder) -> UserStorageCoding<Encoder, Decoder> {
.init(encoder: encoder, decoder: decoder)
}

Check warning on line 51 in Sources/SpeziScheduler/UserInfo/UserStorageCoding.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziScheduler/UserInfo/UserStorageCoding.swift#L49-L51

Added lines #L49 - L51 were not covered by tests
}


extension UserStorageCoding where Encoder == JSONEncoder, Decoder == JSONDecoder {
/// JSON encoder and decoder.
public static let json = UserStorageCoding(encoder: JSONEncoder(), decoder: JSONDecoder())
}


extension UserStorageCoding where Encoder == PropertyListEncoder, Decoder == PropertyListDecoder {
/// PropertyList encoder and decoder.
public static let propertyList = UserStorageCoding(encoder: PropertyListEncoder(), decoder: PropertyListDecoder())
}
19 changes: 18 additions & 1 deletion Sources/SpeziSchedulerMacros/UserStorageEntryMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ extension UserStorageEntryMacro: AccessorMacro {


extension UserStorageEntryMacro: PeerMacro {
public static func expansion( // swiftlint:disable:this function_body_length
public static func expansion( // swiftlint:disable:this function_body_length cyclomatic_complexity
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
Expand Down Expand Up @@ -96,6 +96,22 @@ extension UserStorageEntryMacro: PeerMacro {
)
}

let codingExpression: ExprSyntax
if case let .argumentList(argumentList) = node.arguments,
let coding = argumentList.first(where: { $0.label?.text == "coding" }) {
if var memberAccess = coding.expression.as(MemberAccessExprSyntax.self) {
memberAccess.base = ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("UserStorageCoding")))
codingExpression = ExprSyntax(memberAccess)
} else {
codingExpression = coding.expression
}
} else {
codingExpression = ExprSyntax(MemberAccessExprSyntax(
base: ExprSyntax(DeclReferenceExprSyntax(baseName: .identifier("UserStorageCoding"))),
declName: DeclReferenceExprSyntax(baseName: .identifier("propertyList"))
))
}

let keyProtocol: TokenSyntax

if let identifier = extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.name.identifier,
Expand Down Expand Up @@ -147,6 +163,7 @@ extension UserStorageEntryMacro: PeerMacro {

"""
static let identifier: String = "\(identifier)"
static let coding = \(codingExpression)
"""
}

Expand Down
96 changes: 96 additions & 0 deletions Tests/SpeziSchedulerMacrosTest/UserStorageEntryMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.propertyList
}
}
""",
Expand Down Expand Up @@ -72,6 +73,7 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.propertyList
}
}
""",
Expand Down Expand Up @@ -102,6 +104,7 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.propertyList
}
}
""",
Expand Down Expand Up @@ -132,6 +135,7 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.propertyList
}
}
""",
Expand Down Expand Up @@ -162,6 +166,7 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.propertyList
}
}
""",
Expand Down Expand Up @@ -297,6 +302,97 @@ final class UserStorageEntryMacroTests: XCTestCase { // swiftlint:disable:this t
macros: testMacros
)
}

func testJsonCoding() { // swiftlint:disable:this function_body_length
assertMacroExpansion(
"""
extension Task.Context {
@Property(coding: .json) var testMacro: String?
}
""",
expandedSource:
"""
extension Task.Context {
var testMacro: String? {
get {
self[__Key_testMacro.self]
}
set {
self[__Key_testMacro.self] = newValue
}
}

private struct __Key_testMacro: TaskStorageKey {
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.json
}
}
""",
macros: testMacros
)

assertMacroExpansion(
"""
extension Task.Context {
@Property(coding: UserInfoCoding.json) var testMacro: String?
}
""",
expandedSource:
"""
extension Task.Context {
var testMacro: String? {
get {
self[__Key_testMacro.self]
}
set {
self[__Key_testMacro.self] = newValue
}
}

private struct __Key_testMacro: TaskStorageKey {
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserStorageCoding.json
}
}
""",
macros: testMacros
)
}

func testCustomCoding() {
assertMacroExpansion(
"""
extension Task.Context {
@Property(coding: UserInfoCoding(encoder: TestEncoder(), decoder: TestDecoder())) var testMacro: String?
}
""",
expandedSource:
"""
extension Task.Context {
var testMacro: String? {
get {
self[__Key_testMacro.self]
}
set {
self[__Key_testMacro.self] = newValue
}
}

private struct __Key_testMacro: TaskStorageKey {
typealias Value = String

static let identifier: String = "testMacro"
static let coding = UserInfoCoding(encoder: TestEncoder(), decoder: TestDecoder())
}
}
""",
macros: testMacros
)
}
}

#endif
Loading