From 1c4c347c116cba5d1b10879e2aa6c601a515f9e2 Mon Sep 17 00:00:00 2001 From: Wendell Date: Wed, 31 Dec 2025 16:33:44 +0800 Subject: [PATCH 1/2] Add CodableKitCore shared options target --- Package.swift | 5 + Sources/CodableKit/CodableKeyOptions.swift | 234 +----------------- Sources/CodableKit/CodableKit.swift | 2 + Sources/CodableKit/CodableOptions.swift | 33 +-- .../CodableKitCore/CodableKeyOptions.swift | 50 ++++ Sources/CodableKitCore/CodableOptions.swift | 38 +++ .../CodableKitMacros/CodableKeyOptions.swift | 18 +- Sources/CodableKitMacros/CodableMacro.swift | 1 + Sources/CodableKitMacros/CodableOptions.swift | 13 +- Sources/CodableKitMacros/CodeGenCore.swift | 1 + 10 files changed, 106 insertions(+), 289 deletions(-) create mode 100644 Sources/CodableKitCore/CodableKeyOptions.swift create mode 100644 Sources/CodableKitCore/CodableOptions.swift diff --git a/Package.swift b/Package.swift index 823fe45..697e8c1 100644 --- a/Package.swift +++ b/Package.swift @@ -25,9 +25,13 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0"..<"603.0.0") ], targets: [ + .target( + name: "CodableKitCore" + ), .macro( name: "CodableKitMacros", dependencies: [ + "CodableKitCore", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), @@ -36,6 +40,7 @@ let package = Package( .target( name: "CodableKit", dependencies: [ + "CodableKitCore", "CodableKitMacros" ] ), diff --git a/Sources/CodableKit/CodableKeyOptions.swift b/Sources/CodableKit/CodableKeyOptions.swift index e43a952..e325a78 100644 --- a/Sources/CodableKit/CodableKeyOptions.swift +++ b/Sources/CodableKit/CodableKeyOptions.swift @@ -2,237 +2,9 @@ // CodableKeyOptions.swift // CodableKit // -// Created by WendellXY on 2024/5/14 -// Copyright © 2024 WendellXY. All rights reserved. +// Compatibility shim: the canonical definitions live in `CodableKitCore`. // -public struct CodableKeyOptions: OptionSet, Sendable { - public let rawValue: Int32 +import CodableKitCore - public init(rawValue: Int32) { - self.rawValue = rawValue - } - - /// The default options for a `CodableKey`, which is equivalent to an empty set. - public static let `default`: Self = [] - - /// A convenience option combining ``transcodeRawString`` and ``useDefaultOnFailure`` for safe JSON string transcoding. - /// - /// This option provides a safer way to handle string-encoded JSON by automatically falling back to - /// default values or `nil` when the JSON string is invalid or malformed. It's equivalent to - /// `[.transcodeRawString, .useDefaultOnFailure]`. - /// - /// Example usage with invalid JSON handling: - /// - /// ```json - /// { - /// "name": "Tom", - /// "validCar": "{\"brand\":\"XYZ\",\"year\":9999}", - /// "invalidCar": "corrupted json string", - /// "optionalCar": null - /// } - /// ``` - /// - /// ```swift - /// @Codable - /// struct Person { - /// let name: String - /// - /// // Successfully decodes valid JSON string - /// @CodableKey(options: .safeTranscodeRawString) - /// var validCar: Car = Car(brand: "Default", year: 2024) - /// - /// // Uses default value for invalid JSON string - /// @CodableKey(options: .safeTranscodeRawString) - /// var invalidCar: Car = Car(brand: "Default", year: 2024) - /// - /// // Becomes nil for invalid JSON string or null - /// @CodableKey(options: .safeTranscodeRawString) - /// var optionalCar: Car? - /// } - /// ``` - /// - /// - Note: This is a convenience option. It's identical to using - /// `@CodableKey(options: [.transcodeRawString, .useDefaultOnFailure])` - /// - Important: When using this option, ensure your properties either: - /// - Have an explicit default value, or - /// - Are optional (implicitly having `nil` as default) - public static let safeTranscodeRawString: Self = [.transcodeRawString, .useDefaultOnFailure] - - /// The key will be ignored during encoding and decoding. - /// - /// This option is useful when you want to add a local or runtime-only property to the structure without creating - /// another structure. - /// - /// - Important: Using the `.ignored` option for an enum case may lead to runtime issues if you attempt to decode - /// or encode the enum with that case using the `.ignored` option. - public static let ignored = Self(rawValue: 1 << 0) - - /// The key will be explicitly set to `nil` (`null`) when encoding and decoding. - /// By default, the key will be omitted if the value is `nil`. - public static let explicitNil = Self(rawValue: 1 << 1) - - /// If the key has a custom CodableKey, a computed property will be generated to access the key; otherwise, this - /// option is ignored. - /// - /// For example, if you have a custom key `myKey` and the original key `key`, a computed property `myKey` will be - /// generated to access the original key `key`. - /// - /// ```swift - /// @Codable - /// struct MyStruct { - /// @CodableKey("key", options: .generateCustomKey) - /// var myKey: String - /// } - /// ``` - /// - /// The generated code will be: - /// ```swift - /// struct MyStruct { - /// var myKey: String - /// var key: String { - /// myKey - /// } - /// } - /// ``` - public static let generateCustomKey = Self(rawValue: 1 << 2) - - /// Transcode the value between raw string and the target type. - /// - /// This option enables automatic conversion between a JSON string representation and a - /// strongly-typed model during encoding and decoding. The property type must conform to - /// the appropriate coding protocol based on usage: - /// - For decoding: must conform to `Decodable` - /// - For encoding: must conform to `Encodable` - /// - For both operations: must conform to `Codable` (which combines both protocols) - /// - /// This is particularly useful when dealing with APIs that encode nested objects as - /// string-encoded JSON, eliminating the need for custom encoding/decoding logic. - /// - /// For example, given this JSON response where `car` is a string-encoded JSON object: - /// - /// ```json - /// { - /// "name": "Tom", - /// "car": "{\"brand\":\"XYZ\",\"year\":9999}" - /// } - /// ``` - /// - /// You can decode it directly into typed models: - /// - /// ```swift - /// @Codable - /// struct Car { - /// let brand: String - /// let year: Int - /// } - /// - /// @Codable - /// struct Person { - /// let name: String - /// @CodableKey(options: .transcodeRawString) - /// var car: Car - /// } - /// ``` - /// - /// When dealing with potentially invalid JSON strings, you can combine with other options. - /// For example: - /// - /// ```json - /// { - /// "name": "Tom", - /// "car": "invalid json string" - /// } - /// ``` - /// - /// ```swift - /// @Codable - /// struct SafePerson { - /// let name: String - /// - /// // Will use the default car when JSON string is invalid - /// @CodableKey(options: [.transcodeRawString, .useDefaultOnFailure]) - /// var car: Car = Car(brand: "Default", year: 2024) - /// - /// // Will be nil when JSON string is invalid - /// @CodableKey(options: [.transcodeRawString, .useDefaultOnFailure]) - /// var optionalCar: Car? - /// } - /// ``` - /// - /// Without this option, you would need to: - /// 1. First decode the car field as a String - /// 2. Parse that string into JSON data - /// 3. Decode the JSON data into the Car type - /// 4. Implement the reverse process for encoding - /// - /// The `transcodeRawString` option handles all these steps automatically. - /// - /// - Note: The property type must conform to the appropriate coding protocol based on usage: - /// `Decodable` for decoding, `Encodable` for encoding, or `Codable` for both. - /// A compile-time error will occur if the type does not satisfy these requirements. - /// - Important: The string value must contain valid JSON that matches the structure of - /// the target type. If the JSON is invalid or doesn't match the expected structure, - /// a decoding error will be thrown at runtime. See ``useDefaultOnFailure`` option - /// for handling invalid JSON strings gracefully. - public static let transcodeRawString = Self(rawValue: 1 << 3) - - /// Use the default value or `nil` when decoding or encoding fails. - /// - /// This option provides fallback behavior when coding operations fail, with two scenarios: - /// - /// 1. For properties with explicit default values: - /// ```swift - /// @CodableKey(options: .useDefaultOnFailure) - /// var status: Status = .unknown - /// ``` - /// The default value (`.unknown`) will be used when decoding fails. - /// - /// 2. For optional properties: - /// ```swift - /// @CodableKey(options: .useDefaultOnFailure) - /// var status: Status? - /// ``` - /// The property will be set to `nil` when decoding fails. - /// - /// This is particularly useful for: - /// - Enum properties where the raw value might not match any defined cases - /// - Handling backward compatibility when adding new properties - /// - Gracefully handling malformed or unexpected data - /// - /// Example handling an enum with invalid raw value: - /// ```swift - /// enum Status: String, Codable { - /// case active - /// case inactive - /// case unknown - /// } - /// - /// struct User { - /// let id: Int - /// @CodableKey(options: .useDefaultOnFailure) - /// var status: Status = .unknown // Will use .unknown if JSON contains invalid status - /// } - /// - /// // JSON: {"id": 1, "status": "invalid_value"} - /// // Decodes without error, status will be .unknown - /// ``` - /// - /// - Note: This option must be used with either: - /// - A property that has an explicit default value - /// - An optional property (which implicitly has `nil` as default) - /// - Important: Without this option, decoding failures would throw an error and halt the - /// entire decoding process. With this option, failures are handled gracefully - /// by falling back to the default value or `nil`. - public static let useDefaultOnFailure = Self(rawValue: 1 << 4) - - /// Decode the value in a lossy way for collections. - /// - /// - For arrays and sets: invalid elements are dropped. - /// - For dictionaries: entries with invalid values (or keys that cannot be - /// converted from JSON string keys) are dropped. - /// - /// This option is useful when you want to tolerate partially-invalid data - /// from APIs without failing the entire decode. - public static let lossy = Self(rawValue: 1 << 5) -} +public typealias CodableKeyOptions = CodableKitCore.CodableKeyOptions diff --git a/Sources/CodableKit/CodableKit.swift b/Sources/CodableKit/CodableKit.swift index 654317f..ef3a575 100644 --- a/Sources/CodableKit/CodableKit.swift +++ b/Sources/CodableKit/CodableKit.swift @@ -5,6 +5,8 @@ // Created by Wendell on 3/30/24. // +import CodableKitCore + /// A macro that generates complete `Codable` conformance for structs and classes. /// /// This macro automatically generates all the boilerplate code needed for `Codable` conformance, diff --git a/Sources/CodableKit/CodableOptions.swift b/Sources/CodableKit/CodableOptions.swift index f3e8454..f7ce10a 100644 --- a/Sources/CodableKit/CodableOptions.swift +++ b/Sources/CodableKit/CodableOptions.swift @@ -2,35 +2,10 @@ // CodableOptions.swift // CodableKit // -// Created by Wendell Wang on 2025/1/13. +// Compatibility shim: the canonical definitions live in `CodableKitCore`. // -/// Options that customize the behavior of the `@Codable` macro expansion. -public struct CodableOptions: OptionSet, Sendable { - public let rawValue: Int32 +import CodableKitCore - public init(rawValue: Int32) { - self.rawValue = rawValue - } - - /// The default options, which perform standard Codable expansion with super encode/decode calls. - public static let `default`: Self = [] - - /// Skips generating super encode/decode calls in the expanded code. - /// - /// Use this option when the superclass doesn't conform to `Codable`. - /// When enabled: - /// - Replaces `super.init(from: decoder)` with `super.init()` - /// - Removes `super.encode(to: encoder)` call entirely - public static let skipSuperCoding = Self(rawValue: 1 << 0) - - /// Skips adding protocol conformances (`Codable`/`Decodable`/`Encodable`) to the generated extension. - /// - /// Use this option when you want to explicitly declare the conformance on the type yourself, e.g.: - /// - /// ```swift - /// @Codable(options: .skipProtocolConformance) - /// struct User: Codable { ... } - /// ``` - public static let skipProtocolConformance = Self(rawValue: 1 << 1) -} +/// Options that customize the behavior of the `@Codable` / `@Decodable` / `@Encodable` macro expansion. +public typealias CodableOptions = CodableKitCore.CodableOptions diff --git a/Sources/CodableKitCore/CodableKeyOptions.swift b/Sources/CodableKitCore/CodableKeyOptions.swift new file mode 100644 index 0000000..e533dc6 --- /dev/null +++ b/Sources/CodableKitCore/CodableKeyOptions.swift @@ -0,0 +1,50 @@ +// +// CodableKeyOptions.swift +// CodableKitCore +// +// Shared (runtime + macro implementation) option definitions. +// + +public struct CodableKeyOptions: OptionSet, Sendable { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// The default options for a `CodableKey`, which is equivalent to an empty set. + public static let `default`: Self = [] + + /// A convenience option combining ``transcodeRawString`` and ``useDefaultOnFailure`` for safe JSON string transcoding. + /// + /// This option provides a safer way to handle string-encoded JSON by automatically falling back to + /// default values or `nil` when the JSON string is invalid or malformed. It's equivalent to + /// `[.transcodeRawString, .useDefaultOnFailure]`. + public static let safeTranscodeRawString: Self = [.transcodeRawString, .useDefaultOnFailure] + + /// The key will be ignored during encoding and decoding. + public static let ignored = Self(rawValue: 1 << 0) + + /// The key will be explicitly set to `nil` (`null`) when encoding and decoding. + /// By default, the key will be omitted if the value is `nil`. + public static let explicitNil = Self(rawValue: 1 << 1) + + /// If the key has a custom CodableKey, a computed property will be generated to access the key; otherwise, this + /// option is ignored. + public static let generateCustomKey = Self(rawValue: 1 << 2) + + /// Transcode the value between raw string and the target type. + public static let transcodeRawString = Self(rawValue: 1 << 3) + + /// Use the default value or `nil` when decoding or encoding fails. + public static let useDefaultOnFailure = Self(rawValue: 1 << 4) + + /// Decode the value in a lossy way for collections. + /// + /// - For arrays and sets: invalid elements are dropped. + /// - For dictionaries: entries with invalid values (or keys that cannot be + /// converted from JSON string keys) are dropped. + public static let lossy = Self(rawValue: 1 << 5) +} + + diff --git a/Sources/CodableKitCore/CodableOptions.swift b/Sources/CodableKitCore/CodableOptions.swift new file mode 100644 index 0000000..4740432 --- /dev/null +++ b/Sources/CodableKitCore/CodableOptions.swift @@ -0,0 +1,38 @@ +// +// CodableOptions.swift +// CodableKitCore +// +// Shared (runtime + macro implementation) option definitions. +// + +/// Options that customize the behavior of the `@Codable` / `@Decodable` / `@Encodable` macro expansion. +public struct CodableOptions: OptionSet, Sendable { + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + /// The default options, which perform standard Codable expansion with super encode/decode calls. + public static let `default`: Self = [] + + /// Skips generating super encode/decode calls in the expanded code. + /// + /// Use this option when the superclass doesn't conform to `Codable`. + /// When enabled: + /// - Replaces `super.init(from: decoder)` with `super.init()` + /// - Removes `super.encode(to: encoder)` call entirely + public static let skipSuperCoding = Self(rawValue: 1 << 0) + + /// Skips adding protocol conformances (`Codable`/`Decodable`/`Encodable`) to the generated extension. + /// + /// Use this option when you want to explicitly declare the conformance on the type yourself, e.g.: + /// + /// ```swift + /// @Codable(options: .skipProtocolConformance) + /// struct User: Codable { ... } + /// ``` + public static let skipProtocolConformance = Self(rawValue: 1 << 1) +} + + diff --git a/Sources/CodableKitMacros/CodableKeyOptions.swift b/Sources/CodableKitMacros/CodableKeyOptions.swift index 12793cd..0dc263b 100644 --- a/Sources/CodableKitMacros/CodableKeyOptions.swift +++ b/Sources/CodableKitMacros/CodableKeyOptions.swift @@ -5,25 +5,9 @@ // Created by Wendell on 4/3/24. // +import CodableKitCore import SwiftSyntax -struct CodableKeyOptions: OptionSet, Sendable { - let rawValue: Int32 - - init(rawValue: Int32) { - self.rawValue = rawValue - } - - static let `default`: Self = [] - static let safeTranscodeRawString: Self = [.transcodeRawString, .useDefaultOnFailure] - static let ignored = Self(rawValue: 1 << 0) - static let explicitNil = Self(rawValue: 1 << 1) - static let generateCustomKey = Self(rawValue: 1 << 2) - static let transcodeRawString = Self(rawValue: 1 << 3) - static let useDefaultOnFailure = Self(rawValue: 1 << 4) - static let lossy = Self(rawValue: 1 << 5) -} - extension CodableKeyOptions { init(from expr: MemberAccessExprSyntax) { self = diff --git a/Sources/CodableKitMacros/CodableMacro.swift b/Sources/CodableKitMacros/CodableMacro.swift index 2c32e7d..45a14f9 100644 --- a/Sources/CodableKitMacros/CodableMacro.swift +++ b/Sources/CodableKitMacros/CodableMacro.swift @@ -6,6 +6,7 @@ // import Foundation +import CodableKitCore import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/CodableKitMacros/CodableOptions.swift b/Sources/CodableKitMacros/CodableOptions.swift index fd991ad..611d2c2 100644 --- a/Sources/CodableKitMacros/CodableOptions.swift +++ b/Sources/CodableKitMacros/CodableOptions.swift @@ -5,20 +5,9 @@ // Created by Wendell Wang on 2025/10/2. // +import CodableKitCore import SwiftSyntax -struct CodableOptions: OptionSet, Sendable { - let rawValue: Int32 - - init(rawValue: Int32) { - self.rawValue = rawValue - } - - static let `default`: Self = [] - static let skipSuperCoding = Self(rawValue: 1 << 0) - static let skipProtocolConformance = Self(rawValue: 1 << 1) -} - extension CodableOptions { init(from expr: MemberAccessExprSyntax) { let variableName = expr.declName.baseName.text diff --git a/Sources/CodableKitMacros/CodeGenCore.swift b/Sources/CodableKitMacros/CodeGenCore.swift index f711fbd..33f1bda 100644 --- a/Sources/CodableKitMacros/CodeGenCore.swift +++ b/Sources/CodableKitMacros/CodeGenCore.swift @@ -7,6 +7,7 @@ // import Foundation +import CodableKitCore import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder From 8f711c402710bcd1a9e08b38ad85a0f6c0cbe4a6 Mon Sep 17 00:00:00 2001 From: Wendell Date: Wed, 31 Dec 2025 16:38:38 +0800 Subject: [PATCH 2/2] Format files --- Package.swift | 2 +- Sources/CodableKitCore/CodableKeyOptions.swift | 2 -- Sources/CodableKitCore/CodableOptions.swift | 2 -- Sources/CodableKitMacros/CodableMacro.swift | 2 +- Sources/CodableKitMacros/CodeGenCore.swift | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index 697e8c1..9b2d82b 100644 --- a/Package.swift +++ b/Package.swift @@ -41,7 +41,7 @@ let package = Package( name: "CodableKit", dependencies: [ "CodableKitCore", - "CodableKitMacros" + "CodableKitMacros", ] ), .testTarget( diff --git a/Sources/CodableKitCore/CodableKeyOptions.swift b/Sources/CodableKitCore/CodableKeyOptions.swift index e533dc6..51f2805 100644 --- a/Sources/CodableKitCore/CodableKeyOptions.swift +++ b/Sources/CodableKitCore/CodableKeyOptions.swift @@ -46,5 +46,3 @@ public struct CodableKeyOptions: OptionSet, Sendable { /// converted from JSON string keys) are dropped. public static let lossy = Self(rawValue: 1 << 5) } - - diff --git a/Sources/CodableKitCore/CodableOptions.swift b/Sources/CodableKitCore/CodableOptions.swift index 4740432..974d16c 100644 --- a/Sources/CodableKitCore/CodableOptions.swift +++ b/Sources/CodableKitCore/CodableOptions.swift @@ -34,5 +34,3 @@ public struct CodableOptions: OptionSet, Sendable { /// ``` public static let skipProtocolConformance = Self(rawValue: 1 << 1) } - - diff --git a/Sources/CodableKitMacros/CodableMacro.swift b/Sources/CodableKitMacros/CodableMacro.swift index 45a14f9..90bd8f5 100644 --- a/Sources/CodableKitMacros/CodableMacro.swift +++ b/Sources/CodableKitMacros/CodableMacro.swift @@ -5,8 +5,8 @@ // Created by Wendell on 3/30/24. // -import Foundation import CodableKitCore +import Foundation import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder diff --git a/Sources/CodableKitMacros/CodeGenCore.swift b/Sources/CodableKitMacros/CodeGenCore.swift index 33f1bda..337885d 100644 --- a/Sources/CodableKitMacros/CodeGenCore.swift +++ b/Sources/CodableKitMacros/CodeGenCore.swift @@ -6,8 +6,8 @@ // Copyright © 2024 WendellXY. All rights reserved. // -import Foundation import CodableKitCore +import Foundation import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder