Skip to content

Commit 495cea4

Browse files
authored
fix: fixed nested decoding with missing container (#44)
1 parent 31db2fd commit 495cea4

19 files changed

+4008
-1339
lines changed

Sources/CodableMacroPlugin/Registration/Node.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,26 @@ extension Registrar.Node {
143143
)
144144
}
145145
case .container(let container, let key):
146-
let nestedContainer: TokenSyntax = "\(key.raw)_\(container)"
147-
"""
148-
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
149-
"""
150-
for (cKey, node) in children {
151-
node.decoding(
152-
in: context,
153-
from: .container(nestedContainer, key: cKey)
154-
)
155-
}
146+
children.lazy
147+
.flatMap(\.value.linkedVariables)
148+
.map(\.decodingFallback)
149+
.aggregate
150+
.represented(
151+
decodingContainer: container,
152+
fromKey: key
153+
) { nestedContainer in
154+
return CodeBlockItemListSyntax {
155+
for (cKey, node) in children {
156+
node.decoding(
157+
in: context,
158+
from: .container(
159+
nestedContainer,
160+
key: cKey
161+
)
162+
)
163+
}
164+
}
165+
}
156166
}
157167
}
158168
}

Sources/CodableMacroPlugin/Variables/AnyVariable.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ struct AnyVariable<Initialization: VariableInitialization>: Variable {
4949
/// `Encodable` conformance.
5050
var requireEncodable: Bool? { base.requireEncodable }
5151

52+
/// The fallback behavior when decoding fails.
53+
///
54+
/// In the event this decoding this variable is failed,
55+
/// appropriate fallback would be applied.
56+
///
57+
/// Provides fallback for the underlying variable value.
58+
var decodingFallback: DecodingFallback { base.decodingFallback }
59+
5260
/// Wraps the provided variable erasing its type and
5361
/// initialization type.
5462
///

Sources/CodableMacroPlugin/Variables/BasicVariable.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ struct BasicVariable: BasicCodingVariable {
4949
/// initialization.
5050
var requireEncodable: Bool? { self.encode }
5151

52+
/// The fallback behavior when decoding fails.
53+
///
54+
/// In the event this decoding this variable is failed,
55+
/// appropriate fallback would be applied.
56+
///
57+
/// If variable is of optional type, variable will be assigned
58+
/// `nil` value only when missing or `null`.
59+
var decodingFallback: DecodingFallback {
60+
guard type.isOptional else { return .throw }
61+
return .ifMissing("self.\(name) = nil")
62+
}
63+
5264
/// Creates a new variable with provided data.
5365
///
5466
/// Basic implementation for this variable provided

Sources/CodableMacroPlugin/Variables/ComposedVariable.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ extension ComposedVariable {
3636
/// Provides type of the underlying variable value.
3737
var type: TypeSyntax { base.type }
3838

39+
/// The fallback behavior when decoding fails.
40+
///
41+
/// In the event this decoding this variable is failed,
42+
/// appropriate fallback would be applied.
43+
///
44+
/// Provides fallback for the underlying variable value.
45+
var decodingFallback: DecodingFallback { base.decodingFallback }
46+
3947
/// Provides the code syntax for decoding this variable
4048
/// at the provided location.
4149
///
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
@_implementationOnly import SwiftSyntax
2+
3+
/// Represents possible fallback options for decoding failure.
4+
///
5+
/// When decoding fails for variable, variable can have fallback
6+
/// to throw the failure error or handle it completely or handle
7+
/// it only when variable is missing or `null`.
8+
enum DecodingFallback {
9+
/// Represents no fallback option.
10+
///
11+
/// Indicates decoding failure error
12+
/// is thrown without any handling.
13+
case `throw`
14+
/// Represents fallback option for missing
15+
/// or `null` value.
16+
///
17+
/// Indicates if variable data is missing or `null`,
18+
/// provided fallback syntax will be used for initialization.
19+
case ifMissing(CodeBlockItemListSyntax)
20+
/// Represents fallback option handling
21+
/// decoding failure completely.
22+
///
23+
/// Indicates for any type of failure error in decoding,
24+
/// provided fallback syntax will be used for initialization.
25+
case ifError(CodeBlockItemListSyntax)
26+
27+
/// Provides the code block list syntax for decoding provided
28+
/// container applying current fallback options.
29+
///
30+
/// - Parameters:
31+
/// - container: The container to decode from.
32+
/// - key: The key from where to decode.
33+
/// - decoding: The nested container decoding
34+
/// code block generator.
35+
///
36+
/// - Returns: The generated code block.
37+
func represented(
38+
decodingContainer container: TokenSyntax,
39+
fromKey key: Registrar.Key,
40+
nestedDecoding decoding: (TokenSyntax) -> CodeBlockItemListSyntax
41+
) -> CodeBlockItemListSyntax {
42+
let nestedContainer: TokenSyntax = "\(key.raw)_\(container)"
43+
return CodeBlockItemListSyntax {
44+
switch self {
45+
case .throw:
46+
"""
47+
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
48+
"""
49+
decoding(nestedContainer)
50+
case .ifMissing(let fallbacks):
51+
try! IfExprSyntax(
52+
"""
53+
if (try? \(container).decodeNil(forKey: \(key.expr))) == false
54+
"""
55+
) {
56+
"""
57+
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
58+
"""
59+
decoding(nestedContainer)
60+
} else: {
61+
fallbacks
62+
}
63+
case .ifError(let fallbacks):
64+
try! IfExprSyntax(
65+
"""
66+
if let \(nestedContainer) = try? \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
67+
"""
68+
) {
69+
decoding(nestedContainer)
70+
} else: {
71+
fallbacks
72+
}
73+
}
74+
}
75+
}
76+
}
77+
78+
extension Collection where Element == DecodingFallback {
79+
/// The combined fallback option for all variable elements.
80+
///
81+
/// Represents the fallback to use when decoding container
82+
/// of all the element variables fails.
83+
var aggregate: Element {
84+
var aggregated = Element.ifError(.init())
85+
for fallback in self {
86+
switch (aggregated, fallback) {
87+
case (_, .throw), (.throw, _):
88+
return .throw
89+
case (.ifMissing(var a), .ifMissing(let f)),
90+
(.ifMissing(var a), .ifError(let f)),
91+
(.ifError(var a), .ifMissing(let f)):
92+
a.append(contentsOf: f)
93+
aggregated = .ifMissing(a)
94+
case (.ifError(var a), .ifError(let f)):
95+
a.append(contentsOf: f)
96+
aggregated = .ifError(a)
97+
}
98+
}
99+
return aggregated
100+
}
101+
}

Sources/CodableMacroPlugin/Variables/DefaultValueVariable.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ where Var.Initialization == RequiredInitialization {
5555
/// `Encodable` conformance.
5656
var requireEncodable: Bool? { base.requireEncodable }
5757

58+
/// The fallback behavior when decoding fails.
59+
///
60+
/// In the event this decoding this variable is failed,
61+
/// appropriate fallback would be applied.
62+
///
63+
/// This variable will be initialized with default expression
64+
/// provided, if decoding fails.
65+
var decodingFallback: DecodingFallback {
66+
return .ifError("self.\(name) = \(options.expr)")
67+
}
68+
5869
/// Indicates the initialization type for this variable.
5970
///
6071
/// Provides default initialization value in initialization

Sources/CodableMacroPlugin/Variables/Variable.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ protocol Variable<Initialization> {
6262
/// generic where clause by default.
6363
var requireEncodable: Bool? { get }
6464

65+
/// The fallback behavior when decoding fails.
66+
///
67+
/// In the event this decoding this variable is failed,
68+
/// appropriate fallback would be applied.
69+
var decodingFallback: DecodingFallback { get }
70+
6571
/// Indicates the initialization type for this variable.
6672
///
6773
/// Indicates whether initialization is required, optional
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#if SWIFT_SYNTAX_EXTENSION_MACRO_FIXED
2+
import SwiftDiagnostics
3+
import XCTest
4+
5+
@testable import CodableMacroPlugin
6+
7+
final class CodedByTests: XCTestCase {
8+
9+
func testMisuseOnNonVariableDeclaration() throws {
10+
assertMacroExpansion(
11+
"""
12+
struct SomeCodable {
13+
@CodedBy(Since1970DateCoder())
14+
func someFunc() {
15+
}
16+
}
17+
""",
18+
expandedSource:
19+
"""
20+
struct SomeCodable {
21+
func someFunc() {
22+
}
23+
}
24+
""",
25+
diagnostics: [
26+
.init(
27+
id: CodedBy.misuseID,
28+
message:
29+
"@CodedBy only applicable to variable declarations",
30+
line: 2, column: 5,
31+
fixIts: [
32+
.init(message: "Remove @CodedBy attribute")
33+
]
34+
)
35+
]
36+
)
37+
}
38+
39+
func testMisuseOnStaticVariable() throws {
40+
assertMacroExpansion(
41+
"""
42+
struct SomeCodable {
43+
@CodedBy(Since1970DateCoder())
44+
static let value: String
45+
}
46+
""",
47+
expandedSource:
48+
"""
49+
struct SomeCodable {
50+
static let value: String
51+
}
52+
""",
53+
diagnostics: [
54+
.init(
55+
id: CodedBy.misuseID,
56+
message:
57+
"@CodedBy can't be used with static variables declarations",
58+
line: 2, column: 5,
59+
fixIts: [
60+
.init(message: "Remove @CodedBy attribute")
61+
]
62+
)
63+
]
64+
)
65+
}
66+
67+
func testDuplicatedMisuse() throws {
68+
assertMacroExpansion(
69+
"""
70+
struct SomeCodable {
71+
@CodedBy(Since1970DateCoder())
72+
@CodedBy(Since1970DateCoder())
73+
let one: String
74+
}
75+
""",
76+
expandedSource:
77+
"""
78+
struct SomeCodable {
79+
let one: String
80+
}
81+
""",
82+
diagnostics: [
83+
.init(
84+
id: CodedBy.misuseID,
85+
message:
86+
"@CodedBy can only be applied once per declaration",
87+
line: 2, column: 5,
88+
fixIts: [
89+
.init(message: "Remove @CodedBy attribute")
90+
]
91+
),
92+
.init(
93+
id: CodedBy.misuseID,
94+
message:
95+
"@CodedBy can only be applied once per declaration",
96+
line: 3, column: 5,
97+
fixIts: [
98+
.init(message: "Remove @CodedBy attribute")
99+
]
100+
),
101+
]
102+
)
103+
}
104+
}
105+
#endif

0 commit comments

Comments
 (0)