Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions Sources/CoreModel/Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Foundation

public extension ModelData {

func decode<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: AttributeDecodable, K: CodingKey {
func decode<T, K>(_ 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 {
Expand All @@ -32,7 +32,7 @@ public extension ModelData {
return decodable
}

func decodeRelationship<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: ObjectIDConvertible, K: CodingKey {
func decodeRelationship<T, K>(_ 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 {
Expand All @@ -51,7 +51,7 @@ public extension ModelData {
}
}

func decodeRelationship<T, K>(_ type: T?.Type, forKey key: K) throws -> T? where T: ObjectIDConvertible, K: CodingKey {
func decodeRelationship<T, K>(_ 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 {
Expand All @@ -70,7 +70,7 @@ public extension ModelData {
}
}

func decodeRelationship<T, K>(_ type: [T].Type, forKey key: K) throws -> [T] where T: ObjectIDConvertible, K: CodingKey {
func decodeRelationship<T, K>(_ 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 {
Expand All @@ -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
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions Sources/CoreModel/EmbeddedFixture.swift
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Sources/CoreModel/Entity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/CoreModel/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading