Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4014202
Remove unnecessary Foundation import from Codable.swift
colemancda Jul 14, 2026
6a56380
Remove unnecessary Foundation import from Collection extension
colemancda Jul 14, 2026
99186ab
Gate Model's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
8ebb184
Gate FetchRequest's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
28d584f
Gate Entity's Embedded-incompatible default implementations
colemancda Jul 14, 2026
77ceba4
Gate PropertyKey's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
5f77262
Add Embedded-safe decoding error cases and factories
colemancda Jul 14, 2026
66d468e
Gate EntityDescription's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
d924286
Gate EntityName's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
d56cfa4
Gate AttributeType's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
a240ed4
Gate Attribute's Codable conformance behind hasFeature(Embedded)
colemancda Jul 14, 2026
ae8049e
Gate Relationship and RelationshipType Codable conformances behind ha…
colemancda Jul 14, 2026
0330c62
Gate Predicate's Codable conformances behind hasFeature(Embedded)
colemancda Jul 14, 2026
2dd33fb
Gate Compound's Codable conformances behind hasFeature(Embedded)
colemancda Jul 14, 2026
a893d3a
Gate Comparison's Codable conformances behind hasFeature(Embedded)
colemancda Jul 14, 2026
0877cc9
Gate Expression's Codable conformances behind hasFeature(Embedded)
colemancda Jul 14, 2026
39f283d
Gate ModelStorage and ViewContext's Embedded-incompatible members
colemancda Jul 14, 2026
8333f7b
Adopt FoundationEssentials cascade and gate AttributeValue/Relationsh…
colemancda Jul 14, 2026
709f00f
Adopt FoundationEssentials import cascade in Encodable.swift
colemancda Jul 14, 2026
6f268f3
Adopt FoundationEssentials cascade and route decoding errors through …
colemancda Jul 14, 2026
4eb5c60
Adopt FoundationEssentials cascade and gate ObjectID's Codable confor…
colemancda Jul 14, 2026
2b8601b
Gate the Codable-based ModelDataEncoder bridge behind hasFeature(Embe…
colemancda Jul 14, 2026
2c70443
Gate the Codable-based ModelDataDecoder bridge behind hasFeature(Embe…
colemancda Jul 14, 2026
ecb5999
Gate CodingKey path/sanitizedName helpers behind hasFeature(Embedded)
colemancda Jul 14, 2026
1b97868
Gate CodingUserInfoKey extension behind hasFeature(Embedded)
colemancda Jul 14, 2026
c69575c
Gate Locale-based string comparison helpers behind canImport(Foundation)
colemancda Jul 14, 2026
5ec95a1
Gate SortDescriptor's Codable conformance and scope Foundation import…
colemancda Jul 14, 2026
a41e61b
Add shadow CodingKey protocol for Embedded Swift
colemancda Jul 14, 2026
020e2e8
Add Foundation-free UUID shim for Embedded Swift
colemancda Jul 14, 2026
c5e8fc6
Add Foundation-free Date shim for Embedded Swift
colemancda Jul 14, 2026
b6e1bb7
Add Foundation-free Data shim for Embedded Swift
colemancda Jul 14, 2026
b446f3e
Add Foundation-free URL shim for Embedded Swift
colemancda Jul 14, 2026
ef89385
Add Foundation-free Decimal shim for Embedded Swift
colemancda Jul 14, 2026
227e2fe
Add Embedded WebAssembly CI job
colemancda Jul 14, 2026
fb5a842
Document Embedded Swift support
colemancda Jul 14, 2026
a552855
Fix embedded WASM SDK install: use the shared artifact bundle
colemancda Jul 14, 2026
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
30 changes: 30 additions & 0 deletions .github/workflows/swift-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,33 @@ jobs:
swift build
-c ${{ matrix.config }}
--swift-sdk swift-6.3.2-RELEASE_wasm

embedded:
name: Embedded WebAssembly
runs-on: ubuntu-latest
container: swift:6.3.2
strategy:
fail-fast: false
matrix:
config: [debug, release]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Swift Version
run: swift --version
- name: Install dependencies
run: apt-get update -y && apt-get install -y curl
- name: Install Embedded WASM SDK
run: |
set -eux
# The embedded SDK identifier ships inside the same artifact bundle as the regular WASM SDK.
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"
curl -fsSL "$url" -o wasm.artifactbundle.tar.gz
swift sdk install wasm.artifactbundle.tar.gz
swift sdk list
- name: Build
run: >-
SWIFTPM_ENABLE_MACROS=0
swift build
-c ${{ matrix.config }}
--swift-sdk swift-6.3.2-RELEASE_wasm-embedded
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,20 @@ Swift Object Graph
- CoreData
- [SQLite](https://github.com/PureSwift/CoreModel-SQLite)
- [MongoDB](https://github.com/PureSwift/CoreModel-MongoDB)

## Embedded Swift

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.

```
SWIFTPM_ENABLE_MACROS=0 swift build --swift-sdk swift-6.3.2-RELEASE_wasm-embedded
```

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:

- `Entity.entityName`, `attributes`, and `relationships` have no default implementation — implement them explicitly.
- `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).
- `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.
- `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.
- `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`.
- `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.
16 changes: 10 additions & 6 deletions Sources/CoreModel/Attribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
// Created by Alsey Coleman Miller on 8/16/23.
//

import Foundation

/// CoreModel `Attribute`
public struct Attribute: Property, Codable, Equatable, Hashable, Identifiable, Sendable {
public struct Attribute: Property, Equatable, Hashable, Identifiable, Sendable {

public let id: PropertyKey

public var type: AttributeType

public init(
id: PropertyKey,
type: AttributeType
Expand All @@ -22,3 +20,9 @@ public struct Attribute: Property, Codable, Equatable, Hashable, Identifiable, S
self.type = type
}
}

// MARK: - Codable

#if !hasFeature(Embedded)
extension Attribute: Codable {}
#endif
10 changes: 7 additions & 3 deletions Sources/CoreModel/AttributeType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
// Created by Alsey Coleman Miller on 8/16/23.
//

import Foundation

/// CoreModel Attribute type
public enum AttributeType: String, Codable, CaseIterable, Sendable {
public enum AttributeType: String, CaseIterable, Sendable {

/// Boolean number type.
case bool
Expand Down Expand Up @@ -46,3 +44,9 @@ public enum AttributeType: String, Codable, CaseIterable, Sendable {
/// Decimal
case decimal
}

// MARK: - Codable

#if !hasFeature(Embedded)
extension AttributeType: Codable {}
#endif
2 changes: 0 additions & 2 deletions Sources/CoreModel/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@
// Created by Alsey Coleman Miller on 8/18/23.
//

import Foundation

public typealias AttributeCodable = AttributeEncodable & AttributeDecodable
34 changes: 19 additions & 15 deletions Sources/CoreModel/Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,67 @@
// Created by Alsey Coleman Miller on 8/17/23.
//

#if canImport(FoundationEssentials)
import FoundationEssentials
#elseif canImport(Foundation)
import Foundation
#endif

// MARK: - ModelData Decoding

public extension ModelData {

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

let property = PropertyKey(key)
guard let attribute = self.attributes[property] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
throw coreModelKeyNotFoundError(key)
}
// TODO: Optional values
/*
guard attribute != .null else {
return
}*/
guard let decodable = type.init(attributeValue: attribute) else {
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(attribute)"))
throw coreModelTypeMismatchError(type, forKey: key, from: attribute)
}
return decodable
}

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

let property = PropertyKey(key)
guard let relationship = self.relationships[property] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
throw coreModelKeyNotFoundError(key)
}
switch relationship {
case .null:
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
throw coreModelKeyNotFoundError(key)
case .toMany:
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(relationship)"))
throw coreModelTypeMismatchError(type, forKey: key, from: relationship)
case let .toOne(objectID):
guard let id = type.init(objectID: objectID) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \(objectID)"))
throw coreModelInvalidIdentifierError(objectID)
}
return id
}
}

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

let property = PropertyKey(key)
guard let relationship = self.relationships[property] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found"))
throw coreModelKeyNotFoundError(key)
}
switch relationship {
case .null:
return []
case .toOne:
throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(relationship)"))
throw coreModelTypeMismatchError(type, forKey: key, from: relationship)
case let .toMany(objectIDs):
return try objectIDs.map {
guard let id = T.init(objectID: $0) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \($0)"))
throw coreModelInvalidIdentifierError($0)
}
return id
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/CoreModel/Decoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 8/18/23.
//

#if !hasFeature(Embedded)
import Foundation

// MARK: - Default Codable Implementation
Expand Down Expand Up @@ -687,7 +688,6 @@ internal struct IndexCodingKey: CodingKey, RawRepresentable, Equatable, Hashable
init?(intValue: Int) {
self.init(rawValue: intValue)
}




}
#endif
61 changes: 61 additions & 0 deletions Sources/CoreModel/Embedded/EmbeddedCodingKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// CodingKey.swift
// CoreModel
//
// Drop-in replacement for `Swift.CodingKey` under Embedded Swift, where the
// stdlib's Codable machinery (including `CodingKey`) is unavailable.
//

#if hasFeature(Embedded)

/// A type that can be used as a key for encoding and decoding `CoreModel` entities.
///
/// - Note: Under Embedded Swift, `enum CodingKeys: CodingKey { ... }` without an
/// explicit `String` (or `Int`) raw type cannot compile — stdlib `CodingKey`
/// synthesis is compiler magic tied to `Swift.CodingKey`. Declare
/// `enum CodingKeys: String, CodingKey` instead.
public protocol CodingKey: Sendable, CustomStringConvertible, CustomDebugStringConvertible {

var stringValue: String { get }

init?(stringValue: String)

var intValue: Int? { get }

init?(intValue: Int)
}

extension CodingKey {

public var description: String { stringValue }

public var debugDescription: String { stringValue }

public var intValue: Int? { nil }

public init?(intValue: Int) { nil }
}

extension CodingKey where Self: RawRepresentable, Self.RawValue == String {

public var stringValue: String { rawValue }

public init?(stringValue: String) {
self.init(rawValue: stringValue)
}
}

extension CodingKey where Self: RawRepresentable, Self.RawValue == Int {

public var stringValue: String { rawValue.description }

public var intValue: Int? { rawValue }

public init?(stringValue: String) { nil }

public init?(intValue: Int) {
self.init(rawValue: intValue)
}
}

#endif
4 changes: 4 additions & 0 deletions Sources/CoreModel/Encodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// Created by Alsey Coleman Miller on 8/17/23.
//

#if canImport(FoundationEssentials)
import FoundationEssentials
#elseif canImport(Foundation)
import Foundation
#endif

// MARK: - ModelData Encoding

Expand Down
2 changes: 2 additions & 0 deletions Sources/CoreModel/Encoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Alsey Coleman Miller on 8/18/23.
//

#if !hasFeature(Embedded)
import Foundation

extension Entity where Self: Encodable {
Expand Down Expand Up @@ -523,3 +524,4 @@ internal final class ModelUnkeyedEncodingContainer: UnkeyedEncodingContainer {
encoder.data.relationships[key] = value
}
}
#endif
28 changes: 16 additions & 12 deletions Sources/CoreModel/Entity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,45 @@
// Copyright © 2015 PureSwift. All rights reserved.
//

import Foundation

/// CoreModel Entity for Codable types
public protocol Entity: Identifiable, Sendable where CodingKeys: Hashable, Self.ID: ObjectIDConvertible {

static var entityName: EntityName { get }

static var attributes: [CodingKeys: AttributeType] { get }

static var relationships: [CodingKeys: Relationship] { get }

associatedtype CodingKeys: CodingKey

init(from model: ModelData) throws

func encode() throws -> ModelData
}

public extension Entity {


#if !hasFeature(Embedded)
/// - Note: Unavailable under Embedded Swift (relies on runtime type metadata). Implement explicitly.
static var entityName: EntityName {
EntityName(rawValue: String(describing: Self.self))
}

#endif

static var attributes: [CodingKeys: AttributeType] { [:] }

static var relationships: [CodingKeys: Relationship] { [:] }
}

#if !hasFeature(Embedded)
public extension Model {


/// - Note: Unavailable under Embedded Swift (calls a generic initializer through an existential metatype). Construct `Model(entities:)` from concrete `EntityDescription` values instead.
init(entities: any Entity.Type...) {
self.init(entities: entities.map { .init(entity: $0) })
}
}
#endif

public extension EntityDescription {

Expand Down
18 changes: 11 additions & 7 deletions Sources/CoreModel/EntityDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,24 @@
// Created by Alsey Coleman Miller on 8/17/23.
//

import Foundation

/// Defines the model for an entity
public struct EntityDescription: Codable, Identifiable, Hashable, Sendable {
public struct EntityDescription: Identifiable, Hashable, Sendable {

public let id: EntityName

public var attributes: [Attribute]

public var relationships: [Relationship]

public init(id: EntityName, attributes: [Attribute], relationships: [Relationship]) {
self.id = id
self.attributes = attributes
self.relationships = relationships
}
}

// MARK: - Codable

#if !hasFeature(Embedded)
extension EntityDescription: Codable {}
#endif
Loading
Loading