Skip to content

Commit 69cc0de

Browse files
authored
feat: added separate default value options for missing value and other errors (#81)
1 parent c1fe6f4 commit 69cc0de

18 files changed

+3207
-133
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,14 @@ Supercharge `Swift`'s `Codable` implementations with macros.
2222
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
2323
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
2424
- Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``.
25-
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
25+
- Allows to provide default value in case of decoding failures with ``Default(_:)``, or only in case of failures when missing value with ``Default(ifMissing:)``. Different default values can also be used for value missing and other errors respectively with ``Default(ifMissing:forErrors:)``.
2626
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
2727
- Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``.
2828
- Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``.
2929
- Allows decoding/encoding enums that lack distinct identifiers for each case data with ``UnTagged()``.
3030
- Allows to ignore specific properties/cases from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``IgnoreEncoding()``.
3131
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type/case to work with different case style keys with ``CodingKeys(_:)``.
32-
- Allows to ignore all initialized properties of a type/case from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
33-
``CodedBy(_:)``, ``Default(_:)`` etc.
32+
- Allows to ignore all initialized properties of a type/case from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``, ``CodedBy(_:)``, ``Default(_:)`` etc.
3433
- Allows to generate protocol decoding/encoding ``HelperCoder``s with `MetaProtocolCodable` build tool plugin from ``DynamicCodable`` types.
3534

3635
[**See the limitations for this macro**](<doc:Limitations>).

Sources/MetaCodable/Default.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,65 @@
2727
@available(swift 5.9)
2828
public macro Default<T>(_ default: T) =
2929
#externalMacro(module: "MacroPlugin", type: "Default")
30+
31+
/// Provides a `default` value to be used when value is missing
32+
/// and when not initialized explicitly in memberwise initializer(s).
33+
///
34+
/// If the value is missing , the default value will be used instead of
35+
/// throwing error and terminating decoding. i.e. for a field declared as:
36+
/// ```swift
37+
/// @Default(ifMissing: "some")
38+
/// let field: String
39+
/// ```
40+
/// if empty json provided
41+
/// ```json
42+
/// {}
43+
/// ```
44+
/// the default value provided in this case `some` will be used as
45+
/// `field`'s value.
46+
///
47+
/// - Parameter default: The default value to use.
48+
///
49+
/// - Note: This macro on its own only validates if attached declaration
50+
/// is a variable declaration. ``Codable()`` macro uses this macro
51+
/// when generating final implementations.
52+
///
53+
/// - Important: The field type must confirm to `Codable` and
54+
/// default value type `T` must be the same as field type.
55+
@attached(peer)
56+
@available(swift 5.9)
57+
public macro Default<T>(ifMissing default: T) =
58+
#externalMacro(module: "MacroPlugin", type: "Default")
59+
60+
/// Provides different `default` values to be used for missing value
61+
/// and decoding errors..
62+
///
63+
/// If the value is missing, the `missingDefault` value will be used,
64+
/// while for incorrect data type, `errorDefault` value will be used,
65+
/// instead of throwing error and terminating decoding.
66+
/// i.e. for a field declared as:
67+
/// ```swift
68+
/// @Default(ifMissing: "some", forErrors: "another")
69+
/// let field: String
70+
/// ```
71+
/// if type at `CodingKey` is different or empty json provided
72+
/// ```json
73+
/// { "field": 5 } // or {}
74+
/// ```
75+
/// the default value `some` and `another` will be used as
76+
/// `field`'s value respectively.
77+
///
78+
/// - Parameters:
79+
/// - missingDefault: The default value to use when value is missing.
80+
/// - errorDefault: The default value to use for other errors.
81+
///
82+
/// - Note: This macro on its own only validates if attached declaration
83+
/// is a variable declaration. ``Codable()`` macro uses this macro
84+
/// when generating final implementations.
85+
///
86+
/// - Important: The field type must confirm to `Codable` and
87+
/// default value type `T` must be the same as field type.
88+
@attached(peer)
89+
@available(swift 5.9)
90+
public macro Default<T>(ifMissing missingDefault: T, forErrors errorDefault: T) =
91+
#externalMacro(module: "MacroPlugin", type: "Default")

Sources/MetaCodable/MetaCodable.docc/MetaCodable.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ Supercharge `Swift`'s `Codable` implementations with macros.
1717
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
1818
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
1919
- Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``.
20-
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
20+
- Allows to provide default value in case of decoding failures with ``Default(_:)``, or only in case of failures when missing value with ``Default(ifMissing:)``. Different default values can also be used for value missing and other errors respectively with ``Default(ifMissing:forErrors:)``.
2121
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
2222
- Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``.
2323
- Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``.
2424
- Allows decoding/encoding enums that lack distinct identifiers for each case data with ``UnTagged()``.
2525
- Allows to ignore specific properties/cases from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``IgnoreEncoding()``.
2626
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type/case to work with different case style keys with ``CodingKeys(_:)``.
27-
- Allows to ignore all initialized properties of a type/case from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
28-
``CodedBy(_:)``, ``Default(_:)`` etc.
27+
- Allows to ignore all initialized properties of a type/case from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``, ``CodedBy(_:)``, ``Default(_:)`` etc.
2928
- Allows to generate protocol decoding/encoding ``HelperCoder``s with `MetaProtocolCodable` build tool plugin from ``DynamicCodable`` types.
3029

3130
[**See the limitations for this macro**](<doc:Limitations>).
@@ -77,6 +76,8 @@ Supercharge `Swift`'s `Codable` implementations with macros.
7776
- ``CodedAt(_:)``
7877
- ``CodedIn(_:)``
7978
- ``Default(_:)``
79+
- ``Default(ifMissing:)``
80+
- ``Default(ifMissing:forErrors:)``
8081
- ``CodedBy(_:)``
8182
- ``CodedAs()``
8283
- ``CodedAs(_:_:)``

Sources/PluginCore/Attributes/Default.swift

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,34 @@ package struct Default: PropertyAttribute {
1010
/// during initialization.
1111
let node: AttributeSyntax
1212

13-
/// The default value expression provided.
14-
var expr: ExprSyntax {
15-
return node.arguments!
13+
/// The default value expression provided for value missing case.
14+
///
15+
/// This expression should be used only when value is missing
16+
/// in the decoding syntax.
17+
var onMissingExpr: ExprSyntax {
18+
return node.arguments?.as(LabeledExprListSyntax.self)?.first { expr in
19+
expr.label?.tokenKind == .identifier("ifMissing")
20+
}?.expression ?? node.arguments!
1621
.as(LabeledExprListSyntax.self)!.first!.expression
1722
}
1823

24+
/// The default value expression provided for errors.
25+
///
26+
/// This expression should be used for errors other than
27+
/// value is missing in the decoding syntax.
28+
var onErrorExpr: ExprSyntax? {
29+
guard
30+
let exprs = node.arguments?.as(LabeledExprListSyntax.self),
31+
!exprs.isEmpty
32+
else { return nil }
33+
guard
34+
exprs.count > 1 || exprs.first?.label != nil
35+
else { return exprs.first!.expression }
36+
return node.arguments?.as(LabeledExprListSyntax.self)?.first { expr in
37+
expr.label?.tokenKind == .identifier("forErrors")
38+
}?.expression
39+
}
40+
1941
/// Creates a new instance with the provided node.
2042
///
2143
/// The initializer fails to create new instance if the name
@@ -72,7 +94,9 @@ where
7294
func addDefaultValueIfExists() -> Registration<Decl, Key, DefOutput> {
7395
guard let attr = Default(from: self.decl)
7496
else { return self.updating(with: self.variable.any) }
75-
let newVar = self.variable.with(default: attr.expr)
97+
let newVar = self.variable.with(
98+
onMissingExpr: attr.onMissingExpr, onErrorExpr: attr.onErrorExpr
99+
)
76100
return self.updating(with: newVar.any)
77101
}
78102
}
@@ -84,9 +108,16 @@ where Initialization == RequiredInitialization {
84108
/// `DefaultValueVariable` is created with this variable as base
85109
/// and default expression provided.
86110
///
87-
/// - Parameter expr: The default expression to add.
111+
/// - Parameters:
112+
/// - mExpr: The missing value default expression to add.
113+
/// - eExpr: The other errors default expression to add.
114+
///
88115
/// - Returns: Created variable data with default expression.
89-
func with(default expr: ExprSyntax) -> DefaultValueVariable<Self> {
90-
return .init(base: self, options: .init(expr: expr))
116+
func with(
117+
onMissingExpr mExpr: ExprSyntax, onErrorExpr eExpr: ExprSyntax?
118+
) -> DefaultValueVariable<Self> {
119+
return .init(
120+
base: self, options: .init(onMissingExpr: mExpr, onErrorExpr: eExpr)
121+
)
91122
}
92123
}

Sources/PluginCore/Variables/Enum/Switcher/UnTaggedEnumSwitcher.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ struct UnTaggedEnumSwitcher: EnumSwitcherVariable {
6262
func transform(
6363
variable: BasicAssociatedVariable
6464
) -> BasicAssociatedVariable {
65-
let fallback = DecodingFallback.ifError("throw \(error)")
65+
let `throw`: CodeBlockItemListSyntax = "throw \(error)"
66+
let fallback = DecodingFallback.ifMissing(`throw`, ifError: `throw`)
6667
return .init(
6768
base: variable.base, label: variable.label,
6869
fallback: fallback

Sources/PluginCore/Variables/Property/BasicPropertyVariable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ struct BasicPropertyVariable: DefaultPropertyVariable, DeclaredVariable {
7474
/// `nil` value only when missing or `null`.
7575
var decodingFallback: DecodingFallback {
7676
guard hasOptionalType else { return .throw }
77-
return .ifMissing("\(decodePrefix)\(name) = nil")
77+
return .onlyIfMissing("\(decodePrefix)\(name) = nil")
7878
}
7979

8080
/// Creates a new variable with provided data.

Sources/PluginCore/Variables/Property/Data/DecodingFallback.swift

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ package enum DecodingFallback {
1616
///
1717
/// Indicates if variable data is missing or `null`,
1818
/// provided fallback syntax will be used for initialization.
19-
case ifMissing(CodeBlockItemListSyntax)
19+
case onlyIfMissing(CodeBlockItemListSyntax)
2020
/// Represents fallback option handling
2121
/// decoding failure completely.
2222
///
2323
/// Indicates for any type of failure error in decoding,
24-
/// provided fallback syntax will be used for initialization.
25-
case ifError(CodeBlockItemListSyntax)
24+
/// provided fallback syntaxes will be used for initialization.
25+
///
26+
/// First syntax will be used for errors due to missing or `null`
27+
/// value while second syntax will be used for all the other errors.
28+
case ifMissing(CodeBlockItemListSyntax, ifError: CodeBlockItemListSyntax)
2629

2730
/// Represents container for decoding/encoding properties.
2831
typealias Container = PropertyVariableTreeNode.CodingLocation.Container
@@ -76,7 +79,7 @@ package enum DecodingFallback {
7679
let isOptional: Bool
7780
let fallbacks: CodeBlockItemListSyntax
7881
switch self {
79-
case .ifError(let eFallbacks):
82+
case .ifMissing(_, ifError: let eFallbacks):
8083
isOptional = true
8184
fallbacks = eFallbacks
8285
default:
@@ -146,7 +149,7 @@ package enum DecodingFallback {
146149
generated.syntax
147150
}
148151
conditionalSyntax = generated.conditionalSyntax
149-
case .ifMissing(let fallbacks):
152+
case .onlyIfMissing(let fallbacks):
150153
let generated = decoding(.init(name: nContainer, isOptional: true))
151154
syntax = CodeBlockItemListSyntax {
152155
if nestedContainer == nil {
@@ -167,26 +170,40 @@ package enum DecodingFallback {
167170
fallbacks
168171
}
169172
}
170-
case .ifError(let fallbacks):
173+
case let .ifMissing(fallbacks, ifError: eFallbacks):
171174
let generated = decoding(.init(name: nContainer, isOptional: true))
175+
let containerMissing: TokenSyntax = "\(nContainer)Missing"
172176
syntax = CodeBlockItemListSyntax {
173177
if nestedContainer == nil {
174-
"""
175-
let \(nContainer) = try? \(container.syntax).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
176-
"""
178+
"let \(nContainer): KeyedDecodingContainer<\(key.typeName)>?"
179+
"let \(containerMissing): Bool"
180+
try! IfExprSyntax(
181+
"""
182+
if (try? \(container.syntax).decodeNil(forKey: \(key.expr))) == false
183+
"""
184+
) {
185+
"""
186+
\(nContainer) = try? \(container.syntax).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
187+
"""
188+
"\(containerMissing) = false"
189+
} else: {
190+
"\(nContainer) = nil"
191+
"\(containerMissing) = true"
192+
}
177193
}
178194
generated.syntax
179195
}
180196
conditionalSyntax = CodeBlockItemListSyntax {
181197
try! IfExprSyntax(
182-
"""
183-
if let \(nContainer) = \(nContainer)
184-
"""
185-
) {
186-
generated.conditionalSyntax
187-
} else: {
188-
fallbacks
189-
}
198+
"if let \(nContainer) = \(nContainer)",
199+
bodyBuilder: {
200+
generated.conditionalSyntax
201+
}, elseIf: IfExprSyntax("if \(containerMissing)") {
202+
fallbacks
203+
} else: {
204+
eFallbacks
205+
}
206+
)
190207
}
191208
}
192209
return .init(syntax: syntax, conditionalSyntax: conditionalSyntax)
@@ -210,36 +227,37 @@ extension DecodingFallback {
210227
false
211228
}
212229
}
213-
214-
/// The combined fallback option for all variable elements.
230+
231+
/// Adds two decoding fallbacks into a single fallback.
215232
///
216-
/// Represents the fallback to use when decoding container
217-
/// of all the element variables fails.
233+
/// The generated single fallback represents both
234+
/// the fallback syntaxes added.
218235
///
219-
/// - Parameter fallbacks: The fallback values to combine.
220-
/// - Returns: The aggregated fallback value.
221-
static func aggregate<C: Collection>(
222-
fallbacks: C
223-
) -> Self where C.Element == Self {
224-
var aggregated = C.Element.ifError(.init())
225-
for fallback in fallbacks {
226-
switch (aggregated, fallback) {
227-
case (_, .throw), (.throw, _):
228-
return .throw
229-
case (.ifMissing(var a), .ifMissing(let f)),
230-
(.ifMissing(var a), .ifError(let f)),
231-
(.ifError(var a), .ifMissing(let f)):
232-
if !hasEarlyExit(in: a) {
233-
a.append(contentsOf: f)
234-
}
235-
aggregated = .ifMissing(a)
236-
case (.ifError(var a), .ifError(let f)):
237-
if !hasEarlyExit(in: a) {
238-
a.append(contentsOf: f)
239-
}
240-
aggregated = .ifError(a)
236+
/// - Parameters:
237+
/// - lhs: The first value to add.
238+
/// - rhs: The second value to add.
239+
///
240+
/// - Returns: The generated fallback value.
241+
static func + (lhs: Self, rhs: Self) -> Self {
242+
switch (lhs, rhs) {
243+
case (.throw, _), (_, .throw):
244+
return .throw
245+
case (.onlyIfMissing(let lf), .onlyIfMissing(let rf)), (.onlyIfMissing(let lf), .ifMissing(let rf, ifError: _)), (.ifMissing(let lf, ifError: _), .onlyIfMissing(let rf)):
246+
var fallbacks = lf
247+
if !hasEarlyExit(in: fallbacks) {
248+
fallbacks.append(contentsOf: rf)
249+
}
250+
return .onlyIfMissing(fallbacks)
251+
case let (.ifMissing(lf, ifError: lef), .ifMissing(rf, ifError: ref)):
252+
var fallbacks = lf
253+
if !hasEarlyExit(in: fallbacks) {
254+
fallbacks.append(contentsOf: rf)
255+
}
256+
var eFallbacks = lef
257+
if !hasEarlyExit(in: eFallbacks) {
258+
eFallbacks.append(contentsOf: ref)
241259
}
260+
return .ifMissing(fallbacks, ifError: eFallbacks)
242261
}
243-
return aggregated
244262
}
245263
}

0 commit comments

Comments
 (0)