Skip to content

Commit

Permalink
review comments, rethink config & imports
Browse files Browse the repository at this point in the history
  • Loading branch information
rnro committed Jan 17, 2025
1 parent 87197c6 commit 13ddcf3
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 202 deletions.
246 changes: 160 additions & 86 deletions Plugins/GRPCProtobufGenerator/BuildPluginConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,110 +18,184 @@ import Foundation

let configFileName = "grpc-swift-proto-generator-config.json"

/// The configuration of the build plugin.
/// The config of the build plugin.
struct BuildPluginConfig: Codable {
/// The visibility of the generated files.
///
/// Defaults to `Internal`.
var visibility: GenerationConfig.Visibility
/// Whether server code is generated.
///
/// Defaults to `true`.
var server: Bool
/// Whether client code is generated.
///
/// Defaults to `true`.
var client: Bool
/// Whether message code is generated.
///
/// Defaults to `true`.
var message: Bool
/// Whether imports should have explicit access levels.
///
/// Defaults to `false`.
var useAccessLevelOnImports: Bool

/// Specify the directory in which to search for imports.
///
/// Paths are relative to the location of the specifying config file.
/// Build plugins only have access to files within the target's source directory.
/// May be specified multiple times; directories will be searched in order.
/// The target source directory is always appended
/// to the import paths.
var importPaths: [String]

/// The path to the `protoc` binary.
///
/// If this is not set, Swift Package Manager will try to find the tool itself.
var protocPath: String?
/// Config defining which components should be considered when generating source.
struct Generate {
/// Whether server code is generated.
///
/// Defaults to `true`.
var servers: Bool
/// Whether client code is generated.
///
/// Defaults to `true`.
var clients: Bool
/// Whether message code is generated.
///
/// Defaults to `true`.
var messages: Bool

static let defaults = Self(
servers: true,
clients: true,
messages: true
)

private init(servers: Bool, clients: Bool, messages: Bool) {
self.servers = servers
self.clients = clients
self.messages = messages
}
}

/// Config relating to the generated code itself.
struct GeneratedSource {
/// The visibility of the generated files.
///
/// Defaults to `Internal`.
var accessLevel: GenerationConfig.AccessLevel
/// Whether imports should have explicit access levels.
///
/// Defaults to `false`.
var useAccessLevelOnImports: Bool

static let defaults = Self(
accessLevel: .internal,
useAccessLevelOnImports: false
)

private init(accessLevel: GenerationConfig.AccessLevel, useAccessLevelOnImports: Bool) {
self.accessLevel = accessLevel
self.useAccessLevelOnImports = useAccessLevelOnImports
}
}

/// Config relating to the protoc invocation.
struct Protoc {
/// Specify the directory in which to search for imports.
///
/// Paths are relative to the location of the specifying config file.
/// Build plugins only have access to files within the target's source directory.
/// May be specified multiple times; directories will be searched in order.
/// The target source directory is always appended
/// to the import paths.
var importPaths: [String]

/// The path to the `protoc` executable binary.
///
/// If this is not set, Swift Package Manager will try to find the tool itself.
var executablePath: String?

static let defaults = Self(
importPaths: [],
executablePath: nil
)

private init(importPaths: [String], executablePath: String?) {
self.importPaths = importPaths
self.executablePath = executablePath
}
}

/// Config defining which components should be considered when generating source.
var generate: Generate
/// Config relating to the nature of the generated code.
var generatedSource: GeneratedSource
/// Config relating to the protoc invocation.
var protoc: Protoc

static let defaults = Self(
generate: Generate.defaults,
generatedSource: GeneratedSource.defaults,
protoc: Protoc.defaults
)
private init(generate: Generate, generatedSource: GeneratedSource, protoc: Protoc) {
self.generate = generate
self.generatedSource = generatedSource
self.protoc = protoc
}

// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case visibility
case server
case client
case message
case generate
case generatedSource
case protoc
}


init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.generate = try container.decodeIfPresent(Generate.self, forKey: .generate) ?? Self.defaults.generate
self.generatedSource = try container.decodeIfPresent(GeneratedSource.self, forKey: .generatedSource) ?? Self.defaults.generatedSource
self.protoc = try container.decodeIfPresent(Protoc.self, forKey: .protoc) ?? Self.defaults.protoc
}
}

