diff --git a/Sources/CoreDataModel/NSAttributeType.swift b/Sources/CoreDataModel/NSAttributeType.swift index 485442a..62e5bc4 100644 --- a/Sources/CoreDataModel/NSAttributeType.swift +++ b/Sources/CoreDataModel/NSAttributeType.swift @@ -14,7 +14,7 @@ public extension NSAttributeType { init(attributeType: AttributeType) { switch attributeType { - case .boolean: + case .bool: self = .booleanAttributeType case .int16: self = .integer16AttributeType @@ -61,7 +61,7 @@ public extension AttributeType { case .stringAttributeType: self = .string case .booleanAttributeType: - self = .boolean + self = .bool case .dateAttributeType: self = .date case .binaryDataAttributeType: diff --git a/Sources/CoreModel/AttributeType.swift b/Sources/CoreModel/AttributeType.swift index 3c965a2..bcfaf1d 100644 --- a/Sources/CoreModel/AttributeType.swift +++ b/Sources/CoreModel/AttributeType.swift @@ -11,7 +11,7 @@ import Foundation public enum AttributeType: String, Codable, CaseIterable, Sendable { /// Boolean number type. - case boolean + case bool /// 16 bit Integer number type. case int16 diff --git a/Sources/CoreModel/Decodable.swift b/Sources/CoreModel/Decodable.swift index f745d9e..f2b89ee 100644 --- a/Sources/CoreModel/Decodable.swift +++ b/Sources/CoreModel/Decodable.swift @@ -69,18 +69,46 @@ public extension ModelData { } } -// MARK: - Default Codable Implementation +// MARK: - AttributeDecodable -extension Entity where Self: Decodable, Self.ID: Decodable { +public protocol AttributeDecodable { - // TODO: Default implementation for Decodable + init?(attributeValue: AttributeValue) } -// MARK: - AttributeDecodable +extension Optional: AttributeDecodable where Wrapped: AttributeDecodable { + + public init?(attributeValue: AttributeValue) { + switch attributeValue { + case .null: + self = .none + default: + guard let value = Wrapped.init(attributeValue: attributeValue) else { + return nil + } + self = .some(value) + } + } +} -public protocol AttributeDecodable { +extension AttributeDecodable where Self: RawRepresentable, RawValue: AttributeDecodable { - init?(attributeValue: AttributeValue) + public init?(attributeValue: AttributeValue) { + guard let rawValue = RawValue.init(attributeValue: attributeValue) else { + return nil + } + self.init(rawValue: rawValue) + } +} + +extension Bool: AttributeDecodable { + + public init?(attributeValue: AttributeValue) { + guard case let .bool(value) = attributeValue else { + return nil + } + self = value + } } extension String: AttributeDecodable { diff --git a/Sources/CoreModel/Decoder.swift b/Sources/CoreModel/Decoder.swift new file mode 100644 index 0000000..8002cee --- /dev/null +++ b/Sources/CoreModel/Decoder.swift @@ -0,0 +1,673 @@ +// +// Decoder.swift +// +// +// Created by Alsey Coleman Miller on 8/18/23. +// + +import Foundation + +// MARK: - Default Codable Implementation + +extension Entity where Self: Decodable, Self.ID: Decodable, Self.ID: ObjectIDConvertible { + + public init( + from model: ModelData + ) throws { + try self.init(from: model, log: nil) + } + + internal init( + from model: ModelData, + userInfo: [CodingUserInfoKey : Any] = [:], + log: ((String) -> ())? + ) throws { + let idKey = (userInfo[.identifierCodingKey] as? Self.CodingKeys)?.stringValue ?? "id" + let entity = EntityDescription(entity: Self.self) + let decoder = ModelDataDecoder( + referencing: model, + entity: entity, + identifierKey: idKey, + userInfo: userInfo, + log: log + ) + try self.init(from: decoder) + } +} + +internal final class ModelDataDecoder: Decoder { + + // MARK: - Properties + + /// The path of coding keys taken to get to this point in decoding. + fileprivate(set) var codingPath: [CodingKey] + + /// Any contextual information set by the user for decoding. + let userInfo: [CodingUserInfoKey : Any] + + /// Logger + var log: ((String) -> ())? + + /// Container to decode. + let data: ModelData + + /// Property name of identifier + let identifierKey: String + + let attributes: [PropertyKey: Attribute] + + let relationships: [PropertyKey: Relationship] + + // MARK: - Initialization + + fileprivate init(referencing data: ModelData, + entity: EntityDescription, + identifierKey: String, + at codingPath: [CodingKey] = [], + userInfo: [CodingUserInfoKey : Any], + log: ((String) -> ())?) { + + self.data = data + self.codingPath = codingPath + self.userInfo = userInfo + self.log = log + self.identifierKey = identifierKey + assert(data.entity == entity.id) + + // properties cache + var attributes = [PropertyKey: Attribute]() + attributes.reserveCapacity(entity.attributes.count) + for attribute in entity.attributes { + attributes[attribute.id] = attribute + } + self.attributes = attributes //.init(grouping: entity.attributes, by: { $0.id }) + var relationships = [PropertyKey: Relationship]() + relationships.reserveCapacity(entity.relationships.count) + for relationship in entity.relationships { + relationships[relationship.id] = relationship + } + self.relationships = relationships //.init(grouping: entity.relationships, by: { $0.id }) + } + + // MARK: - Methods + + func container (keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") + guard codingPath.isEmpty else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Can only decode root data with keyed container.")) + } + let container = ModelDataKeyedDecodingContainer(referencing: self) + return KeyedDecodingContainer(container) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + log?("Requested unkeyed container for path \"\(codingPath.path)\"") + guard codingPath.isEmpty == false else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Can not decode root data with unkeyed container.")) + } + let container = try ModelDataUnkeyedDecodingContainer(referencing: self) + return container + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + log?("Requested single value container for path \"\(codingPath.path)\"") + guard codingPath.isEmpty == false else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Can not decode root data with single value container.")) + } + let container = ModelDataSingleValueDecodingContainer(referencing: self) + return container + } +} + +internal extension ModelDataDecoder { + + func decodeNil(forKey key: CodingKey) throws -> Bool { + log?("Check if nil at path \"\(codingPath.path)\"") + let property = PropertyKey(key) + if let value = self.data.attributes[property] { + return value == .null + } else if let value = self.data.relationships[property] { + return value == .null + } else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode nil for non-existent property \"\(key.stringValue)\"")) + } + } + + func decodeAttribute(_ type: T.Type, forKey key: CodingKey) throws -> T { + log?("Will decode \(type) at path \"\(codingPath.path)\"") + let property = PropertyKey(key) + guard let attribute = self.data.attributes[property] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(type) for non-existent property \"\(key.stringValue)\"")) + } + guard let value = T.init(attributeValue: attribute) else { + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(type) from \(attribute) for \"\(key.stringValue)\"")) + } + return value + } + + func decodeString(forKey key: CodingKey) throws -> String { + log?("Will decode \(String.self) at path \"\(codingPath.path)\"") + let property = PropertyKey(key) + // determine if objectID, attribute or relationship + if key.stringValue == identifierKey { + return self.data.id.rawValue + } else if let attribute = self.data.attributes[property] { + guard let value = String.init(attributeValue: attribute) else { + throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(String.self) from \(attribute) for \"\(key.stringValue)\"")) + } + return value + } else if let relationship = self.data.relationships[property] { + guard case let .toOne(objectID) = relationship else { + throw DecodingError.typeMismatch(String.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(String.self) from \(relationship) for \"\(key.stringValue)\"")) + } + return objectID.rawValue + } else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(String.self) for non-existent property \"\(key.stringValue)\"")) + } + } + + func decodeNumeric (_ type: T.Type, forKey key: CodingKey) throws -> T { + // Just default to attribute implementation for now + try decodeAttribute(type, forKey: key) + } + + func decodeDouble(forKey key: CodingKey) throws -> Double { + // Just default to attribute implementation for now + try decodeAttribute(Double.self, forKey: key) + } + + func decodeFloat(forKey key: CodingKey) throws -> Float { + // Just default to attribute implementation for now + try decodeAttribute(Float.self, forKey: key) + } + + func decodeDecodable (_ type: T.Type, forKey key: CodingKey) throws -> T { + // override for native types and id + if key.stringValue == identifierKey { + guard let convertible = type as? ObjectIDConvertible.Type else { + throw DecodingError.typeMismatch(ObjectIDConvertible.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode identifer from \(type). Types used as identifiers must conform to \(String(describing: ObjectID.self))")) + } + let id = self.data.id + guard let value = convertible.init(objectID: id) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(type) from identifier \(id)")) + } + return value as! T + } else if type == Data.self { + return try decodeAttribute(Data.self, forKey: key) as! T + } else if type == Date.self { + return try decodeAttribute(Date.self, forKey: key) as! T + } else if type == UUID.self { + return try decodeAttribute(UUID.self, forKey: key) as! T + } else if type == URL.self { + return try decodeAttribute(URL.self, forKey: key) as! T + } else if let decodableType = type as? AttributeDecodable.Type { + return try decodeAttribute(decodableType, forKey: key) as! T + } else { + // decode using Decodable, container should read directly. + log?("Will decode \(type) at path \"\(codingPath.path)\"") + return try T.init(from: self) + } + } +} + +// MARK: - KeyedDecodingContainer + +internal struct ModelDataKeyedDecodingContainer : KeyedDecodingContainerProtocol { + + typealias Key = K + + // MARK: Properties + + /// A reference to the encoder we're reading from. + let decoder: ModelDataDecoder + + /// The path of coding keys taken to get to this point in decoding. + let codingPath: [CodingKey] + + /// All the keys the Decoder has for this container. + let allKeys: [Key] + + // MARK: Initialization + + /// Initializes `self` by referencing the given decoder and container. + init(referencing decoder: ModelDataDecoder) { + assert(decoder.codingPath.isEmpty) + self.decoder = decoder + self.codingPath = decoder.codingPath + // set keys + var keys = [Key]() + keys += decoder.data.relationships.keys + .compactMap { Key(stringValue: $0.rawValue) } + keys += decoder.data.attributes.keys + .compactMap { Key(stringValue: $0.rawValue) } + if let idKey = Key(stringValue: decoder.identifierKey) { + keys.append(idKey) + } + self.allKeys = keys + } + + // MARK: KeyedDecodingContainer Protocol + + func contains(_ key: Key) -> Bool { + self.decoder.log?("Check whether key \"\(key.stringValue)\" exists") + return allKeys.contains(where: { key.stringValue == $0.stringValue }) + } + + func decodeNil(forKey key: Key) throws -> Bool { + + // set coding key context + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try decoder.decodeNil(forKey: key) + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + return try decodeAttribute(type, forKey: key) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + try decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + return try decodeNumeric(type, forKey: key) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try decoder.decodeFloat(forKey: key) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try decoder.decodeDouble(forKey: key) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try self.decoder.decodeString(forKey: key) + } + + func decode (_ type: T.Type, forKey key: Key) throws -> T { + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try self.decoder.decodeDecodable(type, forKey: key) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + fatalError() + } + + func superDecoder() throws -> Decoder { + fatalError() + } + + func superDecoder(forKey key: Key) throws -> Decoder { + fatalError() + } + + // MARK: Private Methods + + /// Decode native value type from CoreModel data. + private func decodeAttribute (_ type: T.Type, forKey key: Key) throws -> T { + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try self.decoder.decodeAttribute(type, forKey: key) + } + + private func decodeNumeric (_ type: T.Type, forKey key: Key) throws -> T { + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + return try self.decoder.decodeNumeric(type, forKey: key) + } +} + +// MARK: - SingleValueDecodingContainer + +internal struct ModelDataSingleValueDecodingContainer: SingleValueDecodingContainer { + + // MARK: Properties + + /// A reference to the decoder we're reading from. + let decoder: ModelDataDecoder + + /// The path of coding keys taken to get to this point in decoding. + let codingPath: [CodingKey] + + // MARK: Initialization + + /// Initializes `self` by referencing the given decoder and container. + init(referencing decoder: ModelDataDecoder) { + assert(decoder.codingPath.isEmpty == false) + self.decoder = decoder + self.codingPath = decoder.codingPath + } + + // MARK: SingleValueDecodingContainer Protocol + + func decodeNil() -> Bool { + do { + let key = try propertyKey() + return try decoder.decodeNil(forKey: key) + } catch { + return true + } + } + + func decode(_ type: Bool.Type) throws -> Bool { + let key = try propertyKey() + return try decoder.decodeAttribute(type, forKey: key) + } + + func decode(_ type: Int.Type) throws -> Int { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int8.Type) throws -> Int8 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int16.Type) throws -> Int16 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int32.Type) throws -> Int32 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: Int64.Type) throws -> Int64 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt.Type) throws -> UInt { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + let key = try propertyKey() + return try decoder.decodeNumeric(type, forKey: key) + } + + func decode(_ type: Float.Type) throws -> Float { + let key = try propertyKey() + return try decoder.decodeFloat(forKey: key) + } + + func decode(_ type: Double.Type) throws -> Double { + let key = try propertyKey() + return try decoder.decodeDouble(forKey: key) + } + + func decode(_ type: String.Type) throws -> String { + let key = try propertyKey() + return try decoder.decodeString(forKey: key) + } + + func decode (_ type: T.Type) throws -> T { + let key = try propertyKey() + return try decoder.decodeDecodable(type, forKey: key) + } + + // MARK: Private Methods + + private func propertyKey() throws -> CodingKey { + guard let key = codingPath.first else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode single value from root data.")) + } + return key + } +} + +// MARK: - UnkeyedDecodingContainer + +internal struct ModelDataUnkeyedDecodingContainer: UnkeyedDecodingContainer { + + // MARK: Properties + + /// A reference to the encoder we're reading from. + let decoder: ModelDataDecoder + + /// The path of coding keys taken to get to this point in decoding. + let codingPath: [CodingKey] + + let objectIDs: [ObjectID] + + private(set) var currentIndex: Int = 0 + + // MARK: Initialization + + /// Initializes `self` by referencing the given decoder and container. + init(referencing decoder: ModelDataDecoder) throws { + + self.decoder = decoder + self.codingPath = decoder.codingPath + // get to-many relationship + guard let key = codingPath.first else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Cannot decode to-many relationship from root data.")) + } + guard let relationship = decoder.data.relationships[PropertyKey(key)] else { + throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No relationship value for \(key.stringValue)")) + } + switch relationship { + case .null: + self.objectIDs = [] + case let .toMany(objectIDs): + self.objectIDs = objectIDs + case .toOne: + throw DecodingError.typeMismatch([String].self, DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid relationship value \(relationship)")) + } + } + + // MARK: UnkeyedDecodingContainer + + var count: Int? { + objectIDs.count + } + + var isAtEnd: Bool { + currentIndex >= objectIDs.count + } + + func decodeNil() throws -> Bool { + throw DecodingError.typeMismatch(Optional.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(Optional.self)")) + } + + func decode(_ type: Bool.Type) throws -> Bool { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Double.Type) throws -> Double { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Float.Type) throws -> Float { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Int.Type) throws -> Int { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Int8.Type) throws -> Int8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Int16.Type) throws -> Int16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Int32.Type) throws -> Int32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: Int64.Type) throws -> Int64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func decode(_ type: UInt.Type) throws -> UInt { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + + func decode(_ type: UInt8.Type) throws -> UInt8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + + func decode(_ type: UInt16.Type) throws -> UInt16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + + func decode(_ type: UInt32.Type) throws -> UInt32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + + func decode(_ type: UInt64.Type) throws -> UInt64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode to-many relationship of \(type)")) + } + + func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + fatalError() + } + + func superDecoder() throws -> Decoder { + decoder + } + + mutating func decode(_ type: String.Type) throws -> String { + let indexKey = IndexCodingKey(rawValue: currentIndex) + self.decoder.codingPath.append(indexKey) + defer { self.decoder.codingPath.removeLast() } + return try decodeRelationship(type) + } + + mutating func decode(_ type: T.Type) throws -> T where T : Decodable { + let indexKey = IndexCodingKey(rawValue: currentIndex) + self.decoder.codingPath.append(indexKey) + defer { self.decoder.codingPath.removeLast() } + let string = try decodeRelationship(type) + let id = ObjectID(rawValue: string) + guard let convertible = type as? ObjectIDConvertible.Type else { + throw DecodingError.typeMismatch(ObjectIDConvertible.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Cannot decode identifer from \(type). Types used as identifiers must conform to \(String(describing: ObjectID.self))")) + } + guard let value = convertible.init(objectID: id) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Cannot decode \(type) from identifier \(id)")) + } + return value as! T + } + + // MARK: Private Methods + + private mutating func decodeRelationship(_ type: T.Type) throws -> String { + guard objectIDs.count > currentIndex else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "End of to many relationship")) + } + let objectID = objectIDs[currentIndex] + // increment index + currentIndex += 1 + // return value + return objectID.rawValue + } +} + +internal struct IndexCodingKey: CodingKey, RawRepresentable, Equatable, Hashable { + + let rawValue: Int + + init(rawValue: Int) { + self.rawValue = rawValue + } + + var stringValue: String { + rawValue.description + } + + init?(stringValue: String) { + guard let rawValue = Int(stringValue) else { + return nil + } + self.init(rawValue: rawValue) + } + + var intValue: Int? { + rawValue + } + + init?(intValue: Int) { + self.init(rawValue: intValue) + } + + + +} diff --git a/Sources/CoreModel/Encodable.swift b/Sources/CoreModel/Encodable.swift index 66c150e..a1c97a4 100644 --- a/Sources/CoreModel/Encodable.swift +++ b/Sources/CoreModel/Encodable.swift @@ -39,6 +39,25 @@ public protocol AttributeEncodable { var attributeValue: AttributeValue { get } } +extension Optional: AttributeEncodable where Wrapped: AttributeEncodable { + + public var attributeValue: AttributeValue { + switch self { + case .none: + return .null + case .some(let wrapped): + return wrapped.attributeValue + } + } +} + +extension AttributeEncodable where Self: RawRepresentable, RawValue: AttributeEncodable { + + public var attributeValue: AttributeValue { + rawValue.attributeValue + } +} + extension Bool: AttributeEncodable { public var attributeValue: AttributeValue { .bool(self) } diff --git a/Sources/CoreModel/Encoder.swift b/Sources/CoreModel/Encoder.swift index 5522eee..07b39a0 100644 --- a/Sources/CoreModel/Encoder.swift +++ b/Sources/CoreModel/Encoder.swift @@ -7,15 +7,15 @@ import Foundation -extension Entity where Self: Encodable, Self.ID: Encodable, Self.ID: ObjectIDConvertible { +extension Entity where Self: Encodable, Self.ID: Encodable { public func encode() throws -> ModelData { try encode(log: nil) } internal func encode( - log: ((String) -> ())?, - userInfo: [CodingUserInfoKey : Any] = [:] + userInfo: [CodingUserInfoKey : Any] = [:], + log: ((String) -> ())? ) throws -> ModelData { let entity = EntityDescription(entity: Self.self) let id = ObjectID(rawValue: self.id.description) @@ -88,7 +88,7 @@ internal final class ModelDataEncoder: Encoder { } } -fileprivate extension ModelDataEncoder { +internal extension ModelDataEncoder { func setAttribute(_ value: AttributeValue, forKey key: PropertyKey) throws { log?("Will set \(value) for attribute \"\(key)\"") @@ -131,6 +131,8 @@ fileprivate extension ModelDataEncoder { try setAttribute(uuid.attributeValue, forKey: key) } else if let url = value as? URL { try setAttribute(url.attributeValue, forKey: key) + } else if let encodable = value as? AttributeEncodable { + try setAttribute(encodable.attributeValue, forKey: key) } else { // encode using Encodable, container should write directly. try value.encode(to: self) @@ -152,7 +154,7 @@ fileprivate extension ModelDataEncoder { // MARK: - KeyedEncodingContainer -struct ModelKeyedEncodingContainer : KeyedEncodingContainerProtocol { +internal struct ModelKeyedEncodingContainer : KeyedEncodingContainerProtocol { public typealias Key = K @@ -283,7 +285,7 @@ struct ModelKeyedEncodingContainer : KeyedEncodingContainerProtoc // MARK: - SingleValueEncodingContainer -struct ModelSingleValueEncodingContainer: SingleValueEncodingContainer { +internal struct ModelSingleValueEncodingContainer: SingleValueEncodingContainer { // MARK: Properties diff --git a/Sources/CoreModel/Extensions/CodingUserInfoKey.swift b/Sources/CoreModel/Extensions/CodingUserInfoKey.swift new file mode 100644 index 0000000..6384df8 --- /dev/null +++ b/Sources/CoreModel/Extensions/CodingUserInfoKey.swift @@ -0,0 +1,24 @@ +// +// CodingUserInfoKey.swift +// +// +// Created by Alsey Coleman Miller on 8/19/23. +// + +import Foundation + +public extension CodingUserInfoKey { + + init(_ key: ModelCodingUserInfoKey) { + self.init(rawValue: key.rawValue)! + } + + static var identifierCodingKey: CodingUserInfoKey { + .init(.identifierCodingKey) + } +} + +public enum ModelCodingUserInfoKey: String { + + case identifierCodingKey = "org.pureswift.CoreModel.CodingUserInfoKey.identifierCodingKey" +} diff --git a/Tests/CoreModelTests/CoreDataTests.swift b/Tests/CoreModelTests/CoreDataTests.swift index f29a562..becfa99 100644 --- a/Tests/CoreModelTests/CoreDataTests.swift +++ b/Tests/CoreModelTests/CoreDataTests.swift @@ -43,11 +43,12 @@ final class CoreDataTests: XCTestCase { people: [person1.id] ) - let event1Data = try event1.encode(log: { print("CoreModel Encoder:", $0) }) + var event1Data = try event1.encode(log: { print("Encoder:", $0) }) try await store.insert(event1Data) person1 = try await store.fetch(Person.self, for: person1.id)! XCTAssertEqual(person1.events, [event1.id]) - event1 = try await store.fetch(Event.self, for: event1.id)! + event1Data = try await store.fetch(Event.entityName, for: ObjectID(event1.id))! + event1 = try .init(from: event1Data, log: { print("Decoder:", $0) }) XCTAssertEqual(event1.people, [person1.id]) } } diff --git a/Tests/CoreModelTests/TestModel.swift b/Tests/CoreModelTests/TestModel.swift index e628490..385a528 100644 --- a/Tests/CoreModelTests/TestModel.swift +++ b/Tests/CoreModelTests/TestModel.swift @@ -94,6 +94,18 @@ extension Person: Entity { self.age = try container.decode(UInt.self, forKey: Person.CodingKeys.age) self.events = try container.decodeRelationship([Event.ID].self, forKey: Person.CodingKeys.events) } + + func encode() -> ModelData { + var container = ModelData( + entity: Self.entityName, + id: ObjectID(rawValue: self.id.description) + ) + container.encode(self.name, forKey: Person.CodingKeys.name) + container.encode(self.created, forKey: Person.CodingKeys.created) + container.encode(self.age, forKey: Person.CodingKeys.age) + container.encodeRelationship(self.events, forKey: Person.CodingKeys.events) + return container + } } struct Event: Equatable, Hashable, Codable, Identifiable { @@ -164,17 +176,4 @@ extension Event: Entity { inverseRelationship: PropertyKey(Person.CodingKeys.events)) ] } - - init(from container: ModelData) throws { - guard container.entity == Self.entityName else { - throw DecodingError.typeMismatch(Self.self, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: Self.self)) from \(container.entity)")) - } - guard let id = UUID(uuidString: container.id.rawValue) else { - throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \(container.id)")) - } - self.id = id - self.name = try container.decode(String.self, forKey: Event.CodingKeys.name) - self.date = try container.decode(Date.self, forKey: Event.CodingKeys.date) - self.people = try container.decodeRelationship([Person.ID].self, forKey: Event.CodingKeys.people) - } }