├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CodingKeysMacro │ ├── CodingKeysOption.swift │ └── Macro.swift └── CodingKeysMacroPlugin │ ├── CodingKeysGenerator.swift │ ├── CodingKeysMacro+decodeExpansion.swift │ ├── CodingKeysMacro.swift │ ├── CodingKeysMacroDiagnostic.swift │ ├── CodingKeysMacroPlugin.swift │ ├── CodingKeysOption.swift │ └── String+snakeCased.swift └── Tests └── CodingKeysMacroTests └── CodingKeysMacroTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-syntax.git", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "4db7cb20e63f4dba0b030068687996723e352874" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "CodingKeysMacro", 9 | platforms: [ 10 | .iOS(.v11), 11 | .macOS(.v10_15), 12 | .tvOS(.v11), 13 | .watchOS(.v4) 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, making them visible to other packages. 17 | .library( 18 | name: "CodingKeysMacro", 19 | targets: ["CodingKeysMacro"]), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/apple/swift-syntax.git", branch: "main"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package, defining a module or a test suite. 26 | // Targets can depend on other targets in this package and products from dependencies. 27 | .target( 28 | name: "CodingKeysMacro", 29 | dependencies: [ 30 | "CodingKeysMacroPlugin" 31 | ] 32 | ), 33 | .macro( 34 | name: "CodingKeysMacroPlugin", 35 | dependencies: [ 36 | .product(name: "SwiftSyntax", package: "swift-syntax"), 37 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 38 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 39 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 40 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), 41 | ] 42 | ), 43 | .testTarget( 44 | name: "CodingKeysMacroTests", 45 | dependencies: [ 46 | "CodingKeysMacro", 47 | "CodingKeysMacroPlugin", 48 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 49 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax") 50 | ] 51 | ), 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodingKeysMacro 2 | Swift Macro that automatically generates CodingKeys for converting snake_case to lowerCamelCase. 3 | 4 | ## Installation 5 | ```Swift 6 | .package(url: "https://github.com/Ryu0118/CodingKeysMacro", branch: "main") 7 | ``` 8 | ## Usage 9 | ### .all 10 | ```Swift 11 | @CodingKeys(.all) 12 | struct UserResponse: Codable { 13 | let id: String 14 | let age: Int 15 | let userName: String 16 | let userDescription: String 17 | } 18 | // expanded to... 19 | struct UserResponse: Codable { 20 | ... 21 | enum CodingKeys: String, CodingKey { 22 | case id 23 | case age 24 | case userName = "user_name" 25 | case userDescription = "user_description" 26 | } 27 | } 28 | ``` 29 | ### .select 30 | ```Swift 31 | @CodingKeys(.select(["userName"])) 32 | struct UserResponse: Codable { 33 | let id: String 34 | let age: Int 35 | let userName: String 36 | let userDescription: String 37 | } 38 | // expanded to... 39 | struct UserResponse: Codable { 40 | ... 41 | enum CodingKeys: String, CodingKey { 42 | case id 43 | case age 44 | case userName = "user_name" 45 | case userDescription 46 | } 47 | } 48 | ``` 49 | ### .exclude 50 | ```Swift 51 | @CodingKeys(.exclude(["userName", "userDescription"])) 52 | struct UserResponse: Codable { 53 | let id: String 54 | let age: Int 55 | let userName: String 56 | let userDescription: String 57 | } 58 | // expanded to... 59 | struct UserResponse: Codable { 60 | ... 61 | enum CodingKeys: String, CodingKey { 62 | case id 63 | case age 64 | case userName 65 | case userDescription 66 | } 67 | } 68 | ``` 69 | ### .custom 70 | ```Swift 71 | @CodingKeys(.custom(["id": "user_id"])) 72 | struct UserResponse: Codable { 73 | let id: String 74 | let age: Int 75 | let userName: String 76 | let userDescription: String 77 | } 78 | // expanded to... 79 | struct UserResponse: Codable { 80 | ... 81 | enum CodingKeys: String, CodingKey { 82 | case id = "user_id" 83 | case age 84 | case userName = "user_name" 85 | case userDescription = "user_description" 86 | } 87 | } 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacro/CodingKeysOption.swift: -------------------------------------------------------------------------------- 1 | public enum CodingKeysOption { 2 | case all 3 | case select([String]) 4 | case exclude([String]) 5 | case custom([String: String]) 6 | } 7 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacro/Macro.swift: -------------------------------------------------------------------------------- 1 | @attached(member, names: named(CodingKeys)) 2 | public macro CodingKeys(_ type: CodingKeysOption) = #externalMacro(module: "CodingKeysMacroPlugin", type: "CodingKeysMacro") 3 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysGenerator.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | struct CodingKeysGenerator { 5 | enum CodingKeysStrategy { 6 | case equal(String, String) 7 | case skip(String) 8 | 9 | func enumCaseElementSyntax() -> EnumCaseElementSyntax { 10 | switch self { 11 | case .equal(let caseName, let value): 12 | EnumCaseElementSyntax( 13 | identifier: .identifier(caseName), 14 | rawValue: InitializerClauseSyntax( 15 | equal: .equalToken(), 16 | value: StringLiteralExprSyntax(content: value) 17 | ) 18 | ) 19 | case .skip(let caseName): 20 | EnumCaseElementSyntax(identifier: .identifier(caseName)) 21 | } 22 | } 23 | } 24 | 25 | let option: CodingKeysOption 26 | let properties: [String] 27 | 28 | init(option: CodingKeysOption, properties: [String]) { 29 | self.option = option 30 | self.properties = properties 31 | } 32 | 33 | func generate() -> EnumDeclSyntax { 34 | EnumDeclSyntax( 35 | identifier: .identifier("CodingKeys"), 36 | inheritanceClause: TypeInheritanceClauseSyntax { 37 | InheritedTypeSyntax(typeName: TypeSyntax(stringLiteral: "String")) 38 | InheritedTypeSyntax(typeName: TypeSyntax(stringLiteral: "CodingKey")) 39 | } 40 | ) { 41 | MemberDeclListSyntax( 42 | generateStrategy().map { strategy in 43 | MemberDeclListItemSyntax( 44 | decl: EnumCaseDeclSyntax( 45 | elements: EnumCaseElementListSyntax( 46 | arrayLiteral: strategy.enumCaseElementSyntax() 47 | ) 48 | ) 49 | ) 50 | } 51 | ) 52 | } 53 | } 54 | 55 | private func generateStrategy() -> [CodingKeysStrategy] { 56 | properties.map { 57 | switch option { 58 | case .all: 59 | return .equal($0, $0.snakeCased()) 60 | case let .select(selectedProperties): 61 | if selectedProperties.contains($0) { 62 | return .equal($0, $0.snakeCased()) 63 | } else { 64 | return .skip($0) 65 | } 66 | case let .exclude(excludedProperties): 67 | if excludedProperties.contains($0) { 68 | return .skip($0) 69 | } else { 70 | return .equal($0, $0.snakeCased()) 71 | } 72 | case let .custom(customNamePair): 73 | if customNamePair.map(\.key).contains($0), 74 | let value = customNamePair[$0] 75 | { 76 | return .equal($0, value) 77 | } else { 78 | return .equal($0, $0.snakeCased()) 79 | } 80 | } 81 | } 82 | .map { (strategy: CodingKeysStrategy) in 83 | switch strategy { 84 | case let .equal(key, value): 85 | if key == value { 86 | return .skip(key) 87 | } 88 | default: break 89 | } 90 | return strategy 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysMacro+decodeExpansion.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public extension CodingKeysMacro { 6 | static func decodeExpansion( 7 | of attribute: AttributeSyntax, 8 | attachedTo declaration: some DeclGroupSyntax, 9 | in context: some MacroExpansionContext 10 | ) -> (CodingKeysOption, StructDeclSyntax)? { 11 | guard case let .argumentList(arguments) = attribute.argument, 12 | let firstElement = arguments.first?.expression 13 | else { 14 | context.diagnose(CodingKeysMacroDiagnostic.noArgument.diagnose(at: attribute)) 15 | return nil 16 | } 17 | 18 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 19 | context.diagnose(CodingKeysMacroDiagnostic.requiresStruct.diagnose(at: attribute)) 20 | return nil 21 | } 22 | 23 | if let memberAccessExpr = firstElement.as(MemberAccessExprSyntax.self), 24 | let option = decodeMemberAccess(of: attribute, expr: memberAccessExpr, in: context) 25 | { 26 | return (option, structDecl) 27 | } 28 | else if let functionCallExpr = firstElement.as(FunctionCallExprSyntax.self), 29 | let option = decodeFunctionCall(of: attribute, expr: functionCallExpr, in: context) 30 | { 31 | return (option, structDecl) 32 | } 33 | else { 34 | context.diagnose(CodingKeysMacroDiagnostic.invalidArgument.diagnose(at: attribute)) 35 | return nil 36 | } 37 | } 38 | 39 | private static func decodeMemberAccess( 40 | of attribute: AttributeSyntax, 41 | expr memberAccessExpr: MemberAccessExprSyntax, 42 | in context: some MacroExpansionContext 43 | ) -> CodingKeysOption? { 44 | if memberAccessExpr.name.trimmedDescription == "all" 45 | { 46 | return .all 47 | } else { 48 | context.diagnose(CodingKeysMacroDiagnostic.noArgument.diagnose(at: attribute)) 49 | return nil 50 | } 51 | } 52 | 53 | private static func decodeFunctionCall( 54 | of attribute: AttributeSyntax, 55 | expr functionCallExpr: FunctionCallExprSyntax, 56 | in context: some MacroExpansionContext 57 | ) -> CodingKeysOption? { 58 | guard let caseName = functionCallExpr.calledExpression.as(MemberAccessExprSyntax.self)?.name.text, 59 | let expression = functionCallExpr.argumentList.first?.expression else 60 | { 61 | context.diagnose(CodingKeysMacroDiagnostic.noArgument.diagnose(at: attribute)) 62 | return nil 63 | } 64 | 65 | if let arrayExpr = expression.as(ArrayExprSyntax.self), 66 | let stringArray = arrayExpr.stringArray 67 | { 68 | return .associatedValueArray(caseName, associatedValue: stringArray) 69 | } 70 | else if let dictionaryElements = expression.as(DictionaryExprSyntax.self), 71 | let stringDictionary = dictionaryElements.stringDictionary 72 | { 73 | return .associatedValueDictionary(caseName, associatedValue: stringDictionary) 74 | } 75 | else { 76 | context.diagnose(CodingKeysMacroDiagnostic.invalidArgument.diagnose(at: attribute)) 77 | return nil 78 | } 79 | } 80 | } 81 | 82 | fileprivate extension ArrayExprSyntax { 83 | var stringArray: [String]? { 84 | elements.reduce(into: [String]()) { result, element in 85 | guard let string = element.expression.as(StringLiteralExprSyntax.self) else { 86 | return 87 | } 88 | result.append(string.rawValue) 89 | } 90 | } 91 | } 92 | 93 | fileprivate extension StringLiteralExprSyntax { 94 | var rawValue: String { 95 | segments 96 | .compactMap { $0.as(StringSegmentSyntax.self) } 97 | .map(\.content.text) 98 | .joined() 99 | } 100 | } 101 | 102 | fileprivate extension DictionaryExprSyntax { 103 | var stringDictionary: [String: String]? { 104 | guard let elements = content.as(DictionaryElementListSyntax.self) else { 105 | return nil 106 | } 107 | 108 | return elements.reduce(into: [String: String]()) { result, element in 109 | guard let key = element.keyExpression.as(StringLiteralExprSyntax.self), 110 | let value = element.valueExpression.as(StringLiteralExprSyntax.self) 111 | else { 112 | return 113 | } 114 | result.updateValue(value.rawValue, forKey: key.rawValue) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public struct CodingKeysMacro: MemberMacro { 6 | public static func expansion< 7 | Declaration: DeclGroupSyntax, Context: MacroExpansionContext 8 | >( 9 | of node: AttributeSyntax, 10 | providingMembersOf declaration: Declaration, 11 | in context: Context 12 | ) throws -> [DeclSyntax] { 13 | guard let (option, structDecl) = decodeExpansion(of: node, attachedTo: declaration, in: context) else { 14 | return [] 15 | } 16 | 17 | let properties = getProperties(decl: structDecl) 18 | 19 | guard diagnoseInvalidProperties( 20 | option: option, 21 | properties: properties, 22 | structName: structDecl.identifier.text, 23 | declaration: declaration, 24 | in: context 25 | ) else { 26 | return [] 27 | } 28 | 29 | let generator = CodingKeysGenerator(option: option, properties: properties) 30 | 31 | let decl = generator 32 | .generate() 33 | .formatted() 34 | .as(EnumDeclSyntax.self)! 35 | 36 | return [ 37 | DeclSyntax(decl) 38 | ] 39 | } 40 | 41 | private static func getProperties(decl: StructDeclSyntax) -> [String] { 42 | var properties = [String]() 43 | 44 | for decl in decl.memberBlock.members.map(\.decl) { 45 | if let variableDecl = decl.as(VariableDeclSyntax.self) { 46 | properties.append( 47 | contentsOf: variableDecl.bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } 48 | ) 49 | } 50 | } 51 | 52 | return properties 53 | } 54 | 55 | private static func diagnoseInvalidProperties( 56 | option: CodingKeysOption, 57 | properties: [String], 58 | structName: String, 59 | declaration: some DeclGroupSyntax, 60 | in context: some MacroExpansionContext 61 | ) -> Bool { 62 | let invalidProperties = option.necessaryProperties.compactMap { 63 | if !properties.contains($0) { 64 | return $0 65 | } else { 66 | return nil 67 | } 68 | } 69 | 70 | if !invalidProperties.isEmpty { 71 | context.diagnose( 72 | CodingKeysMacroDiagnostic.nonexistentProperty( 73 | structName: structName, 74 | propertyName: invalidProperties.joined(separator: ", ") 75 | ) 76 | .diagnose(at: declaration) 77 | ) 78 | } 79 | 80 | return invalidProperties.isEmpty 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftDiagnostics 3 | 4 | public enum CodingKeysMacroDiagnostic { 5 | case nonexistentProperty(structName: String, propertyName: String) 6 | case noArgument 7 | case requiresStruct 8 | case invalidArgument 9 | } 10 | 11 | extension CodingKeysMacroDiagnostic: DiagnosticMessage { 12 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 13 | Diagnostic(node: Syntax(node), message: self) 14 | } 15 | 16 | public var message: String { 17 | switch self { 18 | case let .nonexistentProperty(structName, propertyName): 19 | return "Property \(propertyName) does not exist in \(structName)" 20 | 21 | case .noArgument: 22 | return "Cannot find argument" 23 | 24 | case .requiresStruct: 25 | return "'CodingKeys' macro can only be applied to struct." 26 | 27 | case .invalidArgument: 28 | return "Invalid Argument" 29 | } 30 | } 31 | 32 | public var severity: DiagnosticSeverity { .error } 33 | 34 | public var diagnosticID: MessageID { 35 | MessageID(domain: "Swift", id: "CodingKeysMacro.\(self)") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysMacroPlugin.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftCompilerPlugin) 2 | import SwiftSyntaxMacros 3 | import SwiftCompilerPlugin 4 | 5 | @main 6 | struct CodingKeysMacroPlugin: CompilerPlugin { 7 | let providingMacros: [Macro.Type] = [ 8 | CodingKeysMacro.self 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/CodingKeysOption.swift: -------------------------------------------------------------------------------- 1 | public enum CodingKeysOption { 2 | case all 3 | case select([String]) 4 | case exclude([String]) 5 | case custom([String: String]) 6 | 7 | var necessaryProperties: [String] { 8 | switch self { 9 | case .all: 10 | [] 11 | case .select(let array), .exclude(let array): 12 | array 13 | case .custom(let dictionary): 14 | .init(dictionary.keys) 15 | } 16 | } 17 | 18 | static func associatedValueArray( 19 | _ caseName: String, 20 | associatedValue: [String] 21 | ) -> Self? { 22 | switch caseName { 23 | case "select": 24 | return .select(associatedValue) 25 | 26 | case "exclude": 27 | return .exclude(associatedValue) 28 | 29 | default: 30 | return nil 31 | } 32 | } 33 | 34 | static func associatedValueDictionary( 35 | _ caseName: String, 36 | associatedValue: [String: String] 37 | ) -> Self? { 38 | if caseName == "custom" { 39 | return .custom(associatedValue) 40 | } else { 41 | return nil 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CodingKeysMacroPlugin/String+snakeCased.swift: -------------------------------------------------------------------------------- 1 | extension String { 2 | func snakeCased() -> Self { 3 | var snakeCaseString = "" 4 | for char in self { 5 | if char.isUppercase { 6 | snakeCaseString += "_" + char.lowercased() 7 | } else { 8 | snakeCaseString += String(char) 9 | } 10 | } 11 | return snakeCaseString 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/CodingKeysMacroTests/CodingKeysMacroTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftSyntaxMacros 3 | import SwiftSyntaxMacrosTestSupport 4 | @testable import CodingKeysMacro 5 | @testable import CodingKeysMacroPlugin 6 | 7 | final class CodingKeysMacroTests: XCTestCase { 8 | let macros: [String: Macro.Type] = [ 9 | "CodingKeysMacro": CodingKeysMacro.self 10 | ] 11 | 12 | func testOptionAll() throws { 13 | assertMacroExpansion( 14 | """ 15 | @CodingKeysMacro(.all) 16 | public struct Hoge { 17 | let hogeHoge: String 18 | let fugaFuga: String 19 | let hoge: String 20 | let fuga: String 21 | } 22 | """, 23 | expandedSource: """ 24 | 25 | public struct Hoge { 26 | let hogeHoge: String 27 | let fugaFuga: String 28 | let hoge: String 29 | let fuga: String 30 | enum CodingKeys: String, CodingKey { 31 | case hogeHoge = "hoge_hoge" 32 | case fugaFuga = "fuga_fuga" 33 | case hoge 34 | case fuga 35 | } 36 | } 37 | """, 38 | macros: macros 39 | ) 40 | } 41 | 42 | func testOptionSelect() throws { 43 | assertMacroExpansion( 44 | """ 45 | @CodingKeysMacro(.select(["hogeHoge", "hoge"]) 46 | public struct Hoge { 47 | let hogeHoge: String 48 | var fugaFuga: String 49 | let hoge: String 50 | var fuga: String 51 | } 52 | """ 53 | , 54 | expandedSource: """ 55 | 56 | public struct Hoge { 57 | let hogeHoge: String 58 | var fugaFuga: String 59 | let hoge: String 60 | var fuga: String 61 | enum CodingKeys: String, CodingKey { 62 | case hogeHoge = "hoge_hoge" 63 | case fugaFuga 64 | case hoge 65 | case fuga 66 | } 67 | } 68 | """, 69 | macros: macros 70 | ) 71 | } 72 | 73 | func testOptionExclude() throws { 74 | assertMacroExpansion( 75 | """ 76 | @CodingKeysMacro(.exclude(["hogeHoge", "fooFoo"]) 77 | public struct Hoge { 78 | let hogeHoge: String 79 | var fugaFuga: String 80 | let fooFoo: String 81 | var hogeHogeHoge: String 82 | } 83 | """ 84 | , 85 | expandedSource: """ 86 | 87 | public struct Hoge { 88 | let hogeHoge: String 89 | var fugaFuga: String 90 | let fooFoo: String 91 | var hogeHogeHoge: String 92 | enum CodingKeys: String, CodingKey { 93 | case hogeHoge 94 | case fugaFuga = "fuga_fuga" 95 | case fooFoo 96 | case hogeHogeHoge = "hoge_hoge_hoge" 97 | } 98 | } 99 | """, 100 | macros: macros 101 | ) 102 | } 103 | 104 | func testOptionCustom() throws { 105 | assertMacroExpansion( 106 | """ 107 | @CodingKeysMacro(.custom(["id": "hoge_id", "hogeHoge": "hogee"]) 108 | public struct Hoge { 109 | let id: String 110 | let hogeHoge: String 111 | var fugaFuga: String 112 | let fooFoo: String 113 | var hogeHogeHoge: String 114 | } 115 | """ 116 | , 117 | expandedSource: """ 118 | 119 | public struct Hoge { 120 | let id: String 121 | let hogeHoge: String 122 | var fugaFuga: String 123 | let fooFoo: String 124 | var hogeHogeHoge: String 125 | enum CodingKeys: String, CodingKey { 126 | case id = "hoge_id" 127 | case hogeHoge = "hogee" 128 | case fugaFuga = "fuga_fuga" 129 | case fooFoo = "foo_foo" 130 | case hogeHogeHoge = "hoge_hoge_hoge" 131 | } 132 | } 133 | """, 134 | macros: macros 135 | ) 136 | } 137 | } 138 | --------------------------------------------------------------------------------