Skip to content

Commit

Permalink
fix: fixed nested decoding with missing container (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Dec 8, 2023
1 parent 31db2fd commit 495cea4
Show file tree
Hide file tree
Showing 19 changed files with 4,008 additions and 1,339 deletions.
30 changes: 20 additions & 10 deletions Sources/CodableMacroPlugin/Registration/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,26 @@ extension Registrar.Node {
)
}
case .container(let container, let key):
let nestedContainer: TokenSyntax = "\(key.raw)_\(container)"
"""
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
"""
for (cKey, node) in children {
node.decoding(
in: context,
from: .container(nestedContainer, key: cKey)
)
}
children.lazy
.flatMap(\.value.linkedVariables)
.map(\.decodingFallback)
.aggregate
.represented(
decodingContainer: container,
fromKey: key
) { nestedContainer in
return CodeBlockItemListSyntax {
for (cKey, node) in children {
node.decoding(
in: context,
from: .container(
nestedContainer,
key: cKey
)
)
}
}
}
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodableMacroPlugin/Variables/AnyVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ struct AnyVariable<Initialization: VariableInitialization>: Variable {
/// `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// The fallback behavior when decoding fails.
///
/// In the event this decoding this variable is failed,
/// appropriate fallback would be applied.
///
/// Provides fallback for the underlying variable value.
var decodingFallback: DecodingFallback { base.decodingFallback }

/// Wraps the provided variable erasing its type and
/// initialization type.
///
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodableMacroPlugin/Variables/BasicVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ struct BasicVariable: BasicCodingVariable {
/// initialization.
var requireEncodable: Bool? { self.encode }

/// The fallback behavior when decoding fails.
///
/// In the event this decoding this variable is failed,
/// appropriate fallback would be applied.
///
/// If variable is of optional type, variable will be assigned
/// `nil` value only when missing or `null`.
var decodingFallback: DecodingFallback {
guard type.isOptional else { return .throw }
return .ifMissing("self.\(name) = nil")
}

/// Creates a new variable with provided data.
///
/// Basic implementation for this variable provided
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodableMacroPlugin/Variables/ComposedVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ extension ComposedVariable {
/// Provides type of the underlying variable value.
var type: TypeSyntax { base.type }

/// The fallback behavior when decoding fails.
///
/// In the event this decoding this variable is failed,
/// appropriate fallback would be applied.
///
/// Provides fallback for the underlying variable value.
var decodingFallback: DecodingFallback { base.decodingFallback }

/// Provides the code syntax for decoding this variable
/// at the provided location.
///
Expand Down
101 changes: 101 additions & 0 deletions Sources/CodableMacroPlugin/Variables/Data/DecodingFallback.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@_implementationOnly import SwiftSyntax

/// Represents possible fallback options for decoding failure.
///
/// When decoding fails for variable, variable can have fallback
/// to throw the failure error or handle it completely or handle
/// it only when variable is missing or `null`.
enum DecodingFallback {
/// Represents no fallback option.
///
/// Indicates decoding failure error
/// is thrown without any handling.
case `throw`
/// Represents fallback option for missing
/// or `null` value.
///
/// Indicates if variable data is missing or `null`,
/// provided fallback syntax will be used for initialization.
case ifMissing(CodeBlockItemListSyntax)
/// Represents fallback option handling
/// decoding failure completely.
///
/// Indicates for any type of failure error in decoding,
/// provided fallback syntax will be used for initialization.
case ifError(CodeBlockItemListSyntax)

/// Provides the code block list syntax for decoding provided
/// container applying current fallback options.
///
/// - Parameters:
/// - container: The container to decode from.
/// - key: The key from where to decode.
/// - decoding: The nested container decoding
/// code block generator.
///
/// - Returns: The generated code block.
func represented(
decodingContainer container: TokenSyntax,
fromKey key: Registrar.Key,
nestedDecoding decoding: (TokenSyntax) -> CodeBlockItemListSyntax
) -> CodeBlockItemListSyntax {
let nestedContainer: TokenSyntax = "\(key.raw)_\(container)"
return CodeBlockItemListSyntax {
switch self {
case .throw:
"""
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
"""
decoding(nestedContainer)
case .ifMissing(let fallbacks):
try! IfExprSyntax(
"""
if (try? \(container).decodeNil(forKey: \(key.expr))) == false
"""
) {
"""
let \(nestedContainer) = try \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
"""
decoding(nestedContainer)
} else: {
fallbacks
}
case .ifError(let fallbacks):
try! IfExprSyntax(
"""
if let \(nestedContainer) = try? \(container).nestedContainer(keyedBy: \(key.type), forKey: \(key.expr))
"""
) {
decoding(nestedContainer)
} else: {
fallbacks
}
}
}
}
}

extension Collection where Element == DecodingFallback {
/// The combined fallback option for all variable elements.
///
/// Represents the fallback to use when decoding container
/// of all the element variables fails.
var aggregate: Element {
var aggregated = Element.ifError(.init())
for fallback in self {
switch (aggregated, fallback) {
case (_, .throw), (.throw, _):
return .throw
case (.ifMissing(var a), .ifMissing(let f)),
(.ifMissing(var a), .ifError(let f)),
(.ifError(var a), .ifMissing(let f)):
a.append(contentsOf: f)
aggregated = .ifMissing(a)
case (.ifError(var a), .ifError(let f)):
a.append(contentsOf: f)
aggregated = .ifError(a)
}
}
return aggregated
}
}
11 changes: 11 additions & 0 deletions Sources/CodableMacroPlugin/Variables/DefaultValueVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ where Var.Initialization == RequiredInitialization {
/// `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// The fallback behavior when decoding fails.
///
/// In the event this decoding this variable is failed,
/// appropriate fallback would be applied.
///
/// This variable will be initialized with default expression
/// provided, if decoding fails.
var decodingFallback: DecodingFallback {
return .ifError("self.\(name) = \(options.expr)")
}

/// Indicates the initialization type for this variable.
///
/// Provides default initialization value in initialization
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodableMacroPlugin/Variables/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ protocol Variable<Initialization> {
/// generic where clause by default.
var requireEncodable: Bool? { get }

/// The fallback behavior when decoding fails.
///
/// In the event this decoding this variable is failed,
/// appropriate fallback would be applied.
var decodingFallback: DecodingFallback { get }

/// Indicates the initialization type for this variable.
///
/// Indicates whether initialization is required, optional
Expand Down
105 changes: 105 additions & 0 deletions Tests/MetaCodableTests/Attributes/CodedByTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#if SWIFT_SYNTAX_EXTENSION_MACRO_FIXED
import SwiftDiagnostics
import XCTest

@testable import CodableMacroPlugin

final class CodedByTests: XCTestCase {

func testMisuseOnNonVariableDeclaration() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@CodedBy(Since1970DateCoder())
func someFunc() {
}
}
""",
expandedSource:
"""
struct SomeCodable {
func someFunc() {
}
}
""",
diagnostics: [
.init(
id: CodedBy.misuseID,
message:
"@CodedBy only applicable to variable declarations",
line: 2, column: 5,
fixIts: [
.init(message: "Remove @CodedBy attribute")
]
)
]
)
}

func testMisuseOnStaticVariable() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@CodedBy(Since1970DateCoder())
static let value: String
}
""",
expandedSource:
"""
struct SomeCodable {
static let value: String
}
""",
diagnostics: [
.init(
id: CodedBy.misuseID,
message:
"@CodedBy can't be used with static variables declarations",
line: 2, column: 5,
fixIts: [
.init(message: "Remove @CodedBy attribute")
]
)
]
)
}

func testDuplicatedMisuse() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@CodedBy(Since1970DateCoder())
@CodedBy(Since1970DateCoder())
let one: String
}
""",
expandedSource:
"""
struct SomeCodable {
let one: String
}
""",
diagnostics: [
.init(
id: CodedBy.misuseID,
message:
"@CodedBy can only be applied once per declaration",
line: 2, column: 5,
fixIts: [
.init(message: "Remove @CodedBy attribute")
]
),
.init(
id: CodedBy.misuseID,
message:
"@CodedBy can only be applied once per declaration",
line: 3, column: 5,
fixIts: [
.init(message: "Remove @CodedBy attribute")
]
),
]
)
}
}
#endif

0 comments on commit 495cea4

Please sign in to comment.