From 2c6792e86e91bfe2eccb8a7c75595db7d4f688d4 Mon Sep 17 00:00:00 2001 From: Khaled Chehabeddine Date: Thu, 4 Sep 2025 18:13:29 +0300 Subject: [PATCH 1/3] build(swift): add Swift package infrastructure for native implementation - Add Package.swift with modern Swift 6.1 toolchain support - Set up SwiftSebuf library target with extensible architecture - Include Xcode scheme configurations for development workflow - Add comprehensive .gitignore for Swift build artifacts - Establish foundation for native Swift protobuf oneof helper implementation This creates the foundation for migrating from Go-based generator to native Swift implementation using Apple's SwiftProtobuf framework for better compatibility and maintainability. --- swift/.gitignore | 8 ++ .../xcschemes/SwiftSebuf.xcscheme | 79 +++++++++++++++++++ .../xcschemes/SwiftSebufTests.xcscheme | 54 +++++++++++++ swift/Package.swift | 66 ++++++++++++++++ swift/Sources/SwiftSebuf/SwiftSebuf.swift | 2 + .../SwiftSebufTests/SwiftSebufTests.swift | 7 ++ 6 files changed, 216 insertions(+) create mode 100644 swift/.gitignore create mode 100644 swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebuf.xcscheme create mode 100644 swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebufTests.xcscheme create mode 100644 swift/Package.swift create mode 100644 swift/Sources/SwiftSebuf/SwiftSebuf.swift create mode 100644 swift/Tests/SwiftSebufTests/SwiftSebufTests.swift diff --git a/swift/.gitignore b/swift/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/swift/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebuf.xcscheme b/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebuf.xcscheme new file mode 100644 index 0000000..ad53a01 --- /dev/null +++ b/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebuf.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebufTests.xcscheme b/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebufTests.xcscheme new file mode 100644 index 0000000..74b29d1 --- /dev/null +++ b/swift/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSebufTests.xcscheme @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/swift/Package.swift b/swift/Package.swift new file mode 100644 index 0000000..c6196b4 --- /dev/null +++ b/swift/Package.swift @@ -0,0 +1,66 @@ +// swift-tools-version: 6.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +@preconcurrency import PackageDescription + +let package = Package( + name: .name, + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .swiftSebuf + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .swiftSebuf, + .swiftSebufTest + ] +) + +private extension String { + + // MARK: Package Name + static let name: Self = "SwiftSebuf" + + // MARK: Modules + static let swiftSebuf: Self = "SwiftSebuf" + + // MARK: Packages + + // MARK: Plugins +// static let swiftLintPlugin: Self = "SwiftLintBuildToolPlugin" +// static let swiftLintPackage: Self = "SwiftLintPlugins" + + var test: Self { + "\(self)Tests" + } +} + +private extension Product { + + static let swiftSebuf: Product = .library(name: .swiftSebuf, targets: [.swiftSebuf]) +} + +private extension Target { + + static let swiftSebuf: Target = target(name: .swiftSebuf) + static let swiftSebufTest: Target = testTarget(name: .swiftSebuf.test, dependencies: [.swiftSebuf]) +} + +private extension Target.Dependency { + + // MARK: Modules + static let swiftSebuf: Self = byName(name: .swiftSebuf) + + // MARK: Packages +} + +private extension Target.PluginUsage { + +// static let swiftLint: Self = plugin(name: .swiftLintPlugin, package: .swiftLintPackage) +} + +private extension Package.Dependency { + +// static let swiftLint: Package.Dependency = package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "0.59.1") +} diff --git a/swift/Sources/SwiftSebuf/SwiftSebuf.swift b/swift/Sources/SwiftSebuf/SwiftSebuf.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/swift/Sources/SwiftSebuf/SwiftSebuf.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book diff --git a/swift/Tests/SwiftSebufTests/SwiftSebufTests.swift b/swift/Tests/SwiftSebufTests/SwiftSebufTests.swift new file mode 100644 index 0000000..2d4a23f --- /dev/null +++ b/swift/Tests/SwiftSebufTests/SwiftSebufTests.swift @@ -0,0 +1,7 @@ +import Testing +@testable import SwiftSebuf + +@Test +func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} From a7bccd621489b460a5968d1ac93f261460b0eaa9 Mon Sep 17 00:00:00 2001 From: Khaled Chehabeddine Date: Thu, 4 Sep 2025 18:32:27 +0300 Subject: [PATCH 2/3] build(deps): add SwiftProtobuf and ArgumentParser dependencies - Add SwiftProtobuf 1.31.0 for native protobuf generation support - Add SwiftArgumentParser 1.6.1 for CLI interface - Include SwiftProtobufPluginLibrary for protoc plugin development - Add Package.resolved for dependency version locking - Configure target dependencies for SwiftSebuf library This enables native Swift implementation of protobuf oneof helpers using Apple's official SwiftProtobuf framework and plugin library. --- swift/Package.resolved | 24 +++++++++++++++++++++ swift/Package.swift | 47 +++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 swift/Package.resolved diff --git a/swift/Package.resolved b/swift/Package.resolved new file mode 100644 index 0000000..a90fb01 --- /dev/null +++ b/swift/Package.resolved @@ -0,0 +1,24 @@ +{ + "originHash" : "2a69ac815441053d6d9489a3e228a5bd57ec01f953ae2f9b067acebdc06dec65", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face", + "version" : "1.31.0" + } + } + ], + "version" : 3 +} diff --git a/swift/Package.swift b/swift/Package.swift index c6196b4..2719e18 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -9,6 +9,10 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .swiftSebuf ], + dependencies: [ + .swiftArgumentParser, + .swiftProtobuf + ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. @@ -26,10 +30,11 @@ private extension String { static let swiftSebuf: Self = "SwiftSebuf" // MARK: Packages - - // MARK: Plugins -// static let swiftLintPlugin: Self = "SwiftLintBuildToolPlugin" -// static let swiftLintPackage: Self = "SwiftLintPlugins" + static let swiftArgumentParser: Self = "ArgumentParser" + static let swiftArgumentParserPackage: Self = "swift-argument-parser" + static let swiftProtobuf: Self = "SwiftProtobuf" + static let swiftProtobufPluginLibrary: Self = "SwiftProtobufPluginLibrary" + static let swiftProtobufPackage: Self = "swift-protobuf" var test: Self { "\(self)Tests" @@ -38,12 +43,19 @@ private extension String { private extension Product { - static let swiftSebuf: Product = .library(name: .swiftSebuf, targets: [.swiftSebuf]) + static let swiftSebuf: Product = library(name: .swiftSebuf, targets: [.swiftSebuf]) } private extension Target { - static let swiftSebuf: Target = target(name: .swiftSebuf) + static let swiftSebuf: Target = target( + name: .swiftSebuf, + dependencies: [ + .swiftArgumentParser, + .swiftProtobuf, + .swiftProtobufPluginLibrary + ] + ) static let swiftSebufTest: Target = testTarget(name: .swiftSebuf.test, dependencies: [.swiftSebuf]) } @@ -53,14 +65,25 @@ private extension Target.Dependency { static let swiftSebuf: Self = byName(name: .swiftSebuf) // MARK: Packages -} - -private extension Target.PluginUsage { - -// static let swiftLint: Self = plugin(name: .swiftLintPlugin, package: .swiftLintPackage) + static let swiftArgumentParser: Self = product( + name: .swiftArgumentParser, + package: .swiftArgumentParserPackage + ) + static let swiftProtobuf: Self = product(name: .swiftProtobuf, package: .swiftProtobufPackage) + static let swiftProtobufPluginLibrary: Self = product( + name: .swiftProtobufPluginLibrary, + package: .swiftProtobufPackage + ) } private extension Package.Dependency { -// static let swiftLint: Package.Dependency = package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "0.59.1") + static let swiftArgumentParser: Package.Dependency = package( + url: "https://github.com/apple/swift-argument-parser.git", + exact: "1.6.1" + ) + static let swiftProtobuf: Package.Dependency = package( + url: "https://github.com/apple/swift-protobuf.git", + exact: "1.31.0" + ) } From 9d457c3c3671718d981b2f1b4e65613fad591f95 Mon Sep 17 00:00:00 2001 From: Khaled Chehabeddine Date: Mon, 8 Sep 2025 23:45:32 +0300 Subject: [PATCH 3/3] feat(swift): implement native Swift protobuf oneof helper generator - Add native Swift implementation using SwiftProtobuf framework - Create Go wrapper that builds and executes Swift binary for protoc integration - Implement OneofHelperGenerator with proper Swift protobuf naming conventions - Add SebufCommand with ArgumentParser for CLI interface - Include Generator base class for extensible protoc plugin architecture - Add SwiftProtobuf typealiases for cleaner code organization - Update Package.swift with executable product and macOS platform requirement - Remove boilerplate SwiftSebuf.swift in favor of modular structure This native implementation provides: - Perfect compatibility with Swift protobuf naming conventions - Automatic Swift binary building and execution from Go wrapper - Extensible architecture for future protoc plugin implementations - Direct use of Apple's SwiftProtobufPluginLibrary for native integration --- cmd/protoc-gen-swift-oneof-helper/main.go | 46 +++++++ swift/Package.swift | 1 + swift/Sources/SwiftSebuf/Generator.swift | 18 +++ .../OneofHelper/OneofHelperCommand.swift | 23 ++++ .../OneofHelper/OneofHelperGenerator.swift | 123 ++++++++++++++++++ swift/Sources/SwiftSebuf/SebufCommand.swift | 40 ++++++ .../SwiftProtobuf+Typealiases.swift | 13 ++ swift/Sources/SwiftSebuf/SwiftSebuf.swift | 2 - 8 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 cmd/protoc-gen-swift-oneof-helper/main.go create mode 100644 swift/Sources/SwiftSebuf/Generator.swift create mode 100644 swift/Sources/SwiftSebuf/OneofHelper/OneofHelperCommand.swift create mode 100644 swift/Sources/SwiftSebuf/OneofHelper/OneofHelperGenerator.swift create mode 100644 swift/Sources/SwiftSebuf/SebufCommand.swift create mode 100644 swift/Sources/SwiftSebuf/SwiftProtobuf+Typealiases.swift delete mode 100644 swift/Sources/SwiftSebuf/SwiftSebuf.swift diff --git a/cmd/protoc-gen-swift-oneof-helper/main.go b/cmd/protoc-gen-swift-oneof-helper/main.go new file mode 100644 index 0000000..a6ac73c --- /dev/null +++ b/cmd/protoc-gen-swift-oneof-helper/main.go @@ -0,0 +1,46 @@ +// cmd/protoc-gen-swift-oneof-helper/main.go +package main + +import ( + "os" + "os/exec" + "path/filepath" +) + +func main() { + // Get the directory of this executable + execPath, err := os.Executable() + if err != nil { + panic(err) + } + + // Look for the Swift binary + swiftBinary := filepath.Join(filepath.Dir(execPath), "protoc-gen-swift-oneof-helper-swift") + + // If Swift binary doesn't exist, try building it + if _, err := os.Stat(swiftBinary); os.IsNotExist(err) { + buildSwiftBinary(swiftBinary) + } + + // Execute the Swift binary + cmd := exec.Command(swiftBinary) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + os.Exit(1) + } +} + +func buildSwiftBinary(outputPath string) { + cmd := exec.Command("swift", "build", "-c", "release", "--product", "protoc-gen-swift-oneof-helper") + if err := cmd.Run(); err != nil { + panic("Failed to build Swift binary: " + err.Error()) + } + + // Copy the built binary to the expected location + builtBinary := ".build/release/protoc-gen-swift-oneof-helper" + cmd = exec.Command("cp", builtBinary, outputPath) + cmd.Run() +} diff --git a/swift/Package.swift b/swift/Package.swift index 2719e18..97c540e 100644 --- a/swift/Package.swift +++ b/swift/Package.swift @@ -5,6 +5,7 @@ let package = Package( name: .name, + platforms: [.macOS(.v14)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .swiftSebuf diff --git a/swift/Sources/SwiftSebuf/Generator.swift b/swift/Sources/SwiftSebuf/Generator.swift new file mode 100644 index 0000000..c9dd908 --- /dev/null +++ b/swift/Sources/SwiftSebuf/Generator.swift @@ -0,0 +1,18 @@ +// +// Generator.swift +// SwiftSebuf +// +// Created by Khaled Chehabeddine on 08/09/2025. +// Copyright © 2025 Sebuf. All rights reserved. +// + +import Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +protocol Generator { + + init(descriptorSet: DescriptorSet) + + func generate() -> CodeGeneratorResponse +} diff --git a/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperCommand.swift b/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperCommand.swift new file mode 100644 index 0000000..8ecb692 --- /dev/null +++ b/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperCommand.swift @@ -0,0 +1,23 @@ +// +// OneofHelperCommand.swift +// SwiftSebuf +// +// Created by Khaled Chehabeddine on 04/09/2025. +// Copyright © 2025 Sebuf. All rights reserved. +// + +import ArgumentParser +import Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +@main +struct OneofHelperCommand: SebufCommand { + + typealias SebufCodeGenerator = OneofHelperGenerator + + static let configuration: CommandConfiguration = .init( + commandName: "protoc-gen-swift-oneof-helper", + abstract: "Generate Swift oneof helper extensions for protobuf messages" + ) +} diff --git a/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperGenerator.swift b/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperGenerator.swift new file mode 100644 index 0000000..1b5406a --- /dev/null +++ b/swift/Sources/SwiftSebuf/OneofHelper/OneofHelperGenerator.swift @@ -0,0 +1,123 @@ +// +// OneofHelperGenerator.swift +// SwiftSebuf +// +// Created by Khaled Chehabeddine on 08/09/2025. +// Copyright © 2025 Sebuf. All rights reserved. +// + +import Foundation +import SwiftProtobufPluginLibrary + +struct OneofHelperGenerator: Generator { + + private let descriptorSet: DescriptorSet + + init(descriptorSet: DescriptorSet) { + self.descriptorSet = descriptorSet + } + + func generate() -> CodeGeneratorResponse { + var response: CodeGeneratorResponse = .init() + response.supportedFeatures = UInt64(CodeGeneratorResponse.Feature.proto3Optional.rawValue) + for file in descriptorSet.files { + guard let generatedFile: CodeGeneratorResponse.File = generateFile(file) else { continue } + + response.file.append(generatedFile) + } + return response + } + + private func generateFile(_ fileDescriptor: FileDescriptor) -> CodeGeneratorResponse.File? { + guard fileDescriptor.shouldGenerate else { return nil } + + let name: String = fileDescriptor.name.replacingOccurrences(of: ".proto", with: "_helper.pb.swift") + + var content: String = generateFileHeader() + for message in fileDescriptor.messages { + content += generateMessageHelper(message) + } + + var file: CodeGeneratorResponse.File = .init() + file.name = name + file.content = content + return file + } + + private func generateFileHeader() -> String { + """ + // Code generated by protoc-gen-swift-oneof-helper. DO NOT EDIT. + + import Foundation + import SwiftProtobuf + + """ + } + + private func generateMessageHelper(_ messageDescriptor: Descriptor) -> String { + func generateInitializer() -> String { + var parameters: [String] = [] + var assignments: [String] = [] + + for field in messageDescriptor.fields where field.oneofIndex == nil { + let parameterName = field.name + let parameterType = field.type + parameters.append("\(parameterName): \(parameterType)") + assignments.append("\t\tself.\(parameterName) = \(parameterName)") + } + + guard !parameters.isEmpty else { return "" } + + for oneof in messageDescriptor.oneofs { + let parameterName = oneof.name + let parameterType = oneof.containingType.typeName + parameters.append("\(parameterName): \(parameterType)") + assignments.append("self.\(parameterName) = \(parameterName)") + } + + let parametersString: String = parameters.joined(separator: ", ") + let assignmentsString: String = assignments.joined(separator: "\n") + return """ + extension \(messageDescriptor.typeName) { + + \tinit(\(parametersString)) { + \t\tself.init() + \t\t\(assignmentsString) + \t} + } + """ + } + + var content: String = "" + content += generateInitializer() + for message in messageDescriptor.messages { +// guard !nestedMessage.options.mapEntry else { continue } + + content += generateInitializer() + } + return content + } +} + +private extension FileDescriptor { + + var shouldGenerate: Bool { + self.messages.contains { message in + message.hasFields + } + } +} + +private extension Descriptor { + + var hasFields: Bool { + let hasRegularField: Bool = self.fields.contains { field in + field.oneofIndex == nil + } + let hasOneofField: Bool = !self.realOneofs.isEmpty + let hasNestedField: Bool = self.messages.contains { message in + message.hasFields + } + return hasRegularField || hasOneofField || hasNestedField + } +} diff --git a/swift/Sources/SwiftSebuf/SebufCommand.swift b/swift/Sources/SwiftSebuf/SebufCommand.swift new file mode 100644 index 0000000..536f113 --- /dev/null +++ b/swift/Sources/SwiftSebuf/SebufCommand.swift @@ -0,0 +1,40 @@ +// +// SebufCommand.swift +// SwiftSebuf +// +// Created by Khaled Chehabeddine on 08/09/2025. +// Copyright © 2025 Sebuf. All rights reserved. +// + +import ArgumentParser +import Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +protocol SebufCommand: ParsableCommand { + + associatedtype SebufCodeGenerator: Generator + + func makeResponse(_ request: CodeGeneratorRequest) -> CodeGeneratorResponse +} + +extension SebufCommand { + + mutating func run() throws { + guard let serializedBytes: Data = try FileHandle.standardInput.readToEnd() else { return } + + let request: CodeGeneratorRequest = try .init(serializedBytes: serializedBytes) + + let response: CodeGeneratorResponse = makeResponse(request) + let responseData: Data = try response.serializedData() + + try FileHandle.standardOutput.write(contentsOf: responseData) + } + + func makeResponse(_ request: CodeGeneratorRequest) -> CodeGeneratorResponse { + let descriptorSet: DescriptorSet = .init(protos: request.protoFile) + let generator: SebufCodeGenerator = .init(descriptorSet: descriptorSet) + let response: CodeGeneratorResponse = generator.generate() + return response + } +} diff --git a/swift/Sources/SwiftSebuf/SwiftProtobuf+Typealiases.swift b/swift/Sources/SwiftSebuf/SwiftProtobuf+Typealiases.swift new file mode 100644 index 0000000..4ec3dd6 --- /dev/null +++ b/swift/Sources/SwiftSebuf/SwiftProtobuf+Typealiases.swift @@ -0,0 +1,13 @@ +// +// SwiftProtobuf+Typealiases.swift +// SwiftSebuf +// +// Created by Khaled Chehabeddine on 08/09/2025. +// Copyright © 2025 Sebuf. All rights reserved. +// + +import Foundation +import SwiftProtobufPluginLibrary + +typealias CodeGeneratorRequest = Google_Protobuf_Compiler_CodeGeneratorRequest +typealias CodeGeneratorResponse = Google_Protobuf_Compiler_CodeGeneratorResponse diff --git a/swift/Sources/SwiftSebuf/SwiftSebuf.swift b/swift/Sources/SwiftSebuf/SwiftSebuf.swift deleted file mode 100644 index 08b22b8..0000000 --- a/swift/Sources/SwiftSebuf/SwiftSebuf.swift +++ /dev/null @@ -1,2 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book