Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Main features include:
- Customize nested containers during Coding using `@CodingContainer`
- Support specified `CodingKey` for Encode, like `EncodingKey("encode_key")`
- Allow using default values when decoding fails to avoid `keyNotFound` errors
- Allow using `@CodingIgnored` to ignore specific properties during encoding/decoding
- Allow using `@CodingIgnored`, `@EncodingIgnored`, and `@DecodingIgnored` to ignore specific properties during coding
- Support automatic conversion between base64 strings and `Data` `[UInt8]` types using `@Base64Coding`
- Through `@CompactDecoding`, ignore `null` values when Decoding `Array`, `Dictionary`, `Set` instead of throwing errors
- Support various encoding/decoding of `Date` through `@DateCoding`
Expand Down Expand Up @@ -333,15 +333,21 @@ struct Preferences {

### 8. Ignore Properties

Use `@CodingIgnored` to ignore specific properties during encoding/decoding. During decoding, non-`Optional` properties must have a default value to satisfy Swift initialization requirements. `ReerCodable` automatically generates default values for basic data types and collection types. For other custom types, users need to provide default values.
Use `@CodingIgnored` to ignore properties during both encoding and decoding, `@EncodingIgnored` to ignore properties only during encoding, and `@DecodingIgnored` to ignore properties only during decoding. When decoding is ignored, non-`Optional` properties must have a default value to satisfy Swift initialization requirements. `ReerCodable` automatically generates default values for basic data types and collection types. For other custom types, users need to provide default values.

```swift
@Codable
struct User {
var name: String

@CodingIgnored
var ignore: Set<String>
var transient: Set<String>

@EncodingIgnored
var serverToken: String

@DecodingIgnored
var localDraft: String = "draft"
}
```

Expand Down
14 changes: 10 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ReerCodable 框架提供了一系列自定义宏,用于生成动态的 Codable
- 通过使用 `@CodingContainer` 自定义 Coding 时的嵌套容器
- 支持 Encode 时指定的 `CodingKey`, 如 `EncodingKey("encode_key")`
- 允许解码失败时使用默认值, 从而避免 `keyNotFound` 错误发生
- 允许使用 `@CodingIgnored` 在编解码过程中忽略特定属性
- 允许使用 `@CodingIgnored`、`@EncodingIgnored`、`@DecodingIgnored` 在编解码过程中按需忽略特定属性
- 支持使用 `@Base64Coding` 自动对 base64 字符串和 `Data` `[UInt8]` 类型进行转换
- 在 Decode `Array`, `Dictionary`, `Set` 时, 通过 `@CompactDecoding` 可以忽略 `null` 值, 而不是抛出错误
- 支持通过 `@DateCoding` 实现对 `Date` 的各种编解码
Expand Down Expand Up @@ -327,15 +327,21 @@ struct Preferences {

### 8. 忽略属性

使用 `@CodingIgnored` 在编解码过程中忽略特定属性. 在解码过程中对于非 `Optional` 属性要有一个默认值才能满足 Swift 初始化的要求, `ReerCodable` 对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.
使用 `@CodingIgnored` 可以同时在编码和解码时忽略属性, `@EncodingIgnored` 只在编码时忽略, `@DecodingIgnored` 只在解码时忽略. 当属性会被解码侧忽略时, 对于非 `Optional` 属性要有一个默认值才能满足 Swift 初始化的要求, `ReerCodable` 对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需由用户提供默认值.

```swift
@Codable
struct User {
var name: String

@CodingIgnored
var ignore: Set<String>
var transient: Set<String>

@EncodingIgnored
var serverToken: String

@DecodingIgnored
var localDraft: String = "draft"
}
```

Expand Down
17 changes: 17 additions & 0 deletions Sources/ReerCodable/MacroDeclarations/CodingIgnored.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,20 @@
/// ```
@attached(peer)
public macro CodingIgnored() = #externalMacro(module: "ReerCodableMacros", type: "CodingIgnored")

/// The `@EncodingIgnored` macro marks a property to be ignored during encoding only.
///
/// When applied to a property in a type marked with `@Codable`, this property will be:
/// - Skipped during encoding (not written to the encoded data)
/// - Still participate in decoding as usual
@attached(peer)
public macro EncodingIgnored() = #externalMacro(module: "ReerCodableMacros", type: "EncodingIgnored")

/// The `@DecodingIgnored` macro marks a property to be ignored during decoding only.
///
/// When applied to a property in a type marked with `@Codable`, this property will be:
/// - Skipped during decoding (not read from the encoded data)
/// - Still participate in encoding as usual
/// - Initialized with its default value or nil if optional
@attached(peer)
public macro DecodingIgnored() = #externalMacro(module: "ReerCodableMacros", type: "DecodingIgnored")
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public struct CustomCoding: PeerMacro {
if variable.attributes.count > 1 {
let incompatibleMacros = [
"CodingIgnored",
"EncodingIgnored",
"DecodingIgnored",
"Base64Coding",
"DateCoding",
"CompactDecoding",
Expand Down
130 changes: 96 additions & 34 deletions Sources/ReerCodableMacros/MacroImplementations/IgnoreCodingImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,109 @@
import SwiftSyntax
import SwiftSyntaxMacros

private enum IgnoreCodingMode: Equatable {
case both
case encoding
case decoding

var macroName: String {
switch self {
case .both:
return "CodingIgnored"
case .encoding:
return "EncodingIgnored"
case .decoding:
return "DecodingIgnored"
}
}

var diagnosticPrefix: String {
"@\(macroName) macro"
}
}

private func validateIgnoredProperty(
declaration: some DeclSyntaxProtocol,
mode: IgnoreCodingMode
) throws {
guard
let variable = declaration.as(VariableDeclSyntax.self),
let name = variable.name
else {
throw MacroError(text: "\(mode.diagnosticPrefix) is only for property.")
}

let ignoreMacros = ["CodingIgnored", "EncodingIgnored", "DecodingIgnored"]
let usedIgnoreMacros = ignoreMacros.filter { variable.attributes.containsAttribute(named: $0) }
if usedIgnoreMacros.count > 1 {
throw MacroError(
text: "\(mode.diagnosticPrefix) cannot be used together with @\(usedIgnoreMacros.filter { $0 != mode.macroName }.joined(separator: ", @"))."
)
}

if mode == .encoding, variable.attributes.containsAttribute(named: "EncodingKey") {
throw MacroError(text: "@EncodingIgnored macro cannot be used together with @EncodingKey.")
}

if mode != .encoding {
if variable.isOptional {
return
}
if variable.initExpr != nil {
return
}
if let type = variable.type,
canGenerateDefaultValue(for: type) {
return
}
throw MacroError(text: "The ignored property `\(name)` should have a default value, or be set as an optional type.")
}
}

public struct CodingIgnored: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard
let variable = declaration.as(VariableDeclSyntax.self),
let name = variable.name
else {
throw MacroError(text: "@CodingIgnored macro is only for property.")
}

if variable.attributes.firstAttribute(named: "CodingIgnored") != nil {
if variable.isOptional {
return []
}
if variable.initExpr != nil {
return []
}
if let type = variable.type,
canGenerateDefaultValue(for: type) {
return []
}
throw MacroError(text: "The ignored property `\(name)` should have a default value, or be set as an optional type.")
}
try validateIgnoredProperty(declaration: declaration, mode: .both)
return []
}

static func canGenerateDefaultValue(for type: String) -> Bool {
let trimmed = type.trimmingCharacters(in: .whitespaces)
let basicType = [
"Int", "Int8", "Int16", "Int32", "Int64", "Int128",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128",
"Bool", "String", "Float", "Double"
].contains(trimmed)
if basicType
|| (trimmed.hasPrefix("[") && trimmed.hasSuffix("]"))
|| trimmed.hasPrefix("Set<") {
return true
}
return false
}

public struct EncodingIgnored: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
try validateIgnoredProperty(declaration: declaration, mode: .encoding)
return []
}
}

public struct DecodingIgnored: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
try validateIgnoredProperty(declaration: declaration, mode: .decoding)
return []
}
}