extension BuildPluginConfig.Generate: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case servers
case clients
case messages
}

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.servers = try container.decodeIfPresent(Bool.self, forKey: .servers) ?? Self.defaults.servers
self.clients = try container.decodeIfPresent(Bool.self, forKey: .clients) ?? Self.defaults.clients
self.messages = try container.decodeIfPresent(Bool.self, forKey: .messages) ?? Self.defaults.messages
}
}

extension BuildPluginConfig.GeneratedSource: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case accessLevel
case useAccessLevelOnImports
case importPaths
case protocPath
}

let defaultVisibility: GenerationConfig.Visibility = .internal
let defaultServer = true
let defaultClient = true
let defaultMessage = true
let defaultUseAccessLevelOnImports = false
let defaultImportPaths: [String] = []
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.accessLevel = try container.decodeIfPresent(GenerationConfig.AccessLevel.self, forKey: .accessLevel)
?? Self.defaults.accessLevel
self.useAccessLevelOnImports = try container.decodeIfPresent(Bool.self,forKey: .useAccessLevelOnImports)
?? Self.defaults.useAccessLevelOnImports
}
}

extension BuildPluginConfig.Protoc: Codable {
// Codable conformance with defaults
enum CodingKeys: String, CodingKey {
case importPaths
case executablePath
}

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.visibility =
try container.decodeIfPresent(GenerationConfig.Visibility.self, forKey: .visibility)
?? defaultVisibility
self.server = try container.decodeIfPresent(Bool.self, forKey: .server) ?? defaultServer
self.client = try container.decodeIfPresent(Bool.self, forKey: .client) ?? defaultClient
self.message = try container.decodeIfPresent(Bool.self, forKey: .message) ?? defaultMessage
self.useAccessLevelOnImports =
try container.decodeIfPresent(Bool.self, forKey: .useAccessLevelOnImports)
?? defaultUseAccessLevelOnImports
self.importPaths =
try container.decodeIfPresent([String].self, forKey: .importPaths) ?? defaultImportPaths
self.protocPath = try container.decodeIfPresent(String.self, forKey: .protocPath)
self.importPaths = try container.decodeIfPresent([String].self,forKey: .importPaths)
?? Self.defaults.importPaths
self.executablePath = try container.decodeIfPresent(String.self, forKey: .executablePath)
}
}

extension GenerationConfig {
init(configurationFile: BuildPluginConfig, configurationFilePath: URL, outputPath: URL) {
self.visibility = configurationFile.visibility
self.server = configurationFile.server
self.client = configurationFile.client
self.message = configurationFile.message
init(buildPluginConfig: BuildPluginConfig, configFilePath: URL, outputPath: URL) {
self.server = buildPluginConfig.generate.servers
self.client = buildPluginConfig.generate.clients
self.message = buildPluginConfig.generate.messages
// hard-code full-path to avoid collisions since this goes into a temporary directory anyway
self.fileNaming = .fullPath
self.useAccessLevelOnImports = configurationFile.useAccessLevelOnImports
self.importPaths = []

self.visibility = buildPluginConfig.generatedSource.accessLevel
self.useAccessLevelOnImports = buildPluginConfig.generatedSource.useAccessLevelOnImports
// Generate absolute paths for the imports relative to the config file in which they are specified
self.importPaths = configurationFile.importPaths.map { relativePath in
configurationFilePath.deletingLastPathComponent().relativePath + "/" + relativePath
}
self.protocPath = configurationFile.protocPath
self.outputPath = outputPath.relativePath
}
}

extension GenerationConfig.Visibility: Codable {
init?(rawValue: String) {
switch rawValue.lowercased() {
case "internal":
self = .internal
case "public":
self = .public
case "package":
self = .package
default:
return nil
self.importPaths = buildPluginConfig.protoc.importPaths.map { relativePath in
configFilePath.deletingLastPathComponent().absoluteStringNoScheme + "/" + relativePath
}
self.protocPath = buildPluginConfig.protoc.executablePath
self.outputPath = outputPath.absoluteStringNoScheme
}
}
Loading

0 comments on commit 13ddcf3

Please sign in to comment.