Skip to content

Commit 535f446

Browse files
committed
feat: added protocol support
1 parent 665306f commit 535f446

File tree

143 files changed

+3066
-399
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+3066
-399
lines changed

Package.swift

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,66 @@ let package = Package(
1515
products: [
1616
.library(name: "MetaCodable", targets: ["MetaCodable"]),
1717
.library(name: "HelperCoders", targets: ["HelperCoders"]),
18+
.plugin(name: "MetaProtocolCodable", targets: ["MetaProtocolCodable"]),
1819
],
1920
dependencies: [
20-
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
21+
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.1.0"),
2122
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
23+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
2224
.package(url: "https://github.com/apple/swift-format", from: "509.0.0"),
2325
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
2426
],
2527
targets: [
26-
.macro(
27-
name: "CodableMacroPlugin",
28+
// MARK: Core
29+
.target(
30+
name: "PluginCore",
2831
dependencies: [
2932
.product(name: "SwiftSyntax", package: "swift-syntax"),
3033
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
3134
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
3235
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
33-
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
3436
.product(name: "OrderedCollections", package: "swift-collections"),
3537
]
3638
),
37-
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),
39+
40+
// MARK: Macro
41+
.macro(
42+
name: "MacroPlugin",
43+
dependencies: [
44+
"PluginCore",
45+
.product(name: "SwiftSyntax", package: "swift-syntax"),
46+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
47+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
48+
]
49+
),
50+
.target(name: "MetaCodable", dependencies: ["MacroPlugin"]),
3851
.target(name: "HelperCoders", dependencies: ["MetaCodable"]),
52+
53+
// MARK: Build Tool
54+
.executableTarget(
55+
name: "ProtocolGen",
56+
dependencies: [
57+
"PluginCore", "MetaCodable",
58+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
59+
.product(name: "SwiftSyntax", package: "swift-syntax"),
60+
.product(name: "SwiftParser", package: "swift-syntax"),
61+
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
62+
]
63+
),
64+
.plugin(
65+
name: "MetaProtocolCodable", capability: .buildTool(),
66+
dependencies: ["ProtocolGen"]
67+
),
68+
69+
// MARK: Test
3970
.testTarget(
4071
name: "MetaCodableTests",
4172
dependencies: [
42-
"CodableMacroPlugin", "MetaCodable", "HelperCoders",
73+
"PluginCore", "ProtocolGen",
74+
"MacroPlugin", "MetaCodable", "HelperCoders",
4375
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
44-
]
76+
],
77+
plugins: ["MetaProtocolCodable"]
4578
),
4679
]
4780
)
@@ -56,6 +89,7 @@ if Context.environment["SWIFT_SYNTAX_EXTENSION_MACRO_FIXED"] != nil {
5689
)
5790

