Skip to content

Commit 498d763

Browse files
committed
feat: added Codable generation macro with
- per field custom key definition - per field nested key definition - composition definition per field - default value definition for decoding failure per field - memberwise initializer generation with above defaults - helper decoder/encoder definition per field, i.e. `LossySequenceCoder`
1 parent 88427b9 commit 498d763

File tree

18 files changed

+2410
-9
lines changed

18 files changed

+2410
-9
lines changed

.gitignore

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ DerivedData/
2222
*.perspectivev3
2323
!default.perspectivev3
2424

25+
# OS generated files #
26+
######################
27+
.DS_Store
28+
.DS_Store?
29+
._*
30+
.Spotlight-V100
31+
.Trashes
32+
ehthumbs.db
33+
Thumbs.db
34+
2535
## Obj-C/Swift specific
2636
*.hmap
2737

@@ -37,14 +47,13 @@ playground.xcworkspace
3747
# Swift Package Manager
3848
#
3949
# 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-
#
50+
Packages/
51+
Package.pins
52+
Package.resolved
53+
4554
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
4655
# hence it is not needed unless you have added a package configuration file to your project
47-
# .swiftpm
56+
.swiftpm
4857

4958
.build/
5059

@@ -54,18 +63,24 @@ playground.xcworkspace
5463
# you should judge for yourself, the pros and cons are mentioned at:
5564
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
5665
#
57-
# Pods/
66+
Pods/
5867
#
5968
# Add this line if you want to avoid checking in source code from the Xcode workspace
60-
# *.xcworkspace
69+
*.xcworkspace
6170

6271
# Carthage
6372
#
6473
# Add this line if you want to avoid checking in source code from Carthage dependencies.
65-
# Carthage/Checkouts
74+
Carthage/Checkouts
6675

6776
Carthage/Build/
6877

