This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 6117f5d0b feat(swift): support enum/time serialization for swift
(#3371)
6117f5d0b is described below
commit 6117f5d0b38ba38b298020d43f2282f9a7e347b3
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Feb 19 22:09:36 2026 +0800
feat(swift): support enum/time serialization for swift (#3371)
## Why?
- Align Swift package/module naming with the rest of the project
(`ForySwift` -> `Fory`) and simplify target layout.
- Add missing built-in xlang date/time support and macro-generated enum
serialization support.
- Increase Swift test coverage for these features and for weak-reference
tracking behavior.
## What does this PR do?
- Renames Swift package products/targets and source directories:
- `ForySwift` -> `Fory`
- `ForySwiftMacros` -> `ForyMacro`
- `ForySwiftTests` -> `ForyTests`
- Moves xlang peer target to `Tests/ForyXlangPeer`
- Adds `DateTimeSerializers.swift` with:
- `ForyDate` (`.date`)
- `ForyTimestamp` (`.timestamp`, with second/nanos normalization)
- `Date: Serializer` bridge using timestamp encoding
- Wires date/time support into deserialization paths (`TypeResolver` and
`FieldSkipper`).
- Updates `Context` helper dispatch to call module-global read/write
helpers instead of `ForySwift.*` symbols.
- Extends `@ForyObject` macro to support enums:
- no-payload enums -> ordinal enum serialization (`ForyTypeId.enumType`)
- associated-value enums -> tagged-union serialization
(`ForyTypeId.typedUnion`)
- Simplifies `ForyXlangPeer` by replacing handwritten enum/date/union
serializers with macro-based enums and shared `ForyDate`/`ForyTimestamp`
types.
- Adds/updates tests:
- `DateTimeTests.swift` for date/timestamp type IDs and round-trip
behavior
- `EnumTests.swift` for enum and tagged-union round trips
- `ForySwiftTests.swift` coverage for weak self-reference tracking
## Related issues
#1017
#3349
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
swift/Package.swift | 22 +-
.../{ForySwift => Fory}/AnySerializer.swift | 0
swift/Sources/{ForySwift => Fory}/ByteBuffer.swift | 0
.../CollectionSerializers.swift | 0
swift/Sources/{ForySwift => Fory}/Context.swift | 136 ++++++++++-
swift/Sources/Fory/DateTimeSerializers.swift | 117 ++++++++++
.../Sources/{ForySwift => Fory}/FieldSkipper.swift | 8 +
swift/Sources/{ForySwift => Fory}/Fory.swift | 0
swift/Sources/{ForySwift => Fory}/ForyError.swift | 0
swift/Sources/{ForySwift => Fory}/ForyFlags.swift | 0
swift/Sources/{ForySwift => Fory}/ForyTypeId.swift | 0
.../{ForySwift => Fory}/MacroDeclarations.swift | 4 +-
swift/Sources/{ForySwift => Fory}/MetaString.swift | 0
.../Sources/{ForySwift => Fory}/MurmurHash3.swift | 0
.../{ForySwift => Fory}/OptionalSerializer.swift | 0
.../{ForySwift => Fory}/PrimitiveSerializers.swift | 0
.../Sources/{ForySwift => Fory}/RefResolver.swift | 0
swift/Sources/{ForySwift => Fory}/SchemaHash.swift | 0
swift/Sources/{ForySwift => Fory}/Serializer.swift | 0
swift/Sources/{ForySwift => Fory}/TypeMeta.swift | 0
.../Sources/{ForySwift => Fory}/TypeResolver.swift | 4 +
.../ForyObjectMacro.swift | 252 +++++++++++++++++++++
swift/Tests/ForyTests/DateTimeTests.swift | 75 ++++++
swift/Tests/ForyTests/EnumTests.swift | 120 ++++++++++
.../ForySwiftTests.swift | 30 ++-
.../ForyXlangPeer}/main.swift | 141 ++----------
26 files changed, 767 insertions(+), 142 deletions(-)
diff --git a/swift/Package.swift b/swift/Package.swift
index f787a0b56..f8a929d2c 100644
--- a/swift/Package.swift
+++ b/swift/Package.swift
@@ -10,8 +10,8 @@ let package = Package(
],
products: [
.library(
- name: "ForySwift",
- targets: ["ForySwift"]
+ name: "Fory",
+ targets: ["Fory"]
),
.executable(
name: "ForySwiftXlangPeer",
@@ -23,25 +23,29 @@ let package = Package(
],
targets: [
.macro(
- name: "ForySwiftMacros",
+ name: "ForyMacro",
dependencies: [
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
- ]
+ ],
+ path: "Sources/ForyMacro"
),
.target(
- name: "ForySwift",
- dependencies: ["ForySwiftMacros"]
+ name: "Fory",
+ dependencies: ["ForyMacro"],
+ path: "Sources/Fory"
),
.executableTarget(
name: "ForySwiftXlangPeer",
- dependencies: ["ForySwift"]
+ dependencies: ["Fory"],
+ path: "Tests/ForyXlangPeer"
),
.testTarget(
- name: "ForySwiftTests",
- dependencies: ["ForySwift"]
+ name: "ForyTests",
+ dependencies: ["Fory"],
+ path: "Tests/ForyTests"
),
]
)
diff --git a/swift/Sources/ForySwift/AnySerializer.swift
b/swift/Sources/Fory/AnySerializer.swift
similarity index 100%
rename from swift/Sources/ForySwift/AnySerializer.swift
rename to swift/Sources/Fory/AnySerializer.swift
diff --git a/swift/Sources/ForySwift/ByteBuffer.swift
b/swift/Sources/Fory/ByteBuffer.swift
similarity index 100%
rename from swift/Sources/ForySwift/ByteBuffer.swift
rename to swift/Sources/Fory/ByteBuffer.swift
diff --git a/swift/Sources/ForySwift/CollectionSerializers.swift
b/swift/Sources/Fory/CollectionSerializers.swift
similarity index 100%
rename from swift/Sources/ForySwift/CollectionSerializers.swift
rename to swift/Sources/Fory/CollectionSerializers.swift
diff --git a/swift/Sources/ForySwift/Context.swift
b/swift/Sources/Fory/Context.swift
similarity index 83%
rename from swift/Sources/ForySwift/Context.swift
rename to swift/Sources/Fory/Context.swift
index c7aefa8ad..25285077b 100644
--- a/swift/Sources/ForySwift/Context.swift
+++ b/swift/Sources/Fory/Context.swift
@@ -372,6 +372,126 @@ public final class ReadContext {
}
}
+@inline(__always)
+private func writeAnyGlobal(
+ _ value: Any?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool,
+ hasGenerics: Bool
+) throws {
+ try writeAny(
+ value,
+ context: context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+@inline(__always)
+private func writeAnyListGlobal(
+ _ value: [Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool,
+ hasGenerics: Bool
+) throws {
+ try writeAnyList(
+ value,
+ context: context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+@inline(__always)
+private func writeStringAnyMapGlobal(
+ _ value: [String: Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool,
+ hasGenerics: Bool
+) throws {
+ try writeStringAnyMap(
+ value,
+ context: context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+@inline(__always)
+private func writeInt32AnyMapGlobal(
+ _ value: [Int32: Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool,
+ hasGenerics: Bool
+) throws {
+ try writeInt32AnyMap(
+ value,
+ context: context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+@inline(__always)
+private func readAnyGlobal(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool
+) throws -> Any? {
+ try readAny(
+ context: context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+}
+
+@inline(__always)
+private func readAnyListGlobal(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool
+) throws -> [Any]? {
+ try readAnyList(
+ context: context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+}
+
+@inline(__always)
+private func readStringAnyMapGlobal(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool
+) throws -> [String: Any]? {
+ try readStringAnyMap(
+ context: context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+}
+
+@inline(__always)
+private func readInt32AnyMapGlobal(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool
+) throws -> [Int32: Any]? {
+ try readInt32AnyMap(
+ context: context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+}
+
public extension WriteContext {
func writeAny(
_ value: Any?,
@@ -379,7 +499,7 @@ public extension WriteContext {
writeTypeInfo: Bool = true,
hasGenerics: Bool = false
) throws {
- try ForySwift.writeAny(
+ try writeAnyGlobal(
value,
context: self,
refMode: refMode,
@@ -394,7 +514,7 @@ public extension WriteContext {
writeTypeInfo: Bool = false,
hasGenerics: Bool = true
) throws {
- try ForySwift.writeAnyList(
+ try writeAnyListGlobal(
value,
context: self,
refMode: refMode,
@@ -409,7 +529,7 @@ public extension WriteContext {
writeTypeInfo: Bool = false,
hasGenerics: Bool = true
) throws {
- try ForySwift.writeStringAnyMap(
+ try writeStringAnyMapGlobal(
value,
context: self,
refMode: refMode,
@@ -424,7 +544,7 @@ public extension WriteContext {
writeTypeInfo: Bool = false,
hasGenerics: Bool = true
) throws {
- try ForySwift.writeInt32AnyMap(
+ try writeInt32AnyMapGlobal(
value,
context: self,
refMode: refMode,
@@ -439,7 +559,7 @@ public extension ReadContext {
refMode: RefMode,
readTypeInfo: Bool = true
) throws -> Any? {
- try ForySwift.readAny(
+ try readAnyGlobal(
context: self,
refMode: refMode,
readTypeInfo: readTypeInfo
@@ -450,7 +570,7 @@ public extension ReadContext {
refMode: RefMode,
readTypeInfo: Bool = false
) throws -> [Any]? {
- try ForySwift.readAnyList(
+ try readAnyListGlobal(
context: self,
refMode: refMode,
readTypeInfo: readTypeInfo
@@ -461,7 +581,7 @@ public extension ReadContext {
refMode: RefMode,
readTypeInfo: Bool = false
) throws -> [String: Any]? {
- try ForySwift.readStringAnyMap(
+ try readStringAnyMapGlobal(
context: self,
refMode: refMode,
readTypeInfo: readTypeInfo
@@ -472,7 +592,7 @@ public extension ReadContext {
refMode: RefMode,
readTypeInfo: Bool = false
) throws -> [Int32: Any]? {
- try ForySwift.readInt32AnyMap(
+ try readInt32AnyMapGlobal(
context: self,
refMode: refMode,
readTypeInfo: readTypeInfo
diff --git a/swift/Sources/Fory/DateTimeSerializers.swift
b/swift/Sources/Fory/DateTimeSerializers.swift
new file mode 100644
index 000000000..9d0b2b704
--- /dev/null
+++ b/swift/Sources/Fory/DateTimeSerializers.swift
@@ -0,0 +1,117 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+
+public struct ForyDate: Serializer, Equatable, Hashable {
+ public var daysSinceEpoch: Int32
+
+ public init(daysSinceEpoch: Int32 = 0) {
+ self.daysSinceEpoch = daysSinceEpoch
+ }
+
+ public static func foryDefault() -> ForyDate {
+ .init()
+ }
+
+ public static var staticTypeId: ForyTypeId {
+ .date
+ }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ context.writer.writeInt32(daysSinceEpoch)
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws -> ForyDate
{
+ .init(daysSinceEpoch: try context.reader.readInt32())
+ }
+}
+
+public struct ForyTimestamp: Serializer, Equatable, Hashable {
+ public var seconds: Int64
+ public var nanos: UInt32
+
+ public init(seconds: Int64 = 0, nanos: UInt32 = 0) {
+ let normalized = Self.normalize(seconds: seconds, nanos: Int64(nanos))
+ self.seconds = normalized.seconds
+ self.nanos = normalized.nanos
+ }
+
+ public static func foryDefault() -> ForyTimestamp {
+ .init()
+ }
+
+ public static var staticTypeId: ForyTypeId {
+ .timestamp
+ }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ context.writer.writeInt64(seconds)
+ context.writer.writeUInt32(nanos)
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws ->
ForyTimestamp {
+ .init(seconds: try context.reader.readInt64(), nanos: try
context.reader.readUInt32())
+ }
+
+ public init(date: Date) {
+ let time = date.timeIntervalSince1970
+ let seconds = Int64(floor(time))
+ let nanos = Int64((time - Double(seconds)) * 1_000_000_000.0)
+ let normalized = Self.normalize(seconds: seconds, nanos: nanos)
+ self.seconds = normalized.seconds
+ self.nanos = normalized.nanos
+ }
+
+ public func toDate() -> Date {
+ Date(timeIntervalSince1970: Double(seconds) + Double(nanos) /
1_000_000_000.0)
+ }
+
+ private static func normalize(seconds: Int64, nanos: Int64) -> (seconds:
Int64, nanos: UInt32) {
+ var normalizedSeconds = seconds + nanos / 1_000_000_000
+ var normalizedNanos = nanos % 1_000_000_000
+ if normalizedNanos < 0 {
+ normalizedNanos += 1_000_000_000
+ normalizedSeconds -= 1
+ }
+ return (normalizedSeconds, UInt32(normalizedNanos))
+ }
+}
+
+extension Date: Serializer {
+ public static func foryDefault() -> Date {
+ Date(timeIntervalSince1970: 0)
+ }
+
+ public static var staticTypeId: ForyTypeId {
+ .timestamp
+ }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = hasGenerics
+ let ts = ForyTimestamp(date: self)
+ context.writer.writeInt64(ts.seconds)
+ context.writer.writeUInt32(ts.nanos)
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws -> Date {
+ let ts = ForyTimestamp(seconds: try context.reader.readInt64(), nanos:
try context.reader.readUInt32())
+ return ts.toDate()
+ }
+}
diff --git a/swift/Sources/ForySwift/FieldSkipper.swift
b/swift/Sources/Fory/FieldSkipper.swift
similarity index 92%
rename from swift/Sources/ForySwift/FieldSkipper.swift
rename to swift/Sources/Fory/FieldSkipper.swift
index a9b0a4ce9..7d12b243e 100644
--- a/swift/Sources/ForySwift/FieldSkipper.swift
+++ b/swift/Sources/Fory/FieldSkipper.swift
@@ -84,6 +84,14 @@ public enum FieldSkipper {
return fieldType.nullable
? try String?.foryRead(context, refMode: refMode,
readTypeInfo: false)
: try String.foryRead(context, refMode: refMode, readTypeInfo:
false)
+ case ForyTypeId.timestamp.rawValue:
+ return fieldType.nullable
+ ? try Date?.foryRead(context, refMode: refMode, readTypeInfo:
false)
+ : try Date.foryRead(context, refMode: refMode, readTypeInfo:
false)
+ case ForyTypeId.date.rawValue:
+ return fieldType.nullable
+ ? try ForyDate?.foryRead(context, refMode: refMode,
readTypeInfo: false)
+ : try ForyDate.foryRead(context, refMode: refMode,
readTypeInfo: false)
case ForyTypeId.list.rawValue:
guard fieldType.generics.count == 1,
fieldType.generics[0].typeID == ForyTypeId.string.rawValue
else {
diff --git a/swift/Sources/ForySwift/Fory.swift b/swift/Sources/Fory/Fory.swift
similarity index 100%
rename from swift/Sources/ForySwift/Fory.swift
rename to swift/Sources/Fory/Fory.swift
diff --git a/swift/Sources/ForySwift/ForyError.swift
b/swift/Sources/Fory/ForyError.swift
similarity index 100%
rename from swift/Sources/ForySwift/ForyError.swift
rename to swift/Sources/Fory/ForyError.swift
diff --git a/swift/Sources/ForySwift/ForyFlags.swift
b/swift/Sources/Fory/ForyFlags.swift
similarity index 100%
rename from swift/Sources/ForySwift/ForyFlags.swift
rename to swift/Sources/Fory/ForyFlags.swift
diff --git a/swift/Sources/ForySwift/ForyTypeId.swift
b/swift/Sources/Fory/ForyTypeId.swift
similarity index 100%
rename from swift/Sources/ForySwift/ForyTypeId.swift
rename to swift/Sources/Fory/ForyTypeId.swift
diff --git a/swift/Sources/ForySwift/MacroDeclarations.swift
b/swift/Sources/Fory/MacroDeclarations.swift
similarity index 88%
rename from swift/Sources/ForySwift/MacroDeclarations.swift
rename to swift/Sources/Fory/MacroDeclarations.swift
index db59b374c..a08cc5c57 100644
--- a/swift/Sources/ForySwift/MacroDeclarations.swift
+++ b/swift/Sources/Fory/MacroDeclarations.swift
@@ -26,7 +26,7 @@
named(foryReadData)
)
@attached(extension, conformances: Serializer)
-public macro ForyObject() = #externalMacro(module: "ForySwiftMacros", type:
"ForyObjectMacro")
+public macro ForyObject() = #externalMacro(module: "ForyMacro", type:
"ForyObjectMacro")
public enum ForyFieldEncoding: String {
case varint
@@ -37,4 +37,4 @@ public enum ForyFieldEncoding: String {
@attached(peer)
public macro ForyField(
encoding: ForyFieldEncoding
-) = #externalMacro(module: "ForySwiftMacros", type: "ForyFieldMacro")
+) = #externalMacro(module: "ForyMacro", type: "ForyFieldMacro")
diff --git a/swift/Sources/ForySwift/MetaString.swift
b/swift/Sources/Fory/MetaString.swift
similarity index 100%
rename from swift/Sources/ForySwift/MetaString.swift
rename to swift/Sources/Fory/MetaString.swift
diff --git a/swift/Sources/ForySwift/MurmurHash3.swift
b/swift/Sources/Fory/MurmurHash3.swift
similarity index 100%
rename from swift/Sources/ForySwift/MurmurHash3.swift
rename to swift/Sources/Fory/MurmurHash3.swift
diff --git a/swift/Sources/ForySwift/OptionalSerializer.swift
b/swift/Sources/Fory/OptionalSerializer.swift
similarity index 100%
rename from swift/Sources/ForySwift/OptionalSerializer.swift
rename to swift/Sources/Fory/OptionalSerializer.swift
diff --git a/swift/Sources/ForySwift/PrimitiveSerializers.swift
b/swift/Sources/Fory/PrimitiveSerializers.swift
similarity index 100%
rename from swift/Sources/ForySwift/PrimitiveSerializers.swift
rename to swift/Sources/Fory/PrimitiveSerializers.swift
diff --git a/swift/Sources/ForySwift/RefResolver.swift
b/swift/Sources/Fory/RefResolver.swift
similarity index 100%
rename from swift/Sources/ForySwift/RefResolver.swift
rename to swift/Sources/Fory/RefResolver.swift
diff --git a/swift/Sources/ForySwift/SchemaHash.swift
b/swift/Sources/Fory/SchemaHash.swift
similarity index 100%
rename from swift/Sources/ForySwift/SchemaHash.swift
rename to swift/Sources/Fory/SchemaHash.swift
diff --git a/swift/Sources/ForySwift/Serializer.swift
b/swift/Sources/Fory/Serializer.swift
similarity index 100%
rename from swift/Sources/ForySwift/Serializer.swift
rename to swift/Sources/Fory/Serializer.swift
diff --git a/swift/Sources/ForySwift/TypeMeta.swift
b/swift/Sources/Fory/TypeMeta.swift
similarity index 100%
rename from swift/Sources/ForySwift/TypeMeta.swift
rename to swift/Sources/Fory/TypeMeta.swift
diff --git a/swift/Sources/ForySwift/TypeResolver.swift
b/swift/Sources/Fory/TypeResolver.swift
similarity index 98%
rename from swift/Sources/ForySwift/TypeResolver.swift
rename to swift/Sources/Fory/TypeResolver.swift
index 2e4a95fa9..a9959ba6e 100644
--- a/swift/Sources/ForySwift/TypeResolver.swift
+++ b/swift/Sources/Fory/TypeResolver.swift
@@ -294,6 +294,10 @@ public final class TypeResolver {
value = try Double.foryRead(context, refMode: .none, readTypeInfo:
false)
case .string:
value = try String.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .timestamp:
+ value = try Date.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .date:
+ value = try ForyDate.foryRead(context, refMode: .none,
readTypeInfo: false)
case .binary, .uint8Array:
value = try Data.foryRead(context, refMode: .none, readTypeInfo:
false)
case .boolArray:
diff --git a/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
b/swift/Sources/ForyMacro/ForyObjectMacro.swift
similarity index 84%
rename from swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
rename to swift/Sources/ForyMacro/ForyObjectMacro.swift
index b0a5bc5f3..0a71a9ca2 100644
--- a/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
+++ b/swift/Sources/ForyMacro/ForyObjectMacro.swift
@@ -33,6 +33,11 @@ public struct ForyObjectMacro: MemberMacro, ExtensionMacro {
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext
) throws -> [DeclSyntax] {
+ if let enumDecl = declaration.as(EnumDeclSyntax.self) {
+ let parsedEnum = try parseEnumDecl(enumDecl)
+ return buildEnumDecls(parsedEnum)
+ }
+
let parsed = try parseFields(declaration)
let sortedFields = sortFields(parsed.fields)
@@ -75,6 +80,8 @@ public struct ForyObjectMacro: MemberMacro, ExtensionMacro {
typeName = structDecl.name
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
typeName = classDecl.name
+ } else if let enumDecl = declaration.as(EnumDeclSyntax.self) {
+ typeName = enumDecl.name
} else {
return []
}
@@ -129,11 +136,252 @@ private struct ParsedDecl {
let fields: [ParsedField]
}
+private enum ParsedEnumKind {
+ case ordinal
+ case taggedUnion
+}
+
+private struct ParsedEnumPayloadField {
+ let label: String?
+ let typeText: String
+ let hasGenerics: Bool
+}
+
+private struct ParsedEnumCase {
+ let name: String
+ let payload: [ParsedEnumPayloadField]
+}
+
+private struct ParsedEnumDecl {
+ let kind: ParsedEnumKind
+ let cases: [ParsedEnumCase]
+}
+
private struct FieldTypeResolution {
let classification: TypeClassification
let customCodecType: String?
}
+private func parseEnumDecl(_ enumDecl: EnumDeclSyntax) throws ->
ParsedEnumDecl {
+ var cases: [ParsedEnumCase] = []
+
+ for member in enumDecl.memberBlock.members {
+ guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
+ continue
+ }
+
+ for element in caseDecl.elements {
+ let caseName = element.name.text
+ if caseName.isEmpty {
+ continue
+ }
+
+ var payloadFields: [ParsedEnumPayloadField] = []
+ if let parameterClause = element.parameterClause {
+ for parameter in parameterClause.parameters {
+ if parameter.defaultValue != nil {
+ throw MacroExpansionErrorMessage(
+ "@ForyObject enum associated values cannot have
default values"
+ )
+ }
+
+ let payloadType = parameter.type.trimmedDescription
+ let optional = unwrapOptional(payloadType)
+ let classification = classifyType(optional.type)
+ let hasGenerics = classification.isCollection ||
classification.isMap
+ let label: String?
+ if let firstName = parameter.firstName, firstName.text !=
"_" {
+ label = firstName.text
+ } else {
+ label = nil
+ }
+
+ payloadFields.append(
+ .init(
+ label: label,
+ typeText: payloadType,
+ hasGenerics: hasGenerics
+ )
+ )
+ }
+ }
+ cases.append(.init(name: caseName, payload: payloadFields))
+ }
+ }
+
+ guard !cases.isEmpty else {
+ throw MacroExpansionErrorMessage("@ForyObject enum must define at
least one case")
+ }
+
+ let hasPayload = cases.contains { !$0.payload.isEmpty }
+ if hasPayload {
+ return .init(kind: .taggedUnion, cases: cases)
+ }
+
+ return .init(kind: .ordinal, cases: cases)
+}
+
+private func buildEnumDecls(_ parsedEnum: ParsedEnumDecl) -> [DeclSyntax] {
+ switch parsedEnum.kind {
+ case .ordinal:
+ return buildOrdinalEnumDecls(parsedEnum.cases)
+ case .taggedUnion:
+ return buildTaggedUnionEnumDecls(parsedEnum.cases)
+ }
+}
+
+private func buildOrdinalEnumDecls(_ cases: [ParsedEnumCase]) -> [DeclSyntax] {
+ let defaultCase = cases[0].name
+ let writeSwitchCases = cases.enumerated().map { index, enumCase in
+ """
+ case .\(enumCase.name):
+ context.writer.writeVarUInt32(\(index))
+ """
+ }.joined(separator: "\n ")
+ let readSwitchCases = cases.enumerated().map { index, enumCase in
+ "case \(index): return .\(enumCase.name)"
+ }.joined(separator: "\n ")
+
+ let defaultDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ static func foryDefault() -> Self {
+ .\(defaultCase)
+ }
+ """
+ )
+
+ let staticTypeIDDecl: DeclSyntax = """
+ static var staticTypeId: ForyTypeId { .enumType }
+ """
+
+ let writeDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
+ _ = hasGenerics
+ switch self {
+ \(writeSwitchCases)
+ }
+ }
+ """
+ )
+
+ let readDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ static func foryReadData(_ context: ReadContext) throws -> Self {
+ let ordinal = try context.reader.readVarUInt32()
+ switch ordinal {
+ \(readSwitchCases)
+ default:
+ throw ForyError.invalidData("unknown enum ordinal \\(ordinal)")
+ }
+ }
+ """
+ )
+
+ return [defaultDecl, staticTypeIDDecl, writeDecl, readDecl]
+}
+
+private func buildTaggedUnionEnumDecls(_ cases: [ParsedEnumCase]) ->
[DeclSyntax] {
+ let defaultExpr = enumCaseDefaultExpr(cases[0])
+ let writeSwitchCases = cases.enumerated().map { index, enumCase in
+ var lines: [String] = []
+ lines.append("case \(enumCasePattern(enumCase)):")
+ lines.append(" context.writer.writeVarUInt32(\(index))")
+ for payloadIndex in enumCase.payload.indices {
+ let variableName = "__value\(payloadIndex)"
+ let hasGenerics = enumCase.payload[payloadIndex].hasGenerics ?
"true" : "false"
+ lines.append(
+ " try \(variableName).foryWrite(context, refMode:
.tracking, writeTypeInfo: true, hasGenerics: \(hasGenerics))"
+ )
+ }
+ return lines.joined(separator: "\n")
+ }.joined(separator: "\n ")
+
+ let readSwitchCases = cases.enumerated().map { index, enumCase in
+ if enumCase.payload.isEmpty {
+ return """
+ case \(index):
+ return .\(enumCase.name)
+ """
+ }
+
+ var lines: [String] = ["case \(index):"]
+ for (payloadIndex, payloadField) in enumCase.payload.enumerated() {
+ lines.append(
+ " let __value\(payloadIndex) = try
\(payloadField.typeText).foryRead(context, refMode: .tracking, readTypeInfo:
true)"
+ )
+ }
+ let ctorArgs = enumCase.payload.enumerated().map { payloadIndex,
payloadField in
+ if let label = payloadField.label {
+ return "\(label): __value\(payloadIndex)"
+ }
+ return "__value\(payloadIndex)"
+ }.joined(separator: ", ")
+ lines.append(" return .\(enumCase.name)(\(ctorArgs))")
+ return lines.joined(separator: "\n")
+ }.joined(separator: "\n ")
+
+ let defaultDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ static func foryDefault() -> Self {
+ \(defaultExpr)
+ }
+ """
+ )
+
+ let staticTypeIDDecl: DeclSyntax = """
+ static var staticTypeId: ForyTypeId { .typedUnion }
+ """
+
+ let writeDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
+ _ = hasGenerics
+ switch self {
+ \(writeSwitchCases)
+ }
+ }
+ """
+ )
+
+ let readDecl: DeclSyntax = DeclSyntax(
+ stringLiteral: """
+ static func foryReadData(_ context: ReadContext) throws -> Self {
+ let caseID = try context.reader.readVarUInt32()
+ switch caseID {
+ \(readSwitchCases)
+ default:
+ throw ForyError.invalidData("unknown union tag \\(caseID)")
+ }
+ }
+ """
+ )
+
+ return [defaultDecl, staticTypeIDDecl, writeDecl, readDecl]
+}
+
+private func enumCasePattern(_ enumCase: ParsedEnumCase) -> String {
+ guard !enumCase.payload.isEmpty else {
+ return ".\(enumCase.name)"
+ }
+ let bindings = enumCase.payload.indices.map { "let __value\($0)"
}.joined(separator: ", ")
+ return ".\(enumCase.name)(\(bindings))"
+}
+
+private func enumCaseDefaultExpr(_ enumCase: ParsedEnumCase) -> String {
+ guard !enumCase.payload.isEmpty else {
+ return ".\(enumCase.name)"
+ }
+ let args = enumCase.payload.map { payloadField in
+ let defaultValue = "\(payloadField.typeText).foryDefault()"
+ if let label = payloadField.label {
+ return "\(label): \(defaultValue)"
+ }
+ return defaultValue
+ }.joined(separator: ", ")
+ return ".\(enumCase.name)(\(args))"
+}
+
private func parseFields(_ declaration: some DeclGroupSyntax) throws ->
ParsedDecl {
let isClass = declaration.is(ClassDeclSyntax.self)
guard isClass || declaration.is(StructDeclSyntax.self) else {
@@ -1046,6 +1294,10 @@ private func classifyType(_ typeText: String) ->
TypeClassification {
return .init(typeID: 21, isPrimitive: false, isBuiltIn: true,
isCollection: false, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
case "Data", "Foundation.Data":
return .init(typeID: 41, isPrimitive: false, isBuiltIn: true,
isCollection: false, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
+ case "Date", "Foundation.Date", "ForyTimestamp":
+ return .init(typeID: 38, isPrimitive: false, isBuiltIn: true,
isCollection: false, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
+ case "ForyDate":
+ return .init(typeID: 39, isPrimitive: false, isBuiltIn: true,
isCollection: false, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
default:
break
}
diff --git a/swift/Tests/ForyTests/DateTimeTests.swift
b/swift/Tests/ForyTests/DateTimeTests.swift
new file mode 100644
index 000000000..399b20479
--- /dev/null
+++ b/swift/Tests/ForyTests/DateTimeTests.swift
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@ForyObject
+private struct DateMacroHolder {
+ var day: ForyDate = .init()
+ var instant: Date = .foryDefault()
+ var timestamp: ForyTimestamp = .init()
+}
+
+@Test
+func dateAndTimestampTypeIds() {
+ #expect(ForyDate.staticTypeId == .date)
+ #expect(ForyTimestamp.staticTypeId == .timestamp)
+ #expect(Date.staticTypeId == .timestamp)
+}
+
+@Test
+func dateAndTimestampRoundTrip() throws {
+ let fory = Fory()
+
+ let day = ForyDate(daysSinceEpoch: 18_745)
+ let dayData = try fory.serialize(day)
+ let dayDecoded: ForyDate = try fory.deserialize(dayData)
+ #expect(dayDecoded == day)
+
+ let ts = ForyTimestamp(seconds: -123, nanos: 987_654_321)
+ let tsData = try fory.serialize(ts)
+ let tsDecoded: ForyTimestamp = try fory.deserialize(tsData)
+ #expect(tsDecoded == ts)
+
+ let instant = Date(timeIntervalSince1970: 1_731_234_567.123_456_7)
+ let instantData = try fory.serialize(instant)
+ let instantDecoded: Date = try fory.deserialize(instantData)
+ let diff = abs(instantDecoded.timeIntervalSince1970 -
instant.timeIntervalSince1970)
+ #expect(diff < 0.000_001)
+}
+
+@Test
+func dateAndTimestampMacroFieldRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
true))
+ fory.register(DateMacroHolder.self, id: 901)
+
+ let value = DateMacroHolder(
+ day: .init(daysSinceEpoch: 20_001),
+ instant: Date(timeIntervalSince1970: 123_456.000_001),
+ timestamp: .init(seconds: 44, nanos: 12_345)
+ )
+
+ let data = try fory.serialize(value)
+ let decoded: DateMacroHolder = try fory.deserialize(data)
+
+ #expect(decoded.day == value.day)
+ #expect(decoded.timestamp == value.timestamp)
+ let diff = abs(decoded.instant.timeIntervalSince1970 -
value.instant.timeIntervalSince1970)
+ #expect(diff < 0.000_001)
+}
diff --git a/swift/Tests/ForyTests/EnumTests.swift
b/swift/Tests/ForyTests/EnumTests.swift
new file mode 100644
index 000000000..4053066ee
--- /dev/null
+++ b/swift/Tests/ForyTests/EnumTests.swift
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+@testable import Fory
+
+@ForyObject
+private enum Color: Equatable {
+ case red
+ case green
+ case blue
+}
+
+@ForyObject
+private enum StringOrLong: Equatable {
+ case text(String)
+ case number(Int64)
+}
+
+@ForyObject
+private struct StructWithEnum: Equatable {
+ var name: String = ""
+ var color: Color = .red
+ var value: Int32 = 0
+}
+
+@ForyObject
+private struct StructWithUnion: Equatable {
+ var unionField: StringOrLong = .foryDefault()
+}
+
+@ForyObject
+private indirect enum Token: Equatable {
+ case plus
+ case number(Int64)
+ case ident(String)
+ case assign(target: String, value: Int32)
+ case other(Int64?)
+ case child(Token)
+ case map([String: Token])
+}
+
+@Test
+func enumTypeIdClassification() {
+ #expect(Color.staticTypeId == .enumType)
+ #expect(StringOrLong.staticTypeId == .typedUnion)
+}
+
+@Test
+func structWithEnumFieldRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
false))
+ fory.register(Color.self, id: 100)
+ fory.register(StructWithEnum.self, id: 101)
+
+ let value = StructWithEnum(name: "test", color: .green, value: 42)
+ let data = try fory.serialize(value)
+ let decoded: StructWithEnum = try fory.deserialize(data)
+ #expect(decoded == value)
+}
+
+@Test
+func taggedUnionXlangRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
false))
+ fory.register(StringOrLong.self, id: 300)
+ fory.register(StructWithUnion.self, id: 301)
+
+ let first = StructWithUnion(unionField: .text("hello"))
+ let second = StructWithUnion(unionField: .number(42))
+
+ let firstData = try fory.serialize(first)
+ let secondData = try fory.serialize(second)
+
+ let firstDecoded: StructWithUnion = try fory.deserialize(firstData)
+ let secondDecoded: StructWithUnion = try fory.deserialize(secondData)
+
+ #expect(firstDecoded == first)
+ #expect(secondDecoded == second)
+}
+
+@Test
+func mixedEnumShapesRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: false, trackRef: true, compatible:
false))
+ fory.register(Token.self, id: 1000)
+
+ let nestedMap: [String: Token] = [
+ "one": .number(1),
+ "plus": .plus,
+ "nested": .child(.ident("deep")),
+ ]
+
+ let tokens: [Token] = [
+ .plus,
+ .number(1),
+ .ident("foo"),
+ .assign(target: "bar", value: 42),
+ .other(42),
+ .other(nil),
+ .child(.child(.other(nil))),
+ .map(nestedMap),
+ ]
+
+ let data = try fory.serialize(tokens)
+ let decoded: [Token] = try fory.deserialize(data)
+ #expect(decoded == tokens)
+}
diff --git a/swift/Tests/ForySwiftTests/ForySwiftTests.swift
b/swift/Tests/ForyTests/ForySwiftTests.swift
similarity index 97%
rename from swift/Tests/ForySwiftTests/ForySwiftTests.swift
rename to swift/Tests/ForyTests/ForySwiftTests.swift
index 7f85a7233..4d6667b67 100644
--- a/swift/Tests/ForySwiftTests/ForySwiftTests.swift
+++ b/swift/Tests/ForyTests/ForySwiftTests.swift
@@ -17,7 +17,7 @@
import Foundation
import Testing
-@testable import ForySwift
+@testable import Fory
@ForyObject
struct Address: Equatable {
@@ -66,6 +66,19 @@ final class Node {
}
}
+@ForyObject
+final class WeakNode {
+ var value: Int32 = 0
+ weak var next: WeakNode?
+
+ required init() {}
+
+ init(value: Int32, next: WeakNode? = nil) {
+ self.value = value
+ self.next = next
+ }
+}
+
@ForyObject
struct AnyObjectHolder {
var value: AnyObject
@@ -232,6 +245,21 @@ func macroClassReferenceTracking() throws {
#expect(decoded.next === decoded)
}
+@Test
+func macroClassWeakReferenceTracking() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(WeakNode.self, id: 201)
+
+ let node = WeakNode(value: 13)
+ node.next = node
+
+ let data = try fory.serialize(node)
+ let decoded: WeakNode = try fory.deserialize(data)
+
+ #expect(decoded.value == 13)
+ #expect(decoded.next === decoded)
+}
+
@Test
func topLevelAnyRoundTrip() throws {
let fory = Fory()
diff --git a/swift/Sources/ForySwiftXlangPeer/main.swift
b/swift/Tests/ForyXlangPeer/main.swift
similarity index 90%
rename from swift/Sources/ForySwiftXlangPeer/main.swift
rename to swift/Tests/ForyXlangPeer/main.swift
index d29404f8a..c6b935802 100644
--- a/swift/Sources/ForySwiftXlangPeer/main.swift
+++ b/swift/Tests/ForyXlangPeer/main.swift
@@ -16,93 +16,23 @@
// under the License.
import Foundation
-import ForySwift
+import Fory
// MARK: - Shared test types
-private enum PeerColor: UInt32 {
- case green = 0
- case red = 1
- case blue = 2
- case white = 3
-}
-
-private enum PeerTestEnum: UInt32 {
- case valueA = 0
- case valueB = 1
- case valueC = 2
-}
-
-private protocol PeerUInt32EnumSerializer: RawRepresentable, Serializer where
RawValue == UInt32 {}
-
-extension PeerUInt32EnumSerializer {
- static func foryDefault() -> Self {
- Self(rawValue: 0)!
- }
-
- static var staticTypeId: ForyTypeId {
- .enumType
- }
-
- func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
- _ = hasGenerics
- context.writer.writeVarUInt32(rawValue)
- }
-
- static func foryReadData(_ context: ReadContext) throws -> Self {
- let ordinal = try context.reader.readVarUInt32()
- guard let value = Self(rawValue: ordinal) else {
- throw ForyError.invalidData("unknown enum ordinal \(ordinal)")
- }
- return value
- }
-}
-
-extension PeerColor: PeerUInt32EnumSerializer {}
-extension PeerTestEnum: PeerUInt32EnumSerializer {}
-
-private struct PeerDate: Serializer, Equatable {
- var daysSinceEpoch: Int32 = 0
-
- static func foryDefault() -> PeerDate {
- PeerDate()
- }
-
- static var staticTypeId: ForyTypeId {
- .date
- }
-
- func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
- _ = hasGenerics
- context.writer.writeInt32(daysSinceEpoch)
- }
-
- static func foryReadData(_ context: ReadContext) throws -> PeerDate {
- PeerDate(daysSinceEpoch: try context.reader.readInt32())
- }
+@ForyObject
+private enum PeerColor {
+ case green
+ case red
+ case blue
+ case white
}
-private struct PeerTimestamp: Serializer, Equatable {
- var seconds: Int64 = 0
- var nanos: Int32 = 0
-
- static func foryDefault() -> PeerTimestamp {
- PeerTimestamp()
- }
-
- static var staticTypeId: ForyTypeId {
- .timestamp
- }
-
- func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
- _ = hasGenerics
- context.writer.writeInt64(seconds)
- context.writer.writeInt32(nanos)
- }
-
- static func foryReadData(_ context: ReadContext) throws -> PeerTimestamp {
- PeerTimestamp(seconds: try context.reader.readInt64(), nanos: try
context.reader.readInt32())
- }
+@ForyObject
+private enum PeerTestEnum {
+ case valueA
+ case valueB
+ case valueC
}
@ForyObject
@@ -286,7 +216,7 @@ private final class RefOverrideContainer {
@ForyObject
private final class CircularRefStruct {
var name: String = ""
- var selfRef: CircularRefStruct? = nil
+ weak var selfRef: CircularRefStruct? = nil
required init() {}
}
@@ -349,43 +279,10 @@ private struct AnimalMapHolder {
var animalMap: [String: Any] = [:]
}
-private enum StringOrLong: Serializer, Equatable {
- case str(String)
- case long(Int64)
-
- static func foryDefault() -> StringOrLong {
- .str("")
- }
-
- static var staticTypeId: ForyTypeId {
- .typedUnion
- }
-
- func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
- _ = hasGenerics
- switch self {
- case .str(let value):
- context.writer.writeVarUInt32(0)
- try value.foryWrite(context, refMode: .tracking, writeTypeInfo:
true, hasGenerics: false)
- case .long(let value):
- context.writer.writeVarUInt32(1)
- try value.foryWrite(context, refMode: .tracking, writeTypeInfo:
true, hasGenerics: false)
- }
- }
-
- static func foryReadData(_ context: ReadContext) throws -> StringOrLong {
- let tag = try context.reader.readVarUInt32()
- switch tag {
- case 0:
- let value: String = try String.foryRead(context, refMode:
.tracking, readTypeInfo: true)
- return .str(value)
- case 1:
- let value: Int64 = try Int64.foryRead(context, refMode: .tracking,
readTypeInfo: true)
- return .long(value)
- default:
- throw ForyError.invalidData("unknown union tag \(tag)")
- }
- }
+@ForyObject
+private enum StringOrLong {
+ case text(String)
+ case number(Int64)
}
@ForyObject
@@ -601,8 +498,8 @@ private func handleCrossLanguageSerializer(_ bytes:
[UInt8]) throws -> [UInt8] {
let f32: Float = try fory.deserializeFrom(reader)
let f64: Double = try fory.deserializeFrom(reader)
let str: String = try fory.deserializeFrom(reader)
- let day: PeerDate = try fory.deserializeFrom(reader)
- let ts: PeerTimestamp = try fory.deserializeFrom(reader)
+ let day: ForyDate = try fory.deserializeFrom(reader)
+ let ts: ForyTimestamp = try fory.deserializeFrom(reader)
let boolArray: [Bool] = try fory.deserializeFrom(reader)
let byteArray: [UInt8] = try fory.deserializeFrom(reader)
let shortArray: [Int16] = try fory.deserializeFrom(reader)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]