Skip to content

Commit 960636d

Browse files
authored
Merge pull request #24 from PureSwift/feature/embedded
Fix Embedded Swift support for Entity decoding
2 parents 2fc6856 + ea27f47 commit 960636d

4 files changed

Lines changed: 76 additions & 9 deletions

File tree

‎Sources/CoreModel/Decodable.swift‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Foundation
1515

1616
public extension ModelData {
1717

18-
func decode<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: AttributeDecodable, K: CodingKey {
18+
func decode<T, K>(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: AttributeDecodable, K: CodingKey {
1919

2020
let property = PropertyKey(key)
2121
guard let attribute = self.attributes[property] else {
@@ -32,7 +32,7 @@ public extension ModelData {
3232
return decodable
3333
}
3434

35-
func decodeRelationship<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: ObjectIDConvertible, K: CodingKey {
35+
func decodeRelationship<T, K>(_ type: T.Type, forKey key: K) throws(ModelDataDecodingError) -> T where T: ObjectIDConvertible, K: CodingKey {
3636

3737
let property = PropertyKey(key)
3838
guard let relationship = self.relationships[property] else {
@@ -51,7 +51,7 @@ public extension ModelData {
5151
}
5252
}
5353

54-
func decodeRelationship<T, K>(_ type: T?.Type, forKey key: K) throws -> T? where T: ObjectIDConvertible, K: CodingKey {
54+
func decodeRelationship<T, K>(_ type: T?.Type, forKey key: K) throws(ModelDataDecodingError) -> T? where T: ObjectIDConvertible, K: CodingKey {
5555

5656
let property = PropertyKey(key)
5757
guard let relationship = self.relationships[property] else {
@@ -70,7 +70,7 @@ public extension ModelData {
7070
}
7171
}
7272

73-
func decodeRelationship<T, K>(_ type: [T].Type, forKey key: K) throws -> [T] where T: ObjectIDConvertible, K: CodingKey {
73+
func decodeRelationship<T, K>(_ type: [T].Type, forKey key: K) throws(ModelDataDecodingError) -> [T] where T: ObjectIDConvertible, K: CodingKey {
7474

7575
let property = PropertyKey(key)
7676
guard let relationship = self.relationships[property] else {
@@ -82,12 +82,19 @@ public extension ModelData {
8282
case .toOne:
8383
throw coreModelTypeMismatchError(type, forKey: key, from: relationship)
8484
case let .toMany(objectIDs):
85-
return try objectIDs.map {
86-
guard let id = T.init(objectID: $0) else {
87-
throw coreModelInvalidIdentifierError($0)
85+
// - Note: An explicit loop rather than `objectIDs.map`, because the
86+
// `rethrows` closure overload erases the thrown error back to
87+
// `any Error`, which cannot convert to the typed `throws` clause
88+
// under Embedded Swift.
89+
var ids = [T]()
90+
ids.reserveCapacity(objectIDs.count)
91+
for objectID in objectIDs {
92+
guard let id = T.init(objectID: objectID) else {
93+
throw coreModelInvalidIdentifierError(objectID)
8894
}
89-
return id
95+
ids.append(id)
9096
}
97+
return ids
9198
}
9299
}
93100
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// EmbeddedFixture.swift
3+
// CoreModel
4+
//
5+
// Created by Alsey Coleman Miller on 7/18/26.
6+
//
7+
8+
// - Note: A concrete `Entity` conformance that only exists under Embedded Swift.
9+
// It forces the compiler to specialize the generic `Entity.init(from:)` /
10+
// `ModelData.decode(...)` / `decodeRelationship(...)` decode path for a real
11+
// conforming type — the exact instantiation the wasm-embedded CI job never
12+
// exercises through the test target. Without it, error-boxing regressions in
13+
// that path (`#EmbeddedRestrictions`) can silently reappear. Dead weight on
14+
// non-Embedded builds, so it is gated out entirely there.
15+
16+
#if hasFeature(Embedded)
17+
18+
#if canImport(FoundationEssentials)
19+
import FoundationEssentials
20+
#elseif canImport(Foundation)
21+
import Foundation
22+
#endif
23+
24+
private struct EmbeddedEntityFixture: Entity {
25+
26+
let id: UUID
27+
28+
static var entityName: EntityName { "EmbeddedEntityFixture" }
29+
30+
static var attributes: [CodingKeys: AttributeType] { [:] }
31+
32+
static var relationships: [CodingKeys: Relationship] { [:] }
33+
34+
enum CodingKeys: String, CodingKey { case id }
35+
36+
init(from model: ModelData) throws(ModelDataDecodingError) {
37+
guard let id = Self.ID(objectID: model.id) else {
38+
throw .invalidIdentifier(model.id)
39+
}
40+
self.id = id
41+
}
42+
43+
func encode() -> ModelData {
44+
ModelData(entity: Self.entityName, id: ObjectID(id))
45+
}
46+
}
47+
48+
#endif

‎Sources/CoreModel/Entity.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public protocol Entity: Identifiable, Sendable where CodingKeys: Hashable, Self.
1717

1818
associatedtype CodingKeys: CodingKey
1919

20-
init(from model: ModelData) throws
20+
init(from model: ModelData) throws(ModelDataDecodingError)
2121

2222
func encode() throws -> ModelData
2323
}

‎Sources/CoreModel/Error.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ public enum CoreModelError: Error {
2121

2222
// MARK: - Decoding Error Factories
2323

24+
#if hasFeature(Embedded)
25+
/// The concrete error type thrown by `ModelData.decode`/`decodeRelationship`
26+
/// and by hand-written `Entity.init(from:)` witnesses under Embedded Swift,
27+
/// where `any Error` existentials are disallowed. Resolves to `any Swift.Error`
28+
/// on non-Embedded platforms, where the existing untyped-`throws` behavior is
29+
/// unchanged.
30+
public typealias ModelDataDecodingError = CoreModelError
31+
#else
32+
/// See the Embedded branch of this typealias above.
33+
public typealias ModelDataDecodingError = any Swift.Error
34+
#endif
35+
2436
#if hasFeature(Embedded)
2537

2638
// - Note: Embedded Swift disallows `any Error` as a value/return type (existential

0 commit comments

Comments
 (0)