diff --git a/Sources/CoreModel/Decodable.swift b/Sources/CoreModel/Decodable.swift index 086f524..f90d20c 100644 --- a/Sources/CoreModel/Decodable.swift +++ b/Sources/CoreModel/Decodable.swift @@ -15,7 +15,7 @@ import Foundation public extension ModelData { - func decode(_ type: T.Type, forKey key: K) throws -> T where T: AttributeDecodable, K: CodingKey { + func decode(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: AttributeDecodable, K: CodingKey { let property = PropertyKey(key) guard let attribute = self.attributes[property] else { @@ -32,7 +32,7 @@ public extension ModelData { return decodable } - func decodeRelationship(_ type: T.Type, forKey key: K) throws -> T where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -51,7 +51,7 @@ public extension ModelData { } } - func decodeRelationship(_ type: T?.Type, forKey key: K) throws -> T? where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: T?.Type, forKey key: K) throws(ModelDataDecodingError) -> T? where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -70,7 +70,7 @@ public extension ModelData { } } - func decodeRelationship(_ type: [T].Type, forKey key: K) throws -> [T] where T: ObjectIDConvertible, K: CodingKey { + func decodeRelationship(_ type: [T].Type, forKey key: K) throws(ModelDataDecodingError) -> [T] where T: ObjectIDConvertible, K: CodingKey { let property = PropertyKey(key) guard let relationship = self.relationships[property] else { @@ -82,12 +82,19 @@ public extension ModelData { case .toOne: throw coreModelTypeMismatchError(type, forKey: key, from: relationship) case let .toMany(objectIDs): - return try objectIDs.map { - guard let id = T.init(objectID: $0) else { - throw coreModelInvalidIdentifierError($0) + // - Note: An explicit loop rather than `objectIDs.map`, because the + // `rethrows` closure overload erases the thrown error back to + // `any Error`, which cannot convert to the typed `throws` clause + // under Embedded Swift. + var ids = [T]() + ids.reserveCapacity(objectIDs.count) + for objectID in objectIDs { + guard let id = T.init(objectID: objectID) else { + throw coreModelInvalidIdentifierError(objectID) } - return id + ids.append(id) } + return ids } } } diff --git a/Sources/CoreModel/EmbeddedFixture.swift b/Sources/CoreModel/EmbeddedFixture.swift new file mode 100644 index 0000000..8d7901c --- /dev/null +++ b/Sources/CoreModel/EmbeddedFixture.swift @@ -0,0 +1,48 @@ +// +// EmbeddedFixture.swift +// CoreModel +// +// Created by Alsey Coleman Miller on 7/18/26. +// + +// - Note: A concrete `Entity` conformance that only exists under Embedded Swift. +// It forces the compiler to specialize the generic `Entity.init(from:)` / +// `ModelData.decode(...)` / `decodeRelationship(...)` decode path for a real +// conforming type — the exact instantiation the wasm-embedded CI job never +// exercises through the test target. Without it, error-boxing regressions in +// that path (`#EmbeddedRestrictions`) can silently reappear. Dead weight on +// non-Embedded builds, so it is gated out entirely there. + +#if hasFeature(Embedded) + +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) +import Foundation +#endif + +private struct EmbeddedEntityFixture: Entity { + + let id: UUID + + static var entityName: EntityName { "EmbeddedEntityFixture" } + + static var attributes: [CodingKeys: AttributeType] { [:] } + + static var relationships: [CodingKeys: Relationship] { [:] } + + enum CodingKeys: String, CodingKey { case id } + + init(from model: ModelData) throws(ModelDataDecodingError) { + guard let id = Self.ID(objectID: model.id) else { + throw .invalidIdentifier(model.id) + } + self.id = id + } + + func encode() -> ModelData { + ModelData(entity: Self.entityName, id: ObjectID(id)) + } +} + +#endif diff --git a/Sources/CoreModel/Entity.swift b/Sources/CoreModel/Entity.swift index 9637b75..225ac8a 100644 --- a/Sources/CoreModel/Entity.swift +++ b/Sources/CoreModel/Entity.swift @@ -17,7 +17,7 @@ public protocol Entity: Identifiable, Sendable where CodingKeys: Hashable, Self. associatedtype CodingKeys: CodingKey - init(from model: ModelData) throws + init(from model: ModelData) throws(ModelDataDecodingError) func encode() throws -> ModelData } diff --git a/Sources/CoreModel/Error.swift b/Sources/CoreModel/Error.swift index 47e1a1e..2115f72 100644 --- a/Sources/CoreModel/Error.swift +++ b/Sources/CoreModel/Error.swift @@ -21,6 +21,18 @@ public enum CoreModelError: Error { // MARK: - Decoding Error Factories +#if hasFeature(Embedded) +/// The concrete error type thrown by `ModelData.decode`/`decodeRelationship` +/// and by hand-written `Entity.init(from:)` witnesses under Embedded Swift, +/// where `any Error` existentials are disallowed. Resolves to `any Swift.Error` +/// on non-Embedded platforms, where the existing untyped-`throws` behavior is +/// unchanged. +public typealias ModelDataDecodingError = CoreModelError +#else +/// See the Embedded branch of this typealias above. +public typealias ModelDataDecodingError = any Swift.Error +#endif + #if hasFeature(Embedded) // - Note: Embedded Swift disallows `any Error` as a value/return type (existential