diff --git a/Sources/CodableMacroPlugin/Registration/ConstraintGenerator.swift b/Sources/CodableMacroPlugin/Registration/Options/ConstraintGenerator.swift similarity index 100% rename from Sources/CodableMacroPlugin/Registration/ConstraintGenerator.swift rename to Sources/CodableMacroPlugin/Registration/Options/ConstraintGenerator.swift diff --git a/Sources/CodableMacroPlugin/Registration/Options/DeclModifiersGenerator.swift b/Sources/CodableMacroPlugin/Registration/Options/DeclModifiersGenerator.swift new file mode 100644 index 00000000..1b9fe225 --- /dev/null +++ b/Sources/CodableMacroPlugin/Registration/Options/DeclModifiersGenerator.swift @@ -0,0 +1,34 @@ +@_implementationOnly import SwiftSyntax + +extension Registrar.Options { + /// A declaration modifiers generator for `Codable` + /// conformance implementations. + /// + /// This generator keeps track of original declaration + /// and modifiers, then generates modifiers for + /// `Decodable` or `Encodable` implementations. + struct DeclModifiersGenerator { + /// The declaration for which modifiers generated. + let decl: DeclGroupSyntax + + /// The generated list of modifiers. + /// + /// If declaration has `public` or `package` modifier + /// then same is generated, otherwise no extra modifiers + /// generated. + var generated: DeclModifierListSyntax { + let `public` = DeclModifierSyntax(name: "public") + let package = DeclModifierSyntax(name: "package") + var modifiers = DeclModifierListSyntax() + let accessModifier = [`public`, package].first { accessModifier in + decl.modifiers.contains { modifier in + modifier.name.text == accessModifier.name.text + } + } + if let accessModifier { + modifiers.append(accessModifier) + } + return modifiers + } + } +} diff --git a/Sources/CodableMacroPlugin/Registration/Options/Options.swift b/Sources/CodableMacroPlugin/Registration/Options/Options.swift new file mode 100644 index 00000000..0b02ede2 --- /dev/null +++ b/Sources/CodableMacroPlugin/Registration/Options/Options.swift @@ -0,0 +1,37 @@ +@_implementationOnly import SwiftSyntax + +extension Registrar { + /// A type indicating various configurations available + /// for `Registrar`. + /// + /// These options are used as global level customization + /// performed on the final generated implementation + /// of `Codable` conformance. + struct Options { + /// The list of modifiers generator for + /// conformance implementation declarations. + let modifiersGenerator: DeclModifiersGenerator + /// The where clause generator for generic type arguments. + let constraintGenerator: ConstraintGenerator + + /// Memberwise initialization generator with provided options. + /// + /// Creates memberwise initialization generator by passing + /// the provided access modifiers. + var initGenerator: MemberwiseInitGenerator { + let modifiers = modifiersGenerator.generated + return .init(options: .init(modifiers: modifiers)) + } + + /// Creates a new options instance with provided declaration group. + /// + /// - Parameters: + /// - decl: The declaration group options will be applied to. + /// + /// - Returns: The newly created options. + init(decl: DeclGroupSyntax) { + self.modifiersGenerator = .init(decl: decl) + self.constraintGenerator = .init(decl: decl) + } + } +} diff --git a/Sources/CodableMacroPlugin/Registration/Registrar.swift b/Sources/CodableMacroPlugin/Registration/Registrar.swift index 49cdd1ae..25aece88 100644 --- a/Sources/CodableMacroPlugin/Registration/Registrar.swift +++ b/Sources/CodableMacroPlugin/Registration/Registrar.swift @@ -8,39 +8,6 @@ /// use `decoding`, `encoding` and `codingKeys` methods /// to get final generated implementation of `Codable` conformance. struct Registrar { - /// A type indicating various configurations available - /// for `Registrar`. - /// - /// These options are used as global level customization - /// performed on the final generated implementation - /// of `Codable` conformance. - struct Options { - /// The default list of modifiers to be applied to generated - /// conformance implementation declarations. - fileprivate let modifiers: DeclModifierListSyntax - /// The where clause generator for generic type arguments. - fileprivate let constraintGenerator: ConstraintGenerator - - /// Memberwise initialization generator with provided options. - /// - /// Creates memberwise initialization generator by passing - /// the provided access modifiers. - var initGenerator: MemberwiseInitGenerator { - return .init(options: .init(modifiers: modifiers)) - } - - /// Creates a new options instance with provided declaration group. - /// - /// - Parameters: - /// - decl: The declaration group options will be applied to. - /// - /// - Returns: The newly created options. - init(decl: DeclGroupSyntax) { - self.modifiers = decl.modifiers - self.constraintGenerator = .init(decl: decl) - } - } - /// The root node containing all the keys /// and associated field metadata maps. private var root: Node @@ -190,7 +157,7 @@ struct Registrar { ) ) { InitializerDeclSyntax.decode( - modifiers: options.modifiers + modifiers: options.modifiersGenerator.generated ) { decoder in let type = caseMap.type root.decoding(in: context, from: .coder(decoder, keyType: type)) @@ -223,7 +190,9 @@ struct Registrar { conformingTo: `protocol` ) ) { - FunctionDeclSyntax.encode(modifiers: options.modifiers) { encoder in + FunctionDeclSyntax.encode( + modifiers: options.modifiersGenerator.generated + ) { encoder in let type = caseMap.type root.encoding(in: context, from: .coder(encoder, keyType: type)) } diff --git a/Tests/MetaCodableTests/AccessModifierTests.swift b/Tests/MetaCodableTests/AccessModifierTests.swift new file mode 100644 index 00000000..52b0d5b1 --- /dev/null +++ b/Tests/MetaCodableTests/AccessModifierTests.swift @@ -0,0 +1,137 @@ +#if SWIFT_SYNTAX_EXTENSION_MACRO_FIXED +import XCTest + +@testable import CodableMacroPlugin + +final class AccessModifierTests: XCTestCase { + + func testPublic() throws { + assertMacroExpansion( + """ + @Codable + @MemberInit + public struct SomeCodable { + let value: String + } + """, + expandedSource: + """ + public struct SomeCodable { + let value: String + + public init(value: String) { + self.value = value + } + } + + extension SomeCodable: Decodable { + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: CodingKeys.value) + } + } + + extension SomeCodable: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.value, forKey: CodingKeys.value) + } + } + + extension SomeCodable { + enum CodingKeys: String, CodingKey { + case value = "value" + } + } + """ + ) + } + + func testPackage() throws { + assertMacroExpansion( + """ + @Codable + @MemberInit + package struct SomeCodable { + let value: String + } + """, + expandedSource: + """ + package struct SomeCodable { + let value: String + + package init(value: String) { + self.value = value + } + } + + extension SomeCodable: Decodable { + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: CodingKeys.value) + } + } + + extension SomeCodable: Encodable { + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.value, forKey: CodingKeys.value) + } + } + + extension SomeCodable { + enum CodingKeys: String, CodingKey { + case value = "value" + } + } + """ + ) + } + + func testOthers() throws { + for modifier in ["internal", "fileprivate", "private", ""] { + let prefix = modifier.isEmpty ? "" : "\(modifier) " + assertMacroExpansion( + """ + @Codable + @MemberInit + \(prefix)struct SomeCodable { + let value: String + } + """, + expandedSource: + """ + \(prefix)struct SomeCodable { + let value: String + + init(value: String) { + self.value = value + } + } + + extension SomeCodable: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.value = try container.decode(String.self, forKey: CodingKeys.value) + } + } + + extension SomeCodable: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.value, forKey: CodingKeys.value) + } + } + + extension SomeCodable { + enum CodingKeys: String, CodingKey { + case value = "value" + } + } + """ + ) + } + } +} +#endif