Skip to content

Commit

Permalink
Adapter & Firebase Improvements (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
PSchmiedmayer authored Feb 14, 2023
1 parent 1fe1079 commit f86fd8e
Show file tree
Hide file tree
Showing 22 changed files with 282 additions and 178 deletions.
9 changes: 9 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let package = Package(
.library(name: "FHIR", targets: ["FHIR"]),
.library(name: "FHIRToFirestoreAdapter", targets: ["FHIRToFirestoreAdapter"]),
.library(name: "FirestoreDataStorage", targets: ["FirestoreDataStorage"]),
.library(name: "FirestoreStoragePrefixUserIdAdapter", targets: ["FirestoreStoragePrefixUserIdAdapter"]),
.library(name: "FirebaseConfiguration", targets: ["FirebaseConfiguration"]),
.library(name: "FirebaseAccount", targets: ["FirebaseAccount"]),
.library(name: "HealthKitDataSource", targets: ["HealthKitDataSource"]),
Expand Down Expand Up @@ -131,6 +132,14 @@ let package = Package(
.product(name: "FirebaseFirestoreSwift", package: "firebase-ios-sdk")
]
),
.target(
name: "FirestoreStoragePrefixUserIdAdapter",
dependencies: [
.target(name: "CardinalKit"),
.target(name: "FirestoreDataStorage"),
.product(name: "FirebaseAuth", package: "firebase-ios-sdk")
]
),
.target(
name: "HealthKitDataSource",
dependencies: [
Expand Down
4 changes: 2 additions & 2 deletions Sources/Account/Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public struct Login<Header: View>: View {
}

/// - Parameter header: A SwiftUI `View` displayed as a header above all login buttons.
public init(header: Header) {
self.header = header
public init(@ViewBuilder header: () -> (Header)) {
self.header = header()
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Account/SignUp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public struct SignUp<Header: View>: View {
}

/// - Parameter header: A SwiftUI `View` displayed as a header above all login buttons.
public init(header: Header) {
self.header = header
public init(@ViewBuilder header: () -> (Header)) {
self.header = header()
}
}

Expand Down
19 changes: 19 additions & 0 deletions Sources/CardinalKit/Adapter/Adapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,22 @@ public protocol Adapter<InputElement, InputRemovalContext, OutputElement, Output
_ asyncSequence: some TypedAsyncSequence<DataChange<InputElement, InputRemovalContext>>
) async -> any TypedAsyncSequence<DataChange<OutputElement, OutputRemovalContext>>
}


extension Adapter {
/// Map a collection of `DataChange` elements using the adapter to an ``TypedAsyncSequence``.
/// - Parameter dataChanges: The data changes that should be transformed.
/// - Returns: Returns the mapped elements using the ``Adapter``'s ``transform(_:)`` function.
public func transformDataChanges(
_ dataChanges: [DataChange<InputElement, InputRemovalContext>]
) async -> any TypedAsyncSequence<DataChange<OutputElement, OutputRemovalContext>> {
await transform(
AsyncStream { continuation in
for dataChange in dataChanges {
continuation.yield(dataChange)
}
continuation.finish()
}
)
}
}
33 changes: 0 additions & 33 deletions Sources/FHIRToFirestoreAdapter/FHIRFirestoreElement.swift

This file was deleted.

25 changes: 0 additions & 25 deletions Sources/FHIRToFirestoreAdapter/FHIRFirestoreRemovalContext.swift

This file was deleted.

21 changes: 15 additions & 6 deletions Sources/FHIRToFirestoreAdapter/FHIRToFirestoreAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import CardinalKit
import FHIR
import FirestoreDataStorage
import Foundation


/// Adapts the output of the `FHIR` standard to be used with the `Firestore` data storage provider.
Expand All @@ -28,18 +30,25 @@ import FHIR
public actor FHIRToFirestoreAdapter: SingleValueAdapter {
public typealias InputElement = FHIR.BaseType
public typealias InputRemovalContext = FHIR.RemovalContext
public typealias OutputElement = FHIRFirestoreElement
public typealias OutputRemovalContext = FHIRFirestoreRemovalContext
public typealias OutputElement = FirestoreElement
public typealias OutputRemovalContext = FirestoreRemovalContext


public init() {}


public func transform(element: InputElement) throws -> FHIRFirestoreElement {
FHIRFirestoreElement(element)
public func transform(element: InputElement) throws -> FirestoreElement {
FirestoreElement(
id: element.id?.value?.string ?? UUID().uuidString,
collectionPath: ResourceProxy(with: element).resourceType,
element
)
}

public func transform(removalContext: InputRemovalContext) throws -> FHIRFirestoreRemovalContext {
FHIRFirestoreRemovalContext(removalContext)
public func transform(removalContext: InputRemovalContext) throws -> FirestoreRemovalContext {
FirestoreRemovalContext(
id: removalContext.id?.value?.string ?? UUID().uuidString,
collectionPath: removalContext.resourceType.rawValue
)
}
}
29 changes: 24 additions & 5 deletions Sources/FirebaseAccount/FirebaseAccountConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@

import Account
import CardinalKit
import FirebaseAuth
@_exported import class FirebaseAuth.User
import class FirebaseAuth.Auth
import protocol FirebaseAuth.AuthStateDidChangeListenerHandle
import FirebaseConfiguration
import FirebaseCore
import Foundation


/// <#Description#>
/// Configures Firebase Auth `AccountService`s that can be used in any views of the `Account` module.
///
/// The ``FirebaseAccountConfiguration`` offers a ``user`` property to access the current Firebase Auth user from, e.g., a SwiftUI view's environment:
/// ```
/// @EnvironmentObject var firebaseAccountConfiguration: FirebaseAccountConfiguration</* ... */>
/// ```
///
/// The ``FirebaseAccountConfiguration`` can, e.g., be used to to connect to the Firebase Auth emulator:
/// ```
/// class ExampleAppDelegate: CardinalKitAppDelegate {
/// override var configuration: Configuration {
/// Configuration(standard: /* ... */) {
/// FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099))
/// // ...
/// }
/// }
/// }
/// ```
public final class FirebaseAccountConfiguration<ComponentStandard: Standard>: Component, ObservableObject, ObservableObjectProvider {
@Dependency private var configureFirebaseApp: ConfigureFirebaseApp

Expand All @@ -35,10 +54,9 @@ public final class FirebaseAccountConfiguration<ComponentStandard: Standard>: Co
}


/// <#Description#>
/// - Parameters:
/// - emulatorSettings: <#emulatorSettings description#>
/// - authenticationMethods: <#authenticationMethods description#>
/// - emulatorSettings: The emulator settings. The default value is `nil`, connecting the FirebaseAccount module to the Firebase Auth cloud instance.
/// - authenticationMethods: The authentication methods that should be supported.
public init(
emulatorSettings: (host: String, port: Int)? = nil,
authenticationMethods: FirebaseAuthAuthenticationMethods = .all
Expand All @@ -64,6 +82,7 @@ public final class FirebaseAccountConfiguration<ComponentStandard: Standard>: Co
guard let user else {
Task {
await MainActor.run {
self.user = nil
self.account.signedIn = false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
//


/// <#Description#>
/// Definition of the authentication methods supported by the FirebaseAccount module.
public struct FirebaseAuthAuthenticationMethods: OptionSet {
/// <#Description#>
/// E-Mail and password-based authentication.
public static let emailAndPassword = FirebaseAuthAuthenticationMethods(rawValue: 1 << 0)

/// <#Description#>
/// All authentication methods.
public static let all: FirebaseAuthAuthenticationMethods = [.emailAndPassword]


Expand Down
14 changes: 14 additions & 0 deletions Sources/FirestoreDataStorage/AnyFirestoreElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// 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
//


/// Provides the nescessary information context for the ``Firestore`` date storage component to store a new element or update an existing element.
public protocol AnyFirestoreElement: Encodable, Identifiable, Sendable where ID == String {
/// The collection path where the ``FirestoreElement`` should be stored at.
var collectionPath: String { get }
}
14 changes: 14 additions & 0 deletions Sources/FirestoreDataStorage/AnyFirestoreRemovalContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// 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
//


/// Provides the nescessary removal context for the ``Firestore`` date storage component.
public protocol AnyFirestoreRemovalContext: Identifiable, Sendable where ID == String {
/// The collection path where the ``FirestoreElement`` is located at.
var collectionPath: String { get }
}
42 changes: 29 additions & 13 deletions Sources/FirestoreDataStorage/Firestore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import SwiftUI
/// }
/// ```
public actor Firestore<ComponentStandard: Standard>: Module, DataStorageProvider {
public typealias FirestoreAdapter = any FirestoreElementAdapter<ComponentStandard.BaseType, ComponentStandard.RemovalContext>
public typealias FirestoreAdapter = any Adapter<
ComponentStandard.BaseType,
ComponentStandard.RemovalContext,
FirestoreElement,
FirestoreRemovalContext
>


@Dependency private var configureFirebaseApp: ConfigureFirebaseApp
Expand All @@ -41,17 +46,20 @@ public actor Firestore<ComponentStandard: Standard>: Module, DataStorageProvider

/// - Parameter settings: The firestore settings according to the [Firebase Firestore Swift Package](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/FirestoreSettings)
public init(settings: FirestoreSettings = FirestoreSettings())
where ComponentStandard.BaseType: FirestoreElement, ComponentStandard.RemovalContext: FirestoreRemovalContext {
self.adapter = DefaultFirestoreElementAdapter()
where ComponentStandard.BaseType: AnyFirestoreElement, ComponentStandard.RemovalContext: AnyFirestoreRemovalContext {
self.adapter = DefaultFirestoreElementAdapter<ComponentStandard.BaseType, ComponentStandard.RemovalContext>()
self.settings = settings
}

/// - Parameters:
/// - adapter: A chain of adapter from your standard basetype and
/// removal context to ``FirestoreElement`` and ``FirestoreRemovalContext`` instances.
/// - settings: The firestore settings according to the [Firebase Firestore Swift Package](https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/FirestoreSettings)
public init(adapter: FirestoreAdapter, settings: FirestoreSettings = FirestoreSettings()) {
self.adapter = adapter
public init(
@AdapterBuilder<FirestoreElement, FirestoreRemovalContext> adapter: () -> (FirestoreAdapter),
settings: FirestoreSettings = FirestoreSettings()
) {
self.adapter = adapter()
self.settings = settings
}

Expand All @@ -63,16 +71,25 @@ public actor Firestore<ComponentStandard: Standard>: Module, DataStorageProvider
}

public func process(_ element: DataChange<ComponentStandard.BaseType, ComponentStandard.RemovalContext>) async throws {
switch element {
try await process(asyncSequence: adapter.transformDataChanges([element]))
}

private func process(asyncSequence: some TypedAsyncSequence<DataChange<FirestoreElement, FirestoreRemovalContext>>) async throws {
for try await dataChange in asyncSequence {
try await process(dataChange: dataChange)
}
}

private func process(dataChange: DataChange<FirestoreElement, FirestoreRemovalContext>) async throws {
switch dataChange {
case let .addition(element):
let firebaseElement = try await adapter.transform(element: element)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
do {
let firestore = FirebaseFirestore.Firestore.firestore()
try firestore
.collection(firebaseElement.collectionPath)
.document(firebaseElement.id)
.setData(from: firebaseElement, merge: false) { error in
.collection(element.collectionPath)
.document(element.id)
.setData(from: element, merge: false) { error in
if let error {
continuation.resume(throwing: error)
} else {
Expand All @@ -84,12 +101,11 @@ public actor Firestore<ComponentStandard: Standard>: Module, DataStorageProvider
}
}
case let .removal(removalContext):
let firebaseRemovalContext = try await adapter.transform(removalContext: removalContext)
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
let firestore = FirebaseFirestore.Firestore.firestore()
firestore
.collection(firebaseRemovalContext.collectionPath)
.document(firebaseRemovalContext.id)
.collection(removalContext.collectionPath)
.document(removalContext.id)
.delete { error in
if let error {
continuation.resume(throwing: error)
Expand Down
31 changes: 27 additions & 4 deletions Sources/FirestoreDataStorage/FirestoreElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,31 @@
//


/// Provides the nescessary information context for the ``Firestore`` date storage component to store a new element or update an existing element.
public protocol FirestoreElement: Encodable, Identifiable, Sendable where ID == String {
/// The collection path where the ``FirestoreElement`` should be stored at.
var collectionPath: String { get }
/// Provides a mapping from a FHIR `Resource` to a type conforming to `FirestoreElement`.
public struct FirestoreElement: Encodable, Identifiable, Sendable {
/// The identifier that is used for the Firestore document.
public let id: String
/// The collection path that is used for the Firestore document.
public var collectionPath: String
let body: Encodable & Sendable


/// - Parameters:
/// - id: The identifier that is used for the Firestore document.
/// - collectionPath: The collection path that is used for the Firestore document.
/// - body: The body of the document, including all fields as defined by the `Encodable` implementation.
public init<Body: Encodable & Sendable>(
id: String,
collectionPath: String,
_ body: Body
) {
self.id = id
self.collectionPath = collectionPath
self.body = body
}


public func encode(to encoder: Encoder) throws {
try body.encode(to: encoder)
}
}
Loading

0 comments on commit f86fd8e

Please sign in to comment.