func canGenerateDefaultValue(for type: String) -> Bool {
let trimmed = type.trimmingCharacters(in: .whitespaces)
let basicType = [
"Int", "Int8", "Int16", "Int32", "Int64", "Int128",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128",
"Bool", "String", "Float", "Double"
].contains(trimmed)
if basicType
|| (trimmed.hasPrefix("[") && trimmed.hasSuffix("]"))
|| trimmed.hasPrefix("Set<") {
return true
}
return false
}
2 changes: 2 additions & 0 deletions Sources/ReerCodableMacros/Plugins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ struct ReerCodablePlugin: CompilerPlugin {
CodingKey.self,
EncodingKey.self,
CodingIgnored.self,
EncodingIgnored.self,
DecodingIgnored.self,
DecodingDefault.self,
EncodingDefault.self,
CodingDefault.self,
Expand Down
3 changes: 2 additions & 1 deletion Sources/ReerCodableMacros/PropertyInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ struct PropertyInfo {
var name: String
var type: String
var isOptional = false
var isIgnored = false
var ignoreEncoding = false
var ignoreDecoding = false
var keys: [String] = []
var encodingKey: String?
var treatDotAsNestedWhenEncoding: Bool = true
Expand Down
15 changes: 11 additions & 4 deletions Sources/ReerCodableMacros/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,14 @@ extension TypeInfo {
property.caseStyles = propertyCaseStyles.uniqueMerged(with: caseStyles)
// ignore coding
if variable.attributes.firstAttribute(named: "CodingIgnored") != nil {
property.isIgnored = true
property.ignoreEncoding = true
property.ignoreDecoding = true
}
if variable.attributes.firstAttribute(named: "EncodingIgnored") != nil {
property.ignoreEncoding = true
}
if variable.attributes.firstAttribute(named: "DecodingIgnored") != nil {
property.ignoreDecoding = true
}
// base64 coding
if variable.attributes.containsAttribute(named: "Base64Coding") {
Expand Down Expand Up @@ -681,7 +688,7 @@ extension TypeInfo {
} else {
assignments = try properties
.compactMap { property in
if property.isIgnored {
if property.ignoreDecoding {
if property.isOptional { return nil }
if let initExpr = property.initExpr {
return "self.\(property.name) = \(initExpr)"
Expand Down Expand Up @@ -824,7 +831,7 @@ extension TypeInfo {
} else {
encoding = properties
.compactMap { property in
if property.isIgnored { return nil }
if property.ignoreEncoding { return nil }
let valueExpr = property.encodingValueExpr
let resolvedValueExpr = property.resolvedEncodingValueExpr
let needsOptionalHandling = property.needsOptionalEncodingHandling
Expand Down Expand Up @@ -914,7 +921,7 @@ extension TypeInfo {
text += ": \(property.type)"
if let initExpr = property.initExpr {
text += "= \(initExpr)"
} else if property.isIgnored, let defaultValue = property.defaultValue {
} else if property.ignoreDecoding, let defaultValue = property.defaultValue {
text += "= \(defaultValue)"
} else if property.isOptional {
text += "= nil"
Expand Down
Loading
Loading