Skip to content

Commit

Permalink
feat: added protocol support
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jan 9, 2024
1 parent 665306f commit 535f446
Show file tree
Hide file tree
Showing 143 changed files with 3,066 additions and 399 deletions.
48 changes: 41 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,66 @@ let package = Package(
products: [
.library(name: "MetaCodable", targets: ["MetaCodable"]),
.library(name: "HelperCoders", targets: ["HelperCoders"]),
.plugin(name: "MetaProtocolCodable", targets: ["MetaProtocolCodable"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.1.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
.package(url: "https://github.com/apple/swift-format", from: "509.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
.macro(
name: "CodableMacroPlugin",
// MARK: Core
.target(
name: "PluginCore",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftDiagnostics", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "OrderedCollections", package: "swift-collections"),
]
),
.target(name: "MetaCodable", dependencies: ["CodableMacroPlugin"]),

// MARK: Macro
.macro(
name: "MacroPlugin",
dependencies: [
"PluginCore",
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
.target(name: "MetaCodable", dependencies: ["MacroPlugin"]),
.target(name: "HelperCoders", dependencies: ["MetaCodable"]),

// MARK: Build Tool
.executableTarget(
name: "ProtocolGen",
dependencies: [
"PluginCore", "MetaCodable",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
]
),
.plugin(
name: "MetaProtocolCodable", capability: .buildTool(),
dependencies: ["ProtocolGen"]
),

// MARK: Test
.testTarget(
name: "MetaCodableTests",
dependencies: [
"CodableMacroPlugin", "MetaCodable", "HelperCoders",
"PluginCore", "ProtocolGen",
"MacroPlugin", "MetaCodable", "HelperCoders",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
],
plugins: ["MetaProtocolCodable"]
),
]
)
Expand All @@ -56,6 +89,7 @@ if Context.environment["SWIFT_SYNTAX_EXTENSION_MACRO_FIXED"] != nil {
)

package.targets.forEach { target in
guard target.isTest else { return }
var settings = target.swiftSettings ?? []
settings.append(.define("SWIFT_SYNTAX_EXTENSION_MACRO_FIXED"))
target.swiftSettings = settings
Expand Down
45 changes: 45 additions & 0 deletions Plugins/MetaProtocolCodable/Config.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// The configuration data for plugin.
///
/// Depending on the configuration data, source file check and
/// syntax generation is performed.
struct Config {
/// The source file scan mode.
///
/// Specifies which source files need to be parsed for syntax generation.
let scan: ScanMode

/// The source file scan mode.
///
/// Specifies which source files need to be parsed for syntax generation.
enum ScanMode: String, Codable {
/// Represents to check current target.
///
/// Files only from the target which includes plugin are checked.
case target
/// Represents to check current target and target dependencies.
///
/// Files from the target which includes plugin and target dependencies
/// present in current package manifest are checked.
case local
/// Represents to check current target and all dependencies.
///
/// Files from the target which includes plugin and all its
/// dependencies are checked.
case recursive
}
}

extension Config: Codable {
/// Creates a new instance by decoding from the given decoder.
///
/// The scanning mode is set to only scan target unless specified
/// explicitly.
///
/// - Parameter decoder: The decoder to read data from.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.scan = try container.decodeIfPresent(
ScanMode.self, forKey: .scan
) ?? .target
}
}
160 changes: 160 additions & 0 deletions Plugins/MetaProtocolCodable/Plugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import Foundation
import PackagePlugin

/// Provides `protocol` decoding/encoding syntax generation.
///
/// Creates build commands that produces syntax for `protocol`s
/// that indicate dynamic decoding/encoding with `Codable` macro.
@main
struct MetaProtocolCodable: BuildToolPlugin {
/// Fetches config data from file.
///
/// The alphanumeric characters of file name must case-insensitively
/// match `"metacodableconfig"`, and the data contained must be
/// either `plist` or `json` format, i.e. `metacodable-config.json`,
/// `metacodable_config.json`, `MetaCodableConfig.plist` are
/// all valid names.
///
/// - Parameter target: The target including plugin.
/// - Returns: The config if provided, otherwise default config.
func fetchConfig(for target: SourceModuleTarget) async throws -> Config {
let fileManager = FileManager.default
let directory = target.directory.string
let contents = try fileManager.contentsOfDirectory(atPath: directory)
let file = contents.first { file in
let path = Path(file)
let name = path.stem
.components(separatedBy: .alphanumerics.inverted)
.joined(separator: "")
.lowercased()
return name == "metacodableconfig"
}
guard let file else { return .init(scan: .target) }
let path = if #available(macOS 13, *) {
URL(filePath: target.directory.appending([file]).string)
} else {
URL(fileURLWithPath: target.directory.appending([file]).string)
}
let (conf, _) = try await URLSession.shared.data(from: path)
let pConf = try? PropertyListDecoder().decode(Config.self, from: conf)
let config = try pConf ?? JSONDecoder().decode(Config.self, from: conf)
return config
}

/// Invoked by SwiftPM to create build commands for a particular target.
///
/// Creates build commands that produces intermediate files scanning
/// swift source files according to configuration. Final build command
/// generates syntax aggregating all intermediate files.
///
/// - Parameters:
/// - context: The package and environmental inputs context.
/// - target: The target including plugin.
///
/// - Returns: The commands to be executed during build.
func createBuildCommands(
context: PluginContext, target: Target
) async throws -> [Command] {
guard let target = target as? SourceModuleTarget else { return [] }
let tool = try context.tool(named: "ProtocolGen")
// Get Config
let config = try await fetchConfig(for: target)
let (allTargets, imports) = config.scanInput(for: target)
// Setup folder
let genFolder = context.pluginWorkDirectory.appending(["ProtocolGen"])
try FileManager.default.createDirectory(
atPath: genFolder.string, withIntermediateDirectories: true
)

// Create source scan commands
var intermFiles: [Path] = []
var buildCommands = allTargets.flatMap { target in
return target.sourceFiles(withSuffix: "swift").map { file in
let moduleName = target.moduleName
let fileName = file.path.stem
let genFileName = "\(moduleName)-\(fileName)-gen.json"
let genFile = genFolder.appending([genFileName])
intermFiles.append(genFile)
return Command.buildCommand(
displayName: """
Parse source file "\(fileName)" in module "\(moduleName)"
""",
executable: tool.path,
arguments: [
"parse",
file.path.string,
"--output",
genFile.string,
],
inputFiles: [file.path],
outputFiles: [genFile]
)
}
}

// Create syntax generation command
let moduleName = target.moduleName
let genFileName = "\(moduleName)+ProtocolHelperCoders.swift"
let genPath = genFolder.appending(genFileName)
var genArgs = ["generate", "--output", genPath.string]
for `import` in imports {
genArgs.append(contentsOf: ["--module", `import`])
}
for file in intermFiles {
genArgs.append(file.string)
}
buildCommands.append(.buildCommand(
displayName: """
Generate protocol decoding/encoding syntax for "\(moduleName)"
""",
executable: tool.path,
arguments: genArgs,
inputFiles: intermFiles,
outputFiles: [genPath]
))
return buildCommands
}
}

extension Config {
/// Returns targets to scan and import modules based on current
/// configuration.
///
/// Based on configuration, the targets for which source files need
/// to be checked and the modules that will be imported in final syntax
/// generated is returned.
///
/// - Parameter target: The target including plugin.
/// - Returns: The targets to scan and modules to import.
func scanInput(
for target: SourceModuleTarget
) -> (targets: [SourceModuleTarget], modules: [String]) {
let allTargets: [SourceModuleTarget]
let modules: [String]
switch scan {
case .target:
allTargets = [target]
modules = []
case .local:
var targets = target.dependencies.compactMap { dependency in
return switch dependency {
case .target(let target):
target.sourceModule
default:
nil
}
}
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
case .recursive:
var targets = target.recursiveTargetDependencies.compactMap {
return $0 as? SourceModuleTarget
}
modules = targets.map(\.moduleName)
targets.append(target)
allTargets = targets
}
return (allTargets, modules)
}
}
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ Supercharge `Swift`'s `Codable` implementations with macros.
- Allows custom `CodingKey` value declaration per variable, instead of requiring you to write all the `CodingKey` values with ``CodedAt(_:)`` passing single argument.
- Allows to create flattened model for nested `CodingKey` values with ``CodedAt(_:)`` and ``CodedIn(_:)``.
- Allows to create composition of multiple `Codable` types with ``CodedAt(_:)`` passing no arguments.
- Allows to read data from additional fallback `CodingKey`s provided with ``CodedAs(_:_:)``.
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` and types from ``HelperCoders`` module.
- Allows to ignore specific properties from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``@IgnoreEncoding()``.
- 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(_:)``.
- 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(_:)``,
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
- Allows specifying different case values with ``CodedAs(_:_:)`` and case value/protocol type identifier type different from `String` with ``CodedAs()``.
- Allows specifying enum-case/protocol type identifier path with ``CodedAt(_:)`` and case content path with ``ContentAt(_:_:)``.
- Allows to ignore specific properties/cases from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``IgnoreEncoding()``.
- 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(_:)``.
- 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(_:)``,
``CodedBy(_:)``, ``Default(_:)`` etc.
- Allows to generate protocol decoding/encoding ``HelperCoder``s with `MetaProtocolCodable` build tool plugin from ``DynamicCodable`` types.

[**See the limitations for this macro**](<doc:Limitations>).

## Requirements

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

<details>
<summary>Represent data with variations in the form of external/internal/adjacent tagging, with single enum with each case as a variation.</summary>
<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>

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

Expand Down

0 comments on commit 535f446

Please sign in to comment.