434 lines
13 KiB
Go

// ndpgen generates Navidrome Plugin Development Kit (PDK) code from annotated Go interfaces.
//
// This is the unified code generator that replaces hostgen and handles both host function
// wrappers and capability wrappers (when implemented).
//
// Usage:
//
// ndpgen -input=./plugins/host -output=./plugins/pdk
//
// This generates code into language-specific subdirectories:
// - Go: $output/go/host/
// - Python: $output/python/host/
// - Rust: $output/rust/host/
//
// Flags:
//
// -input Input directory containing Go source files with annotated interfaces
// -output Output directory base for generated files (default: same as input)
// -package Output package name for Go (default: host)
// -host-only Generate only host function wrappers (default: true, capability support TBD)
// -go Generate Go client wrappers (default: true when not using -python/-rust)
// -python Generate Python client wrappers (default: false)
// -rust Generate Rust client wrappers (default: false)
// -v Verbose output
// -dry-run Preview generated code without writing files
package main
import (
"flag"
"fmt"
"go/format"
"os"
"path/filepath"
"strings"
"github.com/navidrome/navidrome/plugins/cmd/ndpgen/internal"
)
// config holds the parsed command-line configuration.
type config struct {
inputDir string
outputDir string // Base output directory (e.g., plugins/pdk)
goOutputDir string // Go output: $outputDir/go/host
pythonOutputDir string // Python output: $outputDir/python/host
rustOutputDir string // Rust output: $outputDir/rust/host
pkgName string
hostOnly bool
generateGoClient bool
generatePyClient bool
generateRsClient bool
verbose bool
dryRun bool
}
func main() {
cfg, err := parseConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
services, err := parseServices(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if len(services) == 0 {
return
}
if err := generateAllCode(cfg, services); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// parseConfig parses command-line flags and returns the configuration.
func parseConfig() (*config, error) {
var (
inputDir = flag.String("input", ".", "Input directory containing Go source files")
outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)")
pkgName = flag.String("package", "host", "Output package name for Go (default: host)")
hostOnly = flag.Bool("host-only", true, "Generate only host function wrappers (capability support TBD)")
goClient = flag.Bool("go", false, "Generate Go client wrappers")
pyClient = flag.Bool("python", false, "Generate Python client wrappers")
rsClient = flag.Bool("rust", false, "Generate Rust client wrappers")
verbose = flag.Bool("v", false, "Verbose output")
dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files")
)
flag.Parse()
if *outputDir == "" {
*outputDir = *inputDir
}
absInput, err := filepath.Abs(*inputDir)
if err != nil {
return nil, fmt.Errorf("resolving input path: %w", err)
}
absOutput, err := filepath.Abs(*outputDir)
if err != nil {
return nil, fmt.Errorf("resolving output path: %w", err)
}
// Set output directories for each language: $output/$lang/host/
absGoOutput := filepath.Join(absOutput, "go", "host")
absPythonOutput := filepath.Join(absOutput, "python", "host")
absRustOutput := filepath.Join(absOutput, "rust", "host")
// Determine what to generate
// Default: generate Go clients if no language flag is specified
anyLangFlag := *goClient || *pyClient || *rsClient
return &config{
inputDir: absInput,
outputDir: absOutput,
goOutputDir: absGoOutput,
pythonOutputDir: absPythonOutput,
rustOutputDir: absRustOutput,
pkgName: *pkgName,
hostOnly: *hostOnly,
generateGoClient: *goClient || !anyLangFlag,
generatePyClient: *pyClient,
generateRsClient: *rsClient,
verbose: *verbose,
dryRun: *dryRun,
}, nil
}
// parseServices parses source files and returns discovered services.
func parseServices(cfg *config) ([]internal.Service, error) {
if cfg.verbose {
fmt.Printf("Input directory: %s\n", cfg.inputDir)
fmt.Printf("Base output directory: %s\n", cfg.outputDir)
if cfg.generateGoClient {
fmt.Printf("Go output directory: %s\n", cfg.goOutputDir)
}
if cfg.generatePyClient {
fmt.Printf("Python output directory: %s\n", cfg.pythonOutputDir)
}
if cfg.generateRsClient {
fmt.Printf("Rust output directory: %s\n", cfg.rustOutputDir)
}
fmt.Printf("Package name: %s\n", cfg.pkgName)
fmt.Printf("Host-only mode: %v\n", cfg.hostOnly)
fmt.Printf("Generate Go client code: %v\n", cfg.generateGoClient)
fmt.Printf("Generate Python client code: %v\n", cfg.generatePyClient)
fmt.Printf("Generate Rust client code: %v\n", cfg.generateRsClient)
}
services, err := internal.ParseDirectory(cfg.inputDir)
if err != nil {
return nil, fmt.Errorf("parsing source files: %w", err)
}
if len(services) == 0 {
if cfg.verbose {
fmt.Println("No host services found")
}
return nil, nil
}
if cfg.verbose {
fmt.Printf("Found %d host service(s)\n", len(services))
for _, svc := range services {
fmt.Printf(" - %s (%d methods)\n", svc.Name, len(svc.Methods))
}
}
return services, nil
}
// generateAllCode generates all requested code for the services.
func generateAllCode(cfg *config, services []internal.Service) error {
for _, svc := range services {
if cfg.generateGoClient {
if err := generateGoClientCode(svc, cfg.goOutputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go client code for %s: %w", svc.Name, err)
}
}
if cfg.generatePyClient {
if err := generatePythonClientCode(svc, cfg.pythonOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Python client code for %s: %w", svc.Name, err)
}
}
if cfg.generateRsClient {
if err := generateRustClientCode(svc, cfg.rustOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust client code for %s: %w", svc.Name, err)
}
}
}
if cfg.generateRsClient && len(services) > 0 {
if err := generateRustLibFile(services, cfg.rustOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Rust lib.rs: %w", err)
}
}
if cfg.generateGoClient && len(services) > 0 {
if err := generateGoDocFile(services, cfg.goOutputDir, cfg.pkgName, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go doc.go: %w", err)
}
if err := generateGoModFile(cfg.goOutputDir, cfg.dryRun, cfg.verbose); err != nil {
return fmt.Errorf("generating Go go.mod: %w", err)
}
}
return nil
}
// generateGoClientCode generates Go client-side code for a service.
func generateGoClientCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGo(svc, pkgName)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code)
}
// Client code goes directly in the output directory
clientFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+".go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, formatted)
} else {
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(clientFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client code: %s\n", clientFile)
}
}
// Also generate stub file for non-WASM platforms
return generateGoClientStubCode(svc, outputDir, pkgName, dryRun, verbose)
}
// generateGoClientStubCode generates stub code for non-WASM platforms.
func generateGoClientStubCode(svc internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateClientGoStub(svc, pkgName)
if err != nil {
return fmt.Errorf("generating stub code: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, code)
}
// Stub code goes directly in output directory with _stub suffix
stubFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+"_stub.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", stubFile, formatted)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(stubFile, formatted, 0600); err != nil {
return fmt.Errorf("writing stub file: %w", err)
}
if verbose {
fmt.Printf("Generated Go client stub: %s\n", stubFile)
}
return nil
}
// generatePythonClientCode generates Python client-side code for a service.
func generatePythonClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientPython(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
// Python code goes directly in the output directory
clientFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+".py")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating python client directory: %w", err)
}
if err := os.WriteFile(clientFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Python client code: %s\n", clientFile)
}
return nil
}
// generateRustClientCode generates Rust client-side code for a service.
func generateRustClientCode(svc internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateClientRust(svc)
if err != nil {
return fmt.Errorf("generating code: %w", err)
}
// Rust code goes directly in the output directory
clientFile := filepath.Join(outputDir, "nd_host_"+strings.ToLower(svc.Name)+".rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", clientFile, code)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating rust client directory: %w", err)
}
if err := os.WriteFile(clientFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust client code: %s\n", clientFile)
}
return nil
}
// generateRustLibFile generates the lib.rs file that exposes all Rust modules.
func generateRustLibFile(services []internal.Service, outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateRustLib(services)
if err != nil {
return fmt.Errorf("generating lib.rs: %w", err)
}
// lib.rs goes directly in the output directory
libFile := filepath.Join(outputDir, "lib.rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", libFile, code)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating rust client directory: %w", err)
}
if err := os.WriteFile(libFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Rust lib.rs: %s\n", libFile)
}
return nil
}
// generateGoDocFile generates the doc.go file for the Go library.
func generateGoDocFile(services []internal.Service, outputDir, pkgName string, dryRun, verbose bool) error {
code, err := internal.GenerateGoDoc(services, pkgName)
if err != nil {
return fmt.Errorf("generating doc.go: %w", err)
}
formatted, err := format.Source(code)
if err != nil {
return fmt.Errorf("formatting doc.go: %w\nRaw code:\n%s", err, code)
}
docFile := filepath.Join(outputDir, "doc.go")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", docFile, formatted)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(docFile, formatted, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go doc.go: %s\n", docFile)
}
return nil
}
// generateGoModFile generates the go.mod file for the Go library.
func generateGoModFile(outputDir string, dryRun, verbose bool) error {
code, err := internal.GenerateGoMod()
if err != nil {
return fmt.Errorf("generating go.mod: %w", err)
}
modFile := filepath.Join(outputDir, "go.mod")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", modFile, code)
return nil
}
// Create output directory if needed
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory: %w", err)
}
if err := os.WriteFile(modFile, code, 0600); err != nil {
return fmt.Errorf("writing file: %w", err)
}
if verbose {
fmt.Printf("Generated Go go.mod: %s\n", modFile)
}
return nil
}