From 6f9a3f7882323661a802a4dbd52c88f196e71040 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:06 -0400 Subject: [PATCH 1/9] Require CoreModel 2.8.0 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 0b885f6..caa3480 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( ), .package( url: "https://github.com/PureSwift/CoreModel", - from: "2.4.3" + from: "2.8.0" ) ], targets: [ From f523eabf24b87adf986772c32265b96b511a9135 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:06 -0400 Subject: [PATCH 2/9] Reject function expressions in server-side predicate translation --- Sources/MongoDBModel/Predicate.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/MongoDBModel/Predicate.swift b/Sources/MongoDBModel/Predicate.swift index db6fdb3..eac3563 100644 --- a/Sources/MongoDBModel/Predicate.swift +++ b/Sources/MongoDBModel/Predicate.swift @@ -56,7 +56,8 @@ public extension BSONDocument { } let valueBSON: BSON switch predicate.right { - case .keyPath: + case .keyPath, .function: + // custom functions cannot be executed by the server return nil case let .attribute(value): guard let bson = try? BSON(attributeValue: value) else { From 7cff1755a505af520e411dc39a6d7ac47349ac6d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:06 -0400 Subject: [PATCH 3/9] Handle SortDescriptor.term in sort document builder --- Sources/MongoDBModel/FetchRequest.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/MongoDBModel/FetchRequest.swift b/Sources/MongoDBModel/FetchRequest.swift index 89a11d0..3ae0ff6 100644 --- a/Sources/MongoDBModel/FetchRequest.swift +++ b/Sources/MongoDBModel/FetchRequest.swift @@ -14,7 +14,11 @@ public extension BSONDocument { init(sort sortDescriptors: [FetchRequest.SortDescriptor]) { self.init() for sort in sortDescriptors { - self[sort.property.rawValue] = sort.ascending ? 1 : -1 + guard case let .property(property) = sort.term else { + // function sort terms are evaluated in memory + continue + } + self[property.rawValue] = sort.ascending ? 1 : -1 } } } From 2914daca60c104def6c508aa85792ad5ccce82fd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:06 -0400 Subject: [PATCH 4/9] Add in-memory function evaluation --- Sources/MongoDBModel/FunctionEvaluation.swift | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 Sources/MongoDBModel/FunctionEvaluation.swift diff --git a/Sources/MongoDBModel/FunctionEvaluation.swift b/Sources/MongoDBModel/FunctionEvaluation.swift new file mode 100644 index 0000000..407018f --- /dev/null +++ b/Sources/MongoDBModel/FunctionEvaluation.swift @@ -0,0 +1,299 @@ +// +// FunctionEvaluation.swift +// MongoDBModel +// +// Created by Alsey Coleman Miller on 7/16/26. +// + +import Foundation +import CoreModel + +// MARK: - Function detection + +internal extension FetchRequest { + + /// Whether this fetch request references any custom `.function` expression and + /// therefore requires in-memory evaluation — MongoDB cannot execute a custom + /// function as part of a native query. + var requiresInMemoryEvaluation: Bool { + if predicate?.containsFunction == true { + return true + } + return sortDescriptors.contains { descriptor in + if case .function = descriptor.term { return true } else { return false } + } + } +} + +internal extension FetchRequest.Predicate { + + /// Whether this predicate references any `.function` expression. + var containsFunction: Bool { + switch self { + case .value: + return false + case let .comparison(comparison): + return comparison.left.containsFunction || comparison.right.containsFunction + case let .compound(compound): + return compound.subpredicates.contains { $0.containsFunction } + } + } + + /// Replace every comparison that references a function with `.value(true)`, so the + /// remaining predicate can be executed natively by MongoDB as a superset filter. + /// The full predicate is then re-applied in memory. + func strippingFunctionComparisons() -> FetchRequest.Predicate { + switch self { + case .value: + return self + case let .comparison(comparison): + let usesFunction = comparison.left.containsFunction || comparison.right.containsFunction + return usesFunction ? .value(true) : self + case let .compound(compound): + switch compound { + case let .and(subpredicates): + return .compound(.and(subpredicates.map { $0.strippingFunctionComparisons() })) + case let .or(subpredicates): + return .compound(.or(subpredicates.map { $0.strippingFunctionComparisons() })) + case let .not(subpredicate): + return .compound(.not(subpredicate.strippingFunctionComparisons())) + } + } + } +} + +internal extension FetchRequest.Predicate.Expression { + + var containsFunction: Bool { + switch self { + case .function: + return true + case .attribute, .relationship, .keyPath: + return false + } + } +} + +// MARK: - In-memory evaluation + +internal extension FetchRequest.Predicate { + + /// Evaluate this predicate against a fetched object in memory, calling registered + /// functions for any `.function` expression. + func evaluate( + with data: ModelData, + functions: [String: DatabaseFunction] + ) -> Bool { + switch self { + case let .value(value): + return value + case let .compound(compound): + switch compound { + case let .and(subpredicates): + return subpredicates.allSatisfy { $0.evaluate(with: data, functions: functions) } + case let .or(subpredicates): + return subpredicates.contains { $0.evaluate(with: data, functions: functions) } + case let .not(subpredicate): + return subpredicate.evaluate(with: data, functions: functions) == false + } + case let .comparison(comparison): + return comparison.evaluate(with: data, functions: functions) + } + } +} + +internal extension FetchRequest.Predicate.Comparison { + + func evaluate( + with data: ModelData, + functions: [String: DatabaseFunction] + ) -> Bool { + let lhs = left.evaluate(with: data, functions: functions) + let rhs = right.evaluate(with: data, functions: functions) + return type.evaluate(lhs, rhs, options: options) + } +} + +internal extension FetchRequest.Predicate.Expression { + + /// Resolve this expression to a value for a fetched object. + func evaluate( + with data: ModelData, + functions: [String: DatabaseFunction] + ) -> AttributeValue? { + switch self { + case let .attribute(value): + return value + case let .keyPath(keyPath): + return data.attributes[PropertyKey(rawValue: keyPath.rawValue)] + case let .function(function): + guard let registered = functions[function.name] else { + return nil + } + let arguments = function.arguments.map { $0.evaluate(with: data, functions: functions) } + return registered.evaluate(arguments) + case .relationship: + // relationships aren't compared by the in-memory function path + return nil + } + } +} + +// MARK: - Operator evaluation + +private extension FetchRequest.Predicate.Comparison.Operator { + + func evaluate( + _ lhs: AttributeValue?, + _ rhs: AttributeValue?, + options: Set + ) -> Bool { + let caseInsensitive = options.contains(.caseInsensitive) + switch self { + case .equalTo: + return AttributeValue.areEqual(lhs, rhs, caseInsensitive: caseInsensitive) + case .notEqualTo: + return AttributeValue.areEqual(lhs, rhs, caseInsensitive: caseInsensitive) == false + case .lessThan: + return (AttributeValue.order(lhs, rhs)).map { $0 < 0 } ?? false + case .lessThanOrEqualTo: + return (AttributeValue.order(lhs, rhs)).map { $0 <= 0 } ?? false + case .greaterThan: + return (AttributeValue.order(lhs, rhs)).map { $0 > 0 } ?? false + case .greaterThanOrEqualTo: + return (AttributeValue.order(lhs, rhs)).map { $0 >= 0 } ?? false + case .beginsWith: + return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.hasPrefix($1) } + case .endsWith: + return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.hasSuffix($1) } + case .contains: + return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { $0.contains($1) } + case .like, .matches: + return AttributeValue.stringCompare(lhs, rhs, caseInsensitive: caseInsensitive) { subject, pattern in + subject.range(of: like(pattern: pattern, matches: self == .matches), options: .regularExpression) != nil + } + case .in, .between: + // right-hand collections aren't represented as a single AttributeValue + return false + } + } + + /// Convert a Cocoa-style `LIKE` pattern (`*`, `?`) or a full regular expression into + /// an anchored regular expression string. + private func like(pattern: String, matches: Bool) -> String { + guard matches == false else { + return pattern // already a regular expression + } + let escaped = NSRegularExpression.escapedPattern(for: pattern) + .replacingOccurrences(of: "\\*", with: ".*") + .replacingOccurrences(of: "\\?", with: ".") + return "^" + escaped + "$" + } +} + +private extension AttributeValue { + + /// A numeric representation for comparable value types, for ordering comparisons. + var comparableDouble: Double? { + switch self { + case let .bool(value): return value ? 1 : 0 + case let .int16(value): return Double(value) + case let .int32(value): return Double(value) + case let .int64(value): return Double(value) + case let .float(value): return Double(value) + case let .double(value): return value + case let .decimal(value): return NSDecimalNumber(decimal: value).doubleValue + case let .date(value): return value.timeIntervalSinceReferenceDate + default: return nil + } + } + + var stringValue: String? { + if case let .string(value) = self { return value } + return nil + } + + static func areEqual(_ lhs: AttributeValue?, _ rhs: AttributeValue?, caseInsensitive: Bool) -> Bool { + switch (lhs, rhs) { + case (.none, .none), (.some(.null), .none), (.none, .some(.null)), (.some(.null), .some(.null)): + return true + case let (.some(left), .some(right)): + if caseInsensitive, let l = left.stringValue, let r = right.stringValue { + return l.caseInsensitiveCompare(r) == .orderedSame + } + return left == right + default: + return false + } + } + + /// Ordering of two values: negative if `lhs < rhs`, zero if equal, positive if greater; + /// `nil` if the values aren't order-comparable. + static func order(_ lhs: AttributeValue?, _ rhs: AttributeValue?) -> Int? { + guard let lhs, let rhs else { return nil } + if let l = lhs.comparableDouble, let r = rhs.comparableDouble { + if l < r { return -1 } + if l > r { return 1 } + return 0 + } + if let l = lhs.stringValue, let r = rhs.stringValue { + switch l.compare(r) { + case .orderedAscending: return -1 + case .orderedSame: return 0 + case .orderedDescending: return 1 + } + } + return nil + } + + static func stringCompare( + _ lhs: AttributeValue?, + _ rhs: AttributeValue?, + caseInsensitive: Bool, + _ compare: (String, String) -> Bool + ) -> Bool { + guard var subject = lhs?.stringValue, var pattern = rhs?.stringValue else { + return false + } + if caseInsensitive { + subject = subject.lowercased() + pattern = pattern.lowercased() + } + return compare(subject, pattern) + } +} + +// MARK: - In-memory sorting + +internal extension Array where Element == ModelData { + + /// Sort in memory by the given descriptors, resolving function terms with the + /// registered functions. Property terms fall back to attribute ordering. + func sortedInMemory( + by sortDescriptors: [FetchRequest.SortDescriptor], + functions: [String: DatabaseFunction] + ) -> [ModelData] { + guard sortDescriptors.isEmpty == false else { return self } + return sorted { first, second in + for descriptor in sortDescriptors { + let lhs: AttributeValue? + let rhs: AttributeValue? + switch descriptor.term { + case let .property(property): + lhs = first.attributes[property] + rhs = second.attributes[property] + case let .function(function): + let expression = FetchRequest.Predicate.Expression.function(function) + lhs = expression.evaluate(with: first, functions: functions) + rhs = expression.evaluate(with: second, functions: functions) + } + guard let comparison = AttributeValue.order(lhs, rhs), comparison != 0 else { + continue + } + return descriptor.ascending ? comparison < 0 : comparison > 0 + } + // stable tiebreaker on id, matching the native fetch's trailing id sort + return first.id.rawValue < second.id.rawValue + } + } +} From a2cb2887238ffa6f9c48f125d2c0c930b382aa3a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:06 -0400 Subject: [PATCH 5/9] Implement DatabaseFunction registration and in-memory fetch --- Sources/MongoDBModel/MongoDatabase.swift | 60 ++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/Sources/MongoDBModel/MongoDatabase.swift b/Sources/MongoDBModel/MongoDatabase.swift index 4f8aa3b..cb8eb82 100644 --- a/Sources/MongoDBModel/MongoDatabase.swift +++ b/Sources/MongoDBModel/MongoDatabase.swift @@ -2,7 +2,8 @@ import Foundation @_exported import CoreModel @_exported @preconcurrency import MongoSwift -extension MongoCollectionOptions: @unchecked Sendable {} +extension MongoCollectionOptions: @unchecked @retroactive Sendable {} +extension MongoDatabase: @unchecked @retroactive Sendable {} public actor MongoModelStorage: ModelStorage { @@ -11,7 +12,9 @@ public actor MongoModelStorage: ModelStorage { public let model: Model public let options: Configuration - + + private var registeredFunctions = [String: DatabaseFunction]() + public init( database: MongoDatabase, model: Model, @@ -33,14 +36,27 @@ public actor MongoModelStorage: ModelStorage { public func fetch(_ fetchRequest: FetchRequest) async throws -> [ModelData] { let entity = try model(for: fetchRequest.entity) let collectionOptions = options.collections[entity.id] + guard fetchRequest.requiresInMemoryEvaluation == false else { + return try await database.fetchInMemory(fetchRequest, entity: entity, options: collectionOptions, functions: registeredFunctions) + } return try await database.fetch(fetchRequest, entity: entity, options: collectionOptions) } - + /// Fetch and return result count. public func count(_ fetchRequest: FetchRequest) async throws -> UInt { let collectionOptions = options.collections[fetchRequest.entity] + guard fetchRequest.requiresInMemoryEvaluation == false else { + let entity = try model(for: fetchRequest.entity) + let results = try await database.fetchInMemory(fetchRequest, entity: entity, options: collectionOptions, functions: registeredFunctions) + return UInt(results.count) + } return try await database.count(fetchRequest, options: collectionOptions) } + + /// Register a custom function for use in predicates and sort descriptors. + public func register(function: DatabaseFunction) { + registeredFunctions[function.name] = function + } /// Create or edit a managed object. public func insert(_ value: ModelData) async throws { @@ -67,6 +83,11 @@ public actor MongoModelStorage: ModelStorage { public func fetchID(_ fetchRequest: FetchRequest) async throws -> [ObjectID] { let collectionOptions = options.collections[fetchRequest.entity] + guard fetchRequest.requiresInMemoryEvaluation == false else { + let entity = try model(for: fetchRequest.entity) + let results = try await database.fetchInMemory(fetchRequest, entity: entity, options: collectionOptions, functions: registeredFunctions) + return results.map { $0.id } + } return try await database.fetchIDs(fetchRequest, options: collectionOptions) } @@ -220,6 +241,39 @@ internal extension MongoDatabase { return results } + /// Fetch objects for a request that references custom functions, which MongoDB + /// cannot execute natively: fetch a native superset (function comparisons stripped + /// from the predicate), then filter, sort, and paginate in memory. + func fetchInMemory( + _ fetchRequest: FetchRequest, + entity: EntityDescription, + options: MongoCollectionOptions?, + functions: [String: DatabaseFunction] + ) async throws -> [ModelData] { + let collection = self.collection(fetchRequest.entity, options: options) + // a fetch-all superset is still correct if the stripped predicate is untranslatable, + // since the full predicate is re-applied in memory below + let filter = fetchRequest.predicate + .map { $0.strippingFunctionComparisons() } + .flatMap { BSONDocument(predicate: $0) } ?? [:] + let stream = try await collection.find(filter, options: nil) + var results = [ModelData]() + for try await document in stream { + results.append(try ModelData(bson: document, model: entity)) + } + if let predicate = fetchRequest.predicate { + results = results.filter { predicate.evaluate(with: $0, functions: functions) } + } + results = results.sortedInMemory(by: fetchRequest.sortDescriptors, functions: functions) + if fetchRequest.fetchOffset > 0 { + results = Array(results.dropFirst(fetchRequest.fetchOffset)) + } + if fetchRequest.fetchLimit > 0 { + results = Array(results.prefix(fetchRequest.fetchLimit)) + } + return results + } + func fetchIDs( _ fetchRequest: FetchRequest, options: MongoCollectionOptions? From 59a8190694083e5f2196f41ed791cf47cc0ef1dd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:11 -0400 Subject: [PATCH 6/9] Mark test model value types Sendable --- Tests/CoreModelMongoDBTests/TestModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/CoreModelMongoDBTests/TestModel.swift b/Tests/CoreModelMongoDBTests/TestModel.swift index 3ea50f2..1de8755 100644 --- a/Tests/CoreModelMongoDBTests/TestModel.swift +++ b/Tests/CoreModelMongoDBTests/TestModel.swift @@ -276,7 +276,7 @@ extension Campground: Entity { public extension Campground { /// Campground Amenities - enum Amenity: String, Codable, CaseIterable { + enum Amenity: String, Codable, CaseIterable, Sendable { case water case amp30 @@ -333,7 +333,7 @@ extension Array: AttributeDecodable where Self.Element == Campground.Amenity { public extension Campground { /// Location Coordinates - struct LocationCoordinates: Equatable, Hashable, Codable { + struct LocationCoordinates: Equatable, Hashable, Codable, Sendable { /// Latitude public var latitude: Double @@ -374,7 +374,7 @@ extension Campground.LocationCoordinates: AttributeDecodable { public extension Campground { /// Schedule (e.g. Check in, Check Out) - struct Schedule: Equatable, Hashable, Codable { + struct Schedule: Equatable, Hashable, Codable, Sendable { public var start: UInt From e14bd9f2c4b0fdfc6abf2a7e1478fa926482b3f0 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:17:12 -0400 Subject: [PATCH 7/9] Add Codable-to-ModelData bridge for test entities --- .../LegacyCodable/CodingKey.swift | 30 + .../LegacyCodable/CodingUserInfoKey.swift | 24 + .../LegacyCodable/Decoder.swift | 694 ++++++++++++++++++ .../LegacyCodable/Encoder.swift | 528 +++++++++++++ 4 files changed, 1276 insertions(+) create mode 100644 Tests/CoreModelMongoDBTests/LegacyCodable/CodingKey.swift create mode 100644 Tests/CoreModelMongoDBTests/LegacyCodable/CodingUserInfoKey.swift create mode 100644 Tests/CoreModelMongoDBTests/LegacyCodable/Decoder.swift create mode 100644 Tests/CoreModelMongoDBTests/LegacyCodable/Encoder.swift diff --git a/Tests/CoreModelMongoDBTests/LegacyCodable/CodingKey.swift b/Tests/CoreModelMongoDBTests/LegacyCodable/CodingKey.swift new file mode 100644 index 0000000..9b53023 --- /dev/null +++ b/Tests/CoreModelMongoDBTests/LegacyCodable/CodingKey.swift @@ -0,0 +1,30 @@ +// +// CodingKey.swift +// +// +// Created by Alsey Coleman Miller on 8/18/23. +// + +#if !hasFeature(Embedded) +internal extension Sequence where Element == CodingKey { + + /// KVC path string for current coding path. + var path: String { + return reduce("", { $0 + "\($0.isEmpty ? "" : ".")" + $1.stringValue }) + } +} + +internal extension CodingKey { + + static var sanitizedName: String { + + let rawName = String(reflecting: self) + var elements = rawName.split(separator: ".") + guard elements.count > 2 + else { return rawName } + elements.removeFirst() + elements.removeAll { $0.contains("(unknown context") } + return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) + } +} +#endif diff --git a/Tests/CoreModelMongoDBTests/LegacyCodable/CodingUserInfoKey.swift b/Tests/CoreModelMongoDBTests/LegacyCodable/CodingUserInfoKey.swift new file mode 100644 index 0000000..ed412bc --- /dev/null +++ b/Tests/CoreModelMongoDBTests/LegacyCodable/CodingUserInfoKey.swift @@ -0,0 +1,24 @@ +// +// CodingUserInfoKey.swift +// +// +// Created by Alsey Coleman Miller on 8/19/23. +// + +#if !hasFeature(Embedded) +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" +} +#endif diff --git a/Tests/CoreModelMongoDBTests/LegacyCodable/Decoder.swift b/Tests/CoreModelMongoDBTests/LegacyCodable/Decoder.swift new file mode 100644 index 0000000..8083c04 --- /dev/null +++ b/Tests/CoreModelMongoDBTests/LegacyCodable/Decoder.swift @@ -0,0 +1,694 @@ +// +// Decoder.swift +// +// +// Created by Alsey Coleman Miller on 8/18/23. +// + +#if !hasFeature(Embedded) +import Foundation +import CoreModel + +// MARK: - Default Codable Implementation + +extension Entity where Self: Decodable { + + 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 + ) + log?("Will decode \(model.entity) \(model.id)") + 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 attributes.keys.contains(property) else { + throw DecodingError.typeMismatch(T.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Unknown attribute for \"\(key.stringValue)\"")) + } + 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 { + let property = PropertyKey(key) + // override for native types and id + if key.stringValue == identifierKey { + log?("Will decode \(type) at path \"\(codingPath.path)\"") + 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: ObjectIDConvertible.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 let relationship = relationships[property] { + log?("Will decode \(type) at path \"\(codingPath.path)\"") + guard let relationshipValue = self.data.relationships[property] else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Missing relationship value for \(key.stringValue)")) + } + switch (relationship.type, relationshipValue) { + case (_, .null): + //assertionFailure() + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Expected \(type) value for \(key.stringValue)")) + case (.toMany, .toMany): + return try T.init(from: self) + case (.toOne, .toOne(let objectID)): + 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: ObjectIDConvertible.self))")) + } + guard let value = convertible.init(objectID: objectID) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode \(type) from identifier \(objectID)")) + } + return value as! T + default: + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode relationship from \(type).")) + } + } 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) + } + +} +#endif diff --git a/Tests/CoreModelMongoDBTests/LegacyCodable/Encoder.swift b/Tests/CoreModelMongoDBTests/LegacyCodable/Encoder.swift new file mode 100644 index 0000000..da07318 --- /dev/null +++ b/Tests/CoreModelMongoDBTests/LegacyCodable/Encoder.swift @@ -0,0 +1,528 @@ +// +// Encoder.swift +// +// +// Created by Alsey Coleman Miller on 8/18/23. +// + +#if !hasFeature(Embedded) +import Foundation +import CoreModel + +extension Entity where Self: Encodable { + + public func encode() throws -> ModelData { + try encode(log: nil) + } + + internal func encode( + userInfo: [CodingUserInfoKey : Any] = [:], + log: ((String) -> ())? + ) throws -> ModelData { + let entity = EntityDescription(entity: Self.self) + let id = ObjectID(rawValue: self.id.description) + let encoder = ModelDataEncoder( + entity: entity, + id: id, + userInfo: userInfo, + log: log + ) + log?("Will encode \(Self.entityName) \(self.id)") + try self.encode(to: encoder) + return encoder.data + } +} + +internal final class ModelDataEncoder: Encoder { + + fileprivate(set) var codingPath: [CodingKey] + + let userInfo: [CodingUserInfoKey : Any] + + fileprivate(set) var data: ModelData + + fileprivate let log: ((String) -> ())? + + let attributes: [PropertyKey: Attribute] + + let relationships: [PropertyKey: Relationship] + + init( + entity: EntityDescription, + id: ObjectID, + codingPath: [CodingKey] = [], + userInfo: [CodingUserInfoKey : Any] = [:], + log: ((String) -> ())? = nil + ) { + self.codingPath = codingPath + self.userInfo = userInfo + self.log = log + self.data = ModelData(entity: entity.id, id: 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 }) + } + + func container(keyedBy type: Key.Type) -> Swift.KeyedEncodingContainer where Key : CodingKey { + log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") + let container = ModelKeyedEncodingContainer(referencing: self) + return Swift.KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> Swift.UnkeyedEncodingContainer { + log?("Requested unkeyed container for path \"\(codingPath.path)\"") + return ModelUnkeyedEncodingContainer(referencing: self) + } + + func singleValueContainer() -> Swift.SingleValueEncodingContainer { + log?("Requested single value container for path \"\(codingPath.path)\"") + assert(self.codingPath.last != nil) + return ModelSingleValueEncodingContainer(referencing: self) + } +} + +internal extension ModelDataEncoder { + + func setAttribute(_ value: AttributeValue, forKey key: PropertyKey) throws { + log?("Will set \(value) for attribute \"\(key)\"") + guard relationships.keys.contains(key) == false else { + assertionFailure("Trying to set \(value) for relationship \(key)") + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot set attribute value \(value) for relationship \(key).")) + } + guard attributes.keys.contains(key) else { + // TODO: Determine if blacklisted key (e.g. _id) + //throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "No attribute found for \"\(key)\"")) + return + } + data.attributes[key] = value + } + + func setRelationship(_ value: RelationshipValue, forKey key: PropertyKey) throws { + log?("Will set \(value) for relationship \"\(key)\"") + guard relationships.keys.contains(key) else { + //throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: "No relationship found for \"\(key)\"")) + return + } + data.relationships[key] = value + } + + func setNil(for key: PropertyKey) throws { + log?("Will set nil for \"\(key)\"") + if attributes.keys.contains(key) { + data.attributes[key] = .null + } else if relationships.keys.contains(key) { + data.relationships[key] = .null + } else { + return + //throw EncodingError.invalidValue(Optional.self, EncodingError.Context(codingPath: codingPath, debugDescription: "No property found for \"\(key)\"")) + } + } + + func setEncodable (_ value: T, forKey key: PropertyKey) throws { + + if attributes.keys.contains(key), let encodable = value as? AttributeEncodable { + try setAttribute(encodable.attributeValue, forKey: key) + } else if relationships.keys.contains(key), let id = value as? ObjectIDConvertible { + try setRelationship(.toOne(ObjectID(id)), forKey: key) + } else { + // encode using Encodable, container should write directly. + try value.encode(to: self) + } + } + + func setString(_ string: String, forKey key: PropertyKey) throws { + log?("Will set \"\(string)\" for \"\(key)\"") + if attributes.keys.contains(key) { + data.attributes[key] = .string(string) + } else if relationships.keys.contains(key) { + data.relationships[key] = .toOne(ObjectID(rawValue: string)) + } else { + return + //throw EncodingError.invalidValue(Optional.self, EncodingError.Context(codingPath: codingPath, debugDescription: "No property found for \"\(key)\"")) + } + } +} + +// MARK: - KeyedEncodingContainer + +internal struct ModelKeyedEncodingContainer : KeyedEncodingContainerProtocol { + + public typealias Key = K + + // MARK: Properties + + /// A reference to the encoder we're writing to. + let encoder: ModelDataEncoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + // MARK: Initialization + + init(referencing encoder: ModelDataEncoder) { + self.encoder = encoder + self.codingPath = encoder.codingPath + } + + // MARK: Methods + + func encodeNil(forKey key: Key) throws { + // set coding path + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + // set value + try encoder.setNil(for: PropertyKey(key)) + } + + func encode(_ value: String, forKey key: Self.Key) throws { + // Determine if attribute or relationship + // set coding path + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + // set value + try encoder.setString(value, forKey: PropertyKey(key)) + } + + func encode(_ value: Bool, forKey key: Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Double, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Float, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Int, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Int8, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Int16, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Int32, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: Int64, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: UInt, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: UInt8, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: UInt16, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: UInt32, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: UInt64, forKey key: Self.Key) throws { + try writeAttribute(value, forKey: key) + } + + func encode(_ value: T, forKey key: Self.Key) throws where T : Encodable { + + // set coding key context + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + + // set value + try encoder.setEncodable(value, forKey: PropertyKey(key)) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer { + fatalError() + } + + func superEncoder() -> Encoder { + fatalError() + } + + func superEncoder(forKey key: K) -> Encoder { + fatalError() + } + + // MARK: Private Methods + + private func writeAttribute(_ value: T, forKey key: Key) throws { + + // set coding key context + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + + // set value + try encoder.setAttribute(value.attributeValue, forKey: PropertyKey(key)) + } +} + +// MARK: - SingleValueEncodingContainer + +internal struct ModelSingleValueEncodingContainer: SingleValueEncodingContainer { + + // MARK: Properties + + /// A reference to the encoder we're writing to. + let encoder: ModelDataEncoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + // MARK: Initialization + + init(referencing encoder: ModelDataEncoder) { + self.encoder = encoder + self.codingPath = encoder.codingPath + } + + // MARK: - Methods + + func encodeNil() throws { + let key = try propertyKey() + try encoder.setNil(for: key) + } + + func encode(_ value: String) throws { + let key = try propertyKey() + try encoder.setString(value, forKey: key) + } + + func encode(_ value: Bool) throws { + try writeAttribute(value) + } + + func encode(_ value: Double) throws { + try writeAttribute(value) + } + + func encode(_ value: Float) throws { + try writeAttribute(value) + } + + func encode(_ value: Int) throws { + try writeAttribute(value) + } + + func encode(_ value: Int8) throws { + try writeAttribute(value) + } + + func encode(_ value: Int16) throws { + try writeAttribute(value) + } + + func encode(_ value: Int32) throws { + try writeAttribute(value) + } + + func encode(_ value: Int64) throws { + try writeAttribute(value) + } + + func encode(_ value: UInt) throws { + try writeAttribute(value) + } + + func encode(_ value: UInt8) throws{ + try writeAttribute(value) + } + + func encode(_ value: UInt16) throws{ + try writeAttribute(value) + } + + func encode(_ value: UInt32) throws{ + try writeAttribute(value) + } + + func encode(_ value: UInt64) throws { + try writeAttribute(value) + } + + func encode (_ value: T) throws { + let key = try self.propertyKey() + try encoder.setEncodable(value, forKey: key) + } + + // MARK: Private Methods + + private func propertyKey() throws -> PropertyKey { + guard let key = codingPath.first else { + throw EncodingError.invalidValue(Any.self, EncodingError.Context(codingPath: codingPath, debugDescription: "Invalid coding path")) + } + return PropertyKey(key) + } + + private func writeAttribute(_ value: T) throws { + let key = try self.propertyKey() + try encoder.setAttribute(value.attributeValue, forKey: key) + } +} + + +// MARK: - UnkeyedEncodingContainer + +internal final class ModelUnkeyedEncodingContainer: UnkeyedEncodingContainer { + + // MARK: Properties + + /// A reference to the encoder we're writing to. + let encoder: ModelDataEncoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + private var objectIDs = [ObjectID]() + + // MARK: Initialization + + init(referencing encoder: ModelDataEncoder) { + self.encoder = encoder + self.codingPath = encoder.codingPath + } + + deinit { + writeArray() + } + + // MARK: - Methods + + var count: Int { + objectIDs.count + } + + func encodeNil() throws { + throw EncodingError.invalidValue(type(of: Optional.self), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(Optional.self)")) + } + + func encode(_ value: String) throws { + try encodeRelationship(value) + } + + func encode(_ value: Bool) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Double) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Float) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Int) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Int8) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Int16) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Int32) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: Int64) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: UInt) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: UInt8) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: UInt16) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: UInt32) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode(_ value: UInt64) throws { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + + func encode (_ value: T) throws { + guard let stringConvertible = value as? CustomStringConvertible else { + throw EncodingError.invalidValue(type(of: value), EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode to-many relationship of \(type(of: value))")) + } + let string = stringConvertible.description + try encodeRelationship(string) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + fatalError() + } + + func superEncoder() -> Encoder { + encoder + } + + // MARK: Private Methods + + private func propertyKey() throws -> PropertyKey { + guard let key = codingPath.first else { + throw EncodingError.invalidValue(Any.self, EncodingError.Context(codingPath: codingPath, debugDescription: "Invalid coding path")) + } + return PropertyKey(key) + } + + private func encodeRelationship(_ string: String) throws { + objectIDs.append(ObjectID(rawValue: string)) + } + + private func writeArray() { + // set key to be written + guard let codingKey = codingPath.first else { + //throw EncodingError.invalidValue(Any.self, EncodingError.Context(codingPath: codingPath, debugDescription: "Invalid coding path")) + return + } + let key = PropertyKey(codingKey) + // write final value + let value = RelationshipValue.toMany(objectIDs) + encoder.log?("Will set \(value) for relationship \"\(key)\"") + encoder.data.relationships[key] = value + } +} +#endif From 49a8b586754bd052b3022ad927452ad8cc684a69 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:25:14 -0400 Subject: [PATCH 8/9] Update CI to Swift 6.3.3 on latest Ubuntu --- .github/workflows/swift.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1f554ce..15af6d7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -7,16 +7,13 @@ jobs: name: Build strategy: matrix: - swift: [6.0.2, 6.1.2] - os: [ubuntu-20.04] + swift: ["6.3.3"] + os: [ubuntu-latest] runs-on: ${{ matrix.os }} + container: swift:${{ matrix.swift }} steps: - - name: Install Swift - uses: slashmo/install-swift@v0.3.0 - with: - version: ${{ matrix.swift }} - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Swift Version run: swift --version - name: Build (Debug) From 11635e1cc40a8b28727f8ceb9c0b2ea6d4488ce4 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:37:27 -0400 Subject: [PATCH 9/9] Install MongoDB C driver dependencies in CI --- .github/workflows/swift.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 15af6d7..b807b9f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Install MongoDB C driver dependencies + run: apt-get update && apt-get install -y libssl-dev libsasl2-dev zlib1g-dev - name: Swift Version run: swift --version - name: Build (Debug)