mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
434 lines
13 KiB
Go
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
|
|
}
|