5891
package.targets.forEach { target in
92+
guard target.isTest else { return }
5993
var settings = target.swiftSettings ?? []
6094
settings.append(.define("SWIFT_SYNTAX_EXTENSION_MACRO_FIXED"))
6195
target.swiftSettings = settings
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/// The configuration data for plugin.
2+
///
3+
/// Depending on the configuration data, source file check and
4+
/// syntax generation is performed.
5+
struct Config {
6+
/// The source file scan mode.
7+
///
8+
/// Specifies which source files need to be parsed for syntax generation.
9+
let scan: ScanMode
10+
11+
/// The source file scan mode.
12+
///
13+
/// Specifies which source files need to be parsed for syntax generation.
14+
enum ScanMode: String, Codable {
15+
/// Represents to check current target.
16+
///
17+
/// Files only from the target which includes plugin are checked.
18+
case target
19+
/// Represents to check current target and target dependencies.
20+
///
21+
/// Files from the target which includes plugin and target dependencies
22+
/// present in current package manifest are checked.
23+
case local
24+
/// Represents to check current target and all dependencies.
25+
///
26+
/// Files from the target which includes plugin and all its
27+
/// dependencies are checked.
28+
case recursive
29+
}
30+
}
31+
32+
extension Config: Codable {
33+
/// Creates a new instance by decoding from the given decoder.
34+
///
35+
/// The scanning mode is set to only scan target unless specified
36+
/// explicitly.
37+
///
38+
/// - Parameter decoder: The decoder to read data from.
39+
init(from decoder: Decoder) throws {
40+
let container = try decoder.container(keyedBy: CodingKeys.self)
41+
self.scan = try container.decodeIfPresent(
42+
ScanMode.self, forKey: .scan
43+
) ?? .target
44+
}
45+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import Foundation
2+
import PackagePlugin
3+
4+
/// Provides `protocol` decoding/encoding syntax generation.
5+
///
6+
/// Creates build commands that produces syntax for `protocol`s
7+
/// that indicate dynamic decoding/encoding with `Codable` macro.
8+
@main
9+
struct MetaProtocolCodable: BuildToolPlugin {
10+
/// Fetches config data from file.
11+
///
12+
/// The alphanumeric characters of file name must case-insensitively
13+
/// match `"metacodableconfig"`, and the data contained must be
14+
/// either `plist` or `json` format, i.e. `metacodable-config.json`,
15+
/// `metacodable_config.json`, `MetaCodableConfig.plist` are
16+
/// all valid names.
17+
///
18+
/// - Parameter target: The target including plugin.
19+
/// - Returns: The config if provided, otherwise default config.
20+
func fetchConfig(for target: SourceModuleTarget) async throws -> Config {
21+
let fileManager = FileManager.default
22+
let directory = target.directory.string
23+
let contents = try fileManager.contentsOfDirectory(atPath: directory)
24+
let file = contents.first { file in
25+
let path = Path(file)
26+
let name = path.stem
27+
.components(separatedBy: .alphanumerics.inverted)
28+
.joined(separator: "")
29+
.lowercased()
30+
return name == "metacodableconfig"
31+
}
32+
guard let file else { return .init(scan: .target) }
33+
let path = if #available(macOS 13, *) {
34+
URL(filePath: target.directory.appending([file]).string)
35+
} else {
36+
URL(fileURLWithPath: target.directory.appending([file]).string)
37+
}
38+
let (conf, _) = try await URLSession.shared.data(from: path)
39+
let pConf = try? PropertyListDecoder().decode(Config.self, from: conf)
40+
let config = try pConf ?? JSONDecoder().decode(Config.self, from: conf)
41+
return config
42+
}
43+
44+
/// Invoked by SwiftPM to create build commands for a particular target.
45+
///
46+
/// Creates build commands that produces intermediate files scanning
47+
/// swift source files according to configuration. Final build command
48+
/// generates syntax aggregating all intermediate files.
49+
///
50+
/// - Parameters:
51+
/// - context: The package and environmental inputs context.
52+
/// - target: The target including plugin.
53+
///
54+
/// - Returns: The commands to be executed during build.
55+
func createBuildCommands(
56+
context: PluginContext, target: Target
57+
) async throws -> [Command] {
58+
guard let target = target as? SourceModuleTarget else { return [] }
59+
let tool = try context.tool(named: "ProtocolGen")
60+
// Get Config
61+
let config = try await fetchConfig(for: target)
62+
let (allTargets, imports) = config.scanInput(for: target)
63+
// Setup folder
64+
let genFolder = context.pluginWorkDirectory.appending(["ProtocolGen"])
65+
try FileManager.default.createDirectory(
66+
atPath: genFolder.string, withIntermediateDirectories: true
67+
)
68+
69+
// Create source scan commands
70+
var intermFiles: [Path] = []
71+
var buildCommands = allTargets.flatMap { target in
72+
return target.sourceFiles(withSuffix: "swift").map { file in
73+
let moduleName = target.moduleName
74+
let fileName = file.path.stem
75+
let genFileName = "\(moduleName)-\(fileName)-gen.json"
76+
let genFile = genFolder.appending([genFileName])
77+
intermFiles.append(genFile)
78+
return Command.buildCommand(
79+
displayName: """
80+
Parse source file "\(fileName)" in module "\(moduleName)"
81+
""",
82+
executable: tool.path,
83+
arguments: [
84+
"parse",
85+
file.path.string,
86+
"--output",
87+
genFile.string,
88+
],
89+
inputFiles: [file.path],
90+
outputFiles: [genFile]
91+
)
92+
}
93+
}
94+
95+
// Create syntax generation command
96+
let moduleName = target.moduleName
97+
let genFileName = "\(moduleName)+ProtocolHelperCoders.swift"
98+
let genPath = genFolder.appending(genFileName)
99+
var genArgs = ["generate", "--output", genPath.string]
100+
for `import` in imports {
101+
genArgs.append(contentsOf: ["--module", `import`])
102+
}
103+
for file in intermFiles {
104+
genArgs.append(file.string)
105+
}
106+
buildCommands.append(.buildCommand(
107+
displayName: """
108+
Generate protocol decoding/encoding syntax for "\(moduleName)"
109+
""",
110+
executable: tool.path,
111+
arguments: genArgs,
112+
inputFiles: intermFiles,
113+
outputFiles: [genPath]
114+
))
115+
return buildCommands
116+
}
117+
}
118+
119+
extension Config {
120+
/// Returns targets to scan and import modules based on current
121+
/// configuration.
122+
///
123+
/// Based on configuration, the targets for which source files need
124+
/// to be checked and the modules that will be imported in final syntax
125+
/// generated is returned.
126+
///
127+
/// - Parameter target: The target including plugin.
128+
/// - Returns: The targets to scan and modules to import.
129+
func scanInput(
130+
for target: SourceModuleTarget
131+
) -> (targets: [SourceModuleTarget], modules: [String]) {
132+
let allTargets: [SourceModuleTarget]
133+
let modules: [String]
134+
switch scan {
135+
case .target:
136+
allTargets = [target]
137+
modules = []
138+
case .local:
139+
var targets = target.dependencies.compactMap { dependency in
140+
return switch dependency {
141+
case .target(let target):
142+
target.sourceModule
143+
default:
144+
nil
145+
}
146+
}
147+
modules = targets.map(\.moduleName)
148+
targets.append(target)
149+
allTargets = targets
150+
case .recursive:
151+
var targets = target.recursiveTargetDependencies.compactMap {
152+
return $0 as? SourceModuleTarget
153+
}
154+
modules = targets.map(\.moduleName)
155+
targets.append(target)
156+
allTargets = targets
157+
}
158+
return (allTargets, modules)
159+
}
160+
}

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ Supercharge `Swift`'s `Codable` implementations with macros.
2020
- Allows custom `CodingKey` value declaration per variable, instead of requiring you to write all the `CodingKey` values with ``CodedAt(_:)`` passing single argument.
2121
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
2222
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
23+
- Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``.
2324
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
24-
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` and types from ``HelperCoders`` module.
25-
- Allows to ignore specific properties from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``@IgnoreEncoding()``.
26-
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type to work with different case style keys with ``CodingKeys(_:)``.
27-
- Allows to ignore all initialized properties of a type from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
25+
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
26+
- Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``.
27+
- Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``.
28+
- Allows to ignore specific properties/cases from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``IgnoreEncoding()``.
29+
- Allows to use camel-case names for variables according to [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/#general-conventions), while enabling a type/case to work with different case style keys with ``CodingKeys(_:)``.
30+
- Allows to ignore all initialized properties of a type/case from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
2831
``CodedBy(_:)``, ``Default(_:)`` etc.
32+
- Allows to generate protocol decoding/encoding ``HelperCoder``s with `MetaProtocolCodable` build tool plugin from ``DynamicCodable`` types.
33+
34+
[**See the limitations for this macro**](<doc:Limitations>).
2935

3036
## Requirements
3137

@@ -236,7 +242,7 @@ You can even create your own by conforming to `HelperCoder`.
236242
</details>
237243

238244
<details>
239-
<summary>Represent data with variations in the form of external/internal/adjacent tagging, with single enum with each case as a variation.</summary>
245+
<summary>Represent data with variations in the form of external/internal/adjacent tagging, with single enum with each case as a variation or a protocol type that varies with conformances accross modules.</summary>
240246

241247
i.e. while `Swift` compiler only generates implementation assuming external tagged enums, only following data:
242248

0 commit comments

Comments
 (0)