78+
# Add Xcode project related files required by Carthage
79+
DynamicCodableKit.xcodeproj/*
80+
!DynamicCodableKit.xcodeproj/*.pbxproj
81+
!DynamicCodableKit.xcodeproj/*.plist
82+
!DynamicCodableKit.xcodeproj/xcshareddata
83+
6984
# Accio dependency management
7085
Dependencies/
7186
.accio/
@@ -88,3 +103,125 @@ fastlane/test_output
88103
# https://github.com/johnno1962/injectionforxcode
89104

90105
iOSInjectionProject/
106+
107+
# DocC
108+
.netrc
109+
.docc-build
110+
*.doccarchive*
111+
112+
# Built Products
113+
*.xcframework*
114+
*.zip
115+
*.tar*
116+
117+
# Tuist
118+
Derived/
119+
120+
## Node-Js ignores
121+
# Logs
122+
logs
123+
*.log
124+
npm-debug.log*
125+
yarn-debug.log*
126+
yarn-error.log*
127+
lerna-debug.log*
128+
129+
# Diagnostic reports (https://nodejs.org/api/report.html)
130+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
131+
132+
# Runtime data
133+
pids
134+
*.pid
135+
*.seed
136+
*.pid.lock
137+
138+
# Directory for instrumented libs generated by jscoverage/JSCover
139+
lib-cov
140+
141+
# Coverage directory used by tools like istanbul
142+
coverage
143+
*.lcov
144+
145+
# nyc test coverage
146+
.nyc_output
147+
148+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
149+
.grunt
150+
151+
# Bower dependency directory (https://bower.io/)
152+
bower_components
153+
154+
# node-waf configuration
155+
.lock-wscript
156+
157+
# Compiled binary addons (https://nodejs.org/api/addons.html)
158+
build/Release
159+
160+
# Dependency directories
161+
node_modules/
162+
jspm_packages/
163+
164+
# TypeScript v1 declaration files
165+
typings/
166+
167+
# TypeScript cache
168+
*.tsbuildinfo
169+
170+
# Optional npm cache directory
171+
.npm
172+
173+
# Optional eslint cache
174+
.eslintcache
175+
176+
# Microbundle cache
177+
.rpt2_cache/
178+
.rts2_cache_cjs/
179+
.rts2_cache_es/
180+
.rts2_cache_umd/
181+
182+
# Optional REPL history
183+
.node_repl_history
184+
185+
# Output of 'npm pack'
186+
*.tgz
187+
188+
# Yarn Integrity file
189+
.yarn-integrity
190+
191+
# dotenv environment variables file
192+
.env
193+
.env.test
194+
195+
# parcel-bundler cache (https://parceljs.org/)
196+
.cache
197+
198+
# Next.js build output
199+
.next
200+
201+
# Nuxt.js build / generate output
202+
.nuxt
203+
dist
204+
205+
# Gatsby files
206+
.cache/
207+
# Comment in the public line in if your project uses Gatsby and *not* Next.js
208+
# https://nextjs.org/blog/next-9-1#public-directory-support
209+
# public
210+
211+
# vuepress build output
212+
.vuepress/dist
213+
214+
# Serverless directories
215+
.serverless/
216+
217+
# FuseBox cache
218+
.fusebox/
219+
220+
# DynamoDB Local files
221+
.dynamodb/
222+
223+
# TernJS port file
224+
.tern-port
225+
226+
# NPM package lock
227+
package-lock.json

Package.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
import CompilerPluginSupport
5+
6+
let macroDeps: [Target.Dependency] = [
7+
.product(name: "SwiftSyntax", package: "swift-syntax"),
8+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
9+
.product(name: "SwiftOperators", package: "swift-syntax"),
10+
.product(name: "SwiftParser", package: "swift-syntax"),
11+
.product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
12+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
13+
.product(name: "OrderedCollections", package: "swift-collections"),
14+
]
15+
16+
let testDeps: [Target.Dependency] = [
17+
"CodableMacroPlugin", "MetaCodable",
18+
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
19+
]
20+
21+
let package = Package(
22+
name: "MetaCodable",
23+
platforms: [
24+
.iOS(.v8),
25+
.macOS(.v10_15),
26+
.tvOS(.v9),
27+
.watchOS(.v2),
28+
.macCatalyst(.v13),
29+
],
30+
products: [
31+
.library(name: "MetaCodable", targets: ["MetaCodable"]),
32+
],
33+
dependencies: [
34+
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
35+
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
36+
],
37+
targets: [
38+
.macro(name: "CodableMacroPlugin", dependencies: macroDeps),
39+
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),
40+
.testTarget(name: "MetaCodableTests", dependencies: testDeps),
41+
]
42+
)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import SwiftSyntax
2+
import SwiftDiagnostics
3+
import SwiftSyntaxMacros
4+
5+
/// Describes a macro that provides metadata to `CodableMacro`
6+
/// for individual variable decoding approaches.
7+
///
8+
/// This macro doesn't perform any expansion rather `CodableMacro`
9+
/// uses when performing expansion.
10+
///
11+
/// This macro verifies that it is attached to only variable declarations and
12+
/// necessary metadata provided. If not, then this macro generates diagnostic
13+
/// to remove it.
14+
struct CodableFieldMacro: PeerMacro {
15+
/// The name of macro that allows `CodingKey`
16+
/// path customizations
17+
static var path: String { "CodablePath" }
18+
/// The name of macro that allows
19+
/// composition of decoding/encoding
20+
static var compose: String { "CodableCompose" }
21+
22+
/// Argument label used to provide a default value
23+
/// in case of decoding failure.
24+
static var defaultArgLabel: String { "default" }
25+
/// Argument label used to provide a helper instance
26+
/// for decoding/encoding customizations or
27+
/// custom decoding/encoding implementation.
28+
static var helperArgLabel: String { "helper" }
29+
/// Collection of all the argument labels.
30+
static var argLabels: [String] {
31+
return [
32+
Self.defaultArgLabel,
33+
Self.helperArgLabel,
34+
]
35+
}
36+
37+
/// Provide metadata to `CodableMacro` for final expansion
38+
/// and verify proper usage of this macro.
39+
///
40+
/// This macro doesn't perform any expansion rather `CodableMacro`
41+
/// uses when performing expansion.
42+
///
43+
/// This macro verifies that it is attached to only variable declarations
44+
/// and necessary metadata provided. If not, then this macro generates
45+
/// diagnostic to remove it.
46+
///
47+
/// - Parameters:
48+
/// - node: The attribute describing this macro.
49+
/// - declaration: The declaration this macro attribute is attached to.
50+
/// - context: The context in which to perform the macro expansion.
51+
///
52+
/// - Returns: No declaration is returned, only attached declaration is
53+
/// analyzed.
54+
static func expansion(
55+
of node: AttributeSyntax,
56+
providingPeersOf declaration: some DeclSyntaxProtocol,
57+
in context: some MacroExpansionContext
58+
) throws -> [DeclSyntax] {
59+
let name = node.attributeName
60+
.as(SimpleTypeIdentifierSyntax.self)!.description
61+
62+
let (id, msg, severity): (MessageID?, String?, DiagnosticSeverity?) = {
63+
if !declaration.is(VariableDeclSyntax.self) {
64+
return (
65+
.codableFieldMisuse,
66+
"@\(name) only applicable to variable declarations",
67+
.error
68+
)
69+
} else if name == Self.path,
70+
node.argument?
71+
.as(TupleExprElementListSyntax.self)?.first == nil
72+
{
73+
return (
74+
.codableFieldUnused,
75+
"Unnecessary use of @\(name) without arguments",
76+
.warning
77+
)
78+
} else {
79+
return (nil, nil, nil)
80+
}
81+
}()
82+
83+
guard let id, let msg, let severity else { return [] }
84+
context.diagnose(
85+
Diagnostic(
86+
node: Syntax(node),
87+
message: MetaCodableMessage.diagnostic(
88+
message: msg,
89+
id: id,
90+
severity: severity
91+
),
92+
fixIts: [
93+
.init(
94+
message: MetaCodableMessage.fixIt(
95+
message: "Remove @\(name) attribute",
96+
id: id
97+
),
98+
changes: [
99+
.replace(
100+
oldNode: Syntax(node),
101+
newNode: Syntax("" as DeclSyntax)
102+
)
103+
]
104+
)
105+
]
106+
)
107+
)
108+
return []
109+
}
110+
}
111+
112+
/// An extension that manages `CodableFieldMacro`
113+
/// specific message ids.
114+
fileprivate extension MessageID {
115+
/// Message id for misuse of `CodableFieldMacro` application.
116+
static var codableFieldMisuse: Self { .messageID("codablefield-misuse") }
117+
/// Message id for usage of unnecessary `CodableFieldMacro` application.
118+
///
119+
/// The `CodableFieldMacro` can be omitted in such scenario
120+
/// and the final result will still be the same.
121+
static var codableFieldUnused: Self { .messageID("codablepath-unused") }
122+
}

0 commit comments

Comments
 (0)