diff --git a/.github/workflows/swift-wasm.yml b/.github/workflows/swift-wasm.yml index 887c512..9305c14 100644 --- a/.github/workflows/swift-wasm.yml +++ b/.github/workflows/swift-wasm.yml @@ -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 diff --git a/README.md b/README.md index 0972a71..225c9e1 100644 --- a/README.md +++ b/README.md @@ -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`, `insert`, `delete`, `count`) 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. diff --git a/Sources/CoreModel/Attribute.swift b/Sources/CoreModel/Attribute.swift index 93f9660..1f18622 100644 --- a/Sources/CoreModel/Attribute.swift +++ b/Sources/CoreModel/Attribute.swift @@ -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 @@ -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 diff --git a/Sources/CoreModel/AttributeType.swift b/Sources/CoreModel/AttributeType.swift index 6ee7f20..a4e45d9 100644 --- a/Sources/CoreModel/AttributeType.swift +++ b/Sources/CoreModel/AttributeType.swift @@ -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 @@ -46,3 +44,9 @@ public enum AttributeType: String, Codable, CaseIterable, Sendable { /// Decimal case decimal } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension AttributeType: Codable {} +#endif diff --git a/Sources/CoreModel/Codable.swift b/Sources/CoreModel/Codable.swift index 1ab3771..7821b91 100644 --- a/Sources/CoreModel/Codable.swift +++ b/Sources/CoreModel/Codable.swift @@ -5,6 +5,4 @@ // Created by Alsey Coleman Miller on 8/18/23. // -import Foundation - public typealias AttributeCodable = AttributeEncodable & AttributeDecodable diff --git a/Sources/CoreModel/Decodable.swift b/Sources/CoreModel/Decodable.swift index 202abeb..b178790 100644 --- a/Sources/CoreModel/Decodable.swift +++ b/Sources/CoreModel/Decodable.swift @@ -5,17 +5,21 @@ // 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(_ 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 /* @@ -23,45 +27,45 @@ public extension ModelData { 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(_ 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(_ 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 } diff --git a/Sources/CoreModel/Decoder.swift b/Sources/CoreModel/Decoder.swift index c007d23..907a850 100644 --- a/Sources/CoreModel/Decoder.swift +++ b/Sources/CoreModel/Decoder.swift @@ -5,6 +5,7 @@ // Created by Alsey Coleman Miller on 8/18/23. // +#if !hasFeature(Embedded) import Foundation // MARK: - Default Codable Implementation @@ -687,7 +688,6 @@ internal struct IndexCodingKey: CodingKey, RawRepresentable, Equatable, Hashable init?(intValue: Int) { self.init(rawValue: intValue) } - - - + } +#endif diff --git a/Sources/CoreModel/Embedded/EmbeddedCodingKey.swift b/Sources/CoreModel/Embedded/EmbeddedCodingKey.swift new file mode 100644 index 0000000..57972e4 --- /dev/null +++ b/Sources/CoreModel/Embedded/EmbeddedCodingKey.swift @@ -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 diff --git a/Sources/CoreModel/Encodable.swift b/Sources/CoreModel/Encodable.swift index e4b11fb..b307acf 100644 --- a/Sources/CoreModel/Encodable.swift +++ b/Sources/CoreModel/Encodable.swift @@ -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 diff --git a/Sources/CoreModel/Encoder.swift b/Sources/CoreModel/Encoder.swift index 920a7d6..7806523 100644 --- a/Sources/CoreModel/Encoder.swift +++ b/Sources/CoreModel/Encoder.swift @@ -5,6 +5,7 @@ // Created by Alsey Coleman Miller on 8/18/23. // +#if !hasFeature(Embedded) import Foundation extension Entity where Self: Encodable { @@ -523,3 +524,4 @@ internal final class ModelUnkeyedEncodingContainer: UnkeyedEncodingContainer { encoder.data.relationships[key] = value } } +#endif diff --git a/Sources/CoreModel/Entity.swift b/Sources/CoreModel/Entity.swift index 3f93eee..9637b75 100644 --- a/Sources/CoreModel/Entity.swift +++ b/Sources/CoreModel/Entity.swift @@ -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 { diff --git a/Sources/CoreModel/EntityDescription.swift b/Sources/CoreModel/EntityDescription.swift index 26115b7..86c82b2 100644 --- a/Sources/CoreModel/EntityDescription.swift +++ b/Sources/CoreModel/EntityDescription.swift @@ -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 diff --git a/Sources/CoreModel/EntityName.swift b/Sources/CoreModel/EntityName.swift index b0babac..98e83b2 100644 --- a/Sources/CoreModel/EntityName.swift +++ b/Sources/CoreModel/EntityName.swift @@ -5,10 +5,8 @@ // Created by Alsey Coleman Miller on 8/16/23. // -import Foundation +public struct EntityName: RawRepresentable, Equatable, Hashable, Sendable { -public struct EntityName: RawRepresentable, Codable, Equatable, Hashable, Sendable { - public let rawValue: String public init(rawValue: String) { @@ -38,3 +36,9 @@ extension EntityName: CustomStringConvertible, CustomDebugStringConvertible { rawValue } } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension EntityName: Codable {} +#endif diff --git a/Sources/CoreModel/Error.swift b/Sources/CoreModel/Error.swift index 6e01b90..47e1a1e 100644 --- a/Sources/CoreModel/Error.swift +++ b/Sources/CoreModel/Error.swift @@ -5,11 +5,51 @@ // Created by Alsey Coleman Miller on 8/16/23. // -import Foundation - /// CoreModel Error public enum CoreModelError: Error { - + /// Invalid or unknown entity case invalidEntity(EntityName) + + #if hasFeature(Embedded) + /// Decoding failures under Embedded Swift (`Swift.DecodingError` is unavailable). + case keyNotFound(PropertyKey) + case typeMismatch(PropertyKey) + case invalidIdentifier(ObjectID) + #endif +} + +// MARK: - Decoding Error Factories + +#if hasFeature(Embedded) + +// - Note: Embedded Swift disallows `any Error` as a value/return type (existential +// restriction), so these return the concrete `CoreModelError` instead of `any Error`. + +internal func coreModelKeyNotFoundError(_ key: K) -> CoreModelError { + CoreModelError.keyNotFound(PropertyKey(key)) +} + +internal func coreModelTypeMismatchError(_ type: T.Type, forKey key: K, from value: Any) -> CoreModelError { + CoreModelError.typeMismatch(PropertyKey(key)) } + +internal func coreModelInvalidIdentifierError(_ objectID: ObjectID) -> CoreModelError { + CoreModelError.invalidIdentifier(objectID) +} + +#else + +internal func coreModelKeyNotFoundError(_ key: K) -> any Error { + DecodingError.keyNotFound(key, DecodingError.Context(codingPath: [], debugDescription: "Key \(key.stringValue) not found")) +} + +internal func coreModelTypeMismatchError(_ type: T.Type, forKey key: K, from value: Any) -> any Error { + DecodingError.typeMismatch(type, DecodingError.Context(codingPath: [], debugDescription: "Cannot decode \(String(describing: type)) from \(value)")) +} + +internal func coreModelInvalidIdentifierError(_ objectID: ObjectID) -> any Error { + DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Cannot decode identifier from \(objectID)")) +} + +#endif diff --git a/Sources/CoreModel/Extensions/CodingKey.swift b/Sources/CoreModel/Extensions/CodingKey.swift index 194789d..9b53023 100644 --- a/Sources/CoreModel/Extensions/CodingKey.swift +++ b/Sources/CoreModel/Extensions/CodingKey.swift @@ -5,8 +5,9 @@ // Created by Alsey Coleman Miller on 8/18/23. // +#if !hasFeature(Embedded) internal extension Sequence where Element == CodingKey { - + /// KVC path string for current coding path. var path: String { return reduce("", { $0 + "\($0.isEmpty ? "" : ".")" + $1.stringValue }) @@ -14,9 +15,9 @@ internal extension Sequence where Element == CodingKey { } internal extension CodingKey { - + static var sanitizedName: String { - + let rawName = String(reflecting: self) var elements = rawName.split(separator: ".") guard elements.count > 2 @@ -26,3 +27,4 @@ internal extension CodingKey { return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) } } +#endif diff --git a/Sources/CoreModel/Extensions/CodingUserInfoKey.swift b/Sources/CoreModel/Extensions/CodingUserInfoKey.swift index 6384df8..ed412bc 100644 --- a/Sources/CoreModel/Extensions/CodingUserInfoKey.swift +++ b/Sources/CoreModel/Extensions/CodingUserInfoKey.swift @@ -5,20 +5,20 @@ // Created by Alsey Coleman Miller on 8/19/23. // -import Foundation - +#if !hasFeature(Embedded) public extension CodingUserInfoKey { - + init(_ key: ModelCodingUserInfoKey) { self.init(rawValue: key.rawValue)! } - + static var identifierCodingKey: CodingUserInfoKey { .init(.identifierCodingKey) } } public enum ModelCodingUserInfoKey: String { - + case identifierCodingKey = "org.pureswift.CoreModel.CodingUserInfoKey.identifierCodingKey" } +#endif diff --git a/Sources/CoreModel/Extensions/Collection.swift b/Sources/CoreModel/Extensions/Collection.swift index c9c694c..1e9ce4c 100644 --- a/Sources/CoreModel/Extensions/Collection.swift +++ b/Sources/CoreModel/Extensions/Collection.swift @@ -5,8 +5,6 @@ // Created by Alsey Coleman Miller on 4/13/20. // -import Foundation - internal extension Array where Element: Equatable { func begins(with other: Self) -> Bool { diff --git a/Sources/CoreModel/Extensions/String.swift b/Sources/CoreModel/Extensions/String.swift index 520f44e..21a8a75 100644 --- a/Sources/CoreModel/Extensions/String.swift +++ b/Sources/CoreModel/Extensions/String.swift @@ -5,6 +5,7 @@ // Created by Alsey Coleman Miller on 4/12/20. // +#if canImport(Foundation) import Foundation internal extension String { @@ -72,3 +73,4 @@ internal extension String.CompareOptions { } } } +#endif diff --git a/Sources/CoreModel/FetchRequest.swift b/Sources/CoreModel/FetchRequest.swift index 0b01637..16f789f 100644 --- a/Sources/CoreModel/FetchRequest.swift +++ b/Sources/CoreModel/FetchRequest.swift @@ -6,11 +6,9 @@ // Copyright © 2015 PureSwift. All rights reserved. // -import Foundation - /// CoreModel Fetch Request -public struct FetchRequest: Codable, Equatable, Hashable, Sendable { - +public struct FetchRequest: Equatable, Hashable, Sendable { + public var entity: EntityName public var sortDescriptors: [SortDescriptor] @@ -34,3 +32,9 @@ public struct FetchRequest: Codable, Equatable, Hashable, Sendable { self.fetchOffset = fetchOffset } } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension FetchRequest: Codable {} +#endif diff --git a/Sources/CoreModel/FoundationShims/Data.swift b/Sources/CoreModel/FoundationShims/Data.swift new file mode 100644 index 0000000..59d039f --- /dev/null +++ b/Sources/CoreModel/FoundationShims/Data.swift @@ -0,0 +1,110 @@ +// +// Data.swift +// CoreModel +// +// Minimal Foundation-free `Data` for platforms without Foundation +// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only. +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) + +public struct Data: Sendable { + + internal var bytes: [UInt8] + + public init() { + self.bytes = [] + } + + public init(_ elements: S) where S.Element == UInt8 { + self.bytes = Array(elements) + } + + public init(repeating byte: UInt8, count: Int) { + self.bytes = Array(repeating: byte, count: count) + } +} + +// MARK: - Collection + +extension Data: RandomAccessCollection, MutableCollection { + + public typealias Element = UInt8 + public typealias Index = Int + + public var startIndex: Int { bytes.startIndex } + public var endIndex: Int { bytes.endIndex } + + public subscript(position: Int) -> UInt8 { + get { bytes[position] } + set { bytes[position] = newValue } + } + + public func index(after i: Int) -> Int { bytes.index(after: i) } + public func index(before i: Int) -> Int { bytes.index(before: i) } +} + +extension Data { + + public mutating func append(_ byte: UInt8) { + bytes.append(byte) + } + + public mutating func append(contentsOf newElements: S) where S.Element == UInt8 { + bytes.append(contentsOf: newElements) + } +} + +// MARK: - Equatable, Hashable + +extension Data: Equatable, Hashable { + + public static func == (lhs: Data, rhs: Data) -> Bool { + lhs.bytes == rhs.bytes + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(bytes) + } +} + +// MARK: - CustomStringConvertible + +extension Data: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + "\(count) bytes" + } + + public var debugDescription: String { + description + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension Data: Codable { + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var bytes: [UInt8] = [] + if let count = container.count { + bytes.reserveCapacity(count) + } + while !container.isAtEnd { + bytes.append(try container.decode(UInt8.self)) + } + self.init(bytes) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + for byte in bytes { + try container.encode(byte) + } + } +} +#endif + +#endif diff --git a/Sources/CoreModel/FoundationShims/Date.swift b/Sources/CoreModel/FoundationShims/Date.swift new file mode 100644 index 0000000..199d63d --- /dev/null +++ b/Sources/CoreModel/FoundationShims/Date.swift @@ -0,0 +1,118 @@ +// +// Date.swift +// CoreModel +// +// Minimal Foundation-free `Date` for platforms without Foundation +// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only. +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) + +public typealias TimeInterval = Double + +public struct Date: Sendable { + + /// Number of seconds relative to the reference date of Jan 1, 2001, 00:00:00 UTC. + public var timeIntervalSinceReferenceDate: TimeInterval + + public init(timeIntervalSinceReferenceDate: TimeInterval) { + self.timeIntervalSinceReferenceDate = timeIntervalSinceReferenceDate + } +} + +extension Date { + + /// Seconds between the Unix epoch (Jan 1, 1970) and the reference date (Jan 1, 2001). + private static let referenceDateToUnixEpoch: TimeInterval = 978307200 + + public init(timeIntervalSince1970: TimeInterval) { + self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Self.referenceDateToUnixEpoch) + } + + public var timeIntervalSince1970: TimeInterval { + timeIntervalSinceReferenceDate + Self.referenceDateToUnixEpoch + } + + public func timeIntervalSince(_ other: Date) -> TimeInterval { + timeIntervalSinceReferenceDate - other.timeIntervalSinceReferenceDate + } + + public func addingTimeInterval(_ interval: TimeInterval) -> Date { + Date(timeIntervalSinceReferenceDate: timeIntervalSinceReferenceDate + interval) + } + + public mutating func addTimeInterval(_ interval: TimeInterval) { + timeIntervalSinceReferenceDate += interval + } + + public static func + (lhs: Date, rhs: TimeInterval) -> Date { + lhs.addingTimeInterval(rhs) + } + + public static func - (lhs: Date, rhs: TimeInterval) -> Date { + lhs.addingTimeInterval(-rhs) + } + + public static func - (lhs: Date, rhs: Date) -> TimeInterval { + lhs.timeIntervalSince(rhs) + } + + public static func += (lhs: inout Date, rhs: TimeInterval) { + lhs.addTimeInterval(rhs) + } + + public static func -= (lhs: inout Date, rhs: TimeInterval) { + lhs.addTimeInterval(-rhs) + } +} + +// MARK: - Equatable, Comparable, Hashable + +extension Date: Equatable, Comparable, Hashable { + + public static func == (lhs: Date, rhs: Date) -> Bool { + lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate + } + + public static func < (lhs: Date, rhs: Date) -> Bool { + lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(timeIntervalSinceReferenceDate) + } +} + +// MARK: - CustomStringConvertible + +extension Date: CustomStringConvertible, CustomDebugStringConvertible { + + /// - Note: Not a Foundation-compatible ISO 8601 format — this is a + /// storage-layer replacement. Only used for debug/predicate description. + public var description: String { + timeIntervalSinceReferenceDate.description + } + + public var debugDescription: String { + description + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension Date: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self.init(timeIntervalSinceReferenceDate: try container.decode(TimeInterval.self)) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(timeIntervalSinceReferenceDate) + } +} +#endif + +#endif diff --git a/Sources/CoreModel/FoundationShims/Decimal.swift b/Sources/CoreModel/FoundationShims/Decimal.swift new file mode 100644 index 0000000..4b872cb --- /dev/null +++ b/Sources/CoreModel/FoundationShims/Decimal.swift @@ -0,0 +1,138 @@ +// +// Decimal.swift +// CoreModel +// +// Minimal Foundation-free `Decimal` for platforms without Foundation +// (e.g. Embedded Swift). Storage-layer only: normalized decimal-string +// representation, no arithmetic. Not a replacement for `Foundation.Decimal` +// in numeric contexts. +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) + +public struct Decimal: Sendable { + + /// Normalized decimal string: optional `-` sign, digits, optional `.` fraction + /// with no trailing zeros; `-0` and `-0.0` normalize to `0`. + private let normalized: String + + public init?(string: String) { + guard let normalized = Decimal.normalize(string) else { + return nil + } + self.normalized = normalized + } + + private init(normalized: String) { + self.normalized = normalized + } + + private static func normalize(_ string: String) -> String? { + let chars = Array(string.utf8) + guard chars.isEmpty == false else { return nil } + + var index = 0 + var negative = false + if chars[index] == UInt8(ascii: "-") { + negative = true + index += 1 + } + guard index < chars.count else { return nil } + + var integerDigits: [UInt8] = [] + while index < chars.count, chars[index] >= UInt8(ascii: "0"), chars[index] <= UInt8(ascii: "9") { + integerDigits.append(chars[index]) + index += 1 + } + guard integerDigits.isEmpty == false else { return nil } + + var fractionDigits: [UInt8] = [] + if index < chars.count, chars[index] == UInt8(ascii: ".") { + index += 1 + while index < chars.count, chars[index] >= UInt8(ascii: "0"), chars[index] <= UInt8(ascii: "9") { + fractionDigits.append(chars[index]) + index += 1 + } + guard fractionDigits.isEmpty == false else { return nil } + } + guard index == chars.count else { return nil } + + // Strip leading zeros from the integer part (keep at least one digit). + var integerStart = 0 + while integerStart < integerDigits.count - 1, integerDigits[integerStart] == UInt8(ascii: "0") { + integerStart += 1 + } + integerDigits = Array(integerDigits[integerStart...]) + + // Strip trailing zeros from the fraction part. + var fractionEnd = fractionDigits.count + while fractionEnd > 0, fractionDigits[fractionEnd - 1] == UInt8(ascii: "0") { + fractionEnd -= 1 + } + fractionDigits = Array(fractionDigits[.. Bool { + lhs.normalized == rhs.normalized + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(normalized) + } +} + +// MARK: - CustomStringConvertible + +extension Decimal: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + normalized + } + + public var debugDescription: String { + description + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension Decimal: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let value = Decimal(string: string) else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Attempted to decode Decimal from invalid string.")) + } + self = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } +} +#endif + +#endif diff --git a/Sources/CoreModel/FoundationShims/URL.swift b/Sources/CoreModel/FoundationShims/URL.swift new file mode 100644 index 0000000..9b48796 --- /dev/null +++ b/Sources/CoreModel/FoundationShims/URL.swift @@ -0,0 +1,65 @@ +// +// URL.swift +// CoreModel +// +// Minimal Foundation-free `URL` for platforms without Foundation +// (e.g. Embedded Swift). Not API-complete — storage-layer round-tripping only, +// no real parsing/resolution. +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) + +public struct URL: Sendable { + + public let absoluteString: String + + public init?(string: String) { + guard string.isEmpty == false else { + return nil + } + self.absoluteString = string + } +} + +// MARK: - Equatable, Hashable + +extension URL: Equatable, Hashable {} + +// MARK: - CustomStringConvertible + +extension URL: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + absoluteString + } + + public var debugDescription: String { + description + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension URL: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let url = URL(string: string) else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Attempted to decode URL from invalid string.")) + } + self = url + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(absoluteString) + } +} +#endif + +#endif diff --git a/Sources/CoreModel/FoundationShims/UUID.swift b/Sources/CoreModel/FoundationShims/UUID.swift new file mode 100644 index 0000000..2081cd9 --- /dev/null +++ b/Sources/CoreModel/FoundationShims/UUID.swift @@ -0,0 +1,266 @@ +// +// UUID.swift +// CoreModel +// +// Minimal Foundation-free `UUID` for platforms without Foundation +// (e.g. Embedded Swift). Modeled on PureSwift/Bluetooth's embedded UUID. +// Not API-complete — storage-layer round-tripping only. +// + +#if !canImport(FoundationEssentials) && !canImport(Foundation) + +public struct UUID: Sendable { + + public typealias ByteValue = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + + public let uuid: ByteValue + + public init(uuid: ByteValue) { + self.uuid = uuid + } +} + +// MARK: - Random Initialization + +extension UUID { + + /// Create a new UUID with RFC 4122 version 4 random bytes. + public init() { + var uuidBytes: ByteValue = ( + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255), + .random(in: 0...255) + ) + + // Set the version to 4 (random UUID) + uuidBytes.6 = (uuidBytes.6 & 0x0F) | 0x40 + + // Set the variant to RFC 4122 + uuidBytes.8 = (uuidBytes.8 & 0x3F) | 0x80 + + self.init(uuid: uuidBytes) + } +} + +// MARK: - String Parsing / Formatting + +extension UUID { + + /// Create a UUID from a string such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". + /// + /// Returns nil for invalid strings. + public init?(uuidString string: String) { + guard let value = UInt128.bigEndian(uuidString: string) else { + return nil + } + self.init(uuid: value.bytes) + } + + /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + public var uuidString: String { + UInt128(bytes: uuid).bigEndianUUIDString + } +} + +// MARK: - Equatable + +extension UUID: Equatable { + + public static func == (lhs: UUID, rhs: UUID) -> Bool { + Swift.withUnsafeBytes(of: lhs.uuid) { lhsPtr in + Swift.withUnsafeBytes(of: rhs.uuid) { rhsPtr in + let lhsTuple = lhsPtr.loadUnaligned(as: (UInt64, UInt64).self) + let rhsTuple = rhsPtr.loadUnaligned(as: (UInt64, UInt64).self) + return (lhsTuple.0 ^ rhsTuple.0) | (lhsTuple.1 ^ rhsTuple.1) == 0 + } + } + } +} + +// MARK: - Hashable + +extension UUID: Hashable { + + public func hash(into hasher: inout Hasher) { + Swift.withUnsafeBytes(of: uuid) { buffer in + hasher.combine(bytes: buffer) + } + } +} + +// MARK: - CustomStringConvertible + +extension UUID: CustomStringConvertible, CustomDebugStringConvertible { + + public var description: String { + uuidString + } + + public var debugDescription: String { + description + } +} + +// MARK: - Comparable + +extension UUID: Comparable { + + public static func < (lhs: UUID, rhs: UUID) -> Bool { + var leftUUID = lhs.uuid + var rightUUID = rhs.uuid + var result: Int = 0 + var diff: Int = 0 + Swift.withUnsafeBytes(of: &leftUUID) { leftPtr in + Swift.withUnsafeBytes(of: &rightUUID) { rightPtr in + for offset in (0...size).reversed() { + diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) + result = (result & (((diff - 1) & ~diff) >> 8)) | diff + } + } + } + return result < 0 + } +} + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension UUID: Codable { + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let uuidString = try container.decode(String.self) + guard let uuid = UUID(uuidString: uuidString) else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Attempted to decode UUID from invalid UUID string.")) + } + self = uuid + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.uuidString) + } +} +#endif + +// MARK: - UUID String Parsing + +fileprivate extension UInt128 { + + /// Parse a UUID string and return a value in big endian order. + static func bigEndian(uuidString string: String) -> UInt128? { + guard string.utf8.count == 36, + let separator = "-".utf8.first + else { + return nil + } + let characters = string.utf8 + guard characters[characters.index(characters.startIndex, offsetBy: 8)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 13)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 18)] == separator, + characters[characters.index(characters.startIndex, offsetBy: 23)] == separator, + let a = String(characters[characters.startIndex.. String { + let hexDigits: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] + let high = Int(self >> 4) + let low = Int(self & 0x0F) + return String([hexDigits[high], hexDigits[low]]) + } +} + +#endif diff --git a/Sources/CoreModel/Model.swift b/Sources/CoreModel/Model.swift index 5a9a514..0196a49 100644 --- a/Sources/CoreModel/Model.swift +++ b/Sources/CoreModel/Model.swift @@ -5,22 +5,20 @@ // Created by Alsey Coleman Miller on 11/4/18. // -import Foundation - /** The model contains one or more `Entity` objects representing the entities in the schema. */ public struct Model: Hashable, Sendable { - + public var entities: [EntityDescription] - + public init(entities: [EntityDescription] = []) { self.entities = entities } } public extension Model { - + subscript (id: EntityName) -> EntityDescription? { return entities.first { $0.id == id } } @@ -28,13 +26,15 @@ public extension Model { // MARK: - Codable +#if !hasFeature(Embedded) extension Model: Codable { - + public init(from decoder: Decoder) throws { self.entities = try .init(from: decoder) } - + public func encode(to encoder: Encoder) throws { try entities.encode(to: encoder) } } +#endif diff --git a/Sources/CoreModel/ObjectID.swift b/Sources/CoreModel/ObjectID.swift index 152ef5b..c208a72 100644 --- a/Sources/CoreModel/ObjectID.swift +++ b/Sources/CoreModel/ObjectID.swift @@ -5,11 +5,15 @@ // Created by Alsey Coleman Miller on 8/17/23. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) import Foundation +#endif /// CoreModel Object Identifier -public struct ObjectID: RawRepresentable, Codable, Equatable, Hashable, Sendable { - +public struct ObjectID: RawRepresentable, Equatable, Hashable, Sendable { + public let rawValue: String public init(rawValue: String) { @@ -68,8 +72,14 @@ extension String: ObjectIDConvertible { } extension ObjectIDConvertible where Self: RawRepresentable, Self.RawValue == String { - + public init?(objectID: ObjectID) { self.init(rawValue: objectID.rawValue) } } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension ObjectID: Codable {} +#endif diff --git a/Sources/CoreModel/Predicate/Comparison.swift b/Sources/CoreModel/Predicate/Comparison.swift index 494d97d..d82a347 100644 --- a/Sources/CoreModel/Predicate/Comparison.swift +++ b/Sources/CoreModel/Predicate/Comparison.swift @@ -9,7 +9,7 @@ public extension FetchRequest.Predicate { /// Comparison Predicate - struct Comparison: Equatable, Hashable, Codable, Sendable { + struct Comparison: Equatable, Hashable, Sendable { public var left: Expression @@ -40,29 +40,29 @@ public extension FetchRequest.Predicate { public extension FetchRequest.Predicate.Comparison { - enum Modifier: String, Codable, Sendable { - + enum Modifier: String, Sendable { + case all = "ALL" case any = "ANY" } - - enum Option: String, Codable, Sendable { - + + enum Option: String, Sendable { + /// A case-insensitive predicate. case caseInsensitive = "c" - + /// A diacritic-insensitive predicate. case diacriticInsensitive = "d" - + /// Indicates that the strings to be compared have been preprocessed. case normalized = "n" - + /// Indicates that strings to be compared using `<`, `<=`, `=`, `=>`, `>` /// should be handled in a locale-aware fashion. case localeSensitive = "l" } - - enum Operator: String, Codable, Sendable { + + enum Operator: String, Sendable { /// A less-than predicate. case lessThan = "<" @@ -112,6 +112,15 @@ public extension FetchRequest.Predicate.Comparison { } } +// MARK: - Codable + +#if !hasFeature(Embedded) +extension FetchRequest.Predicate.Comparison: Codable {} +extension FetchRequest.Predicate.Comparison.Modifier: Codable {} +extension FetchRequest.Predicate.Comparison.Option: Codable {} +extension FetchRequest.Predicate.Comparison.Operator: Codable {} +#endif + // MARK: - CustomStringConvertible extension FetchRequest.Predicate.Comparison: CustomStringConvertible { diff --git a/Sources/CoreModel/Predicate/Compound.swift b/Sources/CoreModel/Predicate/Compound.swift index 092d81c..cff33b0 100644 --- a/Sources/CoreModel/Predicate/Compound.swift +++ b/Sources/CoreModel/Predicate/Compound.swift @@ -45,19 +45,23 @@ public extension FetchRequest.Predicate.Compound { public extension FetchRequest.Predicate.Compound { /// Possible Compund Predicate types. - enum Logical​Type: String, Codable, Sendable { - + enum Logical​Type: String, Sendable { + /// A logical NOT predicate. case not = "NOT" - + /// A logical AND predicate. case and = "AND" - + /// A logical OR predicate. case or = "OR" } } +#if !hasFeature(Embedded) +extension FetchRequest.Predicate.Compound.Logical​Type: Codable {} +#endif + // MARK: - CustomStringConvertible extension FetchRequest.Predicate.Compound: CustomStringConvertible { @@ -105,19 +109,20 @@ extension FetchRequest.Predicate.Compound: CustomStringConvertible { // MARK: - Codable +#if !hasFeature(Embedded) extension FetchRequest.Predicate.Compound: Codable { - + internal enum CodingKeys: String, CodingKey { - + case type case predicates } - + public init(from decoder: Decoder) throws { - + let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(FetchRequest.Predicate.Compound.Logical​Type.self, forKey: .type) - + switch type { case .and: let predicates = try container.decode([FetchRequest.Predicate].self, forKey: .predicates) @@ -130,12 +135,12 @@ extension FetchRequest.Predicate.Compound: Codable { self = .not(predicate) } } - + public func encode(to encoder: Encoder) throws { - + var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) - + switch self { case let .and(predicates): try container.encode(predicates, forKey: .predicates) @@ -146,6 +151,7 @@ extension FetchRequest.Predicate.Compound: Codable { } } } +#endif // MARK: - Predicate Operators diff --git a/Sources/CoreModel/Predicate/Expression.swift b/Sources/CoreModel/Predicate/Expression.swift index ca2a269..88ec330 100644 --- a/Sources/CoreModel/Predicate/Expression.swift +++ b/Sources/CoreModel/Predicate/Expression.swift @@ -22,14 +22,18 @@ public extension FetchRequest.Predicate { } /// Type of predicate expression. - enum ExpressionType: String, Codable, Sendable { - + enum ExpressionType: String, Sendable { + case attribute case relationship case keyPath } } +#if !hasFeature(Embedded) +extension FetchRequest.Predicate.ExpressionType: Codable {} +#endif + public extension FetchRequest.Predicate.Expression { var type: FetchRequest.Predicate.ExpressionType { @@ -94,19 +98,20 @@ internal extension RelationshipValue { // MARK: - Codable +#if !hasFeature(Embedded) extension FetchRequest.Predicate.Expression: Codable { - + internal enum CodingKeys: String, CodingKey { - + case type case expression } - + public init(from decoder: Decoder) throws { - + let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(FetchRequest.Predicate.ExpressionType.self, forKey: .type) - + switch type { case .attribute: let expression = try container.decode(AttributeValue.self, forKey: .expression) @@ -119,12 +124,12 @@ extension FetchRequest.Predicate.Expression: Codable { self = .keyPath(PredicateKeyPath(rawValue: keyPath)) } } - + public func encode(to encoder: Encoder) throws { - + var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) - + switch self { case let .attribute(value): try container.encode(value, forKey: .expression) @@ -135,6 +140,7 @@ extension FetchRequest.Predicate.Expression: Codable { } } } +#endif // MARK: - Extensions diff --git a/Sources/CoreModel/Predicate/Predicate.swift b/Sources/CoreModel/Predicate/Predicate.swift index f1727bc..68299ed 100644 --- a/Sources/CoreModel/Predicate/Predicate.swift +++ b/Sources/CoreModel/Predicate/Predicate.swift @@ -19,16 +19,20 @@ public extension FetchRequest { } public extension FetchRequest { - + /// Predicate Type - enum PredicateType: String, Codable, Sendable { - + enum PredicateType: String, Sendable { + case comparison case compound case value } } +#if !hasFeature(Embedded) +extension FetchRequest.PredicateType: Codable {} +#endif + public extension FetchRequest.Predicate { /// Predicate Type @@ -57,19 +61,20 @@ extension FetchRequest.Predicate: CustomStringConvertible { // MARK: - Codable +#if !hasFeature(Embedded) extension FetchRequest.Predicate: Codable { - + internal enum CodingKeys: String, CodingKey { - + case type case predicate } - + public init(from decoder: Decoder) throws { - + let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(FetchRequest.PredicateType.self, forKey: .type) - + switch type { case .comparison: let predicate = try container.decode(Comparison.self, forKey: .predicate) @@ -82,12 +87,12 @@ extension FetchRequest.Predicate: Codable { self = .value(value) } } - + public func encode(to encoder: Encoder) throws { - + var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) - + switch self { case let .comparison(predicate): try container.encode(predicate, forKey: .predicate) @@ -98,3 +103,4 @@ extension FetchRequest.Predicate: Codable { } } } +#endif diff --git a/Sources/CoreModel/PropertyKey.swift b/Sources/CoreModel/PropertyKey.swift index 8277697..97dd2e0 100644 --- a/Sources/CoreModel/PropertyKey.swift +++ b/Sources/CoreModel/PropertyKey.swift @@ -5,10 +5,8 @@ // Created by Alsey Coleman Miller on 8/16/23. // -import Foundation +public struct PropertyKey: RawRepresentable, Equatable, Hashable, Sendable { -public struct PropertyKey: RawRepresentable, Codable, Equatable, Hashable, Sendable { - public let rawValue: String public init(rawValue: String) { @@ -42,9 +40,15 @@ extension PropertyKey: CustomStringConvertible, CustomDebugStringConvertible { // MARK: - Coding Key public extension PropertyKey { - + /// Initialize from ``Swift.CodingKey``. init(_ key: K) { self.init(rawValue: key.stringValue) } } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension PropertyKey: Codable {} +#endif diff --git a/Sources/CoreModel/PropertyValue.swift b/Sources/CoreModel/PropertyValue.swift index 78cb1a3..1244aa8 100644 --- a/Sources/CoreModel/PropertyValue.swift +++ b/Sources/CoreModel/PropertyValue.swift @@ -5,13 +5,17 @@ // Created by Alsey Coleman Miller on 11/4/18. // +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif canImport(Foundation) import Foundation +#endif // MARK: - Attribute /// CoreModel Attribute Value -public enum AttributeValue: Equatable, Hashable, Codable, Sendable { - +public enum AttributeValue: Equatable, Hashable, Sendable { + case null case string(String) case uuid(UUID) @@ -30,9 +34,16 @@ public enum AttributeValue: Equatable, Hashable, Codable, Sendable { // MARK: - Relationship /// CoreModel Relationship Value -public enum RelationshipValue: Equatable, Hashable, Codable, Sendable { - +public enum RelationshipValue: Equatable, Hashable, Sendable { + case null case toOne(ObjectID) case toMany([ObjectID]) } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension AttributeValue: Codable {} +extension RelationshipValue: Codable {} +#endif diff --git a/Sources/CoreModel/Relationship.swift b/Sources/CoreModel/Relationship.swift index a97fa3e..2405aad 100644 --- a/Sources/CoreModel/Relationship.swift +++ b/Sources/CoreModel/Relationship.swift @@ -5,24 +5,22 @@ // Created by Alsey Coleman Miller on 8/16/23. // -import Foundation - /// CoreModel `Relationship` -public struct Relationship: Property, Codable, Equatable, Hashable, Identifiable, Sendable { - +public struct Relationship: Property, Equatable, Hashable, Identifiable, Sendable { + public let id: PropertyKey - + public var type: RelationshipType - + public var destinationEntity: EntityName - + public var inverseRelationship: PropertyKey - + public init(id: PropertyKey, type: RelationshipType, destinationEntity: EntityName, inverseRelationship: PropertyKey) { - + self.id = id self.type = type self.destinationEntity = destinationEntity @@ -31,8 +29,15 @@ public struct Relationship: Property, Codable, Equatable, Hashable, Identifiable } /// CoreModel `Relationship` Type -public enum RelationshipType: String, Codable, CaseIterable, Sendable { - +public enum RelationshipType: String, CaseIterable, Sendable { + case toOne case toMany } + +// MARK: - Codable + +#if !hasFeature(Embedded) +extension Relationship: Codable {} +extension RelationshipType: Codable {} +#endif diff --git a/Sources/CoreModel/SortDescriptor.swift b/Sources/CoreModel/SortDescriptor.swift index 7518d1b..0c0ae05 100644 --- a/Sources/CoreModel/SortDescriptor.swift +++ b/Sources/CoreModel/SortDescriptor.swift @@ -6,16 +6,14 @@ // Copyright © 2015 PureSwift. All rights reserved. // -import Foundation - public extension FetchRequest { - - struct SortDescriptor: Codable, Equatable, Hashable, Sendable { - + + struct SortDescriptor: Equatable, Hashable, Sendable { + public var property: PropertyKey - + public var ascending: Bool - + public init(property: PropertyKey, ascending: Bool = true) { self.property = property self.ascending = ascending @@ -23,12 +21,18 @@ public extension FetchRequest { } } +#if !hasFeature(Embedded) +extension FetchRequest.SortDescriptor: Codable {} +#endif + // MARK: - Foundation #if canImport(Darwin) +import Foundation + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public extension FetchRequest.SortDescriptor { - + /// Creates a ``FetchRequest.SortDescriptor`` from a ``Foundation.SortDescriptor`` init(_ sortDescriptor: Foundation.SortDescriptor) { let sortDescriptor = NSSortDescriptor(sortDescriptor) diff --git a/Sources/CoreModel/Store.swift b/Sources/CoreModel/Store.swift index d5c372b..19bdf4a 100644 --- a/Sources/CoreModel/Store.swift +++ b/Sources/CoreModel/Store.swift @@ -34,19 +34,27 @@ public protocol ModelStorage: AnyObject, Sendable { func delete(_ entity: EntityName, for ids: [ObjectID]) async throws } +#if !hasFeature(Embedded) +// - Note: Unavailable under Embedded Swift (a compiler bug in SILGen crashes on +// an `async` default protocol-extension method calling another `async` +// protocol requirement through `Self`, e.g. https://github.com/swiftlang/swift/issues/78811 +// and related embedded-async SILGen issues). Embedded conformers must +// implement `count(_:)` and `insert(_:)` themselves. public extension ModelStorage { - + func count(_ fetchRequest: FetchRequest) async throws -> UInt { return try await UInt(fetch(fetchRequest).count) } - + func insert(_ values: [ModelData]) async throws { for model in values { try await insert(model) } } } +#endif +#if !hasFeature(Embedded) @MainActor public protocol ViewContext { @@ -62,12 +70,13 @@ public protocol ViewContext { /// Fetch and return result count. func count(_ fetchRequest: FetchRequest) throws -> UInt } +#endif // MARK: - ModelData /// CoreModel Object Instance -public struct ModelData: Equatable, Hashable, Identifiable, Codable, Sendable { - +public struct ModelData: Equatable, Hashable, Identifiable, Sendable { + public let entity: EntityName public let id: ObjectID @@ -89,10 +98,22 @@ public struct ModelData: Equatable, Hashable, Identifiable, Codable, Sendable { } } +// MARK: - Codable + +#if !hasFeature(Embedded) +extension ModelData: Codable {} +#endif + // MARK: - ModelStorage Codable Extensions +#if !hasFeature(Embedded) +// - Note: Unavailable under Embedded Swift (a compiler bug in SILGen crashes on +// `async` default protocol-extension methods that call another `async` +// protocol requirement through `Self`). Embedded consumers should call the +// `ModelStorage` protocol requirements directly with `ModelData`/`ObjectID` +// and use `Entity.init(from:)`/`encode()` themselves. public extension ModelStorage { - + /// Fetch managed object. func fetch(_ entity: T.Type, for id: T.ID) async throws -> T? where T: Entity { let objectID = ObjectID(rawValue: id.description) @@ -140,7 +161,7 @@ public extension ModelStorage { } /// Create or edit a managed object. - func insert(_ value: T) async throws where T: Entity, T: Encodable { + func insert(_ value: T) async throws where T: Entity { let model = try value.encode() // should never fail try await insert(model) } @@ -151,9 +172,11 @@ public extension ModelStorage { try await delete(T.entityName, for: objectID) } } +#endif +#if !hasFeature(Embedded) public extension ViewContext { - + /// Fetch managed object. func fetch(_ entity: T.Type, for id: T.ID) throws -> T? where T: Entity { let objectID = ObjectID(rawValue: id.description) @@ -200,3 +223,4 @@ public extension ViewContext { return try count(fetchRequest) } } +#endif