Skip to content

Commit 2cfe061

Browse files
authored
Merge pull request #17 from PureSwift/feature/embedded-swift
Add Embedded Swift and WASM support
2 parents 40aa474 + a552855 commit 2cfe061

35 files changed

Lines changed: 1120 additions & 156 deletions

‎.github/workflows/swift-wasm.yml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,33 @@ jobs:
2828
swift build
2929
-c ${{ matrix.config }}
3030
--swift-sdk swift-6.3.2-RELEASE_wasm
31+
32+
embedded:
33+
name: Embedded WebAssembly
34+
runs-on: ubuntu-latest
35+
container: swift:6.3.2
36+
strategy:
37+
fail-fast: false
38+
matrix:
39+
config: [debug, release]
40+
steps:
41+
- name: Checkout
42+
uses: actions/checkout@v4
43+
- name: Swift Version
44+
run: swift --version
45+
- name: Install dependencies
46+
run: apt-get update -y && apt-get install -y curl
47+
- name: Install Embedded WASM SDK
48+
run: |
49+
set -eux
50+
# The embedded SDK identifier ships inside the same artifact bundle as the regular WASM SDK.
51+
url="https://download.swift.org/swift-6.3.2-release/wasm-sdk/swift-6.3.2-RELEASE/swift-6.3.2-RELEASE_wasm.artifactbundle.tar.gz"
52+
curl -fsSL "$url" -o wasm.artifactbundle.tar.gz
53+
swift sdk install wasm.artifactbundle.tar.gz
54+
swift sdk list
55+
- name: Build
56+
run: >-
57+
SWIFTPM_ENABLE_MACROS=0
58+
swift build
59+
-c ${{ matrix.config }}
60+
--swift-sdk swift-6.3.2-RELEASE_wasm-embedded

