From d450301f91f05554c6c26a6056dea8a1c40cc502 Mon Sep 17 00:00:00 2001 From: leogdion Date: Fri, 11 Oct 2024 13:13:02 -0400 Subject: [PATCH] Improving API (#9) --- .swift-format | 18 +-- .swiftlint.yml | 129 +++++++++++++++ .../AccentColor.colorset/Contents.json | 11 ++ .../AppIcon.appiconset/Contents.json | 58 +++++++ Example/Sources/Assets.xcassets/Contents.json | 6 + Example/Sources/ContentObject.swift | 133 ++++++++++++++++ Example/Sources/ContentView.swift | 81 ++++++++++ .../Sources/DataThespianExample.entitlements | 10 ++ Example/Sources/DataThespianExampleApp.swift | 37 +++++ Example/Sources/Item.swift | 18 +++ Example/Sources/ItemModel.swift | 28 ++++ .../Preview Assets.xcassets/Contents.json | 6 + Example/Support/.gitkeep | 0 Example/Support/Info.plist | 24 +++ Mintfile | 4 +- Package.swift | 75 +++++---- Scripts/lint.sh | 28 +++- Sources/DataThespian/Assert.swift | 8 +- Sources/DataThespian/BackgroundDatabase.swift | 88 +++++------ Sources/DataThespian/DataMonitor.swift | 20 +-- Sources/DataThespian/Database+Extras.swift | 145 +++++++++++++++++ .../DataThespian/Database+ModelContext.swift | 78 +++++++++ Sources/DataThespian/Database.swift | 149 +----------------- .../DatabaseChangePublicist.swift | 8 +- .../DatabaseChangePublicistKey.swift | 2 +- Sources/DataThespian/DatabaseChangeSet.swift | 3 +- Sources/DataThespian/DatabaseChangeType.swift | 7 +- Sources/DataThespian/DatabaseKey.swift | 56 +------ Sources/DataThespian/FetchDescriptor.swift | 7 +- .../DataThespian/ManagedObjectMetadata.swift | 6 +- .../{ModelID.swift => Model.swift} | 22 ++- ...tainer.swift => ModelActor+Database.swift} | 50 ++---- Sources/DataThespian/ModelActorDatabase.swift | 103 +++++------- .../DataThespian/ModelContext+Extension.swift | 82 ++++++++++ Sources/DataThespian/ModelContext.swift | 13 +- Sources/DataThespian/NSManagedObjectID.swift | 67 +++++--- Sources/DataThespian/Notification.swift | 7 +- .../DataThespian/NotificationDataUpdate.swift | 11 +- Sources/DataThespian/PublishingAgent.swift | 53 ++++--- Sources/DataThespian/PublishingRegister.swift | 14 +- .../DataThespian/RegistrationCollection.swift | 28 ++-- .../{Logging.swift => ThespianLogging.swift} | 6 +- .../DataThespianTests/DataThespianTests.swift | 7 +- project.yml | 33 ++++ 44 files changed, 1218 insertions(+), 521 deletions(-) create mode 100644 .swiftlint.yml 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/ContentObject.swift 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/ItemModel.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 create mode 100644 Sources/DataThespian/Database+Extras.swift create mode 100644 Sources/DataThespian/Database+ModelContext.swift rename Sources/DataThespian/{ModelID.swift => Model.swift} (71%) rename Sources/DataThespian/{ModelContainer.swift => ModelActor+Database.swift} (57%) create mode 100644 Sources/DataThespian/ModelContext+Extension.swift rename Sources/DataThespian/{Logging.swift => ThespianLogging.swift} (98%) diff --git a/.swift-format b/.swift-format index 4f562bf..5c31a3e 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, @@ -29,7 +29,7 @@ "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : true, - "FileScopedDeclarationPrivacy" : true, + "FileScopedDeclarationPrivacy" : false, "FullyIndirectEnum" : true, "GroupNumericLiterals" : true, "IdentifiersMustBeASCII" : true, @@ -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..38f6ff1 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,129 @@ +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 +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/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/ContentObject.swift b/Example/Sources/ContentObject.swift new file mode 100644 index 0000000..19e159b --- /dev/null +++ b/Example/Sources/ContentObject.swift @@ -0,0 +1,133 @@ +// +// ContentObject.swift +// DataThespian +// +// Created by Leo Dion on 10/10/24. +// + +import Combine +import DataThespian +import Foundation +import SwiftData + +@Observable +@MainActor +internal 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 = [] + private var newItem: AnyCancellable? + internal var error: (any Error)? + + internal var selectedItems: [ItemModel] { + let selectedItemsID = self.selectedItemsID + 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 + } + + internal init() { + self.databaseChangeSubscription = self.databaseChangePublisher.sink { _ in + self.beginUpdateItems() + } + } + + 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 { + try await self.updateItems() + } catch { + self.error = error + } + } + } + + private 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 func initialize( + withDatabase database: any Database, databaseChangePublisher: DatabaseChangePublicist + ) { + self.database = database + self.databaseChangeCancellable = databaseChangePublisher(id: "contentView") + .subscribe(self.databaseChangePublisher) + self.beginUpdateItems() + } + + internal func deleteSelectedItems() { + let models = self.selectedItems.map { + Model(persistentIdentifier: $0.id) + } + self.deleteItems(models) + } + internal func deleteItems(offsets: IndexSet) { + let models = + offsets + .compactMap { items[$0].id } + .map(Model.init(persistentIdentifier:)) + + assert(models.count == offsets.count) + + self.deleteItems(models) + } + + internal func deleteItems(_ models: [Model]) { + 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 new file mode 100644 index 0000000..9635d21 --- /dev/null +++ b/Example/Sources/ContentView.swift @@ -0,0 +1,81 @@ +// +// ContentView.swift +// DataThespianExample +// +// Created by Leo Dion on 10/10/24. +// + +import Combine +import DataThespian +import SwiftData +import SwiftUI + +internal struct ContentView: View { + @State private var object = ContentObject() + @Environment(\.database) private var database + @Environment(\.databaseChangePublicist) private var databaseChangePublisher + + internal var body: some View { + 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: 200, ideal: 220) + .toolbar { + ToolbarItem { + Button(action: addItem) { + Label("Add Item", systemImage: "plus") + } + } + 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 { + 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) + + let backgroundDatabase = BackgroundDatabase(modelContainer: modelContainer) { + let context = ModelContext($0) + context.autosaveEnabled = true + return context + } + + ContentView() + .environment(\.databaseChangePublicist, databaseChangePublicist) + .database(backgroundDatabase) +} 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..f3a409c --- /dev/null +++ b/Example/Sources/DataThespianExampleApp.swift @@ -0,0 +1,37 @@ +// +// DataThespianExampleApp.swift +// DataThespianExample +// +// Created by Leo Dion on 10/10/24. +// + +import DataThespian +import SwiftData +import SwiftUI + +@main +internal struct DataThespianExampleApp: App { + 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) + context.autosaveEnabled = true + return context + } + } + + internal var body: some Scene { + WindowGroup { + ContentView() + } + .database(Self.database) + .environment(\.databaseChangePublicist, Self.databaseChangePublicist) + } + + internal init() { + DataMonitor.shared.begin(with: []) + } +} diff --git a/Example/Sources/Item.swift b/Example/Sources/Item.swift new file mode 100644 index 0000000..09b1e81 --- /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 +internal final class Item { + internal private(set) var timestamp: Date + + internal init(timestamp: Date) { + self.timestamp = timestamp + } +} diff --git a/Example/Sources/ItemModel.swift b/Example/Sources/ItemModel.swift new file mode 100644 index 0000000..f13e75b --- /dev/null +++ b/Example/Sources/ItemModel.swift @@ -0,0 +1,28 @@ +// +// ItemModel.swift +// DataThespian +// +// Created by Leo Dion on 10/10/24. +// + +import DataThespian +import Foundation +import SwiftData + +internal struct ItemModel: Identifiable { + internal let model: Model + internal let timestamp: Date + + internal var id: PersistentIdentifier { + model.persistentIdentifier + } + + private init(model: Model, timestamp: Date) { + self.model = model + self.timestamp = timestamp + } + + internal init(item: Item) { + self.init(model: .init(item), timestamp: item.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/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/Package.swift b/Package.swift index 9e49197..30ba2a2 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 c071a32..9e51e5c 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 Example/Sources + 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 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..bb575a2 100644 --- a/Sources/DataThespian/Assert.swift +++ b/Sources/DataThespian/Assert.swift @@ -29,10 +29,12 @@ 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( - 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 e7edaac..10aa1d6 100644 --- a/Sources/DataThespian/BackgroundDatabase.swift +++ b/Sources/DataThespian/BackgroundDatabase.swift @@ -28,46 +28,10 @@ // #if canImport(SwiftData) - - public import Foundation - + import Foundation public import SwiftData - import SwiftUI public final class BackgroundDatabase: Database { - 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) - } - private actor DatabaseContainer { private let factory: @Sendable () -> any Database private var wrappedTask: Task? @@ -78,7 +42,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 @@ -90,21 +56,47 @@ 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) - } + 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 transaction(_ block: @escaping @Sendable (ModelContext) throws -> Void) async throws - { - assert(isMainThread: false) - try await self.database.transaction(block) + public func withModelContext(_ closure: @Sendable @escaping (ModelContext) throws -> T) + 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/DataMonitor.swift b/Sources/DataThespian/DataMonitor.swift index 50e0efb..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 @@ -41,8 +39,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 +48,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,8 +59,10 @@ } } - func addObserver() { - guard object == nil else { return } + private func addObserver() { + guard object == nil else { + return + } object = NotificationCenter.default.addObserver( forName: .NSManagedObjectContextDidSave, object: nil, @@ -74,8 +74,10 @@ ) } - 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 new file mode 100644 index 0000000..15644f5 --- /dev/null +++ b/Sources/DataThespian/Database+Extras.swift @@ -0,0 +1,145 @@ +// +// 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. +// + +#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 + ) + } + 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( + 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: Model = 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: 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) } } + } + + 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) } + } + + 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) } + } + } +#endif diff --git a/Sources/DataThespian/Database+ModelContext.swift b/Sources/DataThespian/Database+ModelContext.swift new file mode 100644 index 0000000..4107355 --- /dev/null +++ b/Sources/DataThespian/Database+ModelContext.swift @@ -0,0 +1,78 @@ +// +// 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 93a0a69..97dfa1b 100644 --- a/Sources/DataThespian/Database.swift +++ b/Sources/DataThespian/Database.swift @@ -29,155 +29,12 @@ #if canImport(SwiftData) - public import Foundation + import Foundation public import SwiftData public protocol Database: Sendable { - func save() async throws - @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/DatabaseChangePublicist.swift b/Sources/DataThespian/DatabaseChangePublicist.swift index 77ff089..f8f3b3d 100644 --- a/Sources/DataThespian/DatabaseChangePublicist.swift +++ b/Sources/DataThespian/DatabaseChangePublicist.swift @@ -30,15 +30,17 @@ #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 - public init(dbWatcher: any DatabaseMonitoring) { self.dbWatcher = dbWatcher } + private let dbWatcher: DatabaseMonitoring + public init(dbWatcher: any DatabaseMonitoring = DataMonitor.shared) { + self.dbWatcher = dbWatcher + } public static func never() -> DatabaseChangePublicist { self.init(dbWatcher: NeverDatabaseMonitor()) 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/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/DatabaseChangeType.swift b/Sources/DataThespian/DatabaseChangeType.swift index cef6ea5..d2418eb 100644 --- a/Sources/DataThespian/DatabaseChangeType.swift +++ b/Sources/DataThespian/DatabaseChangeType.swift @@ -32,8 +32,11 @@ public enum DatabaseChangeType: CaseIterable, Sendable { case deleted case updated #if canImport(SwiftData) - var keyPath: KeyPath> { - switch self { case .inserted: \.inserted case .deleted: \.deleted case .updated: \.updated + internal var keyPath: KeyPath> { + 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..3c2d0fd 100644 --- a/Sources/DataThespian/DatabaseKey.swift +++ b/Sources/DataThespian/DatabaseKey.swift @@ -28,66 +28,21 @@ // #if canImport(SwiftUI) - import Foundation - import SwiftData - public import SwiftUI - fileprivate struct DefaultDatabase: Database { - public 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 } } @@ -104,7 +59,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/FetchDescriptor.swift b/Sources/DataThespian/FetchDescriptor.swift index 9ae2b20..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 { @@ -39,10 +38,12 @@ self.fetchLimit = fetchLimit } - public init(model: ModelID) { + public init(model: Model) { let persistentIdentifier = model.persistentIdentifier self.init( - predicate: #Predicate { $0.persistentModelID == persistentIdentifier }, + predicate: #Predicate { + $0.persistentModelID == persistentIdentifier + }, fetchLimit: 1 ) } diff --git a/Sources/DataThespian/ManagedObjectMetadata.swift b/Sources/DataThespian/ManagedObjectMetadata.swift index be45fd3..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 { @@ -44,10 +43,9 @@ import CoreData extension ManagedObjectMetadata { - init?(managedObject: NSManagedObject) { + internal 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/ModelID.swift b/Sources/DataThespian/Model.swift similarity index 71% rename from Sources/DataThespian/ModelID.swift rename to Sources/DataThespian/Model.swift index e66ed22..ed01dac 100644 --- a/Sources/DataThespian/ModelID.swift +++ b/Sources/DataThespian/Model.swift @@ -1,5 +1,5 @@ // -// ModelID.swift +// Model.swift // DataThespian // // Created by Leo Dion. @@ -28,20 +28,26 @@ // #if canImport(SwiftData) - import Foundation - public import SwiftData - public struct ModelID: Sendable, Identifiable { - public var id: PersistentIdentifier.ID { return persistentIdentifier.id } + + @available(*, deprecated, renamed: "Model") + public typealias ModelID = Model + + public struct Model: Sendable, Identifiable { + public struct NotFoundError: Error { public let persistentIdentifier: PersistentIdentifier } + + public var id: PersistentIdentifier.ID { persistentIdentifier.id } public let persistentIdentifier: PersistentIdentifier - enum Error: Swift.Error { case notFound(PersistentIdentifier) } + public init(persistentIdentifier: PersistentIdentifier) { + self.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/ModelContainer.swift b/Sources/DataThespian/ModelActor+Database.swift similarity index 57% rename from Sources/DataThespian/ModelContainer.swift rename to Sources/DataThespian/ModelActor+Database.swift index 9ee7cd1..6b1e1e1 100644 --- a/Sources/DataThespian/ModelContainer.swift +++ b/Sources/DataThespian/ModelActor+Database.swift @@ -1,5 +1,5 @@ // -// ModelContainer.swift +// ModelActor+Database.swift // DataThespian // // Created by Leo Dion. @@ -27,36 +27,18 @@ // 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 +#if canImport(SwiftData) + 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 { + 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 09532ad..c451b99 100644 --- a/Sources/DataThespian/ModelActorDatabase.swift +++ b/Sources/DataThespian/ModelActorDatabase.swift @@ -28,84 +28,55 @@ // #if canImport(SwiftData) - - public import Foundation - public import SwiftData - 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 - } + // @ModelActor + // public actor ModelActorDatabase: Database {} - public func delete(where predicate: Predicate?) async throws where T: PersistentModel { - try self.modelContext.delete(model: T.self, where: predicate) - } + public actor ModelActorDatabase: Database, ModelActor { + public nonisolated let modelExecutor: any SwiftData.ModelExecutor + public nonisolated let modelContainer: SwiftData.ModelContainer - public func insert(_ closuer: @escaping @Sendable () -> some PersistentModel) async - -> PersistentIdentifier - { - let model = closuer() - self.modelContext.insert(model) - return model.persistentModelID + private init(modelExecutor: any ModelExecutor, modelContainer: ModelContainer) { + self.modelExecutor = modelExecutor + self.modelContainer = modelContainer } - 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 init(modelContainer: SwiftData.ModelContainer) { + self.init( + modelContainer: modelContainer, + modelContext: ModelContext.init + ) } - 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 init( + modelContainer: SwiftData.ModelContainer, + modelContext closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) { + self.init( + modelContainer: modelContainer, + modelExecutor: DefaultSerialModelExecutor.create(from: closure) + ) } - public static var loggingCategory: ThespianLogging.Category { .data } - public func transaction(_ block: @escaping @Sendable (ModelContext) throws -> Void) async throws - { - assert(isMainThread: false) + public init( + modelContainer: SwiftData.ModelContainer, + modelExecutor closure: @Sendable @escaping (ModelContainer) -> any ModelExecutor + ) { + self.init( + modelExecutor: closure(modelContainer), + modelContainer: modelContainer + ) + } + } - try self.modelContext.transaction { - assert(isMainThread: false) - try block(modelContext) + extension DefaultSerialModelExecutor { + fileprivate static func create( + from closure: @Sendable @escaping (ModelContainer) -> ModelContext + ) -> @Sendable (ModelContainer) -> any ModelExecutor { + { + DefaultSerialModelExecutor(modelContext: closure($0)) } } - public func save() throws { - 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 - 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 ModelActorDatabase: SwiftData.ModelActor {} - #endif diff --git a/Sources/DataThespian/ModelContext+Extension.swift b/Sources/DataThespian/ModelContext+Extension.swift new file mode 100644 index 0000000..3013d03 --- /dev/null +++ b/Sources/DataThespian/ModelContext+Extension.swift @@ -0,0 +1,82 @@ +// +// 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. +// + +#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 + } + 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) } + } + } +#endif diff --git a/Sources/DataThespian/ModelContext.swift b/Sources/DataThespian/ModelContext.swift index 3298504..422fae3 100644 --- a/Sources/DataThespian/ModelContext.swift +++ b/Sources/DataThespian/ModelContext.swift @@ -31,12 +31,15 @@ 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 } diff --git a/Sources/DataThespian/NSManagedObjectID.swift b/Sources/DataThespian/NSManagedObjectID.swift index 73bcc48..f7e9303 100644 --- a/Sources/DataThespian/NSManagedObjectID.swift +++ b/Sources/DataThespian/NSManagedObjectID.swift @@ -29,14 +29,12 @@ #if canImport(CoreData) && canImport(SwiftData) public import CoreData - import Foundation - 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, @@ -57,7 +55,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 { @@ -79,38 +100,42 @@ } 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 { 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) } } } // 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..a1f1ce3 100644 --- a/Sources/DataThespian/Notification.swift +++ b/Sources/DataThespian/Notification.swift @@ -29,12 +29,13 @@ #if canImport(CoreData) import CoreData - 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 fa3a932..9d1133f 100644 --- a/Sources/DataThespian/NotificationDataUpdate.swift +++ b/Sources/DataThespian/NotificationDataUpdate.swift @@ -29,15 +29,14 @@ #if canImport(CoreData) && canImport(SwiftData) import CoreData - 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?, @@ -61,7 +60,7 @@ self.updated = updated } - init(_ notification: Notification) { + internal init(_ notification: Notification) { self.init( inserted: notification.managedObjects(key: NSInsertedObjectsKey), deleted: notification.managedObjects(key: NSDeletedObjectsKey), diff --git a/Sources/DataThespian/PublishingAgent.swift b/Sources/DataThespian/PublishingAgent.swift index 635a763..ab019bb 100644 --- a/Sources/DataThespian/PublishingAgent.swift +++ b/Sources/DataThespian/PublishingAgent.swift @@ -28,40 +28,39 @@ // #if canImport(Combine) && canImport(SwiftData) - @preconcurrency import Combine - 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) { @@ -71,7 +70,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 @@ -81,15 +82,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 @@ -97,16 +98,16 @@ completed = nil } - nonisolated func onCompleted(_ closure: @escaping @Sendable () -> Void) { + nonisolated internal func onCompleted(_ closure: @escaping @Sendable () -> Void) { 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 } - func finish() { cancel() } + internal func finish() { cancel() } } #endif diff --git a/Sources/DataThespian/PublishingRegister.swift b/Sources/DataThespian/PublishingRegister.swift index 3d64ae0..a87f355 100644 --- a/Sources/DataThespian/PublishingRegister.swift +++ b/Sources/DataThespian/PublishingRegister.swift @@ -29,14 +29,18 @@ #if canImport(Combine) && canImport(SwiftData) @preconcurrency import Combine - import Foundation - struct PublishingRegister: AgentRegister { - let id: String - let subject: PassthroughSubject + internal struct PublishingRegister: AgentRegister { + internal let id: String + private let subject: PassthroughSubject + + internal init(id: String, subject: PassthroughSubject) { + self.id = id + self.subject = subject + } - func agent() async -> PublishingAgent { + 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 959d246..cadbbbd 100644 --- a/Sources/DataThespian/RegistrationCollection.swift +++ b/Sources/DataThespian/RegistrationCollection.swift @@ -28,35 +28,31 @@ // #if canImport(SwiftData) - 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() - } - else if registrations[id] != nil { + } else if registrations[id] != nil { Self.logger.debug("Can't register \(id). Already exists.") return } @@ -67,7 +63,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 @@ -80,7 +76,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) diff --git a/Sources/DataThespian/Logging.swift b/Sources/DataThespian/ThespianLogging.swift similarity index 98% rename from Sources/DataThespian/Logging.swift rename to Sources/DataThespian/ThespianLogging.swift index 262db81..ad1edc6 100644 --- a/Sources/DataThespian/Logging.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/Tests/DataThespianTests/DataThespianTests.swift b/Tests/DataThespianTests/DataThespianTests.swift index d6315c8..297aa14 100644 --- a/Tests/DataThespianTests/DataThespianTests.swift +++ b/Tests/DataThespianTests/DataThespianTests.swift @@ -1,6 +1,7 @@ -@testable import DataThespian import Testing -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. +@testable import DataThespian + +@Test internal func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. } diff --git a/project.yml b/project.yml index bee9fb8..e6c16c9 100644 --- a/project.yml +++ b/project.yml @@ -11,3 +11,36 @@ aggregateTargets: name: Lint basedOnDependencyAnalysis: false schemes: {} +targets: + DataThespianExample: + type: application + platform: macOS + dependencies: + - package: DataThespian + product: DataThespian + sources: + - path: "Example/Sources" + - path: "Example/Support" + settings: + base: + 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: + CFBundlePackageType: APPL + ITSAppUsesNonExemptEncryption: false + CFBundleShortVersionString: $(MARKETING_VERSION) + CFBundleVersion: $(CURRENT_PROJECT_VERSION) \ No newline at end of file