diff --git a/Sources/Account/Resources/en.lproj/Localizable.strings b/Sources/Account/Resources/en.lproj/Localizable.strings index a29d707d..4f595404 100644 --- a/Sources/Account/Resources/en.lproj/Localizable.strings +++ b/Sources/Account/Resources/en.lproj/Localizable.strings @@ -32,7 +32,7 @@ "UAP_SIGNUP_PASSWORD_REPEAT_TITLE" = "Repeat Password"; "UAP_SIGNUP_PASSWORD_REPEAT_PLACEHOLDER" = "Repeat your password ..."; -"UAP_SIGNUP_PASSWORD_NOT_EQUAL_ERROR" = "The entered passwords are not equal"; +"UAP_SIGNUP_PASSWORD_NOT_EQUAL_ERROR" = "The entered passwords are not equal."; "UAP_SIGNUP_GIVEN_NAME_TITLE" = "Given Name"; "UAP_SIGNUP_GIVEN_NAME_PLACEHOLDER" = "Enter your given name ..."; "UAP_SIGNUP_FAMILY_NAME_TITLE" = "Family Name"; diff --git a/Sources/CardinalKit/Adapter/Adapter.swift b/Sources/CardinalKit/Adapter/Adapter.swift new file mode 100644 index 00000000..22e71c3e --- /dev/null +++ b/Sources/CardinalKit/Adapter/Adapter.swift @@ -0,0 +1,34 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// A ``Adapter`` can be used to transfrom an input `DataChange` (`InputElement` and `InputRemovalContext`) +/// to an output `DataChange` (`OutputElement` and `OutputRemovalContext`). +/// +/// Use the ``AdapterBuilder`` to offer developers to option to pass in a `Adapter` instance to your components. +public protocol Adapter: Actor { + /// The input element of the ``Adapter`` + associatedtype InputElement: Identifiable, Sendable where InputElement.ID: Sendable + /// The input removal context of the ``Adapter`` + associatedtype InputRemovalContext: Identifiable, Sendable where InputElement.ID == InputRemovalContext.ID + /// The output element of the ``Adapter`` + associatedtype OutputElement: Identifiable, Sendable where OutputElement.ID: Sendable + /// The output removal context of the ``Adapter`` + associatedtype OutputRemovalContext: Identifiable, Sendable where OutputElement.ID == OutputRemovalContext.ID + + + /// Transforms any `TypedAsyncSequence>` to an `TypedAsyncSequence` with + /// the `TypedAsyncSequence>` generic constraint fulfilling the transformation, + /// + /// Implement this method in an instance of a `Adapter`. + /// - Parameter asyncSequence: The input `TypedAsyncSequence`. + /// - Returns: The transformed `TypedAsyncSequence`. + func transform( + _ asyncSequence: some TypedAsyncSequence> + ) async -> any TypedAsyncSequence> +} diff --git a/Sources/CardinalKit/Adapter/AdapterBuilder.swift b/Sources/CardinalKit/Adapter/AdapterBuilder.swift new file mode 100644 index 00000000..a9f826ab --- /dev/null +++ b/Sources/CardinalKit/Adapter/AdapterBuilder.swift @@ -0,0 +1,134 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +// swiftlint:disable generic_type_name line_length + +private actor TwoAdapterChain< + InputElement: Identifiable & Sendable, + InputRemovalContext: Identifiable & Sendable, + IntermediateElement: Identifiable & Sendable, + IntermediateRemovalContext: Identifiable & Sendable, + OutputElement: Identifiable & Sendable, + OutputRemovalContext: Identifiable & Sendable + >: Actor, Adapter + where InputElement.ID: Sendable, InputElement.ID == InputRemovalContext.ID, + IntermediateElement.ID: Sendable, IntermediateElement.ID == IntermediateRemovalContext.ID, + OutputElement.ID: Sendable, OutputElement.ID == OutputRemovalContext.ID { + let firstDataSourceRegistryAdapter: any Adapter + let secondDataSourceRegistryAdapter: any Adapter + + + init( + firstDataSourceRegistryAdapter: any Adapter, + secondDataSourceRegistryAdapter: any Adapter + ) { + self.firstDataSourceRegistryAdapter = firstDataSourceRegistryAdapter + self.secondDataSourceRegistryAdapter = secondDataSourceRegistryAdapter + } + + + func transform( + _ asyncSequence: some TypedAsyncSequence> + ) async -> any TypedAsyncSequence> { + let firstDataSourceRegistryTransformation = await firstDataSourceRegistryAdapter.transform(asyncSequence) + return await secondDataSourceRegistryAdapter.transform(firstDataSourceRegistryTransformation) + } +} + +private actor ThreeAdapterChain< + InputElement: Identifiable & Sendable, + InputRemovalContext: Identifiable & Sendable, + IntermediateElementOne: Identifiable & Sendable, + IntermediateRemovalContextOne: Identifiable & Sendable, + IntermediateElementTwo: Identifiable & Sendable, + IntermediateRemovalContextTwo: Identifiable & Sendable, + OutputElement: Identifiable & Sendable, + OutputRemovalContext: Identifiable & Sendable + >: Actor, Adapter + where InputElement.ID: Sendable, InputElement.ID == InputRemovalContext.ID, + IntermediateElementOne.ID: Sendable, IntermediateElementOne.ID == IntermediateRemovalContextOne.ID, + IntermediateElementTwo.ID: Sendable, IntermediateElementTwo.ID == IntermediateRemovalContextTwo.ID, + OutputElement.ID: Sendable, OutputElement.ID == OutputRemovalContext.ID { + let firstDataSourceRegistryAdapter: any Adapter + let secondDataSourceRegistryAdapter: any Adapter + let thirdDataSourceRegistryAdapter: any Adapter + + + init( + firstDataSourceRegistryAdapter: any Adapter, + secondDataSourceRegistryAdapter: any Adapter, + thirdDataSourceRegistryAdapter: any Adapter + ) { + self.firstDataSourceRegistryAdapter = firstDataSourceRegistryAdapter + self.secondDataSourceRegistryAdapter = secondDataSourceRegistryAdapter + self.thirdDataSourceRegistryAdapter = thirdDataSourceRegistryAdapter + } + + + func transform( + _ asyncSequence: some TypedAsyncSequence> + ) async -> any TypedAsyncSequence> { + let firstDataSourceRegistryTransformation = await firstDataSourceRegistryAdapter.transform(asyncSequence) + let secondDataSourceRegistryTransformation = await secondDataSourceRegistryAdapter.transform(firstDataSourceRegistryTransformation) + return await thirdDataSourceRegistryAdapter.transform(secondDataSourceRegistryTransformation) + } +} + + +/// A function builder used to generate data source registry adapter chains. +@resultBuilder +public enum AdapterBuilder where OutputElement.ID: Sendable, OutputElement.ID == OutputRemovalContext.ID { + /// Required by every result builder to build combined results from statement blocks. + public static func buildBlock< + InputElement: Identifiable & Sendable, InputRemovalContext: Identifiable & Sendable + > ( + _ dataSourceRegistryAdapter: any Adapter + ) -> any Adapter + where InputElement.ID: Sendable, InputElement.ID == InputRemovalContext.ID { + dataSourceRegistryAdapter + } + + /// Required by every result builder to build combined results from statement blocks. + public static func buildBlock< + InputElement: Identifiable & Sendable, InputRemovalContext: Identifiable & Sendable, + IntermediateElement: Identifiable & Sendable, IntermediateRemovalContext: Identifiable & Sendable + > ( + _ firstDataSourceRegistryAdapter: any Adapter, + _ secondDataSourceRegistryAdapter: any Adapter + ) -> any Adapter + where InputElement.ID: Sendable, InputElement.ID == InputRemovalContext.ID, + IntermediateElement.ID: Sendable, IntermediateElement.ID == IntermediateRemovalContext.ID { + TwoAdapterChain( + firstDataSourceRegistryAdapter: firstDataSourceRegistryAdapter, + secondDataSourceRegistryAdapter: secondDataSourceRegistryAdapter + ) + } + + /// Required by every result builder to build combined results from statement blocks. + public static func buildBlock< + InputElement: Identifiable & Sendable, InputRemovalContext: Identifiable & Sendable, + IntermediateElement1: Identifiable & Sendable, IntermediateRemovalContext1: Identifiable & Sendable, + IntermediateElement2: Identifiable & Sendable, IntermediateRemovalContext2: Identifiable & Sendable + > ( + _ firstDataSourceRegistryAdapter: any Adapter, + _ secondDataSourceRegistryAdapter: any Adapter, + _ thirdDataSourceRegistryAdapter: any Adapter + ) -> any Adapter + where InputElement.ID: Sendable, InputElement.ID == InputRemovalContext.ID, + IntermediateElement1.ID: Sendable, IntermediateElement1.ID == IntermediateRemovalContext1.ID, + IntermediateElement2.ID: Sendable, IntermediateElement2.ID == IntermediateRemovalContext2.ID { + ThreeAdapterChain( + firstDataSourceRegistryAdapter: firstDataSourceRegistryAdapter, + secondDataSourceRegistryAdapter: secondDataSourceRegistryAdapter, + thirdDataSourceRegistryAdapter: thirdDataSourceRegistryAdapter + ) + } +} diff --git a/Sources/CardinalKit/DataSource/DataChange.swift b/Sources/CardinalKit/Adapter/DataChange.swift similarity index 65% rename from Sources/CardinalKit/DataSource/DataChange.swift rename to Sources/CardinalKit/Adapter/DataChange.swift index 9ba8d08e..96c4bf60 100644 --- a/Sources/CardinalKit/DataSource/DataChange.swift +++ b/Sources/CardinalKit/Adapter/DataChange.swift @@ -8,11 +8,14 @@ /// A ``DataChange`` tracks the addition or removel of elements across components. -public enum DataChange: Sendable where Element.ID: Sendable { +public enum DataChange< + Element: Identifiable & Sendable, + RemovalContext: Identifiable & Sendable +>: Sendable where Element.ID: Sendable, Element.ID == RemovalContext.ID { /// A new element was added case addition(Element) /// An element was removed - case removal(Element.ID) + case removal(RemovalContext) /// The identifier of the `Element`. @@ -20,8 +23,8 @@ public enum DataChange: Sendable where Element switch self { case let .addition(element): return element.id - case let .removal(elementId): - return elementId + case let .removal(removalContext): + return removalContext.id } } @@ -31,15 +34,15 @@ public enum DataChange: Sendable where Element /// - elementMap: The element map function maps the complete `Element` instance used for the ``DataChange/addition(_:)`` case. /// - idMap: The id map function only maps the identifier or an `Element` used for the ``DataChange/removal(_:)`` case. /// - Returns: Returns the mapped element - public func map( - element elementMap: (Element) -> I, - id idMap: (Element.ID) -> I.ID - ) -> DataChange { + public func map ( + element elementMap: (Element) -> (E), + removalContext removalContextMap: (RemovalContext) -> (R) + ) -> DataChange { switch self { case let .addition(element): return .addition(elementMap(element)) - case let .removal(elementId): - return .removal(idMap(elementId)) + case let .removal(removalContext): + return .removal(removalContextMap(removalContext)) } } } diff --git a/Sources/CardinalKit/Adapter/SingleValueAdapter.swift b/Sources/CardinalKit/Adapter/SingleValueAdapter.swift new file mode 100644 index 00000000..29f55c31 --- /dev/null +++ b/Sources/CardinalKit/Adapter/SingleValueAdapter.swift @@ -0,0 +1,42 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +/// A ``SingleValueAdapter`` is a ``Adapter`` that can be used to more simply transform data using the +/// ``SingleValueAdapter/transform(element:)`` and ``SingleValueAdapter/transform(id:)`` functions. +/// +/// See ``Adapter`` for more detail about data source registry adapters. +public protocol SingleValueAdapter: Adapter { + /// Map the element of the transformed async streams from additions. + /// - Parameter element: The element that should be transformed. + /// - Returns: Returns the transformed element + func transform(element: InputElement) -> OutputElement + + /// Map the element's removal of the transformed async streams from removals. + /// - Parameter removalContext: The element's removal context that should be transformed. + /// - Returns: Returns the transformed removal context. + func transform(removalContext: InputRemovalContext) -> OutputRemovalContext +} + + +extension SingleValueAdapter { + // A documentation for this methodd exists in the `Adapter` type which SwiftLint doesn't recognize. + // swiftlint:disable:next missing_docs + public func transform( + _ asyncSequence: some TypedAsyncSequence> + ) async -> any TypedAsyncSequence> { + asyncSequence.map { [self] element in + switch element { + case let .addition(element): + return await .addition(transform(element: element)) + case let .removal(elementId): + return await .removal(transform(removalContext: elementId)) + } + } + } +} diff --git a/Sources/CardinalKit/Adapter/Types+Identifiable.swift b/Sources/CardinalKit/Adapter/Types+Identifiable.swift new file mode 100644 index 00000000..abf11bb4 --- /dev/null +++ b/Sources/CardinalKit/Adapter/Types+Identifiable.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +extension UUID: Identifiable { + public var id: UUID { + self + } +} + +extension Int: Identifiable { + public var id: Int { + self + } +} + +extension Double: Identifiable { + public var id: Double { + self + } +} + +extension Float: Identifiable { + public var id: Float { + self + } +} + +extension String: Identifiable { + public var id: String { + self + } +} diff --git a/Sources/CardinalKit/CardinalKit/CardinalKitAppDelegate.swift b/Sources/CardinalKit/CardinalKit/CardinalKitAppDelegate.swift index 99ddbd61..ae48f167 100644 --- a/Sources/CardinalKit/CardinalKit/CardinalKitAppDelegate.swift +++ b/Sources/CardinalKit/CardinalKit/CardinalKitAppDelegate.swift @@ -34,6 +34,7 @@ import SwiftUI open class CardinalKitAppDelegate: NSObject, UIApplicationDelegate { private actor DefaultStandard: Standard { typealias BaseType = StandardType + typealias RemovalContext = BaseType struct StandardType: Identifiable { @@ -41,7 +42,7 @@ open class CardinalKitAppDelegate: NSObject, UIApplicationDelegate { } - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } } diff --git a/Sources/CardinalKit/DataSource/DataSourceRegistry.swift b/Sources/CardinalKit/DataSource/DataSourceRegistry.swift index 48752a33..4f549229 100644 --- a/Sources/CardinalKit/DataSource/DataSourceRegistry.swift +++ b/Sources/CardinalKit/DataSource/DataSourceRegistry.swift @@ -9,28 +9,30 @@ /// A ``DataSourceRegistry`` can recieve data from data sources using the ``DataSourceRegistry/registerDataSource(_:)`` method. /// Each ``DataSourceRegistry`` has a ``DataSourceRegistry/BaseType`` that all data sources should provide. -/// Use ``DataSourceRegistryAdapter``s to transform data of different data sources. -public protocol DataSourceRegistry: Actor { - /// The ``DataSourceRegistry/BaseType`` that all data sources should provide. - associatedtype BaseType: Identifiable, Sendable where BaseType.ID: Sendable +/// Use ``Adapter``s to transform data of different data sources. +public protocol DataSourceRegistry: Actor { + /// The ``DataSourceRegistry/BaseType`` that all data sources should provide when adding or updating an element. + associatedtype BaseType: Identifiable, Sendable where BaseType.ID: Sendable, BaseType.ID == RemovalContext.ID + /// The ``DataSourceRegistry/RemovalContext`` that all data sources should provide when removing an element. + associatedtype RemovalContext: Identifiable, Sendable /// Registers a new data source for the ``DataSourceRegistry``. /// - Parameter asyncSequence: The `TypedAsyncSequence>` providing the data to the ``DataSourceRegistry``. - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) } extension DataSourceRegistry { /// Overload of the ``DataSourceRegistry/registerDataSource(_:)`` method to recieve `AsyncStream`s. - /// - Parameter asyncStream: The `AsyncStream>` providing the data to the ``DataSourceRegistry``. - public func registerDataSource(asyncStream: AsyncStream>) { + /// - Parameter asyncStream: The `AsyncStream>` providing the data to the ``DataSourceRegistry``. + public func registerDataSource(asyncStream: AsyncStream>) { registerDataSource(asyncStream) } /// Overload of the ``DataSourceRegistry/registerDataSource(_:)`` method to recieve `AsyncThrowingStream`s. - /// - Parameter asyncThrowingStream: The `AsyncThrowingStream>` providing the data to the ``DataSourceRegistry``. - public func registerDataSource(asyncThrowingStream: AsyncThrowingStream, Error>) { + /// - Parameter asyncThrowingStream: The `AsyncThrowingStream>` providing the data to the ``DataSourceRegistry``. + public func registerDataSource(asyncThrowingStream: AsyncThrowingStream, Error>) { registerDataSource(asyncThrowingStream) } } diff --git a/Sources/CardinalKit/DataSource/DataSourceRegistryAdapter.swift b/Sources/CardinalKit/DataSource/DataSourceRegistryAdapter.swift deleted file mode 100644 index fb2f96eb..00000000 --- a/Sources/CardinalKit/DataSource/DataSourceRegistryAdapter.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// A ``DataSourceRegistryAdapter`` can be used to transfrom an ouput of a data source (`InputType`) to an `OutoutType`. -/// -/// Use the ``DataSourceRegistryAdapterBuilder`` to offer developers to option to pass in a `DataSourceRegistryAdapter` instance to your components. -public protocol DataSourceRegistryAdapter: Actor { - /// The input of the ``DataSourceRegistryAdapter`` - associatedtype InputType: Identifiable, Sendable where InputType.ID: Sendable - /// The output of the ``DataSourceRegistryAdapter`` - associatedtype OutputType: Identifiable, Sendable where OutputType.ID: Sendable - - - /// Transforms any `TypedAsyncSequence>` to an `TypedAsyncSequence` with - /// the `TypedAsyncSequence>` generic constraint fulfilling the transformation, - /// - /// Implement this method in an instance of a `DataSourceRegistryAdapter`. - /// - Parameter asyncSequence: The input `TypedAsyncSequence`. - /// - Returns: The transformed `TypedAsyncSequence`. - func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> -} diff --git a/Sources/CardinalKit/DataSource/DataSourceRegistryAdapterBuilder.swift b/Sources/CardinalKit/DataSource/DataSourceRegistryAdapterBuilder.swift deleted file mode 100644 index 568f6dd6..00000000 --- a/Sources/CardinalKit/DataSource/DataSourceRegistryAdapterBuilder.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - - -private actor TwoDataSourceRegistryAdapterChain< - InputType: Identifiable, - IntermediateType: Identifiable, - OutputType: Identifiable - >: Actor, DataSourceRegistryAdapter { - let firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter - let secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter - - - init( - firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter - ) { - self.firstDataSourceRegistryAdapter = firstDataSourceRegistryAdapter - self.secondDataSourceRegistryAdapter = secondDataSourceRegistryAdapter - } - - - func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> { - let firstDataSourceRegistryTransformation = await firstDataSourceRegistryAdapter.transform(asyncSequence) - return await secondDataSourceRegistryAdapter.transform(firstDataSourceRegistryTransformation) - } -} - -private actor ThreeDataSourceRegistryAdapterChain< - InputType: Identifiable, - IntermediateTypeOne: Identifiable, - IntermediateTypeTwo: Identifiable, - OutputType: Identifiable - >: Actor, DataSourceRegistryAdapter { - let firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter - let secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter - let thirdDataSourceRegistryAdapter: any DataSourceRegistryAdapter - - - init( - firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - thirdDataSourceRegistryAdapter: any DataSourceRegistryAdapter - ) { - self.firstDataSourceRegistryAdapter = firstDataSourceRegistryAdapter - self.secondDataSourceRegistryAdapter = secondDataSourceRegistryAdapter - self.thirdDataSourceRegistryAdapter = thirdDataSourceRegistryAdapter - } - - - func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> { - let firstDataSourceRegistryTransformation = await firstDataSourceRegistryAdapter.transform(asyncSequence) - let secondDataSourceRegistryTransformation = await secondDataSourceRegistryAdapter.transform(firstDataSourceRegistryTransformation) - return await thirdDataSourceRegistryAdapter.transform(secondDataSourceRegistryTransformation) - } -} - - -/// A function builder used to generate data source registry adapter chains. -@resultBuilder -public enum DataSourceRegistryAdapterBuilder { - /// Required by every result builder to build combined results from statement blocks. - public static func buildBlock( - _ dataSourceRegistryAdapter: any DataSourceRegistryAdapter - ) -> any DataSourceRegistryAdapter where OutputType == S.BaseType { - dataSourceRegistryAdapter - } - - /// Required by every result builder to build combined results from statement blocks. - public static func buildBlock( - _ firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - _ secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter - ) -> any DataSourceRegistryAdapter where OutputType == S.BaseType { - TwoDataSourceRegistryAdapterChain( - firstDataSourceRegistryAdapter: firstDataSourceRegistryAdapter, - secondDataSourceRegistryAdapter: secondDataSourceRegistryAdapter - ) - } - - /// Required by every result builder to build combined results from statement blocks. - public static func buildBlock( - _ firstDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - _ secondDataSourceRegistryAdapter: any DataSourceRegistryAdapter, - _ thirdDataSourceRegistryAdapter: any DataSourceRegistryAdapter - ) -> any DataSourceRegistryAdapter where OutputType == S.BaseType { - ThreeDataSourceRegistryAdapterChain( - firstDataSourceRegistryAdapter: firstDataSourceRegistryAdapter, - secondDataSourceRegistryAdapter: secondDataSourceRegistryAdapter, - thirdDataSourceRegistryAdapter: thirdDataSourceRegistryAdapter - ) - } -} diff --git a/Sources/CardinalKit/DataSource/SingleValueDataSourceRegistryAdapter.swift b/Sources/CardinalKit/DataSource/SingleValueDataSourceRegistryAdapter.swift deleted file mode 100644 index 37d6d379..00000000 --- a/Sources/CardinalKit/DataSource/SingleValueDataSourceRegistryAdapter.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// A ``SingleValueDataSourceRegistryAdapter`` is a ``DataSourceRegistryAdapter`` that can be used to more simply transform data using the -/// ``SingleValueDataSourceRegistryAdapter/transform(element:)`` and ``SingleValueDataSourceRegistryAdapter/transform(id:)`` functions. -/// -/// See ``DataSourceRegistryAdapter`` for more detail about data source registry adapters. -public protocol SingleValueDataSourceRegistryAdapter: DataSourceRegistryAdapter { - /// Map the element of the transformed async streams from additions. - /// - Parameter element: The element that should be transformed. - /// - Returns: Returns the transformed element - func transform(element: InputType) -> OutputType - - /// Map the element ids of the transformed async streams from removals. - /// - Parameter id: The element's id that should be transformed. - /// - Returns: Returns the transformed element id. - func transform(id: InputType.ID) -> OutputType.ID -} - - -extension SingleValueDataSourceRegistryAdapter { - // A documentation for this methodd exists in the `DataSourceRegistryAdapter` type which SwiftLint doesn't recognize. - // swiftlint:disable:next missing_docs - public func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> { - asyncSequence.map { [self] element in - switch element { - case let .addition(element): - return await .addition(transform(element: element)) - case let .removal(elementId): - return await .removal(transform(id: elementId)) - } - } - } -} diff --git a/Sources/CardinalKit/DataStorageProvider/DataStorageProvider.swift b/Sources/CardinalKit/DataStorageProvider/DataStorageProvider.swift index 96b2c48e..f974093c 100644 --- a/Sources/CardinalKit/DataStorageProvider/DataStorageProvider.swift +++ b/Sources/CardinalKit/DataStorageProvider/DataStorageProvider.swift @@ -12,44 +12,42 @@ /// /// The following example uses an ``EncodableAdapter`` and ``IdentityEncodableAdapter`` to transform elements to an `Encodable` representation: /// ``` -/// private actor DataStorageExample: DataStorageProvider { -/// typealias Adapter = any EncodableAdapter +/// struct DataStorageUploadable: Encodable, Identifiable { +/// let id: String +/// let element: Encodable /// /// -/// let adapter: Adapter +/// func encode(to encoder: Encoder) throws { +/// try element.encode(to: encoder) +/// } +/// } /// +/// actor DataStorageExample: DataStorageProvider { +/// typealias DataStorageExampleAdapter = SingleValueAdapter /// -/// init(adapter: Adapter) { -/// self.adapter = adapter -/// } /// -/// init() { -/// self.adapter = IdentityEncodableAdapter() -/// } +/// let adapter: any DataStorageExampleAdapter /// /// -/// func process(_ element: DataChange) async throws { -/// switch element { -/// case let .addition(element): -/// async let transformedElement = adapter.transform(element: element) -/// let data = try await JSONEncoder().encode(transformedElement) -/// // E.g., Handle the data upload here ... -/// case let .removal(id): -/// async let stringId = transform(id, using: adapter) -/// // E.g., Send out a delete network request here ... -/// } -/// } +/// init(@AdapterBuilder adapter: () -> (any DataStorageExampleAdapter)) { +/// self.adapter = adapter() +/// } /// /// -/// private func transform( -/// _ id: ComponentStandard.BaseType.ID, -/// using adapter: some EncodableAdapter -/// ) async -> String { -/// await adapter.transform(id: id) -/// } +/// func process(_ element: DataChange) async throws { +/// switch element { +/// case let .addition(element): +/// let transformedElement = await adapter.transform(element: element) +/// // Upload the `transformedElement` ... +/// case let .removal(removalContext): +/// let transformedRemovalContext = await adapter.transform(removalContext: removalContext) +/// // Process the `transformedRemovalContext` ... +/// } +/// } /// } +/// ``` public protocol DataStorageProvider: Actor, Component { /// The ``DataStorageProvider/process(_:)`` function is called for every element should be handled by the ``DataStorageProvider`` /// - Parameter element: The ``DataChange`` defines if the element should be added or deleted. - func process(_ element: DataChange) async throws + func process(_ element: DataChange) async throws } diff --git a/Sources/CardinalKit/DataStorageProvider/EncodableAdapter.swift b/Sources/CardinalKit/DataStorageProvider/EncodableAdapter.swift deleted file mode 100644 index 56233e3f..00000000 --- a/Sources/CardinalKit/DataStorageProvider/EncodableAdapter.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// An ``EncodableAdapter`` can be used to transform a type to an `Encodable` instance. It is typically used in instantiations of a ``DataStorageProvider``. -/// -/// You can refer to the ``IdentityEncodableAdapter`` if you require a default adapter for a type that already conforms to all required protocols. -public protocol EncodableAdapter: Actor { - /// The input type conforming to `Sendable` and `Identifiable` where the `InputType.ID` is `Sendable` as well. - associatedtype InputType: Sendable, Identifiable where InputType.ID: Sendable - /// The ouput of the ``transform(id:)`` function that has to confrom to `Sendable` and `Hashable`. - associatedtype ID: Sendable, Hashable - - - /// Transforms an element to an `Encodable` and `Sendable` value. - /// - Parameter element: The element that is tranformed. - /// - Returns: Returns an element conforming to an `Encodable` and `Sendable` - func transform(element: InputType) async -> any Encodable & Sendable - - /// Transforms an id to an ``ID`` instance. - /// - Parameter id: The ``InputType``'s `ID` type that is transformed. - /// - Returns: The transformed ``ID`` instance. - func transform(id: InputType.ID) async -> ID -} diff --git a/Sources/CardinalKit/DataStorageProvider/IdentityEncodableAdapter.swift b/Sources/CardinalKit/DataStorageProvider/IdentityEncodableAdapter.swift deleted file mode 100644 index ba4971d2..00000000 --- a/Sources/CardinalKit/DataStorageProvider/IdentityEncodableAdapter.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -/// The ``IdentityEncodableAdapter`` is an instantiation of the ``EncodableAdapter`` protocol that maps an input type conforming -/// to `Encodable & Sendable & Identifiable` and with the `InputType.ID` type conforming to `LosslessStringConvertible`. -public actor IdentityEncodableAdapter: EncodableAdapter - where InputType: Encodable & Sendable & Identifiable, InputType.ID: LosslessStringConvertible { - /// The ``IdentityEncodableAdapter`` is an instantiation of the ``EncodableAdapter`` protocol that maps an input type conforming - /// to `Encodable & Sendable & Identifiable` and with the `InputType.ID` type conforming to `LosslessStringConvertible`. - public init() {} - - - public func transform(element: InputType) async -> Encodable & Sendable { - element - } - - public func transform(id: InputType.ID) async -> String { - id.description - } -} diff --git a/Sources/FHIR/FHIR+Identifiable.swift b/Sources/FHIR/FHIR+Identifiable.swift new file mode 100644 index 00000000..5a362a6a --- /dev/null +++ b/Sources/FHIR/FHIR+Identifiable.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the CardinalKit open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import ModelsR4 + + +extension Resource: Identifiable { } + +extension FHIRPrimitive: Identifiable where PrimitiveType: Identifiable { } + +extension Optional: Identifiable where Wrapped == FHIRPrimitive { + public var id: FHIRPrimitive? { + switch self { + case let .some(value): + return value + case .none: + return nil + } + } +} diff --git a/Sources/FHIR/FHIR.swift b/Sources/FHIR/FHIR.swift index 5aab9f50..c0cb19e9 100644 --- a/Sources/FHIR/FHIR.swift +++ b/Sources/FHIR/FHIR.swift @@ -8,7 +8,7 @@ import CardinalKit import Foundation -@_exported import ModelsR4 +@_exported @preconcurrency import ModelsR4 import XCTRuntimeAssertions @@ -45,6 +45,27 @@ import XCTRuntimeAssertions public actor FHIR: Standard { /// The FHIR `Resource` type builds the ``Standard/BaseType`` of the ``FHIR`` standard. public typealias BaseType = Resource + /// <#Description#> + public typealias RemovalContext = FHIRRemovalContext + + + /// <#Description#> + public struct FHIRRemovalContext: Sendable, Identifiable { + /// <#Description#> + public let id: BaseType.ID + /// <#Description#> + public let resourceType: String + + + /// <#Description#> + /// - Parameters: + /// - id: <#id description#> + /// - resourceType: <#resourceType description#> + public init(id: BaseType.ID, resourceType: String) { + self.id = id + self.resourceType = resourceType + } + } var resources: [String: ResourceProxy] = [:] @@ -53,7 +74,7 @@ public actor FHIR: Standard { var dataSources: [any DataStorageProvider] - public func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { + public func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { _Concurrency.Task { for try await dateSourceElement in asyncSequence { switch dateSourceElement { @@ -65,13 +86,13 @@ public actor FHIR: Standard { for dataSource in dataSources { try await dataSource.process(.addition(resource)) } - case let .removal(resourceId): - guard let id = resourceId?.value?.string else { + case let .removal(removalContext): + guard let id = removalContext.id?.value?.string else { continue } resources[id] = nil for dataSource in dataSources { - try await dataSource.process(.removal(resourceId)) + try await dataSource.process(.removal(removalContext)) } } } diff --git a/Sources/FHIR/Resource+Identifiable.swift b/Sources/FHIR/Resource+Identifiable.swift deleted file mode 100644 index cc9ee827..00000000 --- a/Sources/FHIR/Resource+Identifiable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// This source file is part of the CardinalKit open-source project -// -// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import ModelsR4 - - -extension Resource: Identifiable { } diff --git a/Sources/HealthKitDataSource/CollectSample/CollectSample.swift b/Sources/HealthKitDataSource/CollectSample/CollectSample.swift index 8821e339..4377074f 100644 --- a/Sources/HealthKitDataSource/CollectSample/CollectSample.swift +++ b/Sources/HealthKitDataSource/CollectSample/CollectSample.swift @@ -30,7 +30,7 @@ public struct CollectSample: HealthKitDataSourceDescri } - public func dataSource(healthStore: HKHealthStore, standard: S, adapter: HealthKit.Adapter) -> HealthKitDataSource { + public func dataSource(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> HealthKitDataSource { HealthKitSampleDataSource( healthStore: healthStore, standard: standard, diff --git a/Sources/HealthKitDataSource/CollectSample/HealthKitSampleDataSource.swift b/Sources/HealthKitDataSource/CollectSample/HealthKitSampleDataSource.swift index ec333beb..1daf5bcc 100644 --- a/Sources/HealthKitDataSource/CollectSample/HealthKitSampleDataSource.swift +++ b/Sources/HealthKitDataSource/CollectSample/HealthKitSampleDataSource.swift @@ -18,7 +18,7 @@ final class HealthKitSampleDataSource.Adapter + let adapter: HealthKit.HKSampleAdapter var didFinishLaunchingWithOptions = false var active = false @@ -36,7 +36,7 @@ final class HealthKitSampleDataSource.Adapter + adapter: HealthKit.HKSampleAdapter ) { self.healthStore = healthStore self.standard = standard @@ -120,7 +120,7 @@ final class HealthKitSampleDataSource AsyncThrowingStream, Error> { + private func anchoredSingleObjectQuery() -> AsyncThrowingStream, Error> { AsyncThrowingStream { continuation in Task { let results = try await healthStore.anchoredSingleObjectQuery( diff --git a/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift b/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift index e5fa0d66..aea7d44f 100644 --- a/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift +++ b/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+AnchoredObjectQuery.swift @@ -27,7 +27,7 @@ extension HKHealthStore { func anchoredContinousObjectQuery( for sampleType: HKSampleType, withPredicate predicate: NSPredicate? = nil - ) async -> any TypedAsyncSequence> { + ) async -> any TypedAsyncSequence> { AsyncThrowingStream { continuation in Task { try await self.requestAuthorization(toShare: [], read: [sampleType]) @@ -65,14 +65,14 @@ extension HKHealthStore { for sampleType: HKSampleType, using anchor: HKQueryAnchor? = nil, withPredicate predicate: NSPredicate? = nil - ) async throws -> (elements: [DataChange], anchor: HKQueryAnchor) { + ) async throws -> (elements: [DataChange], anchor: HKQueryAnchor) { try await self.requestAuthorization(toShare: [], read: [sampleType]) let anchorDescriptor = anchorDescriptor(sampleType: sampleType, predicate: predicate, anchor: anchor) let result = try await anchorDescriptor.result(for: self) - var elements: [DataChange] = [] + var elements: [DataChange] = [] elements.reserveCapacity(result.deletedObjects.count + result.addedSamples.count) for deletedObject in result.deletedObjects { diff --git a/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+SampleQuery.swift b/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+SampleQuery.swift index 4bd06058..a01a81ff 100644 --- a/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+SampleQuery.swift +++ b/Sources/HealthKitDataSource/HealthKit Extensions/HKHealthStore+SampleQuery.swift @@ -34,7 +34,7 @@ extension HKHealthStore { func sampleQueryStream( for sampleType: HKSampleType, withPredicate predicate: NSPredicate? = nil - ) -> AsyncThrowingStream, Error> { + ) -> AsyncThrowingStream, Error> { AsyncThrowingStream { continuation in Task { for sample in try await sampleQuery(for: sampleType, withPredicate: predicate) { diff --git a/Sources/HealthKitDataSource/HealthKit.swift b/Sources/HealthKitDataSource/HealthKit.swift index 235bc6d2..1ce108d8 100644 --- a/Sources/HealthKitDataSource/HealthKit.swift +++ b/Sources/HealthKitDataSource/HealthKit.swift @@ -51,14 +51,14 @@ import SwiftUI /// ``` public final class HealthKit: Module { /// The ``HealthKit/Adapter`` type defines the mapping of `HKSample`s to the component's standard's base type. - public typealias Adapter = any DataSourceRegistryAdapter + public typealias HKSampleAdapter = any Adapter @StandardActor var standard: ComponentStandard let healthStore: HKHealthStore let healthKitDataSourceDescriptions: [HealthKitDataSourceDescription] - let adapter: Adapter + let adapter: HKSampleAdapter lazy var healthKitComponents: [any HealthKitDataSource] = { healthKitDataSourceDescriptions .map { $0.dataSource(healthStore: healthStore, standard: standard, adapter: adapter) } @@ -71,7 +71,7 @@ public final class HealthKit: Module { /// - adapter: The ``HealthKit/Adapter`` type defines the mapping of `HKSample`s to the component's standard's base type. public init( @HealthKitDataSourceDescriptionBuilder _ healthKitDataSourceDescriptions: () -> ([HealthKitDataSourceDescription]), - @DataSourceRegistryAdapterBuilder adapter: () -> (Adapter) + @AdapterBuilder adapter: () -> (HKSampleAdapter) ) { precondition( HKHealthStore.isHealthDataAvailable(), diff --git a/Sources/HealthKitDataSource/HealthKitDataSource/HealthKitDataSourceDescription.swift b/Sources/HealthKitDataSource/HealthKitDataSource/HealthKitDataSourceDescription.swift index 59fecd08..9c525f49 100644 --- a/Sources/HealthKitDataSource/HealthKitDataSource/HealthKitDataSourceDescription.swift +++ b/Sources/HealthKitDataSource/HealthKitDataSource/HealthKitDataSourceDescription.swift @@ -21,5 +21,5 @@ public protocol HealthKitDataSourceDescription { /// - healthStore: The `HKHealthStore` instance that the queries should be performed on. /// - standard: The `Standard` instance that is used in the software system. /// - adapter: An adapter that can adapt HealthKit data to the corresponding data standard. - func dataSource(healthStore: HKHealthStore, standard: S, adapter: HealthKit.Adapter) -> HealthKitDataSource + func dataSource(healthStore: HKHealthStore, standard: S, adapter: HealthKit.HKSampleAdapter) -> HealthKitDataSource } diff --git a/Tests/CardinalKitTests/DataSourceTests/DataSourceTests.swift b/Tests/CardinalKitTests/DataSourceTests/DataSourceTests.swift index 6853aab9..60637516 100644 --- a/Tests/CardinalKitTests/DataSourceTests/DataSourceTests.swift +++ b/Tests/CardinalKitTests/DataSourceTests/DataSourceTests.swift @@ -13,7 +13,7 @@ import XCTRuntimeAssertions final class DataSourceTests: XCTestCase { - private final class DataSourceTestComponentInjector: Component { + private final class DataSourceTestComponentInjector: Component where T == T.ID { typealias ComponentStandard = TypedMockStandard @@ -25,19 +25,22 @@ final class DataSourceTests: XCTestCase { } } - final class DataSourceTestComponent: Component, LifecycleHandler { + final class DataSourceTestComponent< + T: Identifiable, + MockStandardType: Identifiable + >: Component, LifecycleHandler where T.ID: Identifiable, T.ID == T.ID.ID, MockStandardType == MockStandardType.ID { typealias ComponentStandard = TypedMockStandard @StandardActor var standard: TypedMockStandard - var injectedData: [DataChange] - let adapter: any DataSourceRegistryAdapter.BaseType> + var injectedData: [DataChange] + let adapter: any Adapter.BaseType, TypedMockStandard.RemovalContext> init( - injectedData: [DataChange], - @DataSourceRegistryAdapterBuilder> adapter: - () -> (any DataSourceRegistryAdapter) + injectedData: [DataChange], + @AdapterBuilder.BaseType, TypedMockStandard.RemovalContext> adapter: + () -> (any Adapter.BaseType, TypedMockStandard.RemovalContext>) ) { self.injectedData = injectedData self.adapter = adapter() @@ -45,7 +48,7 @@ final class DataSourceTests: XCTestCase { func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) { - let asyncStream = AsyncStream> { + let asyncStream = AsyncStream> { guard !self.injectedData.isEmpty else { return nil } @@ -62,10 +65,13 @@ final class DataSourceTests: XCTestCase { } } - class DataSourceTestApplicationDelegate: CardinalKitAppDelegate { + class DataSourceTestApplicationDelegate: CardinalKitAppDelegate where T == T.ID { let dynamicDependencies: _DynamicDependenciesPropertyWrapper> - let dataSourceExpecations: (DataChange.BaseType>) async throws -> Void - let finishedDataSourceSequence: (any TypedAsyncSequence.BaseType>>.Type) async throws -> Void + let dataSourceExpecations: (DataChange.BaseType, TypedMockStandard.RemovalContext>) async throws -> Void + let finishedDataSourceSequence: ( + any TypedAsyncSequence.BaseType, + TypedMockStandard.RemovalContext>>.Type + ) async throws -> Void override var configuration: Configuration { @@ -80,8 +86,11 @@ final class DataSourceTests: XCTestCase { init( dynamicDependencies: _DynamicDependenciesPropertyWrapper>, - dataSourceExpecations: @escaping (DataChange.BaseType>) async throws -> Void, - finishedDataSourceSequence: @escaping (any TypedAsyncSequence.BaseType>>.Type) async throws -> Void + dataSourceExpecations: @escaping (DataChange.BaseType, TypedMockStandard.RemovalContext>) async throws -> Void, + finishedDataSourceSequence: @escaping ( + any TypedAsyncSequence.BaseType, + TypedMockStandard.RemovalContext>>.Type + ) async throws -> Void ) { self.dynamicDependencies = dynamicDependencies self.dataSourceExpecations = dataSourceExpecations @@ -89,48 +98,54 @@ final class DataSourceTests: XCTestCase { } } - actor IntToStringAdapterActor: DataSourceRegistryAdapter { - typealias InputType = MockStandard.CustomDataSourceType - typealias OutputType = MockStandard.CustomDataSourceType + actor IntToStringAdapterActor: Adapter { + typealias InputElement = MockStandard.CustomDataSourceType + typealias InputRemovalContext = InputElement.ID + typealias OutputElement = MockStandard.CustomDataSourceType + typealias OutputRemovalContext = OutputElement.ID func transform( - _ asyncSequence: some TypedAsyncSequence> - ) async -> any TypedAsyncSequence> { + _ asyncSequence: some TypedAsyncSequence> + ) async -> any TypedAsyncSequence> { asyncSequence.map { element in element.map( element: { MockStandard.CustomDataSourceType(id: String(describing: $0.id)) }, - id: { String(describing: $0) } + removalContext: { OutputRemovalContext($0.id) } ) } } } - actor DoubleToIntAdapterActor: SingleValueDataSourceRegistryAdapter { - typealias InputType = MockStandard.CustomDataSourceType - typealias OutputType = MockStandard.CustomDataSourceType + actor DoubleToIntAdapterActor: SingleValueAdapter { + typealias InputElement = MockStandard.CustomDataSourceType + typealias InputRemovalContext = InputElement.ID + typealias OutputElement = MockStandard.CustomDataSourceType + typealias OutputRemovalContext = OutputElement.ID - func transform(id: InputType.ID) -> OutputType.ID { - OutputType.ID(id) + func transform(element: InputElement) -> OutputElement { + MockStandard.CustomDataSourceType(id: OutputElement.ID(element.id)) } - func transform(element: InputType) -> OutputType { - MockStandard.CustomDataSourceType(id: OutputType.ID(element.id)) + func transform(removalContext: InputRemovalContext) -> OutputRemovalContext { + OutputRemovalContext(removalContext.id) } } - actor FloarToDoubleAdapterActor: SingleValueDataSourceRegistryAdapter { - typealias InputType = MockStandard.CustomDataSourceType - typealias OutputType = MockStandard.CustomDataSourceType + actor FloarToDoubleAdapterActor: SingleValueAdapter { + typealias InputElement = MockStandard.CustomDataSourceType + typealias InputRemovalContext = InputElement.ID + typealias OutputElement = MockStandard.CustomDataSourceType + typealias OutputRemovalContext = OutputElement.ID - func transform(id: InputType.ID) -> OutputType.ID { - OutputType.ID(id) + func transform(element: InputElement) -> OutputElement { + MockStandard.CustomDataSourceType(id: OutputElement.ID(element.id)) } - func transform(element: InputType) -> OutputType { - MockStandard.CustomDataSourceType(id: OutputType.ID(element.id)) + func transform(removalContext: InputRemovalContext) -> OutputRemovalContext { + OutputRemovalContext(removalContext.id) } } @@ -139,7 +154,7 @@ final class DataSourceTests: XCTestCase { let expecation = XCTestExpectation(description: "Recieved all required data source elements") expecation.assertForOverFulfill = true expecation.expectedFulfillmentCount = 3 - var dataChanges: [DataChange.BaseType>] = [] + var dataChanges: [DataChange.BaseType, TypedMockStandard.RemovalContext>] = [] let delegate = DataSourceTestApplicationDelegate( dynamicDependencies: _DynamicDependenciesPropertyWrapper>( diff --git a/Tests/CardinalKitTests/DataStorageProviderTests/DataStorageProviderTests.swift b/Tests/CardinalKitTests/DataStorageProviderTests/DataStorageProviderTests.swift index 1d804ca8..4b6c19ce 100644 --- a/Tests/CardinalKitTests/DataStorageProviderTests/DataStorageProviderTests.swift +++ b/Tests/CardinalKitTests/DataStorageProviderTests/DataStorageProviderTests.swift @@ -36,52 +36,33 @@ final class DataStorageProviderTests: XCTestCase { } - private actor DataStorageExample: DataStorageProvider { - typealias Adapter = any EncodableAdapter - - - let adapter: Adapter + private actor DataStorageExample< + ComponentStandard: Standard + >: DataStorageProvider where ComponentStandard.BaseType: Encodable, ComponentStandard.BaseType.ID == String { let mockUpload: (MockUpload) -> Void - init(adapter: Adapter, mockUpload: @escaping (MockUpload) -> Void) { - self.adapter = adapter - self.mockUpload = mockUpload - } - - init( - mockUpload: @escaping (MockUpload) -> Void - ) where ComponentStandard.BaseType: Encodable & Sendable, ComponentStandard.BaseType.ID: LosslessStringConvertible { - self.adapter = IdentityEncodableAdapter() + init(mockUpload: @escaping (MockUpload) -> Void) { self.mockUpload = mockUpload } - func process(_ element: DataChange) async throws { + func process(_ element: DataChange) async throws { switch element { case let .addition(element): - async let transformedElement = adapter.transform(element: element) - let data = try await JSONEncoder().encode(transformedElement) + let data = try JSONEncoder().encode(element) let string = String(decoding: data, as: UTF8.self) mockUpload(.post(string)) - case let .removal(id): - async let stringId = transform(id, using: adapter) - await mockUpload(.delete(stringId)) + case let .removal(removalContext): + mockUpload(.delete(removalContext.id)) } } - - - private func transform( - _ id: ComponentStandard.BaseType.ID, - using adapter: some EncodableAdapter - ) async -> String { - await adapter.transform(id: id) - } } private actor DataStorageProviderStandard: Standard { typealias BaseType = CustomDataSourceType + typealias RemovalContext = BaseType struct CustomDataSourceType: Encodable, Equatable, Identifiable { @@ -93,7 +74,7 @@ final class DataStorageProviderTests: XCTestCase { var dataSources: [any DataStorageProvider] - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { Task { do { for try await element in asyncSequence { @@ -157,10 +138,10 @@ final class DataStorageProviderTests: XCTestCase { let cardinalKit = try XCTUnwrap(delegate.cardinalKit as? CardinalKit) await cardinalKit.standard.registerDataSource( asyncStream: AsyncStream { continuation in - continuation.yield(DataChange.addition(DataStorageProviderStandard.CustomDataSourceType(id: "42"))) - continuation.yield(DataChange.addition(DataStorageProviderStandard.CustomDataSourceType(id: "43"))) - continuation.yield(DataChange.addition(DataStorageProviderStandard.CustomDataSourceType(id: "44"))) - continuation.yield(DataChange.removal("44")) + continuation.yield(DataChange.addition(DataStorageProviderStandard.BaseType(id: "42"))) + continuation.yield(DataChange.addition(DataStorageProviderStandard.BaseType(id: "43"))) + continuation.yield(DataChange.addition(DataStorageProviderStandard.BaseType(id: "44"))) + continuation.yield(DataChange.removal(DataStorageProviderStandard.RemovalContext(id: "44"))) } ) diff --git a/Tests/CardinalKitTests/Shared/TypedMockStandard.swift b/Tests/CardinalKitTests/Shared/TypedMockStandard.swift index 70bd43ab..7fb736ff 100644 --- a/Tests/CardinalKitTests/Shared/TypedMockStandard.swift +++ b/Tests/CardinalKitTests/Shared/TypedMockStandard.swift @@ -9,23 +9,24 @@ import CardinalKit -actor TypedMockStandard: Standard { +actor TypedMockStandard: Standard where T == T.ID { typealias BaseType = CustomDataSourceType + typealias RemovalContext = BaseType.ID - struct CustomDataSourceType: Equatable, Identifiable { + struct CustomDataSourceType: Equatable, Identifiable { let id: T } - let dataSourceExpecations: (DataChange) async throws -> Void - let finishedDataSourceSequence: (any TypedAsyncSequence>.Type) async throws -> Void + let dataSourceExpecations: (DataChange) async throws -> Void + let finishedDataSourceSequence: (any TypedAsyncSequence>.Type) async throws -> Void init( - dataSourceExpecations: @escaping (DataChange) async throws -> Void + dataSourceExpecations: @escaping (DataChange) async throws -> Void = defaultDataSourceExpecations, - finishedDataSourceSequence: @escaping (any TypedAsyncSequence>.Type) async throws -> Void + finishedDataSourceSequence: @escaping (any TypedAsyncSequence>.Type) async throws -> Void = defaultFinishedDataSourceSequence ) { self.dataSourceExpecations = dataSourceExpecations @@ -34,7 +35,7 @@ actor TypedMockStandard: Standard { static func defaultDataSourceExpecations( - _ element: DataChange + _ element: DataChange ) { switch element { case let .addition(newElement): @@ -45,13 +46,13 @@ actor TypedMockStandard: Standard { } static func defaultFinishedDataSourceSequence( - _ sequenceType: any TypedAsyncSequence>.Type + _ sequenceType: any TypedAsyncSequence>.Type ) { print("Finished: \(String(describing: sequenceType))") } - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { Task { do { for try await element in asyncSequence { diff --git a/Tests/FHIRTests/FHIRTests.swift b/Tests/FHIRTests/FHIRTests.swift index b1afed7f..0d8a498c 100644 --- a/Tests/FHIRTests/FHIRTests.swift +++ b/Tests/FHIRTests/FHIRTests.swift @@ -39,7 +39,6 @@ final class DataStorageProviderTests: XCTestCase { typealias ComponentStandard = FHIR - let adapter = IdentityEncodableAdapter() let mockUpload: (MockUpload) -> Void @@ -48,26 +47,16 @@ final class DataStorageProviderTests: XCTestCase { } - func process(_ element: DataChange) async throws { + func process(_ element: DataChange) async throws { switch element { case let .addition(element): - async let transformedElement = adapter.transform(element: element) - let data = try await JSONEncoder().encode(transformedElement) + let data = try JSONEncoder().encode(element) let string = String(decoding: data, as: UTF8.self) mockUpload(.post(string)) - case let .removal(id): - async let stringId = transform(id, using: adapter) - await mockUpload(.delete(stringId)) + case let .removal(removalContext): + mockUpload(.delete(removalContext.id.description)) } } - - - private func transform( - _ id: ComponentStandard.BaseType.ID, - using adapter: some EncodableAdapter - ) async -> String { - await adapter.transform(id: id) - } } private class DataStorageProviderApplicationDelegate: CardinalKitAppDelegate { @@ -137,8 +126,8 @@ final class DataStorageProviderTests: XCTestCase { continuation.yield(.addition(observation2)) continuation.yield(.addition(observation3)) continuation.yield(.addition(observationNilId)) - continuation.yield(.removal(nil)) - continuation.yield(.removal("1")) + continuation.yield(.removal(FHIR.RemovalContext(id: nil, resourceType: "Observation"))) + continuation.yield(.removal(FHIR.RemovalContext(id: "1", resourceType: "Observation"))) } ) diff --git a/Tests/LocalStorageTests/LocalStorageTests.swift b/Tests/LocalStorageTests/LocalStorageTests.swift index 6df394e1..c0ef604b 100644 --- a/Tests/LocalStorageTests/LocalStorageTests.swift +++ b/Tests/LocalStorageTests/LocalStorageTests.swift @@ -14,14 +14,15 @@ import XCTest final class LocalStorageTests: XCTestCase { private actor LocalStorageTestStandard: Standard { typealias BaseType = StandardType + typealias RemovalContext = StandardType - struct StandardType: Identifiable { + struct StandardType: Sendable, Identifiable { var id: UUID } - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } } struct Letter: Codable, Equatable { diff --git a/Tests/SchedulerTests/SchedulerTests.swift b/Tests/SchedulerTests/SchedulerTests.swift index 942771f8..1fa1ae7f 100644 --- a/Tests/SchedulerTests/SchedulerTests.swift +++ b/Tests/SchedulerTests/SchedulerTests.swift @@ -13,6 +13,7 @@ import XCTest actor SchedulerTestsStandard: Standard { typealias BaseType = CustomDataSourceType + typealias RemovalContext = CustomDataSourceType struct CustomDataSourceType: Equatable, Identifiable { @@ -20,7 +21,7 @@ actor SchedulerTestsStandard: Standard { } - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { } } diff --git a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift index 0787206e..eb2647e6 100644 --- a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift @@ -7,7 +7,7 @@ // import CardinalKit -import HealthKit +@preconcurrency import HealthKit import HealthKitDataSource import LocalStorage import SecureStorage @@ -17,9 +17,6 @@ import SwiftUI class TestAppDelegate: CardinalKitAppDelegate { override var configuration: Configuration { Configuration(standard: TestAppStandard()) { - TestAccountConfiguration() - ObservableComponentTestsComponent(message: "Passed") - MultipleObservableObjectsTestsComponent() if HKHealthStore.isHealthDataAvailable() { HealthKit { CollectSample( @@ -46,8 +43,11 @@ class TestAppDelegate: CardinalKitAppDelegate { TestAppHealthKitAdapter() } } - SecureStorage() LocalStorage() + MultipleObservableObjectsTestsComponent() + ObservableComponentTestsComponent(message: "Passed") + SecureStorage() + TestAccountConfiguration() } } } diff --git a/Tests/UITests/TestApp/Shared/TestAppHealthKitAdapter.swift b/Tests/UITests/TestApp/Shared/TestAppHealthKitAdapter.swift index 7dc495c4..33d4b239 100644 --- a/Tests/UITests/TestApp/Shared/TestAppHealthKitAdapter.swift +++ b/Tests/UITests/TestApp/Shared/TestAppHealthKitAdapter.swift @@ -10,16 +10,18 @@ import CardinalKit @preconcurrency import HealthKit -actor TestAppHealthKitAdapter: SingleValueDataSourceRegistryAdapter { - typealias InputType = HKSample - typealias OutputType = TestAppStandard.BaseType +actor TestAppHealthKitAdapter: SingleValueAdapter { + typealias InputElement = HKSample + typealias InputRemovalContext = UUID + typealias OutputElement = TestAppStandard.BaseType + typealias OutputRemovalContext = TestAppStandard.RemovalContext - func transform(element: HKSample) -> TestAppStandard.BaseType { + func transform(element: InputElement) -> OutputElement { TestAppStandard.BaseType(id: element.sampleType.identifier) } - func transform(id: UUID) -> String { - "Removed Element!" + func transform(removalContext: InputRemovalContext) -> OutputRemovalContext { + OutputRemovalContext(id: removalContext.uuidString) } } diff --git a/Tests/UITests/TestApp/Shared/TestAppStandard.swift b/Tests/UITests/TestApp/Shared/TestAppStandard.swift index 9ecdbb34..d93b76df 100644 --- a/Tests/UITests/TestApp/Shared/TestAppStandard.swift +++ b/Tests/UITests/TestApp/Shared/TestAppStandard.swift @@ -11,15 +11,36 @@ import Foundation actor TestAppStandard: Standard, ObservableObjectProvider, ObservableObject { - typealias BaseType = TestAppStandardDataChange + typealias BaseType = TestAppStandardBaseType + typealias RemovalContext = TestAppStandardRemovalContext - struct TestAppStandardDataChange: Identifiable { + struct TestAppStandardBaseType: Identifiable, Sendable { let id: String + let content: Int + let collectionPath: String + + + init(id: String, content: Int = 42, collectionPath: String = "TestAppStandardDataChange") { + self.collectionPath = collectionPath + self.id = id + self.content = content + } + } + + struct TestAppStandardRemovalContext: Identifiable, Sendable { + let id: TestAppStandardBaseType.ID + let collectionPath: String + + + init(id: TestAppStandardBaseType.ID, collectionPath: String = "TestAppStandardDataChange") { + self.collectionPath = collectionPath + self.id = id + } } - var dataChanges: [DataChange] = [] { + var dataChanges: [DataChange] = [] { willSet { Task { @MainActor in self.objectWillChange.send() @@ -28,7 +49,7 @@ actor TestAppStandard: Standard, ObservableObjectProvider, ObservableObject { } - func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { + func registerDataSource(_ asyncSequence: some TypedAsyncSequence>) { Task { do { for try await element in asyncSequence { @@ -41,7 +62,12 @@ actor TestAppStandard: Standard, ObservableObjectProvider, ObservableObject { dataChanges.append(element) } } catch { - dataChanges = [.removal(error.localizedDescription)] + fatalError( + """ + Unexpected error in \(error). + Do not use `fatalError` in production code. We only use this to validate the tests. + """ + ) } } } diff --git a/Tests/UITests/TestAppUITests/AccountLoginTests.swift b/Tests/UITests/TestAppUITests/AccountLoginTests.swift index 53581b4e..bc82c0a8 100644 --- a/Tests/UITests/TestAppUITests/AccountLoginTests.swift +++ b/Tests/UITests/TestAppUITests/AccountLoginTests.swift @@ -29,8 +29,8 @@ final class AccountLoginTests: TestAppUITests { password: (passwordField, String(password.dropLast(2))) ) - XCTAssertTrue(XCUIApplication().alerts["Credentials do not match"].waitForExistence(timeout: 6.0)) - XCUIApplication().alerts["Credentials do not match"].scrollViews.otherElements.buttons["OK"].tap() + XCTAssertTrue(app.alerts["Credentials do not match"].waitForExistence(timeout: 10.0)) + app.alerts["Credentials do not match"].scrollViews.otherElements.buttons["OK"].tap() try delete( username: (usernameField, username.count), @@ -42,7 +42,7 @@ final class AccountLoginTests: TestAppUITests { password: (passwordField, password) ) - XCTAssertTrue(app.collectionViews.staticTexts[username].waitForExistence(timeout: 6.0)) + XCTAssertTrue(app.collectionViews.staticTexts[username].waitForExistence(timeout: 10.0)) } func testLoginEmailComponents() throws { @@ -62,7 +62,7 @@ final class AccountLoginTests: TestAppUITests { app.enter(value: String(username.dropLast(4)), in: usernameField) app.enter(value: password, in: passwordField, secureTextField: true) - XCTAssertTrue(app.staticTexts["The entered email is not correct."].exists) + XCTAssertTrue(app.staticTexts["The entered email is not correct."].waitForExistence(timeout: 1.0)) XCTAssertFalse(app.scrollViews.otherElements.buttons["Login, In progress"].waitForExistence(timeout: 0.5)) try delete( diff --git a/Tests/UITests/TestAppUITests/AccountResetPasswordTests.swift b/Tests/UITests/TestAppUITests/AccountResetPasswordTests.swift index 03c4d838..ba266896 100644 --- a/Tests/UITests/TestAppUITests/AccountResetPasswordTests.swift +++ b/Tests/UITests/TestAppUITests/AccountResetPasswordTests.swift @@ -53,9 +53,9 @@ final class AccountResetPasswordTests: TestAppUITests { app.enter(value: String(username.dropLast(4)), in: usernameField) - XCTAssertTrue(app.staticTexts["The entered email is not correct."].exists) + XCTAssertTrue(app.staticTexts["The entered email is not correct."].waitForExistence(timeout: 1.0)) - app.delete(count: username.dropLast(4).count, in: usernameField) + app.delete(count: username.count, in: usernameField) app.enter(value: username, in: usernameField) app.testPrimaryButton(enabled: true, title: buttonTitle, navigationBarButtonTitle: navigationBarButtonTitle) diff --git a/Tests/UITests/TestAppUITests/AccountSignUpTests.swift b/Tests/UITests/TestAppUITests/AccountSignUpTests.swift index b5481c2b..f8f6fa76 100644 --- a/Tests/UITests/TestAppUITests/AccountSignUpTests.swift +++ b/Tests/UITests/TestAppUITests/AccountSignUpTests.swift @@ -53,9 +53,9 @@ final class AccountSignUpTests: TestAppUITests { app.enter(value: String(username.dropLast(4)), in: usernameField) app.testPrimaryButton(enabled: false, title: "Sign Up") - XCTAssertTrue(app.staticTexts["The entered email is not correct."].exists) + XCTAssertTrue(app.staticTexts["The entered email is not correct."].waitForExistence(timeout: 1.0)) - app.delete(count: username.dropLast(4).count, in: usernameField) + app.delete(count: username.count, in: usernameField) } } @@ -85,7 +85,7 @@ final class AccountSignUpTests: TestAppUITests { app.enter(value: passwordRepeat, in: passwordRepeatField, secureTextField: true) app.testPrimaryButton(enabled: false, title: buttonTitle) - XCTAssertTrue(app.staticTexts["The entered passwords are not equal"].exists) + XCTAssertTrue(app.staticTexts["The entered passwords are not equal."].waitForExistence(timeout: 1.0)) app.delete(count: passwordRepeat.count, in: passwordRepeatField, secureTextField: true) passwordRepeat = password diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 539fd4cc..d6a3a099 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ 2F7B6CB4294C03C800FDC494 /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = TestApp.xctestplan; path = UITests.xcodeproj/TestApp.xctestplan; sourceTree = ""; }; 2F817A49294B152B00A39983 /* ViewsTestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsTestsView.swift; sourceTree = ""; }; 2F817A4B294B16A000A39983 /* ViewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsTests.swift; sourceTree = ""; }; + 2F87F9F02953EEB400810247 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 2F8A431429130B9B005D2B8F /* TestAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppView.swift; sourceTree = ""; }; 2F8A431629130BBC005D2B8F /* TestAppStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAppStandard.swift; sourceTree = ""; }; 2F8A431829130CA7005D2B8F /* TestAppTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppTestCase.swift; sourceTree = ""; }; @@ -221,6 +222,7 @@ 2F9F07ED29090AF500CDC598 /* Shared */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, 2F01E8CE291493560089C46B /* Info.plist */, + 2F87F9F02953EEB400810247 /* GoogleService-Info.plist */, 2F01E8CD291490B80089C46B /* TestApp.entitlements */, ); path = TestApp;