‎README.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,20 @@ Swift Object Graph
1313
- CoreData
1414
- [SQLite](https://github.com/PureSwift/CoreModel-SQLite)
1515
- [MongoDB](https://github.com/PureSwift/CoreModel-MongoDB)
16+
17+
## Embedded Swift
18+
19+
The `CoreModel` target compiles under [Embedded Swift](https://docs.swift.org/embedded/documentation/embedded), starting with WebAssembly (`wasm32-unknown-none-wasm` / `wasm32-unknown-wasip1` via the embedded Swift SDK). `CoreDataModel` remains a Foundation/CoreData-only target and is unaffected.
20+
21+
```
22+
SWIFTPM_ENABLE_MACROS=0 swift build --swift-sdk swift-6.3.2-RELEASE_wasm-embedded
23+
```
24+
25+
Macros must be disabled (`SWIFTPM_ENABLE_MACROS=0`) since `swift-syntax` isn't available under Embedded. This means a few things `@Entity` normally generates aren't available and must be written by hand:
26+
27+
- `Entity.entityName`, `attributes`, and `relationships` have no default implementation — implement them explicitly.
28+
- `Entity.init(from:)` / `encode()` have no default Codable-derived implementation — implement them using `ModelData.decode(_:forKey:)` / `.encode(_:forKey:)` (see `Person` in `Tests/CoreModelTests/TestModel.swift` for the pattern).
29+
- `enum CodingKeys: CodingKey { ... }` must declare an explicit `String` raw value — `enum CodingKeys: String, CodingKey { ... }` — since Embedded Swift has no `Codable`/`CodingKey` synthesis; `CoreModel` provides its own `CodingKey` protocol under Embedded that relies on the raw value.
30+
- `Model(entities: any Entity.Type...)` is unavailable (calls a generic initializer through an existential); use `Model(entities: [EntityDescription(entity: Person.self), ...])` with concrete types instead.
31+
- `ModelStorage`'s generic `Entity`-based convenience methods (`fetch<T>`, `insert<T>`, `delete<T>`, `count<T>`) and `ViewContext` are unavailable under Embedded (a compiler limitation in `async` default protocol-extension methods). Call the `ModelStorage` protocol requirements directly with `ModelData`/`ObjectID`.
32+
- `UUID`, `Date`, `Data`, `URL`, and `Decimal` are Foundation-free storage-layer replacements on platforms without Foundation — sufficient for round-tripping through `AttributeValue`, not general-purpose Foundation substitutes.

‎Sources/CoreModel/Attribute.swift‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@
55
// Created by Alsey Coleman Miller on 8/16/23.
66
//
77

8-
import Foundation
9-
108
/// CoreModel `Attribute`
11-
public struct Attribute: Property, Codable, Equatable, Hashable, Identifiable, Sendable {
12-
9+
public struct Attribute: Property, Equatable, Hashable, Identifiable, Sendable {
10+
1311
public let id: PropertyKey
14-
12+
1513
public var type: AttributeType
16-
14+
1715
public init(
1816
id: PropertyKey,
1917
type: AttributeType
@@ -22,3 +20,9 @@ public struct Attribute: Property, Codable, Equatable, Hashable, Identifiable, S
2220
self.type = type
2321
}
2422
}
23+
24+
// MARK: - Codable
25+
26+
#if !hasFeature(Embedded)
27+
extension Attribute: Codable {}
28+
#endif

‎Sources/CoreModel/AttributeType.swift‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
// Created by Alsey Coleman Miller on 8/16/23.
66
//
77

8-
import Foundation
9-
108
/// CoreModel Attribute type
11-
public enum AttributeType: String, Codable, CaseIterable, Sendable {
9+
public enum AttributeType: String, CaseIterable, Sendable {
1210

1311
/// Boolean number type.
1412
case bool
@@ -46,3 +44,9 @@ public enum AttributeType: String, Codable, CaseIterable, Sendable {
4644
/// Decimal
4745
case decimal
4846
}
47+
48+
// MARK: - Codable
49+
50+
#if !hasFeature(Embedded)
51+
extension AttributeType: Codable {}
52+
#endif

‎Sources/CoreModel/Codable.swift‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@
55
// Created by Alsey Coleman Miller on 8/18/23.
66
//
77

8-
import Foundation
9-
108
public typealias AttributeCodable = AttributeEncodable & AttributeDecodable

‎Sources/CoreModel/Decodable.swift‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,67 @@
55
// Created by Alsey Coleman Miller on 8/17/23.
66
//
77

8+
#if canImport(FoundationEssentials)
9+
import FoundationEssentials
10+
#elseif canImport(Foundation)
811
import Foundation
12+
#endif
913

1014
// MARK: - ModelData Decoding
1115

1216
public extension ModelData {
13-
17+
1418
func decode<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: AttributeDecodable, K: CodingKey {
15-
19+
1620
let property = PropertyKey(key)
1721
guard let attribute = self.attributes[property] else {
18-
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
22+
throw coreModelKeyNotFoundError(key)
1923
}
2024
// TODO: Optional values
2125
/*
2226
guard attribute != .null else {
2327
return
2428
}*/
2529
guard let decodable = type.init(attributeValue: attribute) else {
26-
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(attribute)"))
30+
throw coreModelTypeMismatchError(type, forKey: key, from: attribute)
2731
}
2832
return decodable
2933
}
30-
34+
3135
func decodeRelationship<T, K>(_ type: T.Type, forKey key: K) throws -> T where T: ObjectIDConvertible, K: CodingKey {
32-
36+
3337
let property = PropertyKey(key)
3438
guard let relationship = self.relationships[property] else {
35-
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
39+
throw coreModelKeyNotFoundError(key)
3640
}
3741
switch relationship {
3842
case .null:
39-
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
43+
throw coreModelKeyNotFoundError(key)
4044
case .toMany:
41-
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(relationship)"))
45+
throw coreModelTypeMismatchError(type, forKey: key, from: relationship)
4246
case let .toOne(objectID):
4347
guard let id = type.init(objectID: objectID) else {
44-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \(objectID)"))
48+
throw coreModelInvalidIdentifierError(objectID)
4549
}
4650
return id
4751
}
4852
}
49-
53+
5054
func decodeRelationship<T, K>(_ type: [T].Type, forKey key: K) throws -> [T] where T: ObjectIDConvertible, K: CodingKey {
51-
55+
5256
let property = PropertyKey(key)
5357
guard let relationship = self.relationships[property] else {
54-
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
58+
throw coreModelKeyNotFoundError(key)
5559
}
5660
switch relationship {
5761
case .null:
5862
return []
5963
case .toOne:
60-
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(relationship)"))
64+
throw coreModelTypeMismatchError(type, forKey: key, from: relationship)
6165
case let .toMany(objectIDs):
6266
return try objectIDs.map {
6367
guard let id = T.init(objectID: $0) else {
64-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \($0)"))
68+
throw coreModelInvalidIdentifierError($0)
6569
}
6670
return id
6771
}

‎Sources/CoreModel/Decoder.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 8/18/23.
66
//
77

8+
#if !hasFeature(Embedded)
89
import Foundation
910

1011
// MARK: - Default Codable Implementation
@@ -687,7 +688,6 @@ internal struct IndexCodingKey: CodingKey, RawRepresentable, Equatable, Hashable
687688
init?(intValue: Int) {
688689
self.init(rawValue: intValue)
689690
}
690-
691-
692-
691+
693692
}
693+
#endif
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// CodingKey.swift
3+
// CoreModel
4+
//
5+
// Drop-in replacement for `Swift.CodingKey` under Embedded Swift, where the
6+
// stdlib's Codable machinery (including `CodingKey`) is unavailable.
7+
//
8+
9+
#if hasFeature(Embedded)
10+
11+
/// A type that can be used as a key for encoding and decoding `CoreModel` entities.
12+
///
13+
/// - Note: Under Embedded Swift, `enum CodingKeys: CodingKey { ... }` without an
14+
/// explicit `String` (or `Int`) raw type cannot compile — stdlib `CodingKey`
15+
/// synthesis is compiler magic tied to `Swift.CodingKey`. Declare
16+
/// `enum CodingKeys: String, CodingKey` instead.
17+
public protocol CodingKey: Sendable, CustomStringConvertible, CustomDebugStringConvertible {
18+
19+
var stringValue: String { get }
20+
21+
init?(stringValue: String)
22+
23+
var intValue: Int? { get }
24+
25+
init?(intValue: Int)
26+
}
27+
28+
extension CodingKey {
29+
30+
public var description: String { stringValue }
31+
32+
public var debugDescription: String { stringValue }
33+
34+
public var intValue: Int? { nil }
35+
36+
public init?(intValue: Int) { nil }
37+
}
38+
39+
extension CodingKey where Self: RawRepresentable, Self.RawValue == String {
40+
41+
public var stringValue: String { rawValue }
42+
43+
public init?(stringValue: String) {
44+
self.init(rawValue: stringValue)
45+
}
46+
}
47+
48+
extension CodingKey where Self: RawRepresentable, Self.RawValue == Int {
49+
50+
public var stringValue: String { rawValue.description }
51+
52+
public var intValue: Int? { rawValue }
53+
54+
public init?(stringValue: String) { nil }
55+
56+
public init?(intValue: Int) {
57+
self.init(rawValue: intValue)
58+
}
59+
}
60+
61+
#endif

‎Sources/CoreModel/Encodable.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
// Created by Alsey Coleman Miller on 8/17/23.
66
//
77

8+
#if canImport(FoundationEssentials)
9+
import FoundationEssentials
10+
#elseif canImport(Foundation)
811
import Foundation
12+
#endif
913

1014
// MARK: - ModelData Encoding
1115

‎Sources/CoreModel/Encoder.swift‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Alsey Coleman Miller on 8/18/23.
66
//
77

8+
#if !hasFeature(Embedded)
89
import Foundation
910

1011
extension Entity where Self: Encodable {
@@ -523,3 +524,4 @@ internal final class ModelUnkeyedEncodingContainer: UnkeyedEncodingContainer {
523524
encoder.data.relationships[key] = value
524525
}
525526
}
527+
#endif

0 commit comments

Comments
 (0)