Files
daily-notification-plugin/ios/Plugin/DailyNotificationDataConversions.swift
Matthew a90d08c425 feat(ios): add Core Data DAO layer and unit tests
Implement comprehensive data access layer for Core Data entities:

- Add NotificationContentDAO, NotificationDeliveryDAO, and NotificationConfigDAO
  with full CRUD operations and query helpers
- Add DailyNotificationDataConversions utility for type conversions
  (Date ↔ Int64, Int ↔ Int32, JSON, optional strings)
- Update PersistenceController with entity verification and migration policies
- Add comprehensive unit tests for all DAO classes and data conversions
- Update Core Data model with NotificationContent, NotificationDelivery,
  and NotificationConfig entities (relationships and indexes)
- Integrate ReactivationManager into DailyNotificationPlugin.load()

DAO Features:
- Create/Insert methods with dictionary support
- Read/Query methods with predicates (by timesafariDid, notificationType,
  scheduledTime range, deliveryStatus, etc.)
- Update methods (touch, updateDeliveryStatus, recordUserInteraction)
- Delete methods (by ID, by key, delete all)
- Relationship management (NotificationContent ↔ NotificationDelivery)
- Cascade delete support

Test Coverage:
- 328 lines: DailyNotificationDataConversionsTests (time, numeric, string, JSON)
- 490 lines: NotificationContentDAOTests (CRUD, queries, updates)
- 415 lines: NotificationDeliveryDAOTests (CRUD, relationships, cascade delete)
- 412 lines: NotificationConfigDAOTests (CRUD, queries, active filtering)

All tests use in-memory Core Data stack for isolation and speed.

Completes sections 4.4, 4.5, and 6.0 of iOS implementation checklist.
2025-12-09 02:23:05 -08:00

195 lines
5.2 KiB
Swift

/**
* DailyNotificationDataConversions.swift
*
* Data type conversion helpers for Core Data operations
* Handles conversions between Swift types and Core Data types,
* especially for time (Date Long/Int64) and numeric types.
*
* @author Matthew Raymer
* @version 1.0.0
* @created 2025-12-08
*/
import Foundation
import CoreData
/**
* Data conversion utilities for Core Data operations
*
* This module provides helper functions for converting between:
* - Date Int64 (epoch milliseconds)
* - Int Int32
* - Long Int64
* - Optional string handling
*/
class DailyNotificationDataConversions {
// MARK: - Constants
private static let TAG = "DNP-DATA-CONVERSIONS"
// MARK: - Time Conversions (Section 6.1)
/**
* Convert epoch milliseconds (Int64) to Date
*
* @param epochMillis Milliseconds since epoch (1970-01-01 00:00:00 UTC)
* @return Date object
*/
static func dateFromEpochMillis(_ epochMillis: Int64) -> Date {
return Date(timeIntervalSince1970: Double(epochMillis) / 1000.0)
}
/**
* Convert Date to epoch milliseconds (Int64)
*
* @param date Date object
* @return Milliseconds since epoch (1970-01-01 00:00:00 UTC)
*/
static func epochMillisFromDate(_ date: Date) -> Int64 {
return Int64(date.timeIntervalSince1970 * 1000.0)
}
/**
* Convert optional epoch milliseconds to optional Date
*
* @param epochMillis Optional milliseconds since epoch
* @return Optional Date object
*/
static func dateFromEpochMillis(_ epochMillis: Int64?) -> Date? {
guard let millis = epochMillis else { return nil }
return dateFromEpochMillis(millis)
}
/**
* Convert optional Date to optional epoch milliseconds
*
* @param date Optional Date object
* @return Optional milliseconds since epoch
*/
static func epochMillisFromDate(_ date: Date?) -> Int64? {
guard let dateValue = date else { return nil }
return epochMillisFromDate(dateValue)
}
// MARK: - Numeric Conversions (Section 6.2)
/**
* Convert Int to Int32 (for Core Data Integer 32)
*
* @param value Int value
* @return Int32 value
*/
static func int32FromInt(_ value: Int) -> Int32 {
return Int32(value)
}
/**
* Convert Int32 to Int
*
* @param value Int32 value
* @return Int value
*/
static func intFromInt32(_ value: Int32) -> Int {
return Int(value)
}
/**
* Convert Int64 to Int32 (with clamping if needed)
*
* @param value Int64 value
* @return Int32 value (clamped if out of range)
*/
static func int32FromInt64(_ value: Int64) -> Int32 {
if value > Int64(Int32.max) {
return Int32.max
} else if value < Int64(Int32.min) {
return Int32.min
}
return Int32(value)
}
/**
* Convert Int32 to Int64
*
* @param value Int32 value
* @return Int64 value
*/
static func int64FromInt32(_ value: Int32) -> Int64 {
return Int64(value)
}
/**
* Convert Long (Int64) to Int64 (no-op, but explicit)
*
* @param value Int64 value
* @return Int64 value
*/
static func int64FromLong(_ value: Int64) -> Int64 {
return value
}
/**
* Convert Boolean to Bool (direct, but explicit)
*
* @param value Boolean value
* @return Bool value
*/
static func boolFromBoolean(_ value: Bool) -> Bool {
return value
}
// MARK: - String Conversions (Section 6.3)
/**
* Safely convert optional String to String
*
* @param value Optional String
* @return String (empty string if nil)
*/
static func stringFromOptional(_ value: String?) -> String {
return value ?? ""
}
/**
* Safely convert String to optional String
*
* @param value String value
* @return Optional String (nil if empty)
*/
static func optionalStringFromString(_ value: String) -> String? {
return value.isEmpty ? nil : value
}
/**
* Convert JSON dictionary to JSON string
*
* @param dict Dictionary to encode
* @return JSON string or nil if encoding fails
*/
static func jsonStringFromDictionary(_ dict: [String: Any]?) -> String? {
guard let dict = dict else { return nil }
guard let data = try? JSONSerialization.data(withJSONObject: dict),
let jsonString = String(data: data, encoding: .utf8) else {
return nil
}
return jsonString
}
/**
* Convert JSON string to dictionary
*
* @param jsonString JSON string to decode
* @return Dictionary or nil if decoding fails
*/
static func dictionaryFromJsonString(_ jsonString: String?) -> [String: Any]? {
guard let jsonString = jsonString,
let data = jsonString.data(using: .utf8),
let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
return dict
}
}