polscm32 aka Marvout 1 год назад
Родитель
Сommit
d60323db84

+ 8 - 5
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -66,14 +66,17 @@ import Swinject
         // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
         cleanupOldData()
 
-        importStuff()
+        migrateDataFromJSON()
     }
 
-    func importStuff() {
+    func migrateDataFromJSON() {
         Task {
             let importer = JSONImporter(context: coreDataStack.newTaskContext())
-            await importer.importPumpHistoryIfNeeded()
-            await importer.importCarbHistoryIfNeeded()
+            async let importPumpHistory: () = importer.importPumpHistoryIfNeeded()
+            async let importCarbHistory: () = importer.importCarbHistoryIfNeeded()
+            
+            await importPumpHistory
+            await importCarbHistory
         }
     }
 
@@ -85,7 +88,7 @@ import Swinject
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
         }
-        .onChange(of: scenePhase) { newScenePhase in
+        .onChange(of: scenePhase) { oldScenePhase, newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
 
             /// If the App goes to the background we should ensure that all the changes are saved from the viewContext to the Persistent Container

+ 23 - 1
Model/Helper/CarbEntryStored+helper.swift

@@ -65,7 +65,10 @@ extension CarbEntryStored {
     }
 }
 
-struct CarbEntryDTO: Codable {
+// MARK: - CarbEntryDTO and Conformance to ImportableDTO
+
+/// Data Transfer Object for Carb entries.
+struct CarbEntryDTO: Decodable, ImportableDTO {
     var id: UUID?
     var carbs: Double
     var date: Date?
@@ -74,6 +77,25 @@ struct CarbEntryDTO: Codable {
     var isFPU: Bool?
     var note: String?
     var enteredBy: String?
+
+    // Conformance to ImportableDTO
+    typealias ManagedObject = CarbEntryStored
+
+    /// Stores the DTO in Core Data by mapping it to the corresponding managed object.
+    func store(in context: NSManagedObjectContext) -> CarbEntryStored {
+        let carbEntry = CarbEntryStored(context: context)
+        carbEntry.id = id ?? UUID()
+        carbEntry.carbs = carbs
+        carbEntry.date = date ?? Date()
+        carbEntry.fat = fat ?? 0.0
+        carbEntry.protein = protein ?? 0.0
+        carbEntry.isFPU = isFPU ?? false
+        carbEntry.note = note
+        carbEntry.isUploadedToNS = false
+        carbEntry.isUploadedToHealth = false
+        carbEntry.isUploadedToTidepool = false
+        return carbEntry
+    }
 }
 
 extension CarbEntryStored: Encodable {

+ 108 - 86
Model/Helper/PumpEvent+helper.swift

@@ -94,48 +94,9 @@ extension NSPredicate {
     }
 }
 
-// Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
-struct BolusDTO: Codable {
-    var id: String
-    var timestamp: String
-    var amount: Double
-    var isExternal: Bool
-    var isSMB: Bool?
-    var duration: Int?
-    var _type: String = "Bolus"
-}
-
-struct TempBasalDTO: Codable {
-    var id: String
-    var timestamp: String
-    var temp: String
-    var rate: Double
-    var _type: String = "TempBasal"
-}
-
-struct TempBasalDurationDTO: Codable {
-    var id: String
-    var timestamp: String
-    var duration: Int
-    var _type: String = "TempBasalDuration"
+// MARK: - PumpEventDTO and Conformance to ImportableDTO
 
-    private enum CodingKeys: String, CodingKey {
-        case id
-        case timestamp
-        case duration = "duration (min)"
-        case _type
-    }
-}
-
-struct PumpSuspendDTO: Codable {
-    var id: String
-    var timestamp: String
-    var reason: String?
-    var _type: String = "PumpSuspend"
-}
-
-// Mask distinct DTO subtypes with a common enum that conforms to Encodable
-enum PumpEventDTO: Encodable, Decodable {
+enum PumpEventDTO: Encodable, Decodable, ImportableDTO {
     case bolus(BolusDTO)
     case tempBasal(TempBasalDTO)
     case tempBasalDuration(TempBasalDurationDTO)
@@ -154,6 +115,12 @@ enum PumpEventDTO: Encodable, Decodable {
         }
     }
 
+    // Coding keys to identify the event type in JSON.
+    private enum CodingKeys: String, CodingKey {
+        case _type
+    }
+
+    // Custom initializer to decode the JSON based on the `_type` field.
     init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         guard let type = try? container.decode(String.self, forKey: ._type) else {
@@ -163,69 +130,124 @@ enum PumpEventDTO: Encodable, Decodable {
             )
         }
 
+        let singleValueContainer = try decoder.singleValueContainer()
+
         switch type {
         case "Bolus":
-            do {
-                let bolus = try BolusDTO(from: decoder)
-                self = .bolus(bolus)
-            } catch {
-                throw DecodingError.typeMismatch(
-                    BolusDTO.self,
-                    DecodingError
-                        .Context(
-                            codingPath: decoder.codingPath,
-                            debugDescription: "Failed to decode BolusDTO",
-                            underlyingError: error
-                        )
-                )
-            }
+            let bolusDTO = try singleValueContainer.decode(BolusDTO.self)
+            self = .bolus(bolusDTO)
         case "TempBasal":
-            do {
-                let tempBasal = try TempBasalDTO(from: decoder)
-                self = .tempBasal(tempBasal)
-            } catch {
-                throw DecodingError.typeMismatch(
-                    TempBasalDTO.self,
-                    DecodingError
-                        .Context(
-                            codingPath: decoder.codingPath,
-                            debugDescription: "Failed to decode TempBasalDTO",
-                            underlyingError: error
-                        )
-                )
-            }
+            let tempBasalDTO = try singleValueContainer.decode(TempBasalDTO.self)
+            self = .tempBasal(tempBasalDTO)
         case "TempBasalDuration":
-            do {
-                let tempBasalDuration = try TempBasalDurationDTO(from: decoder)
-                self = .tempBasalDuration(tempBasalDuration)
-            } catch {
-                throw DecodingError.typeMismatch(
-                    TempBasalDurationDTO.self,
-                    DecodingError
-                        .Context(
-                            codingPath: decoder.codingPath,
-                            debugDescription: "Failed to decode TempBasalDurationDTO",
-                            underlyingError: error
-                        )
-                )
-            }
+            let tempBasalDurationDTO = try singleValueContainer.decode(TempBasalDurationDTO.self)
+            self = .tempBasalDuration(tempBasalDurationDTO)
         case "PumpSuspend":
-            let pumpSuspend = try PumpSuspendDTO(from: decoder)
-            self = .pumpSuspend(pumpSuspend)
+            let pumpSuspendDTO = try singleValueContainer.decode(PumpSuspendDTO.self)
+            self = .pumpSuspend(pumpSuspendDTO)
         default:
             throw DecodingError.dataCorruptedError(
                 forKey: ._type,
                 in: container,
-                debugDescription: "Unbekannter _type-Wert: \(type)"
+                debugDescription: "Unknown _type value: \(type)"
             )
         }
     }
 
+    // Conformance to ImportableDTO
+    typealias ManagedObject = PumpEventStored
+
+    /// Stores the DTO in Core Data by mapping it to the corresponding managed object.
+    func store(in context: NSManagedObjectContext) -> PumpEventStored {
+        switch self {
+        case let .bolus(bolusDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = bolusDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
+            pumpEvent.type = bolusDTO._type
+
+            let bolus = BolusStored(context: context)
+            bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
+            bolus.isExternal = bolusDTO.isExternal
+            bolus.isSMB = bolusDTO.isSMB ?? false
+            pumpEvent.bolus = bolus
+
+            return pumpEvent
+
+        case let .tempBasal(tempBasalDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = tempBasalDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
+            pumpEvent.type = tempBasalDTO._type
+
+            let tempBasal = TempBasalStored(context: context)
+            tempBasal.tempType = tempBasalDTO.temp
+            tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
+            pumpEvent.tempBasal = tempBasal
+
+            return pumpEvent
+
+        case let .tempBasalDuration(tempBasalDurationDTO):
+            let pumpEvent = PumpEventStored(context: context)
+            pumpEvent.id = tempBasalDurationDTO.id
+            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
+            pumpEvent.type = tempBasalDurationDTO._type
+
+            let tempBasal = TempBasalStored(context: context)
+            tempBasal.duration = Int16(tempBasalDurationDTO.duration)
+            pumpEvent.tempBasal = tempBasal
+
+            return pumpEvent
+
+        case .pumpSuspend:
+            // Handle pump suspend event if needed
+            let pumpEvent = PumpEventStored(context: context)
+            // Set properties for pump suspend if applicable
+            return pumpEvent
+        }
+    }
+}
+
+// Declare helper structs ("data transfer objects" = DTO) to utilize parsing a flattened pump history
+struct BolusDTO: Codable {
+    var id: String
+    var timestamp: String
+    var amount: Double
+    var isExternal: Bool
+    var isSMB: Bool?
+    var duration: Int?
+    var _type: String = "Bolus"
+}
+
+struct TempBasalDTO: Codable {
+    var id: String
+    var timestamp: String
+    var temp: String
+    var rate: Double
+    var _type: String = "TempBasal"
+}
+
+struct TempBasalDurationDTO: Codable {
+    var id: String
+    var timestamp: String
+    var duration: Int
+    var _type: String = "TempBasalDuration"
+
     private enum CodingKeys: String, CodingKey {
+        case id
+        case timestamp
+        case duration = "duration (min)"
         case _type
     }
 }
 
+struct PumpSuspendDTO: Codable {
+    var id: String
+    var timestamp: String
+    var reason: String?
+    var _type: String = "PumpSuspend"
+}
+
 // Extension with helper functions to map pump events to DTO objects via uniform masking enum
 extension PumpEventStored {
     static let dateFormatter: ISO8601DateFormatter = {

+ 62 - 120
Model/MigrationScript.swift

@@ -1,169 +1,111 @@
 import CoreData
 import Foundation
 
+// MARK: - Protocol Definition
+
+/// A protocol that ensures a Data Transfer Object (DTO) can be stored in Core Data.
+/// It requires a method to map the DTO to its corresponding Core Data managed object.
+protocol ImportableDTO: Decodable {
+    associatedtype ManagedObject: NSManagedObject
+    /// Converts the DTO into a Core Data managed object.
+    func store(in context: NSManagedObjectContext) -> ManagedObject
+}
+
+// MARK: - JSONImporter Class with Generic Import Function
+
+/// Class responsible for importing JSON data into Core Data.
 class JSONImporter {
     private let context: NSManagedObjectContext
     private let fileManager = FileManager.default
 
+    /// Initializes the importer with a Core Data context.
     init(context: NSManagedObjectContext) {
         self.context = context
     }
 
-    func importPumpHistoryIfNeeded() async {
-        let userDefaultsKey = "pumpHistoryImported"
+    /// Generic function to import data from a JSON file into Core Data.
+    /// - Parameters:
+    ///   - userDefaultsKey: Key to check if data has already been imported.
+    ///   - filePathComponent: Path component of the JSON file.
+    ///   - dtoType: The DTO type conforming to `ImportableDTO`.
+    ///   - dateDecodingStrategy: The date decoding strategy for JSON decoding.
+    func importDataIfNeeded<T: ImportableDTO>(
+        userDefaultsKey: String,
+        filePathComponent: String,
+        dtoType _: T.Type,
+        dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601
+    ) async {
         let hasImported = UserDefaults.standard.bool(forKey: userDefaultsKey)
 
         guard !hasImported else {
-            debugPrint("Pump history already imported. Skipping import.")
+            debugPrint("\(filePathComponent) already imported. Skipping import.")
             return
         }
 
         do {
-            // Get filepath
+            // Get the file path for the JSON file
             guard let filePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
                 .first?
-                .appendingPathComponent(OpenAPS.Monitor.pumpHistory),
+                .appendingPathComponent(filePathComponent),
                 fileManager.fileExists(atPath: filePath.path)
             else {
-                debugPrint("Pump history file not found at path \(OpenAPS.Monitor.pumpHistory)")
+                debugPrint("\(filePathComponent) file not found at path \(filePathComponent)")
                 return
             }
 
-            // Read JSON and decode
+            // Read data from the JSON file
             let data = try Data(contentsOf: filePath)
-            let pumpEvents = try JSONDecoder().decode([PumpEventDTO].self, from: data)
+            let decoder = JSONDecoder()
+            decoder.dateDecodingStrategy = dateDecodingStrategy
+
+            // Decode the data into an array of DTOs
+            let entries = try decoder.decode([T].self, from: data)
 
-            // Save to Core Data
+            // Save the DTOs into Core Data
             await context.perform {
-                for event in pumpEvents {
-                    self.storePumpEventFromDTO(event)
+                for entry in entries {
+                    _ = entry.store(in: self.context)
                 }
 
                 do {
                     guard self.context.hasChanges else { return }
                     try self.context.save()
-                    debugPrint("\(DebuggingIdentifiers.succeeded) Pump history successfully imported into Core Data.")
+                    debugPrint("\(DebuggingIdentifiers.succeeded) \(filePathComponent) successfully imported into Core Data.")
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to save pump history to Core Data: \(error)")
+                    debugPrint("\(DebuggingIdentifiers.failed) Failed to save \(filePathComponent) to Core Data: \(error)")
                 }
             }
 
-            // Delete JSON
+            // Delete the JSON file after successful import
             try fileManager.removeItem(at: filePath)
-            debugPrint("pumphistory.json deleted after successful import.")
+            debugPrint("\(filePathComponent) deleted after successful import.")
 
-            // Update UserDefaults flag
+            // Update UserDefaults to indicate that the data has been imported
             UserDefaults.standard.set(true, forKey: userDefaultsKey)
         } catch {
-            debugPrint("Error importing pump history: \(error)")
+            debugPrint("Error importing \(filePathComponent): \(error)")
         }
     }
+}
 
-    private func storePumpEventFromDTO(_ event: PumpEventDTO) {
-        // Map each type of PumpEventDTO to its corresponding Core Data model.
-        switch event {
-        case let .bolus(bolusDTO):
-            let pumpEvent = PumpEventStored(context: context)
-            pumpEvent.id = bolusDTO.id
-            pumpEvent.timestamp = ISO8601DateFormatter().date(from: bolusDTO.timestamp)
-            pumpEvent.type = bolusDTO._type
-
-            let bolus = BolusStored(context: context)
-            bolus.amount = NSDecimalNumber(value: bolusDTO.amount)
-            bolus.isExternal = bolusDTO.isExternal
-            bolus.isSMB = bolusDTO.isSMB ?? false
-            pumpEvent.bolus = bolus
-
-        case let .tempBasal(tempBasalDTO):
-            let pumpEvent = PumpEventStored(context: context)
-            pumpEvent.id = tempBasalDTO.id
-            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDTO.timestamp)
-            pumpEvent.type = tempBasalDTO._type
-
-            let tempBasal = TempBasalStored(context: context)
-            tempBasal.tempType = tempBasalDTO.temp
-            tempBasal.rate = NSDecimalNumber(value: tempBasalDTO.rate)
-            pumpEvent.tempBasal = tempBasal
-
-        case let .tempBasalDuration(tempBasalDurationDTO):
-            let pumpEvent = PumpEventStored(context: context)
-            pumpEvent.id = tempBasalDurationDTO.id
-            pumpEvent.timestamp = ISO8601DateFormatter().date(from: tempBasalDurationDTO.timestamp)
-            pumpEvent.type = tempBasalDurationDTO._type
-
-            let tempBasal = TempBasalStored(context: context)
-            tempBasal.duration = Int16(tempBasalDurationDTO.duration)
-            pumpEvent.tempBasal = tempBasal
-        case .pumpSuspend:
-            return
-        }
-    }
-
-    func importCarbHistoryIfNeeded() async {
-        let userDefaultsKey = "carbHistoryImported"
-        let hasImported = UserDefaults.standard.bool(forKey: userDefaultsKey)
-
-        guard !hasImported else {
-            debugPrint("Carb history already imported. Skipping import.")
-            return
-        }
-
-        do {
-            // Get filepath
-            guard let filePath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)
-                .first?
-                .appendingPathComponent(OpenAPS.Monitor.carbHistory),
-                fileManager.fileExists(atPath: filePath.path)
-            else {
-                debugPrint("Carb history file not found at path \(OpenAPS.Monitor.carbHistory)")
-                return
-            }
-
-            // Read JSON and decode
-            let data = try Data(contentsOf: filePath)
-            let decoder = JSONDecoder()
-            decoder.dateDecodingStrategy = .iso8601
-
-            // Decode JSON
-            let carbEntries = try decoder.decode([CarbEntryDTO].self, from: data)
-
-            // Save to Core Data
-            await context.perform {
-                for entryDTO in carbEntries {
-                    self.storeCarbEntryFromDTO(entryDTO)
-                }
-
-                do {
-                    guard self.context.hasChanges else { return }
-                    try self.context.save()
-                    debugPrint("\(DebuggingIdentifiers.succeeded) Carb history successfully imported into Core Data.")
-                } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Failed to save carb history to Core Data: \(error)")
-                }
-            }
-
-            // Delete JSON
-            try fileManager.removeItem(at: filePath)
-            debugPrint("carbHistory.json deleted after successful import.")
+// MARK: - Extension for Specific Import Functions
 
-            // Update UserDefaults flag
-            UserDefaults.standard.set(true, forKey: userDefaultsKey)
-        } catch {
-            debugPrint("Error importing carb history: \(error)")
-        }
+extension JSONImporter {
+    func importPumpHistoryIfNeeded() async {
+        await importDataIfNeeded(
+            userDefaultsKey: "pumpHistoryImported",
+            filePathComponent: OpenAPS.Monitor.pumpHistory,
+            dtoType: PumpEventDTO.self,
+            dateDecodingStrategy: .iso8601
+        )
     }
 
-    private func storeCarbEntryFromDTO(_ entryDTO: CarbEntryDTO) {
-        let carbEntry = CarbEntryStored(context: context)
-        carbEntry.id = entryDTO.id ?? UUID()
-        carbEntry.carbs = entryDTO.carbs
-        carbEntry.date = entryDTO.date ?? Date()
-        carbEntry.fat = entryDTO.fat ?? 0.0
-        carbEntry.protein = entryDTO.protein ?? 0.0
-        carbEntry.isFPU = entryDTO.isFPU ?? false
-        carbEntry.note = entryDTO.note
-        carbEntry.isUploadedToNS = false
-        carbEntry.isUploadedToHealth = false
-        carbEntry.isUploadedToTidepool = false
+    func importCarbHistoryIfNeeded() async {
+        await importDataIfNeeded(
+            userDefaultsKey: "carbHistoryImported",
+            filePathComponent: OpenAPS.Monitor.carbHistory,
+            dtoType: CarbEntryDTO.self,
+            dateDecodingStrategy: .iso8601
+        )
     }
 }