From ad8c18028c387b41886c31163d9a6f59b28b497e Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 08:07:54 -0400 Subject: [PATCH 01/12] working on cleaning up code --- .swift-format | 16 +-- .swiftlint.yml | 131 ++++++++++++++++++ Mintfile | 4 +- Scripts/lint.sh | 28 +++- Sources/DataThespian/Database.swift | 2 + Sources/DataThespian/DatabaseChangeType.swift | 5 +- Sources/DataThespian/DatabaseKey.swift | 2 +- .../DataThespian/ManagedObjectMetadata.swift | 3 +- Sources/DataThespian/ModelActorDatabase.swift | 3 + Sources/DataThespian/ModelID.swift | 2 +- Sources/DataThespian/NSManagedObjectID.swift | 10 +- Sources/DataThespian/PublishingAgent.swift | 4 +- .../DataThespian/RegistrationCollection.swift | 3 +- .../{Logging.swift => ThespianLogging.swift} | 0 .../DataThespianTests/DataThespianTests.swift | 5 +- 15 files changed, 190 insertions(+), 28 deletions(-) create mode 100644 .swiftlint.yml rename Sources/DataThespian/{Logging.swift => ThespianLogging.swift} (100%) diff --git a/.swift-format b/.swift-format index 4f562bf..d5fd187 100644 --- a/.swift-format +++ b/.swift-format @@ -6,11 +6,11 @@ "spaces" : 2 }, "indentConditionalCompilationBlocks" : true, - "indentSwitchCaseLabels" : true, - "lineBreakAroundMultilineExpressionChainComponents" : true, - "lineBreakBeforeControlFlowKeywords" : true, - "lineBreakBeforeEachArgument" : true, - "lineBreakBeforeEachGenericRequirement" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, "lineLength" : 100, "maximumBlankLines" : 1, "multiElementCollectionTrailingCommas" : true, @@ -20,7 +20,7 @@ ] }, "prioritizeKeepingFunctionOutputTogether" : false, - "respectsExistingLineBreaks" : false, + "respectsExistingLineBreaks" : true, "rules" : { "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLiteralForEmptyCollectionInit" : false, @@ -42,7 +42,7 @@ "NoCasesWithOnlyFallthrough" : true, "NoEmptyTrailingClosureParentheses" : true, "NoLabelsInCasePatterns" : true, - "NoLeadingUnderscores" : false, + "NoLeadingUnderscores" : true, "NoParensAroundConditions" : true, "NoPlaygroundLiterals" : true, "NoVoidReturnOnFunctionSignature" : true, @@ -67,4 +67,4 @@ "spacesAroundRangeFormationOperators" : false, "tabWidth" : 2, "version" : 1 -} +} \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..5fb47cf --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,131 @@ +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + # - fallthrough + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping +# - function_default_parameter_at_end + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent +# - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool +# - trailing_closure + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 +file_length: + warning: 225 + error: 300 +function_body_length: + - 50 + - 76 +function_parameter_count: 8 +line_length: + - 108 + - 200 +closure_body_length: + - 50 + - 60 +identifier_name: + excluded: + - id + - no +excluded: + - DerivedData + - .build + - Package.swift + - Package +indentation_width: + indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment + - closure_parameter_position + - trailing_comma + - opening_brace \ No newline at end of file diff --git a/Mintfile b/Mintfile index 7060932..3f76adc 100644 --- a/Mintfile +++ b/Mintfile @@ -1,2 +1,4 @@ -apple/swift-format@4b62459 +swiftlang/swift-format@600.0.0 +realm/SwiftLint@0.57.0 +a7ex/xcresultparser@1.7.2 peripheryapp/periphery@2.20.0 \ No newline at end of file diff --git a/Scripts/lint.sh b/Scripts/lint.sh index c071a32..6ca2f13 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -1,5 +1,17 @@ #!/bin/sh +set -o pipefail + +ERRORS=0 + +run_command() { + if [ "$LINT_MODE" == "STRICT" ]; then + "$@" || ERRORS=$((ERRORS + 1)) + else + "$@" + fi +} + if [ "$ACTION" == "install" ]; then if [ -n "$SRCROOT" ]; then exit @@ -23,9 +35,11 @@ fi if [ "$LINT_MODE" == "NONE" ]; then exit elif [ "$LINT_MODE" == "STRICT" ]; then - SWIFTFORMAT_OPTIONS="--strict" + SWIFTFORMAT_OPTIONS="--strict --configuration .swift-format" + SWIFTLINT_OPTIONS="--strict" else - SWIFTFORMAT_OPTIONS="" + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="" fi /opt/homebrew/bin/mint bootstrap @@ -37,14 +51,18 @@ if [ "$LINT_MODE" == "INSTALL" ]; then fi if [ -z "$CI" ]; then - $MINT_RUN swift-format format --recursive --parallel --in-place $PACKAGE_DIR/Sources + run_command $MINT_RUN swiftlint --fix + pushd $PACKAGE_DIR + run_command $MINT_RUN swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests + popd else set -e fi $PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "DataThespian" -$MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS $PACKAGE_DIR/Sources +run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS pushd $PACKAGE_DIR -$MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check +run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests +run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check popd \ No newline at end of file diff --git a/Sources/DataThespian/Database.swift b/Sources/DataThespian/Database.swift index 93a0a69..4a189d6 100644 --- a/Sources/DataThespian/Database.swift +++ b/Sources/DataThespian/Database.swift @@ -35,6 +35,7 @@ public protocol Database: Sendable { func save() async throws + @discardableResult func delete( _ modelType: T.Type, withID id: PersistentIdentifier @@ -49,6 +50,7 @@ _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U ) async throws -> U + func fetch( _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, diff --git a/Sources/DataThespian/DatabaseChangeType.swift b/Sources/DataThespian/DatabaseChangeType.swift index cef6ea5..6efecd8 100644 --- a/Sources/DataThespian/DatabaseChangeType.swift +++ b/Sources/DataThespian/DatabaseChangeType.swift @@ -33,7 +33,10 @@ public enum DatabaseChangeType: CaseIterable, Sendable { case updated #if canImport(SwiftData) var keyPath: KeyPath> { - switch self { case .inserted: \.inserted case .deleted: \.deleted case .updated: \.updated + switch self { + case .inserted: \.inserted + case .deleted: \.deleted + case .updated: \.updated } } #endif diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index 6ca2a39..c817563 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -36,7 +36,7 @@ public import SwiftUI fileprivate struct DefaultDatabase: Database { - public func save() async throws { + func save() async throws { assertionFailure("No Database Set.") throw NotImplmentedError.instance } diff --git a/Sources/DataThespian/ManagedObjectMetadata.swift b/Sources/DataThespian/ManagedObjectMetadata.swift index be45fd3..972898c 100644 --- a/Sources/DataThespian/ManagedObjectMetadata.swift +++ b/Sources/DataThespian/ManagedObjectMetadata.swift @@ -46,8 +46,7 @@ extension ManagedObjectMetadata { init?(managedObject: NSManagedObject) { let persistentIdentifier: PersistentIdentifier - do { persistentIdentifier = try managedObject.objectID.persistentIdentifier() } - catch { + do { persistentIdentifier = try managedObject.objectID.persistentIdentifier() } catch { assertionFailure(error: error) return nil } diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index 09532ad..0368a73 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -92,8 +92,11 @@ assert(isMainThread: false) try self.modelContext.save() } + public nonisolated let modelExecutor: any SwiftData.ModelExecutor + public nonisolated let modelContainer: SwiftData.ModelContainer + public init(modelContainer: SwiftData.ModelContainer, autosaveEnabled: Bool = false) { let modelContext = ModelContext(modelContainer) modelContext.autosaveEnabled = autosaveEnabled diff --git a/Sources/DataThespian/ModelID.swift b/Sources/DataThespian/ModelID.swift index e66ed22..5bdf32b 100644 --- a/Sources/DataThespian/ModelID.swift +++ b/Sources/DataThespian/ModelID.swift @@ -33,7 +33,7 @@ public import SwiftData public struct ModelID: Sendable, Identifiable { - public var id: PersistentIdentifier.ID { return persistentIdentifier.id } + public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier enum Error: Swift.Error { case notFound(PersistentIdentifier) } diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/NSManagedObjectID.swift index 73bcc48..0b6fcc4 100644 --- a/Sources/DataThespian/NSManagedObjectID.swift +++ b/Sources/DataThespian/NSManagedObjectID.swift @@ -89,11 +89,13 @@ ) let encoder = JSONEncoder() let data: Data - do { data = try encoder.encode(json) } - catch let error as EncodingError { throw PersistentIdentifierError.encodingError(error) } + do { data = try encoder.encode(json) } catch let error as EncodingError { + throw PersistentIdentifierError.encodingError(error) + } let decoder = JSONDecoder() - do { return try decoder.decode(PersistentIdentifier.self, from: data) } - catch let error as DecodingError { throw PersistentIdentifierError.decodingError(error) } + do { return try decoder.decode(PersistentIdentifier.self, from: data) } catch let error + as DecodingError + { throw PersistentIdentifierError.decodingError(error) } } } diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/PublishingAgent.swift index 635a763..deedbef 100644 --- a/Sources/DataThespian/PublishingAgent.swift +++ b/Sources/DataThespian/PublishingAgent.swift @@ -71,7 +71,9 @@ private func updateScriptionStatus(byEvent event: SubscriptionEvent) { let oldCount = subscriptionCount let delta: Int = - switch event { case .cancel: -1 case .subscribe: 1 + switch event { + case .cancel: -1 + case .subscribe: 1 } subscriptionCount += delta diff --git a/Sources/DataThespian/RegistrationCollection.swift b/Sources/DataThespian/RegistrationCollection.swift index 959d246..43cc92e 100644 --- a/Sources/DataThespian/RegistrationCollection.swift +++ b/Sources/DataThespian/RegistrationCollection.swift @@ -55,8 +55,7 @@ if let registration = registrations[id], force { Self.logger.debug("Overwriting \(id). Already exists.") await registration.finish() - } - else if registrations[id] != nil { + } else if registrations[id] != nil { Self.logger.debug("Can't register \(id). Already exists.") return } diff --git a/Sources/DataThespian/Logging.swift b/Sources/DataThespian/ThespianLogging.swift similarity index 100% rename from Sources/DataThespian/Logging.swift rename to Sources/DataThespian/ThespianLogging.swift diff --git a/Tests/DataThespianTests/DataThespianTests.swift b/Tests/DataThespianTests/DataThespianTests.swift index d6315c8..57a3cd9 100644 --- a/Tests/DataThespianTests/DataThespianTests.swift +++ b/Tests/DataThespianTests/DataThespianTests.swift @@ -1,6 +1,7 @@ -@testable import DataThespian import Testing +@testable import DataThespian + @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. + // Write your test here and use APIs like `#expect(...)` to check expected conditions. } From e0cccda91a688b2ec40d0a5c9a78e36feda50455 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 09:22:40 -0400 Subject: [PATCH 02/12] working on example project --- .../AccentColor.colorset/Contents.json | 11 +++ .../AppIcon.appiconset/Contents.json | 58 ++++++++++++++ Example/Sources/Assets.xcassets/Contents.json | 6 ++ Example/Sources/ContentView.swift | 77 +++++++++++++++++++ .../Sources/DataThespianExample.entitlements | 10 +++ Example/Sources/DataThespianExampleApp.swift | 32 ++++++++ Example/Sources/Item.swift | 18 +++++ .../Preview Assets.xcassets/Contents.json | 6 ++ Example/Support/.gitkeep | 0 Example/Support/Info.plist | 24 ++++++ Sources/DataThespian/BackgroundDatabase.swift | 4 + Sources/DataThespian/Database.swift | 2 + Sources/DataThespian/DatabaseKey.swift | 4 + Sources/DataThespian/ModelActorDatabase.swift | 11 +++ project.yml | 17 ++++ 15 files changed, 280 insertions(+) create mode 100644 Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/Sources/Assets.xcassets/Contents.json create mode 100644 Example/Sources/ContentView.swift create mode 100644 Example/Sources/DataThespianExample.entitlements create mode 100644 Example/Sources/DataThespianExampleApp.swift create mode 100644 Example/Sources/Item.swift create mode 100644 Example/Sources/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Example/Support/.gitkeep create mode 100644 Example/Support/Info.plist diff --git a/Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Example/Sources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/Example/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/Assets.xcassets/Contents.json b/Example/Sources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Sources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift new file mode 100644 index 0000000..a6728f3 --- /dev/null +++ b/Example/Sources/ContentView.swift @@ -0,0 +1,77 @@ +// +// ContentView.swift +// DataThespianExample +// +// Created by Leo Dion on 10/10/24. +// + +import SwiftUI +import SwiftData +import DataThespian + +struct ContentView: View { + + private var items = [Item]() + private let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self)) + + var body: some View { + NavigationSplitView { + List { + ForEach(items) { item in + NavigationLink { + Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") + } label: { + Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) + } + } + .onDelete(perform: deleteItems) + } + .navigationSplitViewColumnWidth(min: 180, ideal: 200) + .toolbar { + ToolbarItem { + Button(action: addItem) { + Label("Add Item", systemImage: "plus") + } + } + } + } detail: { + Text("Select an item") + }.onAppear { + + } + } + + private func addItem() { + + Task { + await self.database.withModelContext { modelContext in + let newItem = Item(timestamp: Date()) + modelContext.insert(newItem) + } + } + + } + + private func deleteItems(offsets: IndexSet) { + + Task { + + for index in offsets { + let model = ModelID(items[index]) + await self.database.withModelContext { modelContext in + + if let model : Item = modelContext.registeredModel(for: model.persistentIdentifier) { + modelContext.delete(model) + } + } + } + + + } + } +} + +#Preview { + ContentView() + .modelContainer(for: Item.self, inMemory: true) +} diff --git a/Example/Sources/DataThespianExample.entitlements b/Example/Sources/DataThespianExample.entitlements new file mode 100644 index 0000000..18aff0c --- /dev/null +++ b/Example/Sources/DataThespianExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift new file mode 100644 index 0000000..558ff14 --- /dev/null +++ b/Example/Sources/DataThespianExampleApp.swift @@ -0,0 +1,32 @@ +// +// DataThespianExampleApp.swift +// DataThespianExample +// +// Created by Leo Dion on 10/10/24. +// + +import SwiftUI +import SwiftData + +@main +struct DataThespianExampleApp: App { + var sharedModelContainer: ModelContainer = { + let schema = Schema([ + Item.self, + ]) + let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + + do { + return try ModelContainer(for: schema, configurations: [modelConfiguration]) + } catch { + fatalError("Could not create ModelContainer: \(error)") + } + }() + + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(sharedModelContainer) + } +} diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift new file mode 100644 index 0000000..20d3214 --- /dev/null +++ b/Example/Sources/Item.swift @@ -0,0 +1,18 @@ +// +// Item.swift +// DataThespianExample +// +// Created by Leo Dion on 10/10/24. +// + +import Foundation +import SwiftData + +@Model +final class Item { + var timestamp: Date + + init(timestamp: Date) { + self.timestamp = timestamp + } +} diff --git a/Example/Sources/Preview Content/Preview Assets.xcassets/Contents.json b/Example/Sources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Example/Sources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Support/.gitkeep b/Example/Support/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Example/Support/Info.plist b/Example/Support/Info.plist new file mode 100644 index 0000000..edc62ca --- /dev/null +++ b/Example/Support/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + + + diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index e7edaac..17009b1 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -35,6 +35,10 @@ import SwiftUI public final class BackgroundDatabase: Database { + public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T { + return try await self.database.withModelContext(closure) + } + public func delete(_ modelType: (some PersistentModel).Type, withID id: PersistentIdentifier) async -> Bool { await self.database.delete(modelType, withID: id) } diff --git a/Sources/DataThespian/Database.swift b/Sources/DataThespian/Database.swift index 4a189d6..4c8356a 100644 --- a/Sources/DataThespian/Database.swift +++ b/Sources/DataThespian/Database.swift @@ -36,6 +36,8 @@ public protocol Database: Sendable { func save() async throws + func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T + @discardableResult func delete( _ modelType: T.Type, withID id: PersistentIdentifier diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index c817563..add8b67 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -36,6 +36,10 @@ public import SwiftUI fileprivate struct DefaultDatabase: Database { + func withModelContext(_ closure: (ModelContext) throws -> T) async rethrows -> T { + assertionFailure("No Database Set.") + fatalError("No Database Set.") + } func save() async throws { assertionFailure("No Database Set.") throw NotImplmentedError.instance diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index 0368a73..7b9fc4e 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -33,6 +33,12 @@ public import SwiftData +public enum DeleteSelector : Sendable { + case persistentModelID(PersistentIdentifier) + case predicate(Predicate) + case all +} + public actor ModelActorDatabase: Database, Loggable { public func delete(_: T.Type, withID id: PersistentIdentifier) async -> Bool { @@ -52,6 +58,11 @@ self.modelContext.insert(model) return model.persistentModelID } + + public func withModelContext(_ closure: @Sendable (ModelContext) throws -> T) async rethrows -> T { + let modelContext = self.modelContext + return try closure(modelContext) + } public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, diff --git a/project.yml b/project.yml index bee9fb8..ac7778c 100644 --- a/project.yml +++ b/project.yml @@ -11,3 +11,20 @@ aggregateTargets: name: Lint basedOnDependencyAnalysis: false schemes: {} +targets: + DataThespianExample: + type: application + platform: macOS + dependencies: + - package: DataThespian + product: DataThespian + sources: + - path: "Example/Sources" + - path: "Example/Support" + info: + path: Example/Support/Info.plist + properties: + CFBundlePackageType: APPL + ITSAppUsesNonExemptEncryption: false + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) \ No newline at end of file From 650ef4e1e9f4e24edcc78cfac46d37d5850b2765 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 10:42:38 -0400 Subject: [PATCH 03/12] working through example --- Example/Sources/ContentView.swift | 50 ++++++++++++++++++++++-------- Sources/DataThespian/ModelID.swift | 6 +++- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index a6728f3..5c0adc3 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -8,11 +8,25 @@ import SwiftUI import SwiftData import DataThespian +import Combine +struct ItemModel : Identifiable { + internal init(item : Item) { + self.init(id: item.persistentModelID, timestamp: item.timestamp) + } + internal init(id: PersistentIdentifier, timestamp: Date) { + self.id = id + self.timestamp = timestamp + } + + let id : PersistentIdentifier + let timestamp: Date +} struct ContentView: View { - - private var items = [Item]() - private let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self)) + private let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) + @State private var items = [ItemModel]() + @State private var newItem: AnyCancellable? + private static let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self), autosaveEnabled: true) var body: some View { NavigationSplitView { @@ -37,16 +51,24 @@ struct ContentView: View { } detail: { Text("Select an item") }.onAppear { - + self.newItem = self.databaseChangePublicist(id: "contentView").sink { changes in + Task { + self.items = try await Self.database.withModelContext({ modelContext in + let items = try modelContext.fetch(FetchDescriptor()) + return items.map(ItemModel.init) + }) + } + } } } private func addItem() { Task { - await self.database.withModelContext { modelContext in + try await Self.database.withModelContext { modelContext in let newItem = Item(timestamp: Date()) modelContext.insert(newItem) + try modelContext.save() } } @@ -55,15 +77,17 @@ struct ContentView: View { private func deleteItems(offsets: IndexSet) { Task { - - for index in offsets { - let model = ModelID(items[index]) - await self.database.withModelContext { modelContext in - - if let model : Item = modelContext.registeredModel(for: model.persistentIdentifier) { - modelContext.delete(model) + let models = offsets + .compactMap{items[$0].id} + .map(ModelID.init(persistentIdentifier: )) + try await Self.database.withModelContext { modelContext in + let items : [Item] = models.compactMap{ + modelContext.registeredModel(for: $0.persistentIdentifier) } - } + for item in items { + modelContext.delete(item) + } + try modelContext.save() } diff --git a/Sources/DataThespian/ModelID.swift b/Sources/DataThespian/ModelID.swift index 5bdf32b..5d41ea1 100644 --- a/Sources/DataThespian/ModelID.swift +++ b/Sources/DataThespian/ModelID.swift @@ -32,7 +32,11 @@ import Foundation public import SwiftData - public struct ModelID: Sendable, Identifiable { +public struct ModelID: Sendable, Identifiable { + public init(persistentIdentifier: PersistentIdentifier) { + self.persistentIdentifier = persistentIdentifier + } + public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier From aa669da807e95cbba04deaa1d3d4c692cecbe862 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 11:06:36 -0400 Subject: [PATCH 04/12] working example --- Example/Sources/ContentView.swift | 27 ++++++++++++------ Example/Sources/DataThespianExampleApp.swift | 30 +++++++++++--------- project.yml | 3 ++ 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 5c0adc3..7fc7232 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -23,12 +23,19 @@ struct ItemModel : Identifiable { let timestamp: Date } struct ContentView: View { - private let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) + private static let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) @State private var items = [ItemModel]() @State private var newItem: AnyCancellable? private static let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self), autosaveEnabled: true) - var body: some View { + fileprivate func updateItems() async throws { + self.items = try await Self.database.withModelContext({ modelContext in + let items = try modelContext.fetch(FetchDescriptor()) + return items.map(ItemModel.init) + }) + } + + var body: some View { NavigationSplitView { List { ForEach(items) { item in @@ -51,14 +58,14 @@ struct ContentView: View { } detail: { Text("Select an item") }.onAppear { - self.newItem = self.databaseChangePublicist(id: "contentView").sink { changes in + self.newItem = Self.databaseChangePublicist(id: "contentView").sink { changes in Task { - self.items = try await Self.database.withModelContext({ modelContext in - let items = try modelContext.fetch(FetchDescriptor()) - return items.map(ItemModel.init) - }) + try await updateItems() } } + Task { + try await updateItems() + } } } @@ -66,7 +73,8 @@ struct ContentView: View { Task { try await Self.database.withModelContext { modelContext in - let newItem = Item(timestamp: Date()) + let timestamp = Date() + let newItem = Item(timestamp: timestamp) modelContext.insert(newItem) try modelContext.save() } @@ -84,6 +92,7 @@ struct ContentView: View { let items : [Item] = models.compactMap{ modelContext.registeredModel(for: $0.persistentIdentifier) } + assert(items.count == offsets.count) for item in items { modelContext.delete(item) } @@ -97,5 +106,5 @@ struct ContentView: View { #Preview { ContentView() - .modelContainer(for: Item.self, inMemory: true) + //.modelContainer(for: Item.self, inMemory: true) } diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift index 558ff14..95cc78a 100644 --- a/Example/Sources/DataThespianExampleApp.swift +++ b/Example/Sources/DataThespianExampleApp.swift @@ -7,26 +7,30 @@ import SwiftUI import SwiftData +import DataThespian @main struct DataThespianExampleApp: App { - var sharedModelContainer: ModelContainer = { - let schema = Schema([ - Item.self, - ]) - let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - - do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) - } catch { - fatalError("Could not create ModelContainer: \(error)") - } - }() + init () { + DataMonitor.shared.begin(with: []) + } +// var sharedModelContainer: ModelContainer = { +// let schema = Schema([ +// Item.self, +// ]) +// let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) +// +// do { +// return try ModelContainer(for: schema, configurations: [modelConfiguration]) +// } catch { +// fatalError("Could not create ModelContainer: \(error)") +// } +// }() var body: some Scene { WindowGroup { ContentView() } - .modelContainer(sharedModelContainer) +// .modelContainer(sharedModelContainer) } } diff --git a/project.yml b/project.yml index ac7778c..c3155e9 100644 --- a/project.yml +++ b/project.yml @@ -21,6 +21,9 @@ targets: sources: - path: "Example/Sources" - path: "Example/Support" + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.demo.DataThespianExample info: path: Example/Support/Info.plist properties: From f4be17defbaadbce0703bea359fcf043da0a9b87 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 11:44:51 -0400 Subject: [PATCH 05/12] fixed delete --- Example/Sources/ContentView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 7fc7232..64c60c3 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -53,6 +53,7 @@ struct ContentView: View { Button(action: addItem) { Label("Add Item", systemImage: "plus") } + } } } detail: { @@ -90,7 +91,7 @@ struct ContentView: View { .map(ModelID.init(persistentIdentifier: )) try await Self.database.withModelContext { modelContext in let items : [Item] = models.compactMap{ - modelContext.registeredModel(for: $0.persistentIdentifier) + modelContext.model(for: $0.persistentIdentifier) as? Item } assert(items.count == offsets.count) for item in items { From b93cace5e9032d6e906161c11b5e8b9d2d28ebcb Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 16:21:50 -0400 Subject: [PATCH 06/12] finishing sample app --- Example/Sources/ContentObject.swift | 122 +++++++++++++++++ Example/Sources/ContentView.swift | 134 +++++++------------ Example/Sources/DataThespianExampleApp.swift | 19 +-- Example/Sources/ItemModel.swift | 34 +++++ 4 files changed, 208 insertions(+), 101 deletions(-) create mode 100644 Example/Sources/ContentObject.swift create mode 100644 Example/Sources/ItemModel.swift diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift new file mode 100644 index 0000000..8b7b7a3 --- /dev/null +++ b/Example/Sources/ContentObject.swift @@ -0,0 +1,122 @@ +// +// ContentObject.swift +// DataThespian +// +// Created by Leo Dion on 10/10/24. +// + +import Foundation +import SwiftData +import DataThespian +import Combine + + + +@Observable +@MainActor +class ContentObject { + internal let databaseChangePublisher = PassthroughSubject() + private var databaseChangeCancellable : AnyCancellable? + private var databaseChangeSubscription: AnyCancellable? + private var database : (any Database)? + internal private(set) var items = [ItemModel]() + internal var selectedItemsID: Set = [] + + + var selectedItems : [ItemModel] { + let selectedItemsID = self.selectedItemsID + return try! self.items.filter(#Predicate { + selectedItemsID.contains($0.id) + }) + } + private var newItem: AnyCancellable? + var error: (any Error)? + + private func beginUpdateItems() { + Task { + do { + try await self.updateItems() + } catch { + self.error = error + } + } + } + fileprivate func updateItems() async throws { + guard let database else { + return + } + self.items = try await database.withModelContext({ modelContext in + let items = try modelContext.fetch(FetchDescriptor()) + return items.map(ItemModel.init) + }) + } + + internal init () { + self.databaseChangeSubscription = self.databaseChangePublisher.sink { _ in + self.beginUpdateItems() + } + } + + internal func initialize(withDatabase database: any Database, databaseChangePublisher: DatabaseChangePublicist) { + self.database = database + self.databaseChangeCancellable = databaseChangePublisher(id: "contentView") + .subscribe(self.databaseChangePublisher) + self.beginUpdateItems() + } + + + + fileprivate static func deleteModels( _ models: [ModelID], from database: (any Database)) async throws { + try await database.withModelContext { modelContext in + let items : [Item] = models.compactMap{ + modelContext.model(for: $0.persistentIdentifier) as? Item + } + dump(items.first?.persistentModelID) + assert(items.count == models.count) + for item in items { + modelContext.delete(item) + } + try modelContext.save() + } + } + internal func deleteSelectedItems() { + let models = self.selectedItems.map { + ModelID.init(persistentIdentifier: $0.id) + } + self.deleteItems(models) + } + internal func deleteItems(offsets: IndexSet) { + + let models = offsets + .compactMap{items[$0].id} + .map(ModelID.init(persistentIdentifier: )) + + assert(models.count == offsets.count) + + self.deleteItems(models) + } + + internal func deleteItems(_ models: [ModelID]) { + guard let database else { + return + } + Task { + try await Self.deleteModels(models, from: database) + } + } + + internal func addItem(withDate date: Date = .init()) { + guard let database else { + return + } + Task { + try await database.withModelContext { modelContext in + + let newItem = Item(timestamp: date) + modelContext.insert(newItem) + dump(newItem.persistentModelID) + try modelContext.save() + } + } + } +} diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 64c60c3..93729a3 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -10,102 +10,62 @@ import SwiftData import DataThespian import Combine -struct ItemModel : Identifiable { - internal init(item : Item) { - self.init(id: item.persistentModelID, timestamp: item.timestamp) - } - internal init(id: PersistentIdentifier, timestamp: Date) { - self.id = id - self.timestamp = timestamp - } - - let id : PersistentIdentifier - let timestamp: Date -} -struct ContentView: View { - private static let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) - @State private var items = [ItemModel]() - @State private var newItem: AnyCancellable? - private static let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self), autosaveEnabled: true) - fileprivate func updateItems() async throws { - self.items = try await Self.database.withModelContext({ modelContext in - let items = try modelContext.fetch(FetchDescriptor()) - return items.map(ItemModel.init) - }) - } + +struct ContentView: View { + @State var object = ContentObject() + @Environment(\.database) var database + @Environment(\.databaseChangePublicist) var databaseChangePublisher + + var body: some View { - NavigationSplitView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") - } label: { - Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) - } - } - .onDelete(perform: deleteItems) - } - .navigationSplitViewColumnWidth(min: 180, ideal: 200) - .toolbar { - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") + NavigationSplitView { + List(selection: self.$object.selectedItemsID) { + ForEach(object.items) { item in + Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) + } + .onDelete(perform: object.deleteItems) + } + .navigationSplitViewColumnWidth(min: 180, ideal: 200) + .toolbar { + + ToolbarItem { + + Button(action: addItem) { + Label("Add Item", systemImage: "plus") } - - } - } - } detail: { - Text("Select an item") - }.onAppear { - self.newItem = Self.databaseChangePublicist(id: "contentView").sink { changes in - Task { - try await updateItems() - } - } - Task { - try await updateItems() - } } - } - - private func addItem() { - - Task { - try await Self.database.withModelContext { modelContext in - let timestamp = Date() - let newItem = Item(timestamp: timestamp) - modelContext.insert(newItem) - try modelContext.save() - } - } - - } - - private func deleteItems(offsets: IndexSet) { - - Task { - let models = offsets - .compactMap{items[$0].id} - .map(ModelID.init(persistentIdentifier: )) - try await Self.database.withModelContext { modelContext in - let items : [Item] = models.compactMap{ - modelContext.model(for: $0.persistentIdentifier) as? Item - } - assert(items.count == offsets.count) - for item in items { - modelContext.delete(item) - } - try modelContext.save() + ToolbarItem { + Button(action: object.deleteSelectedItems) { + Label("Delete Selected Items", systemImage: "trash") } - - + } + } + } detail: { + let selectedItems = object.selectedItems + if selectedItems.count > 1 { + Text("Multiple Selected") + } else if let item = selectedItems.first { + Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") + } else { + Text("Select an item") } + + }.onAppear { + self.object.initialize(withDatabase: database, databaseChangePublisher: databaseChangePublisher) } + } + + private func addItem() { + self.addItem(withDate: .init()) + } + private func addItem(withDate date: Date ) { + self.object.addItem(withDate: .init()) + } + } #Preview { - ContentView() - //.modelContainer(for: Item.self, inMemory: true) + ContentView() } diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift index 95cc78a..c5b3c9d 100644 --- a/Example/Sources/DataThespianExampleApp.swift +++ b/Example/Sources/DataThespianExampleApp.swift @@ -11,26 +11,17 @@ import DataThespian @main struct DataThespianExampleApp: App { + private static let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) + private static let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self), autosaveEnabled: true) + init () { DataMonitor.shared.begin(with: []) } -// var sharedModelContainer: ModelContainer = { -// let schema = Schema([ -// Item.self, -// ]) -// let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) -// -// do { -// return try ModelContainer(for: schema, configurations: [modelConfiguration]) -// } catch { -// fatalError("Could not create ModelContainer: \(error)") -// } -// }() - var body: some Scene { WindowGroup { ContentView() } -// .modelContainer(sharedModelContainer) + .database(Self.database) + .environment(\.databaseChangePublicist, Self.databaseChangePublicist) } } diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemModel.swift new file mode 100644 index 0000000..4e79e3b --- /dev/null +++ b/Example/Sources/ItemModel.swift @@ -0,0 +1,34 @@ +// +// ItemModel.swift +// DataThespian +// +// Created by Leo Dion on 10/10/24. +// + +import Foundation +import DataThespian +import SwiftData + + + +struct ItemModel : Identifiable { + private init(model: ModelID, timestamp: Date) { + self.model = model + self.timestamp = timestamp + } + + internal init(item : Item) { + self.init(model: .init(item), timestamp: item.timestamp) + } + @available(*, deprecated) + internal init(id: PersistentIdentifier, timestamp: Date) { + self.model = .init(persistentIdentifier: id) + self.timestamp = timestamp + } + + var id : PersistentIdentifier { + return model.persistentIdentifier + } + let model : ModelID + let timestamp: Date +} From 9b8cd52b3a7e19cc02113646ce8def821658c96c Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 20:38:35 -0400 Subject: [PATCH 07/12] fixing API --- .swiftlint.yml | 2 - Example/Sources/ContentObject.swift | 92 ++++++----- Example/Sources/ContentView.swift | 38 +++-- Example/Sources/DataThespianExampleApp.swift | 33 ++-- Example/Sources/Item.swift | 10 +- Example/Sources/ItemModel.swift | 18 +-- Scripts/lint.sh | 4 +- Sources/DataThespian/Assert.swift | 4 + Sources/DataThespian/BackgroundDatabase.swift | 45 +----- Sources/DataThespian/Database+Extras.swift | 143 +++++++++++++++++ .../DataThespian/Database+ModelContext.swift | 102 ++++++++++++ Sources/DataThespian/Database.swift | 151 +----------------- Sources/DataThespian/DatabaseKey.swift | 1 - .../DataThespian/ModelActor+Database.swift | 46 ++++++ Sources/DataThespian/ModelActorDatabase.swift | 71 +------- .../DataThespian/ModelContext+Extension.swift | 81 ++++++++++ Sources/DataThespian/ModelID.swift | 10 +- Sources/DataThespian/ThespianLogging.swift | 6 +- project.yml | 15 +- 19 files changed, 512 insertions(+), 360 deletions(-) create mode 100644 Sources/DataThespian/Database+Extras.swift create mode 100644 Sources/DataThespian/Database+ModelContext.swift create mode 100644 Sources/DataThespian/ModelActor+Database.swift create mode 100644 Sources/DataThespian/ModelContext+Extension.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 5fb47cf..38f6ff1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -114,8 +114,6 @@ identifier_name: excluded: - DerivedData - .build - - Package.swift - - Package indentation_width: indentation_width: 2 file_name: diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift index 8b7b7a3..762a1e4 100644 --- a/Example/Sources/ContentObject.swift +++ b/Example/Sources/ContentObject.swift @@ -5,33 +5,47 @@ // Created by Leo Dion on 10/10/24. // +import Combine +import DataThespian import Foundation import SwiftData -import DataThespian -import Combine - - @Observable @MainActor -class ContentObject { +internal class ContentObject { internal let databaseChangePublisher = PassthroughSubject() - private var databaseChangeCancellable : AnyCancellable? + private var databaseChangeCancellable: AnyCancellable? private var databaseChangeSubscription: AnyCancellable? - private var database : (any Database)? + private var database: (any Database)? internal private(set) var items = [ItemModel]() internal var selectedItemsID: Set = [] - - - var selectedItems : [ItemModel] { + private var newItem: AnyCancellable? + internal var error: (any Error)? + + internal var selectedItems: [ItemModel] { let selectedItemsID = self.selectedItemsID - return try! self.items.filter(#Predicate { - selectedItemsID.contains($0.id) - }) + let items: [ItemModel] + do { + items = try self.items.filter( + #Predicate { + selectedItemsID.contains($0.id) + } + ) + } catch { + assertionFailure("Unable to filter selected items: \(error.localizedDescription)") + self.error = error + items = [] + } + assert(items.count == selectedItemsID.count) + return items } - private var newItem: AnyCancellable? - var error: (any Error)? - + + internal init() { + self.databaseChangeSubscription = self.databaseChangePublisher.sink { _ in + self.beginUpdateItems() + } + } + private func beginUpdateItems() { Task { do { @@ -41,7 +55,8 @@ class ContentObject { } } } - fileprivate func updateItems() async throws { + + private func updateItems() async throws { guard let database else { return } @@ -50,25 +65,21 @@ class ContentObject { return items.map(ItemModel.init) }) } - - internal init () { - self.databaseChangeSubscription = self.databaseChangePublisher.sink { _ in - self.beginUpdateItems() - } - } - - internal func initialize(withDatabase database: any Database, databaseChangePublisher: DatabaseChangePublicist) { + + internal func initialize( + withDatabase database: any Database, databaseChangePublisher: DatabaseChangePublicist + ) { self.database = database - self.databaseChangeCancellable = databaseChangePublisher(id: "contentView") + self.databaseChangeCancellable = databaseChangePublisher(id: "contentView") .subscribe(self.databaseChangePublisher) self.beginUpdateItems() } - - - - fileprivate static func deleteModels( _ models: [ModelID], from database: (any Database)) async throws { + + fileprivate static func deleteModels(_ models: [ModelID], from database: (any Database)) + async throws + { try await database.withModelContext { modelContext in - let items : [Item] = models.compactMap{ + let items: [Item] = models.compactMap { modelContext.model(for: $0.persistentIdentifier) as? Item } dump(items.first?.persistentModelID) @@ -81,21 +92,21 @@ class ContentObject { } internal func deleteSelectedItems() { let models = self.selectedItems.map { - ModelID.init(persistentIdentifier: $0.id) + ModelID(persistentIdentifier: $0.id) } self.deleteItems(models) } internal func deleteItems(offsets: IndexSet) { - - let models = offsets - .compactMap{items[$0].id} - .map(ModelID.init(persistentIdentifier: )) - + let models = + offsets + .compactMap { items[$0].id } + .map(ModelID.init(persistentIdentifier:)) + assert(models.count == offsets.count) - + self.deleteItems(models) } - + internal func deleteItems(_ models: [ModelID]) { guard let database else { return @@ -104,14 +115,13 @@ class ContentObject { try await Self.deleteModels(models, from: database) } } - + internal func addItem(withDate date: Date = .init()) { guard let database else { return } Task { try await database.withModelContext { modelContext in - let newItem = Item(timestamp: date) modelContext.insert(newItem) dump(newItem.persistentModelID) diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 93729a3..4e1e0a8 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -5,20 +5,16 @@ // Created by Leo Dion on 10/10/24. // -import SwiftUI -import SwiftData -import DataThespian import Combine - - +import DataThespian +import SwiftData +import SwiftUI struct ContentView: View { @State var object = ContentObject() @Environment(\.database) var database @Environment(\.databaseChangePublicist) var databaseChangePublisher - - - + var body: some View { NavigationSplitView { List(selection: self.$object.selectedItemsID) { @@ -29,12 +25,10 @@ struct ContentView: View { } .navigationSplitViewColumnWidth(min: 180, ideal: 200) .toolbar { - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } + Label("Add Item", systemImage: "plus") + } } ToolbarItem { Button(action: object.deleteSelectedItems) { @@ -51,21 +45,31 @@ struct ContentView: View { } else { Text("Select an item") } - }.onAppear { - self.object.initialize(withDatabase: database, databaseChangePublisher: databaseChangePublisher) + self.object.initialize( + withDatabase: database, databaseChangePublisher: databaseChangePublisher) } } - + private func addItem() { self.addItem(withDate: .init()) } - private func addItem(withDate date: Date ) { + private func addItem(withDate date: Date) { self.object.addItem(withDate: .init()) } - } #Preview { + let config = ModelConfiguration(isStoredInMemoryOnly: true) + // swiftlint:disable:next force_try + let modelContainer = try! ModelContainer(for: Item.self, configurations: config) + ContentView() + .environment( + \.databaseChangePublicist, + DatabaseChangePublicist( + dbWatcher: DataMonitor.shared + ) + ) + .database(BackgroundDatabase(modelContainer: modelContainer)) } diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift index c5b3c9d..910c7f7 100644 --- a/Example/Sources/DataThespianExampleApp.swift +++ b/Example/Sources/DataThespianExampleApp.swift @@ -5,23 +5,28 @@ // Created by Leo Dion on 10/10/24. // -import SwiftUI -import SwiftData import DataThespian +import SwiftData +import SwiftUI @main -struct DataThespianExampleApp: App { - private static let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) - private static let database = try! BackgroundDatabase(modelContainer: .init(for: Item.self), autosaveEnabled: true) - - init () { +internal struct DataThespianExampleApp: App { + private static let databaseChangePublicist = DatabaseChangePublicist( dbWatcher: DataMonitor.shared) + // swiftlint:disable:next force_try + private static let database = try! BackgroundDatabase( + modelContainer: .init(for: Item.self), + autosaveEnabled: true + ) + + internal var body: some Scene { + WindowGroup { + ContentView() + } + .database(Self.database) + .environment(\.databaseChangePublicist, Self.databaseChangePublicist) + } + + internal init() { DataMonitor.shared.begin(with: []) } - var body: some Scene { - WindowGroup { - ContentView() - } - .database(Self.database) - .environment(\.databaseChangePublicist, Self.databaseChangePublicist) - } } diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index 20d3214..bd8099f 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -10,9 +10,9 @@ import SwiftData @Model final class Item { - var timestamp: Date - - init(timestamp: Date) { - self.timestamp = timestamp - } + var timestamp: Date + + init(timestamp: Date) { + self.timestamp = timestamp + } } diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemModel.swift index 4e79e3b..ab1c360 100644 --- a/Example/Sources/ItemModel.swift +++ b/Example/Sources/ItemModel.swift @@ -5,19 +5,17 @@ // Created by Leo Dion on 10/10/24. // -import Foundation import DataThespian +import Foundation import SwiftData - - -struct ItemModel : Identifiable { +struct ItemModel: Identifiable { private init(model: ModelID, timestamp: Date) { self.model = model self.timestamp = timestamp } - - internal init(item : Item) { + + internal init(item: Item) { self.init(model: .init(item), timestamp: item.timestamp) } @available(*, deprecated) @@ -25,10 +23,10 @@ struct ItemModel : Identifiable { self.model = .init(persistentIdentifier: id) self.timestamp = timestamp } - - var id : PersistentIdentifier { - return model.persistentIdentifier + + var id: PersistentIdentifier { + model.persistentIdentifier } - let model : ModelID + let model: ModelID let timestamp: Date } diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 6ca2f13..4e9c2ac 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -53,7 +53,7 @@ fi if [ -z "$CI" ]; then run_command $MINT_RUN swiftlint --fix pushd $PACKAGE_DIR - run_command $MINT_RUN swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests + run_command $MINT_RUN swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests Example/Sources popd else set -e @@ -63,6 +63,6 @@ $PACKAGE_DIR/scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "Bright run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS pushd $PACKAGE_DIR -run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests +run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests Example/Sources run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check popd \ No newline at end of file diff --git a/Sources/DataThespian/Assert.swift b/Sources/DataThespian/Assert.swift index 3be305c..d3c3226 100644 --- a/Sources/DataThespian/Assert.swift +++ b/Sources/DataThespian/Assert.swift @@ -29,6 +29,10 @@ public import Foundation +@inlinable internal func assert(isMainThread: Bool, if assertIsBackground: Bool) { + assert(!assertIsBackground || isMainThread == Thread.isMainThread) +} + @inlinable internal func assert(isMainThread: Bool) { assert(isMainThread == Thread.isMainThread) } @inlinable internal func assertionFailure( diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index 17009b1..ded4d14 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -35,41 +35,10 @@ import SwiftUI public final class BackgroundDatabase: Database { - public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T { - return try await self.database.withModelContext(closure) - } - - public func delete(_ modelType: (some PersistentModel).Type, withID id: PersistentIdentifier) - async -> Bool - { await self.database.delete(modelType, withID: id) } - - public func delete(where predicate: Predicate?) async throws { - try await self.database.delete(where: predicate) - } - public func save() async throws { try await self.database.save() } - public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) async - -> PersistentIdentifier - { await self.database.insert(closuer) } - - public func fetch( - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T]) throws -> U - ) async throws -> U where T: PersistentModel, U: Sendable { - try await self.database.fetch(selectDescriptor, with: closure) - } - - public func get( - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U where T: PersistentModel, U: Sendable { - try await self.database.get(for: objectID, with: closure) - } - public func fetch( - _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, - _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T], [U]) throws -> V - ) async throws -> V { - try await self.database.fetch(selectDescriptorA, selectDescriptorB, with: closure) + public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) + async rethrows -> T + { + try await self.database.withModelContext(closure) } private actor DatabaseContainer { @@ -104,11 +73,5 @@ internal init(_ factory: @Sendable @escaping () -> any Database) { self.container = .init(factory: factory) } - - public func transaction(_ block: @escaping @Sendable (ModelContext) throws -> Void) async throws - { - assert(isMainThread: false) - try await self.database.transaction(block) - } } #endif diff --git a/Sources/DataThespian/Database+Extras.swift b/Sources/DataThespian/Database+Extras.swift new file mode 100644 index 0000000..977722e --- /dev/null +++ b/Sources/DataThespian/Database+Extras.swift @@ -0,0 +1,143 @@ +// +// Database+Extras.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import SwiftData + +extension Database { + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType + ) async -> ModelID { + let id: PersistentIdentifier = await self.insert(closuer) + return .init(persistentIdentifier: id) + } + + public func with( + _ id: ModelID, + _ closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async throws -> U { + try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in + guard let model else { + throw ModelID.Error.notFound(id.persistentIdentifier) + } + return try closure(model) + } + } + + public func first(_ selectPredicate: Predicate) async throws -> ModelID< + T + >? { try await self.first(selectPredicate, with: ModelID.ifMap) } + + public func first( + _ selectPredicate: Predicate, + with closure: @escaping @Sendable (T?) throws -> U + ) async throws -> U { + try await self.fetch { + .init(predicate: selectPredicate, fetchLimit: 1) + } with: { models in + try closure(models.first) + } + } + + public func first( + fetchWith selectPredicate: Predicate, + otherwiseInsertBy insert: @Sendable @escaping () -> T, + with closure: @escaping @Sendable (T) throws -> U + ) async throws -> U { + let value = try await self.fetch { + .init(predicate: selectPredicate, fetchLimit: 1) + } with: { models in + try models.first.map(closure) + } + + if let value { return value } + + let inserted: ModelID = await self.insert(insert) + + return try await self.with(inserted, closure) + } + + public func delete(model _: T.Type, where predicate: Predicate? = nil) + async throws + { try await self.delete(where: predicate) } + + public func delete(_ model: ModelID) async { + await self.delete(T.self, withID: model.persistentIdentifier) + } + + public func deleteAll(of types: [any PersistentModel.Type]) async throws { + try await self.transaction { context in for type in types { try context.delete(model: type) } + } + } + + public func fetch( + _: T.Type, + with closure: @escaping @Sendable ([T]) throws -> U + ) async throws -> U { + try await self.fetch { + FetchDescriptor() + } with: { models in + try closure(models) + } + } + + public func fetch(_: T.Type) async throws -> [ModelID] { + try await self.fetch(T.self) { models in models.map(ModelID.init) } + } + public func fetch( + _: T.Type, + _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor + ) async throws -> [ModelID] { + try await self.fetch(selectDescriptor) { models in models.map(ModelID.init) } + } + + public func fetch( + of _: T.Type, + for objectIDs: [PersistentIdentifier], + with closure: @escaping @Sendable (T) throws -> U + ) async throws -> [U] where T: PersistentModel { + try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in + for id in objectIDs { + group.addTask { try await self.get(for: id) { model in try model.map(closure) } } + } + + return try await group.reduce(into: []) { partialResult, item in + if let item { partialResult.append(item) } + } + } + } + + public func get( + of _: T.Type, + for objectID: PersistentIdentifier, + with closure: @escaping @Sendable (T?) throws -> U + ) async throws -> U where T: PersistentModel { + try await self.get(for: objectID) { model in try closure(model) } + } +} diff --git a/Sources/DataThespian/Database+ModelContext.swift b/Sources/DataThespian/Database+ModelContext.swift new file mode 100644 index 0000000..5ce35a7 --- /dev/null +++ b/Sources/DataThespian/Database+ModelContext.swift @@ -0,0 +1,102 @@ +// +// Database+ModelContext.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +#if canImport(SwiftData) + + public import Foundation + + public import SwiftData + + extension Database { + public func save() async throws { + try await self.withModelContext { + try $0.save() + } + } + + @discardableResult public func delete( + _ modelType: T.Type, + withID id: PersistentIdentifier + ) async -> Bool { + await self.withModelContext { + $0.delete(modelType, withID: id) + } + } + + public func delete(where predicate: Predicate?) async throws { + try await self.withModelContext { + try $0.delete(where: predicate) + } + } + + public func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async + -> PersistentIdentifier + { + await self.withModelContext { + $0.insert(closuer) + } + } + + public func fetch( + _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T]) throws -> U + ) async rethrows -> U { + try await self.withModelContext { + try $0.fetch(selectDescriptor, with: closure) + } + } + + public func fetch( + _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, + _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T], [U]) throws -> V + ) async rethrows -> V { + try await self.withModelContext { + try $0.fetch(selectDescriptorA, selectDescriptorB, with: closure) + } + } + + public func get( + for objectID: PersistentIdentifier, + with closure: @escaping @Sendable (T?) throws -> U + ) async rethrows -> U where T: PersistentModel { + try await self.withModelContext { + try $0.get(for: objectID, with: closure) + } + } + + public func transaction(_ block: @Sendable @escaping (ModelContext) throws -> Void) async throws + { + try await self.withModelContext { + try $0.transaction(block: block) + } + } + } + +#endif diff --git a/Sources/DataThespian/Database.swift b/Sources/DataThespian/Database.swift index 4c8356a..b17f230 100644 --- a/Sources/DataThespian/Database.swift +++ b/Sources/DataThespian/Database.swift @@ -34,154 +34,7 @@ public import SwiftData public protocol Database: Sendable { - func save() async throws - - func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T - - @discardableResult func delete( - _ modelType: T.Type, - withID id: PersistentIdentifier - ) async -> Bool - - func delete(where predicate: Predicate?) async throws - - func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async - -> PersistentIdentifier - - func fetch( - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T]) throws -> U - ) async throws -> U - - func fetch( - _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, - _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T], [U]) throws -> V - ) async throws -> V - - func get( - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U where T: PersistentModel - - func transaction(_ block: @Sendable @escaping (ModelContext) throws -> Void) async throws + func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) + async rethrows -> T } - - extension Database { - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> ModelID { - let id: PersistentIdentifier = await self.insert(closuer) - return .init(persistentIdentifier: id) - } - - public func with( - _ id: ModelID, - _ closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async throws -> U { - try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in - guard let model else { - throw ModelID.Error.notFound(id.persistentIdentifier) - } - return try closure(model) - } - } - - public func first(_ selectPredicate: Predicate) async throws -> ModelID< - T - >? { try await self.first(selectPredicate, with: ModelID.ifMap) } - - public func first( - _ selectPredicate: Predicate, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U { - try await self.fetch { - .init(predicate: selectPredicate, fetchLimit: 1) - } with: { models in - try closure(models.first) - } - } - - public func first( - fetchWith selectPredicate: Predicate, - otherwiseInsertBy insert: @Sendable @escaping () -> T, - with closure: @escaping @Sendable (T) throws -> U - ) async throws -> U { - let value = try await self.fetch { - .init(predicate: selectPredicate, fetchLimit: 1) - } with: { models in - try models.first.map(closure) - } - - if let value { return value } - - let inserted: ModelID = await self.insert(insert) - - return try await self.with(inserted, closure) - } - - public func delete(model _: T.Type, where predicate: Predicate? = nil) - async throws - { try await self.delete(where: predicate) } - - public func delete(_ model: ModelID) async { - await self.delete(T.self, withID: model.persistentIdentifier) - } - - public func deleteAll(of types: [any PersistentModel.Type]) async throws { - try await self.transaction { context in for type in types { try context.delete(model: type) } - } - } - - public func fetch( - _: T.Type, - with closure: @escaping @Sendable ([T]) throws -> U - ) async throws -> U { - try await self.fetch { - FetchDescriptor() - } with: { models in - try closure(models) - } - } - - public func fetch(_: T.Type) async throws -> [ModelID] { - try await self.fetch(T.self) { models in models.map(ModelID.init) } - } - public func fetch( - _: T.Type, - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor - ) async throws -> [ModelID] { - try await self.fetch(selectDescriptor) { models in models.map(ModelID.init) } - } - - public func fetch( - of _: T.Type, - for objectIDs: [PersistentIdentifier], - with closure: @escaping @Sendable (T) throws -> U - ) async throws -> [U] where T: PersistentModel { - try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in - for id in objectIDs { - group.addTask { try await self.get(for: id) { model in try model.map(closure) } } - } - - return try await group.reduce(into: []) { partialResult, item in - if let item { partialResult.append(item) } - } - } - } - - public func get( - of _: T.Type, - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U where T: PersistentModel { - try await self.get(for: objectID) { model in try closure(model) } - } - } -// public extension Database { -// static var loggingCategory: ThespianLogging.Category { -// .data -// } -// } - #endif diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index add8b67..17f27e3 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -108,7 +108,6 @@ } } - @available(*, deprecated, message: "This is a fix for a bug. Use Scene only eventually.") extension View { public func database(_ database: any Database) -> some View { environment(\.database, database) diff --git a/Sources/DataThespian/ModelActor+Database.swift b/Sources/DataThespian/ModelActor+Database.swift new file mode 100644 index 0000000..74f2ca8 --- /dev/null +++ b/Sources/DataThespian/ModelActor+Database.swift @@ -0,0 +1,46 @@ +// +// ModelActor+Database.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import SwiftData + +extension ModelActor where Self: Database { + public func withModelContext( + _ closure: @Sendable @escaping (ModelContext) throws -> T + ) + async rethrows -> T + { + assert(isMainThread: true, if: Self.assertIsBackground) + let modelContext = self.modelContext + return try closure(modelContext) + } + + public static var assertIsBackground: Bool { + false + } +} diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index 7b9fc4e..9e1a84d 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -29,81 +29,13 @@ #if canImport(SwiftData) - public import Foundation + import Foundation public import SwiftData -public enum DeleteSelector : Sendable { - case persistentModelID(PersistentIdentifier) - case predicate(Predicate) - case all -} - public actor ModelActorDatabase: Database, Loggable { - public func delete(_: T.Type, withID id: PersistentIdentifier) async -> Bool - { - guard let model: T = self.modelContext.registeredModel(for: id) else { return false } - self.modelContext.delete(model) - return true - } - - public func delete(where predicate: Predicate?) async throws where T: PersistentModel { - try self.modelContext.delete(model: T.self, where: predicate) - } - - public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) async - -> PersistentIdentifier - { - let model = closuer() - self.modelContext.insert(model) - return model.persistentModelID - } - - public func withModelContext(_ closure: @Sendable (ModelContext) throws -> T) async rethrows -> T { - let modelContext = self.modelContext - return try closure(modelContext) - } - - public func fetch( - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T]) throws -> U - ) async throws -> U where T: PersistentModel, U: Sendable { - let models = try self.modelContext.fetch(selectDescriptor()) - return try closure(models) - } - public func fetch( - _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, - _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T], [U]) throws -> V - ) async throws -> V { - let a = try self.modelContext.fetch(selectDescriptorA()) - let b = try self.modelContext.fetch(selectDescriptorB()) - return try closure(a, b) - } - - public func get( - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U where T: PersistentModel, U: Sendable { - let model: T? = try self.modelContext.existingModel(for: objectID) - return try closure(model) - } public static var loggingCategory: ThespianLogging.Category { .data } - public func transaction(_ block: @escaping @Sendable (ModelContext) throws -> Void) async throws - { - assert(isMainThread: false) - - try self.modelContext.transaction { - assert(isMainThread: false) - try block(modelContext) - } - } - public func save() throws { - assert(isMainThread: false) - try self.modelContext.save() - } - public nonisolated let modelExecutor: any SwiftData.ModelExecutor public nonisolated let modelContainer: SwiftData.ModelContainer @@ -111,6 +43,7 @@ public enum DeleteSelector : Sendable { public init(modelContainer: SwiftData.ModelContainer, autosaveEnabled: Bool = false) { let modelContext = ModelContext(modelContainer) modelContext.autosaveEnabled = autosaveEnabled + let modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) self.init(modelExecutor: modelExecutor, modelContainer: modelContainer) } diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/ModelContext+Extension.swift new file mode 100644 index 0000000..bd39b45 --- /dev/null +++ b/Sources/DataThespian/ModelContext+Extension.swift @@ -0,0 +1,81 @@ +// +// ModelContext+Extension.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import Foundation +public import SwiftData + +extension ModelContext { + public func delete(_: T.Type, withID id: PersistentIdentifier) -> Bool { + guard let model: T = self.registeredModel(for: id) else { return false } + self.delete(model) + return true + } + + public func delete(where predicate: Predicate?) throws where T: PersistentModel { + try self.delete(model: T.self, where: predicate) + } + + public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) + -> PersistentIdentifier + { + let model = closuer() + self.insert(model) + return model.persistentModelID + } + public func fetch( + _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T]) throws -> U + ) throws -> U where T: PersistentModel, U: Sendable { + let models = try self.fetch(selectDescriptor()) + return try closure(models) + } + public func fetch( + _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, + _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T], [U]) throws -> V + ) throws -> V { + let firstModels = try self.fetch(selectDescriptorA()) + let secondModels = try self.fetch(selectDescriptorB()) + return try closure(firstModels, secondModels) + } + + public func get( + for objectID: PersistentIdentifier, + with closure: @escaping @Sendable (T?) throws -> U + ) throws -> U where T: PersistentModel, U: Sendable { + let model: T? = try self.existingModel(for: objectID) + return try closure(model) + } + + public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { + try self.transaction { + try block(self) + } + } +} diff --git a/Sources/DataThespian/ModelID.swift b/Sources/DataThespian/ModelID.swift index 5d41ea1..c3395ee 100644 --- a/Sources/DataThespian/ModelID.swift +++ b/Sources/DataThespian/ModelID.swift @@ -32,11 +32,11 @@ import Foundation public import SwiftData -public struct ModelID: Sendable, Identifiable { - public init(persistentIdentifier: PersistentIdentifier) { - self.persistentIdentifier = persistentIdentifier - } - + public struct ModelID: Sendable, Identifiable { + public init(persistentIdentifier: PersistentIdentifier) { + self.persistentIdentifier = persistentIdentifier + } + public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier diff --git a/Sources/DataThespian/ThespianLogging.swift b/Sources/DataThespian/ThespianLogging.swift index 262db81..ad1edc6 100644 --- a/Sources/DataThespian/ThespianLogging.swift +++ b/Sources/DataThespian/ThespianLogging.swift @@ -1,5 +1,5 @@ // -// Logging.swift +// ThespianLogging.swift // DataThespian // // Created by Leo Dion. @@ -29,11 +29,11 @@ public import FelinePine +internal protocol Loggable: FelinePine.Loggable where Self.LoggingSystemType == ThespianLogging {} + public enum ThespianLogging: LoggingSystem { public enum Category: String, CaseIterable { case application case data } } - -internal protocol Loggable: FelinePine.Loggable where Self.LoggingSystemType == ThespianLogging {} diff --git a/project.yml b/project.yml index c3155e9..e6c16c9 100644 --- a/project.yml +++ b/project.yml @@ -23,7 +23,20 @@ targets: - path: "Example/Support" settings: base: - PRODUCT_BUNDLE_IDENTIFIER: com.demo.DataThespianExample + PRODUCT_BUNDLE_IDENTIFIER: com.Demo.DataThespianExample + SWIFT_STRICT_CONCURRENCY: complete + SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE: YES + SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN: YES + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION: YES + SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY: YES + SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES: YES + SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY: YES + SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS: YES + SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS: YES + SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES: YES + SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT: YES + SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES: YES + SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION: YES info: path: Example/Support/Info.plist properties: From 078e4ec6e3b3584bca12f79b34fc862755e40b81 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Thu, 10 Oct 2024 20:53:28 -0400 Subject: [PATCH 08/12] working on linting issues --- .swift-format | 4 +- Example/Sources/ContentObject.swift | 37 ++++----- Example/Sources/ItemModel.swift | 20 ++--- Package.swift | 75 +++++++++---------- Scripts/lint.sh | 2 +- Sources/DataThespian/Assert.swift | 4 +- Sources/DataThespian/BackgroundDatabase.swift | 4 +- Sources/DataThespian/DataMonitor.swift | 7 +- Sources/DataThespian/Database+Extras.swift | 48 ++++++------ .../DataThespian/Database+ModelContext.swift | 42 +++-------- Sources/DataThespian/DatabaseChangeSet.swift | 3 +- Sources/DataThespian/DatabaseKey.swift | 3 +- Sources/DataThespian/FetchDescriptor.swift | 6 +- .../{ModelID.swift => Model.swift} | 13 ++-- .../DataThespian/ModelActor+Database.swift | 8 +- .../DataThespian/ModelContext+Extension.swift | 7 +- Sources/DataThespian/ModelContext.swift | 3 +- Sources/DataThespian/NSManagedObjectID.swift | 14 +--- .../DataThespian/NotificationDataUpdate.swift | 14 +--- Sources/DataThespian/PublishingAgent.swift | 42 +++++------ Sources/DataThespian/PublishingRegister.swift | 13 +++- .../DataThespian/RegistrationCollection.swift | 24 +++--- 22 files changed, 165 insertions(+), 228 deletions(-) rename Sources/DataThespian/{ModelID.swift => Model.swift} (80%) diff --git a/.swift-format b/.swift-format index d5fd187..32cd886 100644 --- a/.swift-format +++ b/.swift-format @@ -20,9 +20,9 @@ ] }, "prioritizeKeepingFunctionOutputTogether" : false, - "respectsExistingLineBreaks" : true, + "respectsExistingLineBreaks" : false, "rules" : { - "AllPublicDeclarationsHaveDocumentation" : true, + "AllPublicDeclarationsHaveDocumentation" : false, "AlwaysUseLiteralForEmptyCollectionInit" : false, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true, diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift index 762a1e4..ab0e138 100644 --- a/Example/Sources/ContentObject.swift +++ b/Example/Sources/ContentObject.swift @@ -46,6 +46,22 @@ internal class ContentObject { } } + private static func deleteModels(_ models: [Model], from database: (any Database)) + async throws + { + try await database.withModelContext { modelContext in + let items: [Item] = models.compactMap { + modelContext.model(for: $0.persistentIdentifier) as? Item + } + dump(items.first?.persistentModelID) + assert(items.count == models.count) + for item in items { + modelContext.delete(item) + } + try modelContext.save() + } + } + private func beginUpdateItems() { Task { do { @@ -75,24 +91,9 @@ internal class ContentObject { self.beginUpdateItems() } - fileprivate static func deleteModels(_ models: [ModelID], from database: (any Database)) - async throws - { - try await database.withModelContext { modelContext in - let items: [Item] = models.compactMap { - modelContext.model(for: $0.persistentIdentifier) as? Item - } - dump(items.first?.persistentModelID) - assert(items.count == models.count) - for item in items { - modelContext.delete(item) - } - try modelContext.save() - } - } internal func deleteSelectedItems() { let models = self.selectedItems.map { - ModelID(persistentIdentifier: $0.id) + Model(persistentIdentifier: $0.id) } self.deleteItems(models) } @@ -100,14 +101,14 @@ internal class ContentObject { let models = offsets .compactMap { items[$0].id } - .map(ModelID.init(persistentIdentifier:)) + .map(Model.init(persistentIdentifier:)) assert(models.count == offsets.count) self.deleteItems(models) } - internal func deleteItems(_ models: [ModelID]) { + internal func deleteItems(_ models: [Model]) { guard let database else { return } diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemModel.swift index ab1c360..8ebff74 100644 --- a/Example/Sources/ItemModel.swift +++ b/Example/Sources/ItemModel.swift @@ -10,7 +10,14 @@ import Foundation import SwiftData struct ItemModel: Identifiable { - private init(model: ModelID, timestamp: Date) { + let model: Model + let timestamp: Date + + var id: PersistentIdentifier { + model.persistentIdentifier + } + + private init(model: Model, timestamp: Date) { self.model = model self.timestamp = timestamp } @@ -18,15 +25,4 @@ struct ItemModel: Identifiable { internal init(item: Item) { self.init(model: .init(item), timestamp: item.timestamp) } - @available(*, deprecated) - internal init(id: PersistentIdentifier, timestamp: Date) { - self.model = .init(persistentIdentifier: id) - self.timestamp = timestamp - } - - var id: PersistentIdentifier { - model.persistentIdentifier - } - let model: ModelID - let timestamp: Date } diff --git a/Package.swift b/Package.swift index 9e49197..977745d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,7 @@ import PackageDescription +// swiftlint:disable explicit_acl explicit_top_level_acl let swiftSettings: [SwiftSetting] = [ SwiftSetting.enableExperimentalFeature("AccessLevelOnImport"), SwiftSetting.enableExperimentalFeature("BitwiseCopyable"), @@ -16,46 +17,44 @@ let swiftSettings: [SwiftSetting] = [ SwiftSetting.enableExperimentalFeature("VariadicGenerics"), SwiftSetting.enableUpcomingFeature("FullTypedThrows"), - SwiftSetting.enableUpcomingFeature("InternalImportsByDefault") + SwiftSetting.enableUpcomingFeature("InternalImportsByDefault"), - // SwiftSetting.unsafeFlags([ - // "-Xfrontend", - // "-warn-long-function-bodies=100" - // ]), - // SwiftSetting.unsafeFlags([ - // "-Xfrontend", - // "-warn-long-expression-type-checking=100" - // ]) + SwiftSetting.unsafeFlags([ + "-Xfrontend", + "-warn-long-function-bodies=100" + ]), + SwiftSetting.unsafeFlags([ + "-Xfrontend", + "-warn-long-expression-type-checking=100" + ]) ] let package = Package( - name: "DataThespian", - platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)], - products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. - .library( - name: "DataThespian", - targets: ["DataThespian"] - ) - ], - dependencies: [ - .package(url: "https://github.com/brightdigit/FelinePine.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/swiftlang/swift-testing.git", from: "0.12.0"), - ], - targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - .target( - name: "DataThespian", - dependencies: ["FelinePine"], - swiftSettings: swiftSettings - ), - .testTarget( - name: "DataThespianTests", - dependencies: [ - "DataThespian", - .product(name: "Testing", package: "swift-testing"), - ] - ) - ] + name: "DataThespian", + platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)], + products: [ + .library( + name: "DataThespian", + targets: ["DataThespian"] + ) + ], + dependencies: [ + .package(url: "https://github.com/brightdigit/FelinePine.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/swiftlang/swift-testing.git", from: "0.12.0"), + ], + targets: [ + .target( + name: "DataThespian", + dependencies: ["FelinePine"], + swiftSettings: swiftSettings + ), + .testTarget( + name: "DataThespianTests", + dependencies: [ + "DataThespian", + .product(name: "Testing", package: "swift-testing"), + ] + ) + ] ) +// swiftlint:enable explicit_acl explicit_top_level_acl diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 4e9c2ac..9e51e5c 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -64,5 +64,5 @@ run_command $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS pushd $PACKAGE_DIR run_command $MINT_RUN swift-format lint --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests Example/Sources -run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check +#run_command $MINT_RUN periphery scan $PERIPHERY_OPTIONS --disable-update-check popd \ No newline at end of file diff --git a/Sources/DataThespian/Assert.swift b/Sources/DataThespian/Assert.swift index d3c3226..bb575a2 100644 --- a/Sources/DataThespian/Assert.swift +++ b/Sources/DataThespian/Assert.swift @@ -36,7 +36,5 @@ public import Foundation @inlinable internal func assert(isMainThread: Bool) { assert(isMainThread == Thread.isMainThread) } @inlinable internal func assertionFailure( - error: any Error, - file: StaticString = #file, - line: UInt = #line + error: any Error, file: StaticString = #file, line: UInt = #line ) { assertionFailure(error.localizedDescription, file: file, line: line) } diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index ded4d14..9ed0ee4 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -37,9 +37,7 @@ public final class BackgroundDatabase: Database { public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) async rethrows -> T - { - try await self.database.withModelContext(closure) - } + { try await self.database.withModelContext(closure) } private actor DatabaseContainer { private let factory: @Sendable () -> any Database diff --git a/Sources/DataThespian/DataMonitor.swift b/Sources/DataThespian/DataMonitor.swift index 50e0efb..54c8cdf 100644 --- a/Sources/DataThespian/DataMonitor.swift +++ b/Sources/DataThespian/DataMonitor.swift @@ -64,14 +64,11 @@ func addObserver() { guard object == nil else { return } object = NotificationCenter.default.addObserver( - forName: .NSManagedObjectContextDidSave, - object: nil, - queue: nil, + forName: .NSManagedObjectContextDidSave, object: nil, queue: nil, using: { notification in let update = NotificationDataUpdate(notification) Task { await self.notifyRegisration(update) } - } - ) + }) } func notifyRegisration(_ update: any DatabaseChangeSet) { diff --git a/Sources/DataThespian/Database+Extras.swift b/Sources/DataThespian/Database+Extras.swift index 977722e..a0423e1 100644 --- a/Sources/DataThespian/Database+Extras.swift +++ b/Sources/DataThespian/Database+Extras.swift @@ -33,30 +33,30 @@ public import SwiftData extension Database { public func insert( _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> ModelID { + ) async -> Model { let id: PersistentIdentifier = await self.insert(closuer) return .init(persistentIdentifier: id) } public func with( - _ id: ModelID, + _ id: Model, _ closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async throws -> U { + ) async rethrows -> U { try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in guard let model else { - throw ModelID.Error.notFound(id.persistentIdentifier) + throw Model.NotFoundError( + persistentIdentifier: id.persistentIdentifier) } return try closure(model) } } - public func first(_ selectPredicate: Predicate) async throws -> ModelID< - T - >? { try await self.first(selectPredicate, with: ModelID.ifMap) } + public func first(_ selectPredicate: Predicate) async throws -> Model? { + try await self.first(selectPredicate, with: Model.ifMap) + } public func first( - _ selectPredicate: Predicate, - with closure: @escaping @Sendable (T?) throws -> U + _ selectPredicate: Predicate, with closure: @escaping @Sendable (T?) throws -> U ) async throws -> U { try await self.fetch { .init(predicate: selectPredicate, fetchLimit: 1) @@ -66,8 +66,7 @@ extension Database { } public func first( - fetchWith selectPredicate: Predicate, - otherwiseInsertBy insert: @Sendable @escaping () -> T, + fetchWith selectPredicate: Predicate, otherwiseInsertBy insert: @Sendable @escaping () -> T, with closure: @escaping @Sendable (T) throws -> U ) async throws -> U { let value = try await self.fetch { @@ -78,7 +77,7 @@ extension Database { if let value { return value } - let inserted: ModelID = await self.insert(insert) + let inserted: Model = await self.insert(insert) return try await self.with(inserted, closure) } @@ -87,18 +86,16 @@ extension Database { async throws { try await self.delete(where: predicate) } - public func delete(_ model: ModelID) async { + public func delete(_ model: Model) async { await self.delete(T.self, withID: model.persistentIdentifier) } public func deleteAll(of types: [any PersistentModel.Type]) async throws { - try await self.transaction { context in for type in types { try context.delete(model: type) } - } + try await self.transaction { context in for type in types { try context.delete(model: type) } } } public func fetch( - _: T.Type, - with closure: @escaping @Sendable ([T]) throws -> U + _: T.Type, with closure: @escaping @Sendable ([T]) throws -> U ) async throws -> U { try await self.fetch { FetchDescriptor() @@ -107,19 +104,17 @@ extension Database { } } - public func fetch(_: T.Type) async throws -> [ModelID] { - try await self.fetch(T.self) { models in models.map(ModelID.init) } + public func fetch(_: T.Type) async throws -> [Model] { + try await self.fetch(T.self) { models in models.map(Model.init) } } public func fetch( - _: T.Type, - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor - ) async throws -> [ModelID] { - try await self.fetch(selectDescriptor) { models in models.map(ModelID.init) } + _: T.Type, _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor + ) async throws -> [Model] { + try await self.fetch(selectDescriptor) { models in models.map(Model.init) } } public func fetch( - of _: T.Type, - for objectIDs: [PersistentIdentifier], + of _: T.Type, for objectIDs: [PersistentIdentifier], with closure: @escaping @Sendable (T) throws -> U ) async throws -> [U] where T: PersistentModel { try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in @@ -134,8 +129,7 @@ extension Database { } public func get( - of _: T.Type, - for objectID: PersistentIdentifier, + of _: T.Type, for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) async throws -> U where T: PersistentModel { try await self.get(for: objectID) { model in try closure(model) } diff --git a/Sources/DataThespian/Database+ModelContext.swift b/Sources/DataThespian/Database+ModelContext.swift index 5ce35a7..4107355 100644 --- a/Sources/DataThespian/Database+ModelContext.swift +++ b/Sources/DataThespian/Database+ModelContext.swift @@ -34,42 +34,25 @@ public import SwiftData extension Database { - public func save() async throws { - try await self.withModelContext { - try $0.save() - } - } + public func save() async throws { try await self.withModelContext { try $0.save() } } @discardableResult public func delete( - _ modelType: T.Type, - withID id: PersistentIdentifier - ) async -> Bool { - await self.withModelContext { - $0.delete(modelType, withID: id) - } - } + _ modelType: T.Type, withID id: PersistentIdentifier + ) async -> Bool { await self.withModelContext { $0.delete(modelType, withID: id) } } public func delete(where predicate: Predicate?) async throws { - try await self.withModelContext { - try $0.delete(where: predicate) - } + try await self.withModelContext { try $0.delete(where: predicate) } } public func insert(_ closuer: @Sendable @escaping () -> some PersistentModel) async -> PersistentIdentifier - { - await self.withModelContext { - $0.insert(closuer) - } - } + { await self.withModelContext { $0.insert(closuer) } } public func fetch( _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, with closure: @escaping @Sendable ([T]) throws -> U ) async rethrows -> U { - try await self.withModelContext { - try $0.fetch(selectDescriptor, with: closure) - } + try await self.withModelContext { try $0.fetch(selectDescriptor, with: closure) } } public func fetch( @@ -83,20 +66,13 @@ } public func get( - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U + for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) async rethrows -> U where T: PersistentModel { - try await self.withModelContext { - try $0.get(for: objectID, with: closure) - } + try await self.withModelContext { try $0.get(for: objectID, with: closure) } } public func transaction(_ block: @Sendable @escaping (ModelContext) throws -> Void) async throws - { - try await self.withModelContext { - try $0.transaction(block: block) - } - } + { try await self.withModelContext { try $0.transaction(block: block) } } } #endif diff --git a/Sources/DataThespian/DatabaseChangeSet.swift b/Sources/DataThespian/DatabaseChangeSet.swift index beaa02d..310b70f 100644 --- a/Sources/DataThespian/DatabaseChangeSet.swift +++ b/Sources/DataThespian/DatabaseChangeSet.swift @@ -38,8 +38,7 @@ public var isEmpty: Bool { inserted.isEmpty && deleted.isEmpty && updated.isEmpty } public func update( - of types: Set = .all, - contains filteringEntityNames: Set + of types: Set = .all, contains filteringEntityNames: Set ) -> Bool { let updateEntityNamesArray = types.flatMap { self[keyPath: $0.keyPath] }.map(\.entityName) let updateEntityNames = Set(updateEntityNamesArray) diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index 17f27e3..6a595b8 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -60,8 +60,7 @@ } func fetch( - _: @escaping @Sendable () -> FetchDescriptor, - with _: @escaping @Sendable ([T]) throws -> U + _: @escaping @Sendable () -> FetchDescriptor, with _: @escaping @Sendable ([T]) throws -> U ) async throws -> U where T: PersistentModel, U: Sendable { assertionFailure("No Database Set.") throw NotImplmentedError.instance diff --git a/Sources/DataThespian/FetchDescriptor.swift b/Sources/DataThespian/FetchDescriptor.swift index 9ae2b20..57c2036 100644 --- a/Sources/DataThespian/FetchDescriptor.swift +++ b/Sources/DataThespian/FetchDescriptor.swift @@ -39,12 +39,10 @@ self.fetchLimit = fetchLimit } - public init(model: ModelID) { + public init(model: Model) { let persistentIdentifier = model.persistentIdentifier self.init( - predicate: #Predicate { $0.persistentModelID == persistentIdentifier }, - fetchLimit: 1 - ) + predicate: #Predicate { $0.persistentModelID == persistentIdentifier }, fetchLimit: 1) } } #endif diff --git a/Sources/DataThespian/ModelID.swift b/Sources/DataThespian/Model.swift similarity index 80% rename from Sources/DataThespian/ModelID.swift rename to Sources/DataThespian/Model.swift index c3395ee..550238c 100644 --- a/Sources/DataThespian/ModelID.swift +++ b/Sources/DataThespian/Model.swift @@ -1,5 +1,5 @@ // -// ModelID.swift +// Model.swift // DataThespian // // Created by Leo Dion. @@ -32,7 +32,10 @@ import Foundation public import SwiftData - public struct ModelID: Sendable, Identifiable { + + @available(*, deprecated, renamed: "Model") public typealias ModelID = Model + + public struct Model: Sendable, Identifiable { public init(persistentIdentifier: PersistentIdentifier) { self.persistentIdentifier = persistentIdentifier } @@ -40,12 +43,12 @@ public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier - enum Error: Swift.Error { case notFound(PersistentIdentifier) } + public struct NotFoundError: Error { public let persistentIdentifier: PersistentIdentifier } } - extension ModelID where T: PersistentModel { + extension Model where T: PersistentModel { public init(_ model: T) { self.init(persistentIdentifier: model.persistentModelID) } - internal static func ifMap(_ model: T?) -> ModelID? { model.map(self.init) } + internal static func ifMap(_ model: T?) -> Model? { model.map(self.init) } } #endif diff --git a/Sources/DataThespian/ModelActor+Database.swift b/Sources/DataThespian/ModelActor+Database.swift index 74f2ca8..8143c9d 100644 --- a/Sources/DataThespian/ModelActor+Database.swift +++ b/Sources/DataThespian/ModelActor+Database.swift @@ -32,15 +32,11 @@ public import SwiftData extension ModelActor where Self: Database { public func withModelContext( _ closure: @Sendable @escaping (ModelContext) throws -> T - ) - async rethrows -> T - { + ) async rethrows -> T { assert(isMainThread: true, if: Self.assertIsBackground) let modelContext = self.modelContext return try closure(modelContext) } - public static var assertIsBackground: Bool { - false - } + public static var assertIsBackground: Bool { false } } diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/ModelContext+Extension.swift index bd39b45..48deb13 100644 --- a/Sources/DataThespian/ModelContext+Extension.swift +++ b/Sources/DataThespian/ModelContext+Extension.swift @@ -66,16 +66,13 @@ extension ModelContext { } public func get( - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U + for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) throws -> U where T: PersistentModel, U: Sendable { let model: T? = try self.existingModel(for: objectID) return try closure(model) } public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { - try self.transaction { - try block(self) - } + try self.transaction { try block(self) } } } diff --git a/Sources/DataThespian/ModelContext.swift b/Sources/DataThespian/ModelContext.swift index 3298504..4561378 100644 --- a/Sources/DataThespian/ModelContext.swift +++ b/Sources/DataThespian/ModelContext.swift @@ -39,8 +39,7 @@ if let notRegistered: T = model(for: objectID) as? T { return notRegistered } let fetchDescriptor = FetchDescriptor( - predicate: #Predicate { $0.persistentModelID == objectID } - ) + predicate: #Predicate { $0.persistentModelID == objectID }) return try fetch(fetchDescriptor).first } diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/NSManagedObjectID.swift index 0b6fcc4..e377e95 100644 --- a/Sources/DataThespian/NSManagedObjectID.swift +++ b/Sources/DataThespian/NSManagedObjectID.swift @@ -38,10 +38,7 @@ fileprivate struct PersistentIdentifierJSON: Codable { fileprivate struct Implementation: Codable { fileprivate init( - primaryKey: String, - uriRepresentation: URL, - isTemporary: Bool, - storeIdentifier: String, + primaryKey: String, uriRepresentation: URL, isTemporary: Bool, storeIdentifier: String, entityName: String ) { self.primaryKey = primaryKey @@ -80,13 +77,8 @@ guard let entityName else { throw PersistentIdentifierError.missingProperty(.entityName) } let json = PersistentIdentifierJSON( implementation: .init( - primaryKey: primaryKey, - uriRepresentation: uriRepresentation(), - isTemporary: isTemporaryID, - storeIdentifier: storeIdentifier, - entityName: entityName - ) - ) + primaryKey: primaryKey, uriRepresentation: uriRepresentation(), + isTemporary: isTemporaryID, storeIdentifier: storeIdentifier, entityName: entityName)) let encoder = JSONEncoder() let data: Data do { data = try encoder.encode(json) } catch let error as EncodingError { diff --git a/Sources/DataThespian/NotificationDataUpdate.swift b/Sources/DataThespian/NotificationDataUpdate.swift index fa3a932..2c01097 100644 --- a/Sources/DataThespian/NotificationDataUpdate.swift +++ b/Sources/DataThespian/NotificationDataUpdate.swift @@ -40,20 +40,15 @@ let updated: Set private init( - inserted: Set?, - deleted: Set?, + inserted: Set?, deleted: Set?, updated: Set? ) { self.init( - inserted: inserted ?? .init(), - deleted: deleted ?? .init(), - updated: updated ?? .init() - ) + inserted: inserted ?? .init(), deleted: deleted ?? .init(), updated: updated ?? .init()) } private init( - inserted: Set, - deleted: Set, + inserted: Set, deleted: Set, updated: Set ) { self.inserted = inserted @@ -65,8 +60,7 @@ self.init( inserted: notification.managedObjects(key: NSInsertedObjectsKey), deleted: notification.managedObjects(key: NSDeletedObjectsKey), - updated: notification.managedObjects(key: NSUpdatedObjectsKey) - ) + updated: notification.managedObjects(key: NSUpdatedObjectsKey)) } } #endif diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/PublishingAgent.swift index deedbef..98100ea 100644 --- a/Sources/DataThespian/PublishingAgent.swift +++ b/Sources/DataThespian/PublishingAgent.swift @@ -33,35 +33,33 @@ import Foundation - actor PublishingAgent: DataAgent, Loggable { + internal actor PublishingAgent: DataAgent, Loggable { private enum SubscriptionEvent: Sendable { case cancel case subscribe } - static var loggingCategory: ThespianLogging.Category { .application } + internal static var loggingCategory: ThespianLogging.Category { .application } - let agentID = UUID() - let id: String - let subject: PassthroughSubject - var subscriptionCount = 0 - var cancellable: AnyCancellable? - var completed: (@Sendable () -> Void)? + internal let agentID = UUID() + private let id: String + private let subject: PassthroughSubject + private var subscriptionCount = 0 + private var cancellable: AnyCancellable? + private var completed: (@Sendable () -> Void)? - init(id: String, subject: PassthroughSubject) { + internal init(id: String, subject: PassthroughSubject) { self.id = id self.subject = subject Task { await self.initialize() } } - func initialize() { - cancellable = - subject.handleEvents { _ in - self.onSubscriptionEvent(.subscribe) - } receiveCancel: { - self.onSubscriptionEvent(.cancel) - } - .sink { _ in } + private func initialize() { + cancellable = subject.handleEvents { _ in + self.onSubscriptionEvent(.subscribe) + } receiveCancel: { + self.onSubscriptionEvent(.cancel) + }.sink { _ in } } private nonisolated func onSubscriptionEvent(_ event: SubscriptionEvent) { @@ -83,15 +81,15 @@ ) } - nonisolated func onUpdate(_ update: any DatabaseChangeSet) { + nonisolated internal func onUpdate(_ update: any DatabaseChangeSet) { Task { await self.sendUpdate(update) } } - func sendUpdate(_ update: any DatabaseChangeSet) { + private func sendUpdate(_ update: any DatabaseChangeSet) { Task { @MainActor in await self.subject.send(update) } } - func cancel() { + private func cancel() { Self.logger.debug("Cancelling \(self.id) \(self.agentID)") cancellable?.cancel() cancellable = nil @@ -99,7 +97,7 @@ completed = nil } - nonisolated func onCompleted(_ closure: @escaping @Sendable () -> Void) { + nonisolated internal func onCompleted(_ closure: @escaping @Sendable () -> Void) { Task { await self.setCompleted(closure) } } @@ -109,6 +107,6 @@ completed = closure } - func finish() { cancel() } + internal func finish() { cancel() } } #endif diff --git a/Sources/DataThespian/PublishingRegister.swift b/Sources/DataThespian/PublishingRegister.swift index 3d64ae0..651ad47 100644 --- a/Sources/DataThespian/PublishingRegister.swift +++ b/Sources/DataThespian/PublishingRegister.swift @@ -32,11 +32,16 @@ import Foundation - struct PublishingRegister: AgentRegister { - let id: String - let subject: PassthroughSubject + internal struct PublishingRegister: AgentRegister { + internal let id: String + private let subject: PassthroughSubject - func agent() async -> PublishingAgent { + internal init(id: String, subject: PassthroughSubject) { + self.id = id + self.subject = subject + } + + internal func agent() async -> PublishingAgent { let agent = AgentType(id: id, subject: subject) return agent diff --git a/Sources/DataThespian/RegistrationCollection.swift b/Sources/DataThespian/RegistrationCollection.swift index 43cc92e..721b32d 100644 --- a/Sources/DataThespian/RegistrationCollection.swift +++ b/Sources/DataThespian/RegistrationCollection.swift @@ -31,27 +31,25 @@ import Foundation - actor RegistrationCollection: Loggable { - static var loggingCategory: ThespianLogging.Category { .application } + internal actor RegistrationCollection: Loggable { + internal static var loggingCategory: ThespianLogging.Category { .application } - var registrations = [String: DataAgent]() + private var registrations = [String: DataAgent]() - nonisolated func notify(_ update: any DatabaseChangeSet) { + nonisolated internal func notify(_ update: any DatabaseChangeSet) { Task { await self.onUpdate(update) Self.logger.debug("Notification Complete") } } - nonisolated func add( - withID id: String, - force: Bool, - agent: @Sendable @escaping () async -> DataAgent + nonisolated internal func add( + withID id: String, force: Bool, agent: @Sendable @escaping () async -> DataAgent ) { Task { await self.append(withID: id, force: force, agent: agent) } } - func append(withID id: String, force: Bool, agent: @Sendable @escaping () async -> DataAgent) - async - { + private func append( + withID id: String, force: Bool, agent: @Sendable @escaping () async -> DataAgent + ) async { if let registration = registrations[id], force { Self.logger.debug("Overwriting \(id). Already exists.") await registration.finish() @@ -66,7 +64,7 @@ Self.logger.debug("Registration Count \(self.registrations.count)") } - func remove(withID id: String, agentID: UUID) { + private func remove(withID id: String, agentID: UUID) { guard let agent = registrations[id] else { Self.logger.warning("No matching registration with id: \(id)") return @@ -79,7 +77,7 @@ Self.logger.debug("Registration Count \(self.registrations.count)") } - func onUpdate(_ update: any DatabaseChangeSet) { + private func onUpdate(_ update: any DatabaseChangeSet) { for (id, registration) in registrations { Self.logger.debug("Notifying \(id)") registration.onUpdate(update) From 392117e106cbda89ed8806f22179ff5b716e411f Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 11 Oct 2024 11:39:59 -0400 Subject: [PATCH 09/12] working on more code changes --- .swift-format | 2 +- Example/Sources/ContentObject.swift | 2 +- Example/Sources/ContentView.swift | 32 ++++---- Example/Sources/DataThespianExampleApp.swift | 14 ++-- Example/Sources/Item.swift | 6 +- Example/Sources/ItemModel.swift | 8 +- Example/Sources/ModelActorDatabase.swift | 81 +++++++++++++++++++ Package.swift | 4 +- Sources/DataThespian/BackgroundDatabase.swift | 33 ++++---- Sources/DataThespian/DataMonitor.swift | 25 +++--- Sources/DataThespian/Database+Extras.swift | 18 +++-- Sources/DataThespian/Database.swift | 2 +- .../DatabaseChangePublicist.swift | 4 +- .../DatabaseChangePublicistKey.swift | 2 +- Sources/DataThespian/DatabaseChangeType.swift | 2 +- Sources/DataThespian/DatabaseKey.swift | 55 ++----------- Sources/DataThespian/FetchDescriptor.swift | 6 +- .../DataThespian/ManagedObjectMetadata.swift | 2 +- Sources/DataThespian/Model.swift | 11 +-- .../DataThespian/ModelActor+Database.swift | 4 +- Sources/DataThespian/ModelActorDatabase.swift | 5 +- Sources/DataThespian/ModelContainer.swift | 62 -------------- .../DataThespian/ModelContext+Extension.swift | 4 +- Sources/DataThespian/ModelContext.swift | 16 ++-- Sources/DataThespian/NSManagedObjectID.swift | 57 ++++++++++--- Sources/DataThespian/Notification.swift | 6 +- .../DataThespian/NotificationDataUpdate.swift | 24 +++--- Sources/DataThespian/PublishingAgent.swift | 7 +- .../DataThespianTests/DataThespianTests.swift | 2 +- 29 files changed, 275 insertions(+), 221 deletions(-) create mode 100644 Example/Sources/ModelActorDatabase.swift delete mode 100644 Sources/DataThespian/ModelContainer.swift diff --git a/.swift-format b/.swift-format index 32cd886..fe819cd 100644 --- a/.swift-format +++ b/.swift-format @@ -20,7 +20,7 @@ ] }, "prioritizeKeepingFunctionOutputTogether" : false, - "respectsExistingLineBreaks" : false, + "respectsExistingLineBreaks" : true, "rules" : { "AllPublicDeclarationsHaveDocumentation" : false, "AlwaysUseLiteralForEmptyCollectionInit" : false, diff --git a/Example/Sources/ContentObject.swift b/Example/Sources/ContentObject.swift index ab0e138..19e159b 100644 --- a/Example/Sources/ContentObject.swift +++ b/Example/Sources/ContentObject.swift @@ -36,7 +36,7 @@ internal class ContentObject { self.error = error items = [] } - assert(items.count == selectedItemsID.count) + // assert(items.count == selectedItemsID.count) return items } diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index 4e1e0a8..e1a735d 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -10,12 +10,12 @@ import DataThespian import SwiftData import SwiftUI -struct ContentView: View { - @State var object = ContentObject() - @Environment(\.database) var database - @Environment(\.databaseChangePublicist) var databaseChangePublisher +internal struct ContentView: View { + @State private var object = ContentObject() + @Environment(\.database) private var database + @Environment(\.databaseChangePublicist) private var databaseChangePublisher - var body: some View { + internal var body: some View { NavigationSplitView { List(selection: self.$object.selectedItemsID) { ForEach(object.items) { item in @@ -23,7 +23,7 @@ struct ContentView: View { } .onDelete(perform: object.deleteItems) } - .navigationSplitViewColumnWidth(min: 180, ideal: 200) + .navigationSplitViewColumnWidth(min: 200, ideal: 220) .toolbar { ToolbarItem { Button(action: addItem) { @@ -47,7 +47,9 @@ struct ContentView: View { } }.onAppear { self.object.initialize( - withDatabase: database, databaseChangePublisher: databaseChangePublisher) + withDatabase: database, + databaseChangePublisher: databaseChangePublisher + ) } } @@ -60,16 +62,18 @@ struct ContentView: View { } #Preview { + let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) let config = ModelConfiguration(isStoredInMemoryOnly: true) // swiftlint:disable:next force_try let modelContainer = try! ModelContainer(for: Item.self, configurations: config) + let backgroundDatabase = BackgroundDatabase(modelContainer: modelContainer) { + let context = ModelContext($0) + context.autosaveEnabled = true + return context + } + ContentView() - .environment( - \.databaseChangePublicist, - DatabaseChangePublicist( - dbWatcher: DataMonitor.shared - ) - ) - .database(BackgroundDatabase(modelContainer: modelContainer)) + .environment( \.databaseChangePublicist, databaseChangePublicist ) + .database(backgroundDatabase) } diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift index 910c7f7..db88c86 100644 --- a/Example/Sources/DataThespianExampleApp.swift +++ b/Example/Sources/DataThespianExampleApp.swift @@ -12,11 +12,15 @@ import SwiftUI @main internal struct DataThespianExampleApp: App { private static let databaseChangePublicist = DatabaseChangePublicist( dbWatcher: DataMonitor.shared) - // swiftlint:disable:next force_try - private static let database = try! BackgroundDatabase( - modelContainer: .init(for: Item.self), - autosaveEnabled: true - ) + + private static let database = BackgroundDatabase { + // swiftlint:disable:next force_try + try! ModelActorDatabase(modelContainer: ModelContainer(for: Item.self)) { + let context = ModelContext($0) + context.autosaveEnabled = true + return context + } + } internal var body: some Scene { WindowGroup { diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift index bd8099f..09b1e81 100644 --- a/Example/Sources/Item.swift +++ b/Example/Sources/Item.swift @@ -9,10 +9,10 @@ import Foundation import SwiftData @Model -final class Item { - var timestamp: Date +internal final class Item { + internal private(set) var timestamp: Date - init(timestamp: Date) { + internal init(timestamp: Date) { self.timestamp = timestamp } } diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemModel.swift index 8ebff74..f13e75b 100644 --- a/Example/Sources/ItemModel.swift +++ b/Example/Sources/ItemModel.swift @@ -9,11 +9,11 @@ import DataThespian import Foundation import SwiftData -struct ItemModel: Identifiable { - let model: Model - let timestamp: Date +internal struct ItemModel: Identifiable { + internal let model: Model + internal let timestamp: Date - var id: PersistentIdentifier { + internal var id: PersistentIdentifier { model.persistentIdentifier } diff --git a/Example/Sources/ModelActorDatabase.swift b/Example/Sources/ModelActorDatabase.swift new file mode 100644 index 0000000..0db2acc --- /dev/null +++ b/Example/Sources/ModelActorDatabase.swift @@ -0,0 +1,81 @@ +// +// ModelActorDatabase.swift +// DataThespian +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import DataThespian +public import SwiftData + +// @ModelActor +// public actor ModelActorDatabase: Database {} + +extension BackgroundDatabase { + convenience init (modelContainer: ModelContainer, modelContext closure: (@Sendable (ModelContainer) -> ModelContext)? = nil) { + let closure = closure ?? ModelContext.init + self.init(database: ModelActorDatabase(modelContainer: modelContainer, modelContext: closure)) + } +} + +extension DefaultSerialModelExecutor { + static func create(from closure: @Sendable @escaping (ModelContainer) -> ModelContext) -> @Sendable (ModelContainer) -> any ModelExecutor { + { + DefaultSerialModelExecutor(modelContext: closure($0)) + } + } +} + +public actor ModelActorDatabase: Database, ModelActor { + private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { + self.modelExecutor = modelExecutor + self.modelContainer = modelContainer + } + + public init(modelContainer: SwiftData.ModelContainer) { + self.init( + modelContainer: modelContainer, + modelContext: ModelContext.init + ) + } + + public init(modelContainer: SwiftData.ModelContainer, modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext) { + self.init( + modelContainer: modelContainer, + modelExecutor: DefaultSerialModelExecutor.create(from: closure) + ) + } + + public init(modelContainer: SwiftData.ModelContainer, modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor) { + self.init( + modelExecutor: closure(modelContainer), + modelContainer: modelContainer + ) + } + + public nonisolated let modelExecutor: any SwiftData.ModelExecutor + + public nonisolated let modelContainer: SwiftData.ModelContainer +} diff --git a/Package.swift b/Package.swift index 977745d..30ba2a2 100644 --- a/Package.swift +++ b/Package.swift @@ -34,8 +34,8 @@ let package = Package( platforms: [.iOS(.v17), .macCatalyst(.v17), .macOS(.v14), .tvOS(.v17), .visionOS(.v1), .watchOS(.v10)], products: [ .library( - name: "DataThespian", - targets: ["DataThespian"] + name: "DataThespian", + targets: ["DataThespian"] ) ], dependencies: [ diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index 9ed0ee4..6585952 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -28,17 +28,11 @@ // #if canImport(SwiftData) - - public import Foundation - + import Foundation public import SwiftData import SwiftUI public final class BackgroundDatabase: Database { - public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) - async rethrows -> T - { try await self.database.withModelContext(closure) } - private actor DatabaseContainer { private let factory: @Sendable () -> any Database private var wrappedTask: Task? @@ -49,7 +43,9 @@ // swiftlint:disable:next strict_fileprivate fileprivate var database: any Database { get async { - if let wrappedTask { return await wrappedTask.value } + if let wrappedTask { + return await wrappedTask.value + } let task = Task { factory() } self.wrappedTask = task return await task.value @@ -61,15 +57,24 @@ private var database: any Database { get async { await container.database } } - public convenience init(modelContainer: ModelContainer, autosaveEnabled: Bool = false) { - self.init { - assert(isMainThread: false) - return ModelActorDatabase(modelContainer: modelContainer, autosaveEnabled: autosaveEnabled) - } +// @available(*, deprecated, message: "Use your own `ModelActorDatabase`.") +// public convenience init(modelContainer: ModelContainer, autosaveEnabled: Bool = false) { +// self.init { +// assert(isMainThread: false) +// return ModelActorDatabase(modelContainer: modelContainer, autosaveEnabled: autosaveEnabled) +// } +// } + + public convenience init(database: @Sendable @escaping @autoclosure () -> any Database) { + self.init(database) } - internal init(_ factory: @Sendable @escaping () -> any Database) { + public init(_ factory: @Sendable @escaping () -> any Database) { self.container = .init(factory: factory) } + + public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) + async rethrows -> T + { try await self.database.withModelContext(closure) } } #endif diff --git a/Sources/DataThespian/DataMonitor.swift b/Sources/DataThespian/DataMonitor.swift index 54c8cdf..e479705 100644 --- a/Sources/DataThespian/DataMonitor.swift +++ b/Sources/DataThespian/DataMonitor.swift @@ -41,8 +41,8 @@ public static let shared = DataMonitor() - var object: (any NSObjectProtocol)? - var registrations = RegistrationCollection() + private var object: (any NSObjectProtocol)? + private var registrations = RegistrationCollection() private init() { Self.logger.debug("Creating DatabaseMonitor") } @@ -50,7 +50,7 @@ Task { await self.addRegistration(registration, force: force) } } - func addRegistration(_ registration: any AgentRegister, force: Bool) { + private func addRegistration(_ registration: any AgentRegister, force: Bool) { registrations.add(withID: registration.id, force: force, agent: registration.agent) } @@ -61,18 +61,25 @@ } } - func addObserver() { - guard object == nil else { return } + private func addObserver() { + guard object == nil else { + return + } object = NotificationCenter.default.addObserver( - forName: .NSManagedObjectContextDidSave, object: nil, queue: nil, + forName: .NSManagedObjectContextDidSave, + object: nil, + queue: nil, using: { notification in let update = NotificationDataUpdate(notification) Task { await self.notifyRegisration(update) } - }) + } + ) } - func notifyRegisration(_ update: any DatabaseChangeSet) { - guard !update.isEmpty else { return } + private func notifyRegisration(_ update: any DatabaseChangeSet) { + guard !update.isEmpty else { + return + } Self.logger.debug("Notifying of Update") registrations.notify(update) diff --git a/Sources/DataThespian/Database+Extras.swift b/Sources/DataThespian/Database+Extras.swift index a0423e1..3a78d4f 100644 --- a/Sources/DataThespian/Database+Extras.swift +++ b/Sources/DataThespian/Database+Extras.swift @@ -45,7 +45,8 @@ extension Database { try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in guard let model else { throw Model.NotFoundError( - persistentIdentifier: id.persistentIdentifier) + persistentIdentifier: id.persistentIdentifier + ) } return try closure(model) } @@ -66,7 +67,8 @@ extension Database { } public func first( - fetchWith selectPredicate: Predicate, otherwiseInsertBy insert: @Sendable @escaping () -> T, + fetchWith selectPredicate: Predicate, + otherwiseInsertBy insert: @Sendable @escaping () -> T, with closure: @escaping @Sendable (T) throws -> U ) async throws -> U { let value = try await self.fetch { @@ -75,7 +77,9 @@ extension Database { try models.first.map(closure) } - if let value { return value } + if let value { + return value + } let inserted: Model = await self.insert(insert) @@ -110,11 +114,12 @@ extension Database { public func fetch( _: T.Type, _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor ) async throws -> [Model] { - try await self.fetch(selectDescriptor) { models in models.map(Model.init) } + await self.fetch(selectDescriptor) { models in models.map(Model.init) } } public func fetch( - of _: T.Type, for objectIDs: [PersistentIdentifier], + of _: T.Type, + for objectIDs: [PersistentIdentifier], with closure: @escaping @Sendable (T) throws -> U ) async throws -> [U] where T: PersistentModel { try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in @@ -129,7 +134,8 @@ extension Database { } public func get( - of _: T.Type, for objectID: PersistentIdentifier, + of _: T.Type, + for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U ) async throws -> U where T: PersistentModel { try await self.get(for: objectID) { model in try closure(model) } diff --git a/Sources/DataThespian/Database.swift b/Sources/DataThespian/Database.swift index b17f230..97dfa1b 100644 --- a/Sources/DataThespian/Database.swift +++ b/Sources/DataThespian/Database.swift @@ -29,7 +29,7 @@ #if canImport(SwiftData) - public import Foundation + import Foundation public import SwiftData diff --git a/Sources/DataThespian/DatabaseChangePublicist.swift b/Sources/DataThespian/DatabaseChangePublicist.swift index 77ff089..280181e 100644 --- a/Sources/DataThespian/DatabaseChangePublicist.swift +++ b/Sources/DataThespian/DatabaseChangePublicist.swift @@ -30,14 +30,14 @@ #if canImport(Combine) && canImport(SwiftData) public import Combine - fileprivate struct NeverDatabaseMonitor: DatabaseMonitoring { + private struct NeverDatabaseMonitor: DatabaseMonitoring { func register(_: any AgentRegister, force _: Bool) { assertionFailure("Using Empty Database Listener") } } public struct DatabaseChangePublicist: Sendable { - let dbWatcher: DatabaseMonitoring + private let dbWatcher: DatabaseMonitoring public init(dbWatcher: any DatabaseMonitoring) { self.dbWatcher = dbWatcher } public static func never() -> DatabaseChangePublicist { diff --git a/Sources/DataThespian/DatabaseChangePublicistKey.swift b/Sources/DataThespian/DatabaseChangePublicistKey.swift index ae7720f..1f3b4e6 100644 --- a/Sources/DataThespian/DatabaseChangePublicistKey.swift +++ b/Sources/DataThespian/DatabaseChangePublicistKey.swift @@ -32,7 +32,7 @@ public import SwiftUI - fileprivate struct DatabaseChangePublicistKey: EnvironmentKey { + private struct DatabaseChangePublicistKey: EnvironmentKey { typealias Value = DatabaseChangePublicist nonisolated static let defaultValue: DatabaseChangePublicist = .never() diff --git a/Sources/DataThespian/DatabaseChangeType.swift b/Sources/DataThespian/DatabaseChangeType.swift index 6efecd8..d2418eb 100644 --- a/Sources/DataThespian/DatabaseChangeType.swift +++ b/Sources/DataThespian/DatabaseChangeType.swift @@ -32,7 +32,7 @@ public enum DatabaseChangeType: CaseIterable, Sendable { case deleted case updated #if canImport(SwiftData) - var keyPath: KeyPath> { + internal var keyPath: KeyPath> { switch self { case .inserted: \.inserted case .deleted: \.deleted diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index 6a595b8..379cd25 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -35,62 +35,17 @@ public import SwiftUI - fileprivate struct DefaultDatabase: Database { - func withModelContext(_ closure: (ModelContext) throws -> T) async rethrows -> T { - assertionFailure("No Database Set.") - fatalError("No Database Set.") - } - func save() async throws { - assertionFailure("No Database Set.") - throw NotImplmentedError.instance - } - func delete(_: (some PersistentModel).Type, withID _: PersistentIdentifier) async -> Bool { - assertionFailure("No Database Set.") - return false - } - - func delete(where _: Predicate?) async throws { - assertionFailure("No Database Set.") - throw NotImplmentedError.instance - } - - func insert(_: @escaping @Sendable () -> some PersistentModel) async -> PersistentIdentifier { - assertionFailure("No Database Set.") - fatalError("No Database Set.") - } - - func fetch( - _: @escaping @Sendable () -> FetchDescriptor, with _: @escaping @Sendable ([T]) throws -> U - ) async throws -> U where T: PersistentModel, U: Sendable { - assertionFailure("No Database Set.") - throw NotImplmentedError.instance - } - func fetch( - _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, - _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T], [U]) throws -> V - ) async throws -> V { - assertionFailure("No Database Set.") - throw NotImplmentedError.instance - } - func get(for _: PersistentIdentifier, with _: @escaping @Sendable (T?) throws -> U) - async throws -> U where T: PersistentModel, U: Sendable - { - assertionFailure("No Database Set.") - throw NotImplmentedError.instance - } - - private struct NotImplmentedError: Error { static let instance = NotImplmentedError() } - + private struct DefaultDatabase: Database { static let instance = DefaultDatabase() - func transaction(_: @escaping (ModelContext) throws -> Void) async throws { + // swiftlint:disable:next unavailable_function + func withModelContext(_ closure: (ModelContext) throws -> T) async rethrows -> T { assertionFailure("No Database Set.") - throw NotImplmentedError.instance + fatalError("No Database Set.") } } - fileprivate struct DatabaseKey: EnvironmentKey { + private struct DatabaseKey: EnvironmentKey { static var defaultValue: any Database { DefaultDatabase.instance } } diff --git a/Sources/DataThespian/FetchDescriptor.swift b/Sources/DataThespian/FetchDescriptor.swift index 57c2036..64b60c9 100644 --- a/Sources/DataThespian/FetchDescriptor.swift +++ b/Sources/DataThespian/FetchDescriptor.swift @@ -42,7 +42,11 @@ public init(model: Model) { let persistentIdentifier = model.persistentIdentifier self.init( - predicate: #Predicate { $0.persistentModelID == persistentIdentifier }, fetchLimit: 1) + predicate: #Predicate { + $0.persistentModelID == persistentIdentifier + }, + fetchLimit: 1 + ) } } #endif diff --git a/Sources/DataThespian/ManagedObjectMetadata.swift b/Sources/DataThespian/ManagedObjectMetadata.swift index 972898c..8b667b2 100644 --- a/Sources/DataThespian/ManagedObjectMetadata.swift +++ b/Sources/DataThespian/ManagedObjectMetadata.swift @@ -44,7 +44,7 @@ import CoreData extension ManagedObjectMetadata { - init?(managedObject: NSManagedObject) { + internal init?(managedObject: NSManagedObject) { let persistentIdentifier: PersistentIdentifier do { persistentIdentifier = try managedObject.objectID.persistentIdentifier() } catch { assertionFailure(error: error) diff --git a/Sources/DataThespian/Model.swift b/Sources/DataThespian/Model.swift index 550238c..342c84a 100644 --- a/Sources/DataThespian/Model.swift +++ b/Sources/DataThespian/Model.swift @@ -33,17 +33,18 @@ public import SwiftData - @available(*, deprecated, renamed: "Model") public typealias ModelID = Model + @available(*, deprecated, renamed: "Model") + public typealias ModelID = Model public struct Model: Sendable, Identifiable { - public init(persistentIdentifier: PersistentIdentifier) { - self.persistentIdentifier = persistentIdentifier - } + public struct NotFoundError: Error { public let persistentIdentifier: PersistentIdentifier } public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier - public struct NotFoundError: Error { public let persistentIdentifier: PersistentIdentifier } + public init(persistentIdentifier: PersistentIdentifier) { + self.persistentIdentifier = persistentIdentifier + } } extension Model where T: PersistentModel { diff --git a/Sources/DataThespian/ModelActor+Database.swift b/Sources/DataThespian/ModelActor+Database.swift index 8143c9d..a9ffdef 100644 --- a/Sources/DataThespian/ModelActor+Database.swift +++ b/Sources/DataThespian/ModelActor+Database.swift @@ -30,6 +30,8 @@ public import SwiftData extension ModelActor where Self: Database { + public static var assertIsBackground: Bool { false } + public func withModelContext( _ closure: @Sendable @escaping (ModelContext) throws -> T ) async rethrows -> T { @@ -37,6 +39,4 @@ extension ModelActor where Self: Database { let modelContext = self.modelContext return try closure(modelContext) } - - public static var assertIsBackground: Bool { false } } diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index 9e1a84d..9fecad1 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -33,7 +33,8 @@ public import SwiftData - public actor ModelActorDatabase: Database, Loggable { + @available(*, deprecated, message: "Create your own.") + public actor ModelActorDatabase: ModelActor, Database, Loggable { public static var loggingCategory: ThespianLogging.Category { .data } public nonisolated let modelExecutor: any SwiftData.ModelExecutor @@ -53,6 +54,4 @@ } } - extension ModelActorDatabase: SwiftData.ModelActor {} - #endif diff --git a/Sources/DataThespian/ModelContainer.swift b/Sources/DataThespian/ModelContainer.swift deleted file mode 100644 index 9ee7cd1..0000000 --- a/Sources/DataThespian/ModelContainer.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ModelContainer.swift -// DataThespian -// -// Created by Leo Dion. -// Copyright © 2024 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the “Software”), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -//// -//// ModelContainer.swift -//// Copyright (c) 2024 BrightDigit. -//// -// -// #if canImport(SwiftData) -// -// -// -// -// -// public import SwiftData -// -// extension ModelContainer: @retroactive Loggable { -// public static var loggingCategory: ThespianLogging.Category { -// .data -// } -// -// public static func forTypes(_ forTypes: [any PersistentModel.Type]) -> ModelContainer { -// do { -// return try ModelContainer(for: Schema(forTypes)) -// } catch { -// if EnvironmentConfiguration.shared.disallowDatabaseRebuild { -// assertionFailure(error: error) -// } -// logger.error("Unable to read database. Rebuilding the database.") -// // swiftlint:disable:next force_try -// try! ModelContainer().deleteAllData() -// return self.forTypes(forTypes) -// } -// } -// } -// #endif diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/ModelContext+Extension.swift index 48deb13..21dd557 100644 --- a/Sources/DataThespian/ModelContext+Extension.swift +++ b/Sources/DataThespian/ModelContext+Extension.swift @@ -32,7 +32,9 @@ public import SwiftData extension ModelContext { public func delete(_: T.Type, withID id: PersistentIdentifier) -> Bool { - guard let model: T = self.registeredModel(for: id) else { return false } + guard let model: T = self.registeredModel(for: id) else { + return false + } self.delete(model) return true } diff --git a/Sources/DataThespian/ModelContext.swift b/Sources/DataThespian/ModelContext.swift index 4561378..422fae3 100644 --- a/Sources/DataThespian/ModelContext.swift +++ b/Sources/DataThespian/ModelContext.swift @@ -31,15 +31,19 @@ import Foundation public import SwiftData - extension ModelContext: DataThespian.Loggable { - public static var loggingCategory: ThespianLogging.Category { .data } - func existingModel(for objectID: PersistentIdentifier) throws -> T? + extension ModelContext { + public func existingModel(for objectID: PersistentIdentifier) throws -> T? where T: PersistentModel { - if let registered: T = registeredModel(for: objectID) { return registered } - if let notRegistered: T = model(for: objectID) as? T { return notRegistered } + if let registered: T = registeredModel(for: objectID) { + return registered + } + if let notRegistered: T = model(for: objectID) as? T { + return notRegistered + } let fetchDescriptor = FetchDescriptor( - predicate: #Predicate { $0.persistentModelID == objectID }) + predicate: #Predicate { $0.persistentModelID == objectID } + ) return try fetch(fetchDescriptor).first } diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/NSManagedObjectID.swift index e377e95..32aa376 100644 --- a/Sources/DataThespian/NSManagedObjectID.swift +++ b/Sources/DataThespian/NSManagedObjectID.swift @@ -35,10 +35,13 @@ public import SwiftData // periphery:ignore - fileprivate struct PersistentIdentifierJSON: Codable { - fileprivate struct Implementation: Codable { + private struct PersistentIdentifierJSON: Codable { + private struct Implementation: Codable { fileprivate init( - primaryKey: String, uriRepresentation: URL, isTemporary: Bool, storeIdentifier: String, + primaryKey: String, + uriRepresentation: URL, + isTemporary: Bool, + storeIdentifier: String, entityName: String ) { self.primaryKey = primaryKey @@ -54,7 +57,30 @@ private var entityName: String } - fileprivate var implementation: Implementation + private var implementation: Implementation + + private init(implementation: PersistentIdentifierJSON.Implementation) { + self.implementation = implementation + } + + fileprivate init( + primaryKey: String, + uriRepresentation: URL, + isTemporary: Bool, + storeIdentifier: String, + entityName: String + ) { + self.init( + implementation: + .init( + primaryKey: primaryKey, + uriRepresentation: uriRepresentation, + isTemporary: isTemporary, + storeIdentifier: storeIdentifier, + entityName: entityName + ) + ) + } } extension NSManagedObjectID { @@ -76,9 +102,12 @@ } guard let entityName else { throw PersistentIdentifierError.missingProperty(.entityName) } let json = PersistentIdentifierJSON( - implementation: .init( - primaryKey: primaryKey, uriRepresentation: uriRepresentation(), - isTemporary: isTemporaryID, storeIdentifier: storeIdentifier, entityName: entityName)) + primaryKey: primaryKey, + uriRepresentation: uriRepresentation(), + isTemporary: isTemporaryID, + storeIdentifier: storeIdentifier, + entityName: entityName + ) let encoder = JSONEncoder() let data: Data do { data = try encoder.encode(json) } catch let error as EncodingError { @@ -94,17 +123,21 @@ // Extensions to expose needed implementation details extension NSManagedObjectID { // Primary key is last path component of URI - var primaryKey: String { uriRepresentation().lastPathComponent } + public var primaryKey: String { uriRepresentation().lastPathComponent } // Store identifier is host of URI - var storeIdentifier: String? { - guard let identifier = uriRepresentation().host() else { return nil } + public var storeIdentifier: String? { + guard let identifier = uriRepresentation().host() else { + return nil + } return identifier } // Entity name from entity name - var entityName: String? { - guard let entityName = entity.name else { return nil } + public var entityName: String? { + guard let entityName = entity.name else { + return nil + } return entityName } } diff --git a/Sources/DataThespian/Notification.swift b/Sources/DataThespian/Notification.swift index b8130f9..a338696 100644 --- a/Sources/DataThespian/Notification.swift +++ b/Sources/DataThespian/Notification.swift @@ -33,8 +33,10 @@ import Foundation extension Notification { - func managedObjects(key: String) -> Set? { - guard let objects = userInfo?[key] as? Set else { return nil } + internal func managedObjects(key: String) -> Set? { + guard let objects = userInfo?[key] as? Set else { + return nil + } return Set(objects.compactMap(ManagedObjectMetadata.init(managedObject:))) } diff --git a/Sources/DataThespian/NotificationDataUpdate.swift b/Sources/DataThespian/NotificationDataUpdate.swift index 2c01097..4fcce66 100644 --- a/Sources/DataThespian/NotificationDataUpdate.swift +++ b/Sources/DataThespian/NotificationDataUpdate.swift @@ -32,23 +32,28 @@ import Foundation - struct NotificationDataUpdate: DatabaseChangeSet, Sendable { - let inserted: Set + internal struct NotificationDataUpdate: DatabaseChangeSet, Sendable { + internal let inserted: Set - let deleted: Set + internal let deleted: Set - let updated: Set + internal let updated: Set private init( - inserted: Set?, deleted: Set?, + inserted: Set?, + deleted: Set?, updated: Set? ) { self.init( - inserted: inserted ?? .init(), deleted: deleted ?? .init(), updated: updated ?? .init()) + inserted: inserted ?? .init(), + deleted: deleted ?? .init(), + updated: updated ?? .init() + ) } private init( - inserted: Set, deleted: Set, + inserted: Set, + deleted: Set, updated: Set ) { self.inserted = inserted @@ -56,11 +61,12 @@ self.updated = updated } - init(_ notification: Notification) { + internal init(_ notification: Notification) { self.init( inserted: notification.managedObjects(key: NSInsertedObjectsKey), deleted: notification.managedObjects(key: NSDeletedObjectsKey), - updated: notification.managedObjects(key: NSUpdatedObjectsKey)) + updated: notification.managedObjects(key: NSUpdatedObjectsKey) + ) } } #endif diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/PublishingAgent.swift index 98100ea..a4e0909 100644 --- a/Sources/DataThespian/PublishingAgent.swift +++ b/Sources/DataThespian/PublishingAgent.swift @@ -59,7 +59,10 @@ self.onSubscriptionEvent(.subscribe) } receiveCancel: { self.onSubscriptionEvent(.cancel) - }.sink { _ in } + } + .sink { + _ in + } } private nonisolated func onSubscriptionEvent(_ event: SubscriptionEvent) { @@ -101,7 +104,7 @@ Task { await self.setCompleted(closure) } } - func setCompleted(_ closure: @escaping @Sendable () -> Void) { + internal func setCompleted(_ closure: @escaping @Sendable () -> Void) { Self.logger.debug("SetCompleted \(self.id) \(self.agentID)") assert(completed == nil) completed = closure diff --git a/Tests/DataThespianTests/DataThespianTests.swift b/Tests/DataThespianTests/DataThespianTests.swift index 57a3cd9..297aa14 100644 --- a/Tests/DataThespianTests/DataThespianTests.swift +++ b/Tests/DataThespianTests/DataThespianTests.swift @@ -2,6 +2,6 @@ import Testing @testable import DataThespian -@Test func example() async throws { +@Test internal func example() async throws { // Write your test here and use APIs like `#expect(...)` to check expected conditions. } From 18d86a642220878965e7fd7dce85cacf67043a26 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 11 Oct 2024 12:52:03 -0400 Subject: [PATCH 10/12] reworked ModelActorDatabase --- .swift-format | 2 +- Example/Sources/ContentView.swift | 4 +- Example/Sources/DataThespianExampleApp.swift | 3 +- Example/Sources/ModelActorDatabase.swift | 81 ------------------- Sources/DataThespian/BackgroundDatabase.swift | 39 +++++++-- .../DatabaseChangePublicist.swift | 4 +- Sources/DataThespian/ModelActorDatabase.swift | 61 +++++++++----- 7 files changed, 82 insertions(+), 112 deletions(-) delete mode 100644 Example/Sources/ModelActorDatabase.swift diff --git a/.swift-format b/.swift-format index fe819cd..83797e9 100644 --- a/.swift-format +++ b/.swift-format @@ -29,7 +29,7 @@ "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : true, - "FileScopedDeclarationPrivacy" : true, + "FileScopedDeclarationPrivacy" : false, "FullyIndirectEnum" : true, "GroupNumericLiterals" : true, "IdentifiersMustBeASCII" : true, diff --git a/Example/Sources/ContentView.swift b/Example/Sources/ContentView.swift index e1a735d..9635d21 100644 --- a/Example/Sources/ContentView.swift +++ b/Example/Sources/ContentView.swift @@ -64,6 +64,8 @@ internal struct ContentView: View { #Preview { let databaseChangePublicist = DatabaseChangePublicist(dbWatcher: DataMonitor.shared) let config = ModelConfiguration(isStoredInMemoryOnly: true) + + // swift-format-ignore: NeverUseForceTry // swiftlint:disable:next force_try let modelContainer = try! ModelContainer(for: Item.self, configurations: config) @@ -74,6 +76,6 @@ internal struct ContentView: View { } ContentView() - .environment( \.databaseChangePublicist, databaseChangePublicist ) + .environment(\.databaseChangePublicist, databaseChangePublicist) .database(backgroundDatabase) } diff --git a/Example/Sources/DataThespianExampleApp.swift b/Example/Sources/DataThespianExampleApp.swift index db88c86..f3a409c 100644 --- a/Example/Sources/DataThespianExampleApp.swift +++ b/Example/Sources/DataThespianExampleApp.swift @@ -11,9 +11,10 @@ import SwiftUI @main internal struct DataThespianExampleApp: App { - private static let databaseChangePublicist = DatabaseChangePublicist( dbWatcher: DataMonitor.shared) + private static let databaseChangePublicist = DatabaseChangePublicist() private static let database = BackgroundDatabase { + // swift-format-ignore: NeverUseForceTry // swiftlint:disable:next force_try try! ModelActorDatabase(modelContainer: ModelContainer(for: Item.self)) { let context = ModelContext($0) diff --git a/Example/Sources/ModelActorDatabase.swift b/Example/Sources/ModelActorDatabase.swift deleted file mode 100644 index 0db2acc..0000000 --- a/Example/Sources/ModelActorDatabase.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// ModelActorDatabase.swift -// DataThespian -// -// Created by Leo Dion. -// Copyright © 2024 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the “Software”), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -public import DataThespian -public import SwiftData - -// @ModelActor -// public actor ModelActorDatabase: Database {} - -extension BackgroundDatabase { - convenience init (modelContainer: ModelContainer, modelContext closure: (@Sendable (ModelContainer) -> ModelContext)? = nil) { - let closure = closure ?? ModelContext.init - self.init(database: ModelActorDatabase(modelContainer: modelContainer, modelContext: closure)) - } -} - -extension DefaultSerialModelExecutor { - static func create(from closure: @Sendable @escaping (ModelContainer) -> ModelContext) -> @Sendable (ModelContainer) -> any ModelExecutor { - { - DefaultSerialModelExecutor(modelContext: closure($0)) - } - } -} - -public actor ModelActorDatabase: Database, ModelActor { - private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { - self.modelExecutor = modelExecutor - self.modelContainer = modelContainer - } - - public init(modelContainer: SwiftData.ModelContainer) { - self.init( - modelContainer: modelContainer, - modelContext: ModelContext.init - ) - } - - public init(modelContainer: SwiftData.ModelContainer, modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext) { - self.init( - modelContainer: modelContainer, - modelExecutor: DefaultSerialModelExecutor.create(from: closure) - ) - } - - public init(modelContainer: SwiftData.ModelContainer, modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor) { - self.init( - modelExecutor: closure(modelContainer), - modelContainer: modelContainer - ) - } - - public nonisolated let modelExecutor: any SwiftData.ModelExecutor - - public nonisolated let modelContainer: SwiftData.ModelContainer -} diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index 6585952..ee9db06 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -57,14 +57,6 @@ private var database: any Database { get async { await container.database } } -// @available(*, deprecated, message: "Use your own `ModelActorDatabase`.") -// public convenience init(modelContainer: ModelContainer, autosaveEnabled: Bool = false) { -// self.init { -// assert(isMainThread: false) -// return ModelActorDatabase(modelContainer: modelContainer, autosaveEnabled: autosaveEnabled) -// } -// } - public convenience init(database: @Sendable @escaping @autoclosure () -> any Database) { self.init(database) } @@ -77,4 +69,35 @@ async rethrows -> T { try await self.database.withModelContext(closure) } } + + extension BackgroundDatabase { + public convenience init( + modelContainer: ModelContainer, + modelContext closure: (@Sendable (ModelContainer) -> ModelContext)? = nil + ) { + let closure = closure ?? ModelContext.init + self.init(database: ModelActorDatabase(modelContainer: modelContainer, modelContext: closure)) + } + + public convenience init( + modelContainer: SwiftData.ModelContainer + ) { + self.init( + modelContainer: modelContainer, + modelContext: ModelContext.init + ) + } + + public convenience init( + modelContainer: SwiftData.ModelContainer, + modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor + ) { + self.init( + database: ModelActorDatabase( + modelContainer: modelContainer, + modelExecutor: closure + ) + ) + } + } #endif diff --git a/Sources/DataThespian/DatabaseChangePublicist.swift b/Sources/DataThespian/DatabaseChangePublicist.swift index 280181e..f8f3b3d 100644 --- a/Sources/DataThespian/DatabaseChangePublicist.swift +++ b/Sources/DataThespian/DatabaseChangePublicist.swift @@ -38,7 +38,9 @@ public struct DatabaseChangePublicist: Sendable { private let dbWatcher: DatabaseMonitoring - public init(dbWatcher: any DatabaseMonitoring) { self.dbWatcher = dbWatcher } + public init(dbWatcher: any DatabaseMonitoring = DataMonitor.shared) { + self.dbWatcher = dbWatcher + } public static func never() -> DatabaseChangePublicist { self.init(dbWatcher: NeverDatabaseMonitor()) diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index 9fecad1..ce2d793 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -27,31 +27,54 @@ // OTHER DEALINGS IN THE SOFTWARE. // -#if canImport(SwiftData) +public import SwiftData - import Foundation +// @ModelActor +// public actor ModelActorDatabase: Database {} - public import SwiftData +public actor ModelActorDatabase: Database, ModelActor { + public nonisolated let modelExecutor: any SwiftData.ModelExecutor + public nonisolated let modelContainer: SwiftData.ModelContainer - @available(*, deprecated, message: "Create your own.") - public actor ModelActorDatabase: ModelActor, Database, Loggable { - public static var loggingCategory: ThespianLogging.Category { .data } + private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { + self.modelExecutor = modelExecutor + self.modelContainer = modelContainer + } - public nonisolated let modelExecutor: any SwiftData.ModelExecutor + public init(modelContainer: SwiftData.ModelContainer) { + self.init( + modelContainer: modelContainer, + modelContext: ModelContext.init + ) + } - public nonisolated let modelContainer: SwiftData.ModelContainer + public init( + modelContainer: SwiftData.ModelContainer, + modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) { + self.init( + modelContainer: modelContainer, + modelExecutor: DefaultSerialModelExecutor.create(from: closure) + ) + } - public init(modelContainer: SwiftData.ModelContainer, autosaveEnabled: Bool = false) { - let modelContext = ModelContext(modelContainer) - modelContext.autosaveEnabled = autosaveEnabled + public init( + modelContainer: SwiftData.ModelContainer, + modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor + ) { + self.init( + modelExecutor: closure(modelContainer), + modelContainer: modelContainer + ) + } +} - let modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) - self.init(modelExecutor: modelExecutor, modelContainer: modelContainer) - } - private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { - self.modelExecutor = modelExecutor - self.modelContainer = modelContainer +extension DefaultSerialModelExecutor { + fileprivate static func create( + from closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) -> @Sendable (ModelContainer) -> any ModelExecutor { + { + DefaultSerialModelExecutor(modelContext: closure($0)) } } - -#endif +} From 7fdf52aa42865c5d02ae8911b0b17a1c778712d2 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 11 Oct 2024 12:58:04 -0400 Subject: [PATCH 11/12] improve-api --- Sources/DataThespian/BackgroundDatabase.swift | 1 - Sources/DataThespian/DataMonitor.swift | 2 - Sources/DataThespian/Database+Extras.swift | 186 +++++++++--------- Sources/DataThespian/DatabaseKey.swift | 3 - Sources/DataThespian/FetchDescriptor.swift | 1 - .../DataThespian/ManagedObjectMetadata.swift | 1 - Sources/DataThespian/Model.swift | 2 - .../DataThespian/ModelActor+Database.swift | 22 ++- Sources/DataThespian/ModelActorDatabase.swift | 84 ++++---- .../DataThespian/ModelContext+Extension.swift | 90 ++++----- Sources/DataThespian/NSManagedObjectID.swift | 2 - Sources/DataThespian/Notification.swift | 1 - .../DataThespian/NotificationDataUpdate.swift | 1 - Sources/DataThespian/PublishingAgent.swift | 2 - Sources/DataThespian/PublishingRegister.swift | 1 - .../DataThespian/RegistrationCollection.swift | 1 - 16 files changed, 195 insertions(+), 205 deletions(-) diff --git a/Sources/DataThespian/BackgroundDatabase.swift b/Sources/DataThespian/BackgroundDatabase.swift index ee9db06..10aa1d6 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -30,7 +30,6 @@ #if canImport(SwiftData) import Foundation public import SwiftData - import SwiftUI public final class BackgroundDatabase: Database { private actor DatabaseContainer { diff --git a/Sources/DataThespian/DataMonitor.swift b/Sources/DataThespian/DataMonitor.swift index e479705..413d691 100644 --- a/Sources/DataThespian/DataMonitor.swift +++ b/Sources/DataThespian/DataMonitor.swift @@ -30,9 +30,7 @@ #if canImport(Combine) && canImport(SwiftData) && canImport(CoreData) import Combine - import CoreData - import Foundation import SwiftData diff --git a/Sources/DataThespian/Database+Extras.swift b/Sources/DataThespian/Database+Extras.swift index 3a78d4f..15644f5 100644 --- a/Sources/DataThespian/Database+Extras.swift +++ b/Sources/DataThespian/Database+Extras.swift @@ -27,117 +27,119 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import Foundation -public import SwiftData - -extension Database { - public func insert( - _ closuer: @Sendable @escaping () -> PersistentModelType - ) async -> Model { - let id: PersistentIdentifier = await self.insert(closuer) - return .init(persistentIdentifier: id) - } +#if canImport(SwiftData) + public import Foundation + public import SwiftData + + extension Database { + public func insert( + _ closuer: @Sendable @escaping () -> PersistentModelType + ) async -> Model { + let id: PersistentIdentifier = await self.insert(closuer) + return .init(persistentIdentifier: id) + } - public func with( - _ id: Model, - _ closure: @escaping @Sendable (PersistentModelType) throws -> U - ) async rethrows -> U { - try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in - guard let model else { - throw Model.NotFoundError( - persistentIdentifier: id.persistentIdentifier - ) + public func with( + _ id: Model, + _ closure: @escaping @Sendable (PersistentModelType) throws -> U + ) async rethrows -> U { + try await self.get(for: id.persistentIdentifier) { (model: PersistentModelType?) -> U in + guard let model else { + throw Model.NotFoundError( + persistentIdentifier: id.persistentIdentifier + ) + } + return try closure(model) } - return try closure(model) } - } - public func first(_ selectPredicate: Predicate) async throws -> Model? { - try await self.first(selectPredicate, with: Model.ifMap) - } - - public func first( - _ selectPredicate: Predicate, with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U { - try await self.fetch { - .init(predicate: selectPredicate, fetchLimit: 1) - } with: { models in - try closure(models.first) + public func first(_ selectPredicate: Predicate) async throws -> Model? { + try await self.first(selectPredicate, with: Model.ifMap) } - } - public func first( - fetchWith selectPredicate: Predicate, - otherwiseInsertBy insert: @Sendable @escaping () -> T, - with closure: @escaping @Sendable (T) throws -> U - ) async throws -> U { - let value = try await self.fetch { - .init(predicate: selectPredicate, fetchLimit: 1) - } with: { models in - try models.first.map(closure) - } - - if let value { - return value + public func first( + _ selectPredicate: Predicate, with closure: @escaping @Sendable (T?) throws -> U + ) async throws -> U { + try await self.fetch { + .init(predicate: selectPredicate, fetchLimit: 1) + } with: { models in + try closure(models.first) + } } - let inserted: Model = await self.insert(insert) + public func first( + fetchWith selectPredicate: Predicate, + otherwiseInsertBy insert: @Sendable @escaping () -> T, + with closure: @escaping @Sendable (T) throws -> U + ) async throws -> U { + let value = try await self.fetch { + .init(predicate: selectPredicate, fetchLimit: 1) + } with: { models in + try models.first.map(closure) + } - return try await self.with(inserted, closure) - } + if let value { + return value + } - public func delete(model _: T.Type, where predicate: Predicate? = nil) - async throws - { try await self.delete(where: predicate) } + let inserted: Model = await self.insert(insert) - public func delete(_ model: Model) async { - await self.delete(T.self, withID: model.persistentIdentifier) - } + return try await self.with(inserted, closure) + } - public func deleteAll(of types: [any PersistentModel.Type]) async throws { - try await self.transaction { context in for type in types { try context.delete(model: type) } } - } + public func delete(model _: T.Type, where predicate: Predicate? = nil) + async throws + { try await self.delete(where: predicate) } - public func fetch( - _: T.Type, with closure: @escaping @Sendable ([T]) throws -> U - ) async throws -> U { - try await self.fetch { - FetchDescriptor() - } with: { models in - try closure(models) + public func delete(_ model: Model) async { + await self.delete(T.self, withID: model.persistentIdentifier) } - } - public func fetch(_: T.Type) async throws -> [Model] { - try await self.fetch(T.self) { models in models.map(Model.init) } - } - public func fetch( - _: T.Type, _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor - ) async throws -> [Model] { - await self.fetch(selectDescriptor) { models in models.map(Model.init) } - } + public func deleteAll(of types: [any PersistentModel.Type]) async throws { + try await self.transaction { context in for type in types { try context.delete(model: type) } } + } - public func fetch( - of _: T.Type, - for objectIDs: [PersistentIdentifier], - with closure: @escaping @Sendable (T) throws -> U - ) async throws -> [U] where T: PersistentModel { - try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in - for id in objectIDs { - group.addTask { try await self.get(for: id) { model in try model.map(closure) } } + public func fetch( + _: T.Type, with closure: @escaping @Sendable ([T]) throws -> U + ) async throws -> U { + try await self.fetch { + FetchDescriptor() + } with: { models in + try closure(models) } + } + + public func fetch(_: T.Type) async throws -> [Model] { + try await self.fetch(T.self) { models in models.map(Model.init) } + } + public func fetch( + _: T.Type, _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor + ) async throws -> [Model] { + await self.fetch(selectDescriptor) { models in models.map(Model.init) } + } - return try await group.reduce(into: []) { partialResult, item in - if let item { partialResult.append(item) } + public func fetch( + of _: T.Type, + for objectIDs: [PersistentIdentifier], + with closure: @escaping @Sendable (T) throws -> U + ) async throws -> [U] where T: PersistentModel { + try await withThrowingTaskGroup(of: U?.self, returning: [U].self) { group in + for id in objectIDs { + group.addTask { try await self.get(for: id) { model in try model.map(closure) } } + } + + return try await group.reduce(into: []) { partialResult, item in + if let item { partialResult.append(item) } + } } } - } - public func get( - of _: T.Type, - for objectID: PersistentIdentifier, - with closure: @escaping @Sendable (T?) throws -> U - ) async throws -> U where T: PersistentModel { - try await self.get(for: objectID) { model in try closure(model) } + public func get( + of _: T.Type, + for objectID: PersistentIdentifier, + with closure: @escaping @Sendable (T?) throws -> U + ) async throws -> U where T: PersistentModel { + try await self.get(for: objectID) { model in try closure(model) } + } } -} +#endif diff --git a/Sources/DataThespian/DatabaseKey.swift b/Sources/DataThespian/DatabaseKey.swift index 379cd25..3c2d0fd 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -28,11 +28,8 @@ // #if canImport(SwiftUI) - import Foundation - import SwiftData - public import SwiftUI private struct DefaultDatabase: Database { diff --git a/Sources/DataThespian/FetchDescriptor.swift b/Sources/DataThespian/FetchDescriptor.swift index 64b60c9..1ced50d 100644 --- a/Sources/DataThespian/FetchDescriptor.swift +++ b/Sources/DataThespian/FetchDescriptor.swift @@ -29,7 +29,6 @@ #if canImport(SwiftData) public import Foundation - public import SwiftData extension FetchDescriptor { diff --git a/Sources/DataThespian/ManagedObjectMetadata.swift b/Sources/DataThespian/ManagedObjectMetadata.swift index 8b667b2..1cc8202 100644 --- a/Sources/DataThespian/ManagedObjectMetadata.swift +++ b/Sources/DataThespian/ManagedObjectMetadata.swift @@ -28,7 +28,6 @@ // #if canImport(SwiftData) - public import SwiftData public struct ManagedObjectMetadata: Sendable, Hashable { diff --git a/Sources/DataThespian/Model.swift b/Sources/DataThespian/Model.swift index 342c84a..ed01dac 100644 --- a/Sources/DataThespian/Model.swift +++ b/Sources/DataThespian/Model.swift @@ -28,9 +28,7 @@ // #if canImport(SwiftData) - import Foundation - public import SwiftData @available(*, deprecated, renamed: "Model") diff --git a/Sources/DataThespian/ModelActor+Database.swift b/Sources/DataThespian/ModelActor+Database.swift index a9ffdef..6b1e1e1 100644 --- a/Sources/DataThespian/ModelActor+Database.swift +++ b/Sources/DataThespian/ModelActor+Database.swift @@ -27,16 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -extension ModelActor where Self: Database { - public static var assertIsBackground: Bool { false } + extension ModelActor where Self: Database { + public static var assertIsBackground: Bool { false } - public func withModelContext( - _ closure: @Sendable @escaping (ModelContext) throws -> T - ) async rethrows -> T { - assert(isMainThread: true, if: Self.assertIsBackground) - let modelContext = self.modelContext - return try closure(modelContext) + public func withModelContext( + _ closure: @Sendable @escaping (ModelContext) throws -> T + ) async rethrows -> T { + assert(isMainThread: true, if: Self.assertIsBackground) + let modelContext = self.modelContext + return try closure(modelContext) + } } -} +#endif diff --git a/Sources/DataThespian/ModelActorDatabase.swift b/Sources/DataThespian/ModelActorDatabase.swift index ce2d793..c451b99 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -27,54 +27,56 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import SwiftData +#if canImport(SwiftData) + public import SwiftData -// @ModelActor -// public actor ModelActorDatabase: Database {} + // @ModelActor + // public actor ModelActorDatabase: Database {} -public actor ModelActorDatabase: Database, ModelActor { - public nonisolated let modelExecutor: any SwiftData.ModelExecutor - public nonisolated let modelContainer: SwiftData.ModelContainer + public actor ModelActorDatabase: Database, ModelActor { + public nonisolated let modelExecutor: any SwiftData.ModelExecutor + public nonisolated let modelContainer: SwiftData.ModelContainer - private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { - self.modelExecutor = modelExecutor - self.modelContainer = modelContainer - } + private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { + self.modelExecutor = modelExecutor + self.modelContainer = modelContainer + } - public init(modelContainer: SwiftData.ModelContainer) { - self.init( - modelContainer: modelContainer, - modelContext: ModelContext.init - ) - } + public init(modelContainer: SwiftData.ModelContainer) { + self.init( + modelContainer: modelContainer, + modelContext: ModelContext.init + ) + } - public init( - modelContainer: SwiftData.ModelContainer, - modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext - ) { - self.init( - modelContainer: modelContainer, - modelExecutor: DefaultSerialModelExecutor.create(from: closure) - ) - } + public init( + modelContainer: SwiftData.ModelContainer, + modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) { + self.init( + modelContainer: modelContainer, + modelExecutor: DefaultSerialModelExecutor.create(from: closure) + ) + } - public init( - modelContainer: SwiftData.ModelContainer, - modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor - ) { - self.init( - modelExecutor: closure(modelContainer), - modelContainer: modelContainer - ) + public init( + modelContainer: SwiftData.ModelContainer, + modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor + ) { + self.init( + modelExecutor: closure(modelContainer), + modelContainer: modelContainer + ) + } } -} -extension DefaultSerialModelExecutor { - fileprivate static func create( - from closure: @Sendable @escaping (ModelContainer) -> ModelContext - ) -> @Sendable (ModelContainer) -> any ModelExecutor { - { - DefaultSerialModelExecutor(modelContext: closure($0)) + extension DefaultSerialModelExecutor { + fileprivate static func create( + from closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) -> @Sendable (ModelContainer) -> any ModelExecutor { + { + DefaultSerialModelExecutor(modelContext: closure($0)) + } } } -} +#endif diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/ModelContext+Extension.swift index 21dd557..3013d03 100644 --- a/Sources/DataThespian/ModelContext+Extension.swift +++ b/Sources/DataThespian/ModelContext+Extension.swift @@ -27,54 +27,56 @@ // OTHER DEALINGS IN THE SOFTWARE. // -public import Foundation -public import SwiftData +#if canImport(SwiftData) + public import Foundation + public import SwiftData -extension ModelContext { - public func delete(_: T.Type, withID id: PersistentIdentifier) -> Bool { - guard let model: T = self.registeredModel(for: id) else { - return false + extension ModelContext { + public func delete(_: T.Type, withID id: PersistentIdentifier) -> Bool { + guard let model: T = self.registeredModel(for: id) else { + return false + } + self.delete(model) + return true } - self.delete(model) - return true - } - public func delete(where predicate: Predicate?) throws where T: PersistentModel { - try self.delete(model: T.self, where: predicate) - } + public func delete(where predicate: Predicate?) throws where T: PersistentModel { + try self.delete(model: T.self, where: predicate) + } - public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) - -> PersistentIdentifier - { - let model = closuer() - self.insert(model) - return model.persistentModelID - } - public func fetch( - _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T]) throws -> U - ) throws -> U where T: PersistentModel, U: Sendable { - let models = try self.fetch(selectDescriptor()) - return try closure(models) - } - public func fetch( - _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, - _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, - with closure: @escaping @Sendable ([T], [U]) throws -> V - ) throws -> V { - let firstModels = try self.fetch(selectDescriptorA()) - let secondModels = try self.fetch(selectDescriptorB()) - return try closure(firstModels, secondModels) - } + public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) + -> PersistentIdentifier + { + let model = closuer() + self.insert(model) + return model.persistentModelID + } + public func fetch( + _ selectDescriptor: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T]) throws -> U + ) throws -> U where T: PersistentModel, U: Sendable { + let models = try self.fetch(selectDescriptor()) + return try closure(models) + } + public func fetch( + _ selectDescriptorA: @escaping @Sendable () -> FetchDescriptor, + _ selectDescriptorB: @escaping @Sendable () -> FetchDescriptor, + with closure: @escaping @Sendable ([T], [U]) throws -> V + ) throws -> V { + let firstModels = try self.fetch(selectDescriptorA()) + let secondModels = try self.fetch(selectDescriptorB()) + return try closure(firstModels, secondModels) + } - public func get( - for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U - ) throws -> U where T: PersistentModel, U: Sendable { - let model: T? = try self.existingModel(for: objectID) - return try closure(model) - } + public func get( + for objectID: PersistentIdentifier, with closure: @escaping @Sendable (T?) throws -> U + ) throws -> U where T: PersistentModel, U: Sendable { + let model: T? = try self.existingModel(for: objectID) + return try closure(model) + } - public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { - try self.transaction { try block(self) } + public func transaction(block: @escaping @Sendable (ModelContext) throws -> Void) throws { + try self.transaction { try block(self) } + } } -} +#endif diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/NSManagedObjectID.swift index 32aa376..f7e9303 100644 --- a/Sources/DataThespian/NSManagedObjectID.swift +++ b/Sources/DataThespian/NSManagedObjectID.swift @@ -29,9 +29,7 @@ #if canImport(CoreData) && canImport(SwiftData) public import CoreData - import Foundation - public import SwiftData // periphery:ignore diff --git a/Sources/DataThespian/Notification.swift b/Sources/DataThespian/Notification.swift index a338696..a1f1ce3 100644 --- a/Sources/DataThespian/Notification.swift +++ b/Sources/DataThespian/Notification.swift @@ -29,7 +29,6 @@ #if canImport(CoreData) import CoreData - import Foundation extension Notification { diff --git a/Sources/DataThespian/NotificationDataUpdate.swift b/Sources/DataThespian/NotificationDataUpdate.swift index 4fcce66..9d1133f 100644 --- a/Sources/DataThespian/NotificationDataUpdate.swift +++ b/Sources/DataThespian/NotificationDataUpdate.swift @@ -29,7 +29,6 @@ #if canImport(CoreData) && canImport(SwiftData) import CoreData - import Foundation internal struct NotificationDataUpdate: DatabaseChangeSet, Sendable { diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/PublishingAgent.swift index a4e0909..ab019bb 100644 --- a/Sources/DataThespian/PublishingAgent.swift +++ b/Sources/DataThespian/PublishingAgent.swift @@ -28,9 +28,7 @@ // #if canImport(Combine) && canImport(SwiftData) - @preconcurrency import Combine - import Foundation internal actor PublishingAgent: DataAgent, Loggable { diff --git a/Sources/DataThespian/PublishingRegister.swift b/Sources/DataThespian/PublishingRegister.swift index 651ad47..a87f355 100644 --- a/Sources/DataThespian/PublishingRegister.swift +++ b/Sources/DataThespian/PublishingRegister.swift @@ -29,7 +29,6 @@ #if canImport(Combine) && canImport(SwiftData) @preconcurrency import Combine - import Foundation internal struct PublishingRegister: AgentRegister { diff --git a/Sources/DataThespian/RegistrationCollection.swift b/Sources/DataThespian/RegistrationCollection.swift index 721b32d..cadbbbd 100644 --- a/Sources/DataThespian/RegistrationCollection.swift +++ b/Sources/DataThespian/RegistrationCollection.swift @@ -28,7 +28,6 @@ // #if canImport(SwiftData) - import Foundation internal actor RegistrationCollection: Loggable { From 2850a6383fd9a67c008c5089d2acdfc1aebd13f3 Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Fri, 11 Oct 2024 13:01:23 -0400 Subject: [PATCH 12/12] adding requirement for public documentation --- .swift-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swift-format b/.swift-format index 83797e9..5c31a3e 100644 --- a/.swift-format +++ b/.swift-format @@ -22,7 +22,7 @@ "prioritizeKeepingFunctionOutputTogether" : false, "respectsExistingLineBreaks" : true, "rules" : { - "AllPublicDeclarationsHaveDocumentation" : false, + "AllPublicDeclarationsHaveDocumentation" : true, "AlwaysUseLiteralForEmptyCollectionInit" : false, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true,