Skip to content

Commit

Permalink
feat: added Codable generation macro with
Browse files Browse the repository at this point in the history
- 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`
  • Loading branch information
soumyamahunt committed Jun 19, 2023
1 parent 88427b9 commit 498d763
Show file tree
Hide file tree
Showing 18 changed files with 2,410 additions and 9 deletions.
155 changes: 146 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ DerivedData/
*.perspectivev3
!default.perspectivev3

# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

## Obj-C/Swift specific
*.hmap

Expand All @@ -37,14 +47,13 @@ playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
Packages/
Package.pins
Package.resolved

# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm

.build/

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

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Checkouts

Carthage/Build/

# Add Xcode project related files required by Carthage
DynamicCodableKit.xcodeproj/*
!DynamicCodableKit.xcodeproj/*.pbxproj
!DynamicCodableKit.xcodeproj/*.plist
!DynamicCodableKit.xcodeproj/xcshareddata

# Accio dependency management
Dependencies/
.accio/
Expand All @@ -88,3 +103,125 @@ fastlane/test_output
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

# DocC
.netrc
.docc-build
*.doccarchive*

# Built Products
*.xcframework*
*.zip
*.tar*

# Tuist
Derived/

## Node-Js ignores
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# NPM package lock
package-lock.json
42 changes: 42 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// swift-tools-version: 5.9

import PackageDescription
import CompilerPluginSupport

let macroDeps: [Target.Dependency] = [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "OrderedCollections", package: "swift-collections"),
]

let testDeps: [Target.Dependency] = [
"CodableMacroPlugin", "MetaCodable",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]

let package = Package(
name: "MetaCodable",
platforms: [
.iOS(.v8),
.macOS(.v10_15),
.tvOS(.v9),
.watchOS(.v2),
.macCatalyst(.v13),
],
products: [
.library(name: "MetaCodable", targets: ["MetaCodable"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
],
targets: [
.macro(name: "CodableMacroPlugin", dependencies: macroDeps),
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),
.testTarget(name: "MetaCodableTests", dependencies: testDeps),
]
)
122 changes: 122 additions & 0 deletions Sources/CodableMacroPlugin/CodableFieldMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import SwiftSyntax
import SwiftDiagnostics
import SwiftSyntaxMacros

/// Describes a macro that provides metadata to `CodableMacro`
/// for individual variable decoding approaches.
///
/// This macro doesn't perform any expansion rather `CodableMacro`
/// uses when performing expansion.
///
/// This macro verifies that it is attached to only variable declarations and
/// necessary metadata provided. If not, then this macro generates diagnostic
/// to remove it.
struct CodableFieldMacro: PeerMacro {
/// The name of macro that allows `CodingKey`
/// path customizations
static var path: String { "CodablePath" }
/// The name of macro that allows
/// composition of decoding/encoding
static var compose: String { "CodableCompose" }

/// Argument label used to provide a default value
/// in case of decoding failure.
static var defaultArgLabel: String { "default" }
/// Argument label used to provide a helper instance
/// for decoding/encoding customizations or
/// custom decoding/encoding implementation.
static var helperArgLabel: String { "helper" }
/// Collection of all the argument labels.
static var argLabels: [String] {
return [
Self.defaultArgLabel,
Self.helperArgLabel,
]
}

/// Provide metadata to `CodableMacro` for final expansion
/// and verify proper usage of this macro.
///
/// This macro doesn't perform any expansion rather `CodableMacro`
/// uses when performing expansion.
///
/// This macro verifies that it is attached to only variable declarations
/// and necessary metadata provided. If not, then this macro generates
/// diagnostic to remove it.
///
/// - Parameters:
/// - node: The attribute describing this macro.
/// - declaration: The declaration this macro attribute is attached to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: No declaration is returned, only attached declaration is
/// analyzed.
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let name = node.attributeName
.as(SimpleTypeIdentifierSyntax.self)!.description

let (id, msg, severity): (MessageID?, String?, DiagnosticSeverity?) = {
if !declaration.is(VariableDeclSyntax.self) {
return (
.codableFieldMisuse,
"@\(name) only applicable to variable declarations",
.error
)
} else if name == Self.path,
node.argument?
.as(TupleExprElementListSyntax.self)?.first == nil
{
return (
.codableFieldUnused,
"Unnecessary use of @\(name) without arguments",
.warning
)
} else {
return (nil, nil, nil)
}
}()

guard let id, let msg, let severity else { return [] }
context.diagnose(
Diagnostic(
node: Syntax(node),
message: MetaCodableMessage.diagnostic(
message: msg,
id: id,
severity: severity
),
fixIts: [
.init(
message: MetaCodableMessage.fixIt(
message: "Remove @\(name) attribute",
id: id
),
changes: [
.replace(
oldNode: Syntax(node),
newNode: Syntax("" as DeclSyntax)
)
]
)
]
)
)
return []
}
}

/// An extension that manages `CodableFieldMacro`
/// specific message ids.
fileprivate extension MessageID {
/// Message id for misuse of `CodableFieldMacro` application.
static var codableFieldMisuse: Self { .messageID("codablefield-misuse") }
/// Message id for usage of unnecessary `CodableFieldMacro` application.
///
/// The `CodableFieldMacro` can be omitted in such scenario
/// and the final result will still be the same.
static var codableFieldUnused: Self { .messageID("codablepath-unused") }
}

0 comments on commit 498d763

Please sign in to comment.