feat(plugins): generate Rust lib.rs file to expose host function wrappers

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2025-12-29 14:37:02 -05:00
parent de45079ff6
commit 7afe906d11
5 changed files with 147 additions and 12 deletions

View File

@ -266,9 +266,19 @@ type ServiceB interface {
rustDir := filepath.Join(outputDir, "rust")
rsClientEntries, err := os.ReadDir(rustDir)
Expect(err).ToNot(HaveOccurred())
Expect(rsClientEntries).To(HaveLen(1), "Expected exactly one Rust client file")
Expect(rsClientEntries).To(HaveLen(2), "Expected Rust client file and lib.rs")
rsClientActual, err := os.ReadFile(filepath.Join(rustDir, rsClientEntries[0].Name()))
// Find the client file (not lib.rs)
var rsClientName string
for _, entry := range rsClientEntries {
if entry.Name() != "lib.rs" {
rsClientName = entry.Name()
break
}
}
Expect(rsClientName).ToNot(BeEmpty(), "Expected to find Rust client file")
rsClientActual, err := os.ReadFile(filepath.Join(rustDir, rsClientName))
Expect(err).ToNot(HaveOccurred())
Expect(string(rsClientActual)).To(Equal(rsClientExpected), "Rust client code mismatch")

View File

@ -190,3 +190,45 @@ func GenerateClientRust(svc Service) ([]byte, error) {
return buf.Bytes(), nil
}
// firstLine returns the first line of a multi-line string, with the first word removed.
func firstLine(s string) string {
line := s
if idx := strings.Index(s, "\n"); idx >= 0 {
line = s[:idx]
}
// Remove the first word (service name like "ArtworkService")
if idx := strings.Index(line, " "); idx >= 0 {
line = line[idx+1:]
}
return line
}
// GenerateRustLib generates the lib.rs file that exposes all service modules.
func GenerateRustLib(services []Service) ([]byte, error) {
tmplContent, err := templatesFS.ReadFile("templates/lib_rs.rs.tmpl")
if err != nil {
return nil, fmt.Errorf("reading Rust lib template: %w", err)
}
tmpl, err := template.New("lib_rs").Funcs(template.FuncMap{
"lower": strings.ToLower,
"firstLine": firstLine,
}).Parse(string(tmplContent))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
data := struct {
Services []Service
}{
Services: services,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,43 @@
// Code generated by hostgen. DO NOT EDIT.
//
//! Navidrome Host Function Wrappers for Rust Plugins
//!
//! This crate provides idiomatic Rust wrappers for all Navidrome host services.
//! It is auto-generated by the hostgen tool and should not be edited manually.
//!
//! # Usage
//!
//! Add this crate as a dependency in your plugin's Cargo.toml:
//!
//! ```toml
//! [dependencies]
//! nd-host = { path = "../../host/rust" }
//! ```
//!
//! Then import the services you need:
//!
//! ```ignore
//! use nd_host::{cache, scheduler};
//!
//! fn my_plugin_function() -> Result<(), extism_pdk::Error> {
//! // Use the cache service
//! cache::set_string("my_key", "my_value", 3600)?;
//!
//! // Schedule a recurring task
//! scheduler::schedule_recurring("@every 5m", "payload", "task_id")?;
//!
//! Ok(())
//! }
//! ```
//!
//! # Available Services
//!
{{- range .Services}}
//! - [`{{.Name | lower}}`] - {{if .Doc}}{{.Doc | firstLine}}{{else}}{{.Name}} service{{end}}
{{- end}}
{{range .Services}}
#[path = "nd_host_{{.Name | lower}}.rs"]
pub mod {{.Name | lower}};
{{end}}
// Re-export commonly used types from extism-pdk for convenience
pub use extism_pdk::Error;

View File

@ -147,6 +147,14 @@ func main() {
}
}
}
// Generate Rust lib.rs to expose all modules
if generateRsClient && len(services) > 0 {
if err := generateRustLibFile(services, absOutput, *dryRun, *verbose); err != nil {
fmt.Fprintf(os.Stderr, "Error generating Rust lib.rs: %v\n", err)
os.Exit(1)
}
}
}
// generateHostCode generates host-side code for a service.
@ -275,3 +283,33 @@ func generateRustClientCode(svc internal.Service, outputDir string, dryRun, verb
}
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)
}
clientDir := filepath.Join(outputDir, "rust")
libFile := filepath.Join(clientDir, "lib.rs")
if dryRun {
fmt.Printf("=== %s ===\n%s\n", libFile, code)
return nil
}
// Create rust/ subdirectory if needed
if err := os.MkdirAll(clientDir, 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
}

View File

@ -1,3 +1,5 @@
// Code generated by hostgen. DO NOT EDIT.
//
//! Navidrome Host Function Wrappers for Rust Plugins
//!
//! This crate provides idiomatic Rust wrappers for all Navidrome host services.
@ -15,14 +17,14 @@
//! Then import the services you need:
//!
//! ```ignore
//! use nd_host::{cache, scheduler, kvstore};
//! use nd_host::{cache, scheduler};
//!
//! fn my_plugin_function() -> Result<(), extism_pdk::Error> {
//! // Use the cache service
//! cache::cache_set("my_key", b"my_value", 3600)?;
//! cache::set_string("my_key", "my_value", 3600)?;
//!
//! // Schedule a recurring task
//! scheduler::scheduler_schedule_recurring("@every 5m", "payload", "task_id")?;
//! scheduler::schedule_recurring("@every 5m", "payload", "task_id")?;
//!
//! Ok(())
//! }
@ -30,13 +32,13 @@
//!
//! # Available Services
//!
//! - [`artwork`] - Access album and artist artwork
//! - [`cache`] - Temporary key-value storage with TTL
//! - [`kvstore`] - Persistent key-value storage
//! - [`library`] - Access the music library
//! - [`scheduler`] - Schedule one-time and recurring tasks
//! - [`subsonicapi`] - Make Subsonic API calls
//! - [`websocket`] - Send real-time messages to clients
//! - [`artwork`] - provides artwork URL generation capabilities for plugins.
//! - [`cache`] - provides in-memory TTL-based caching capabilities for plugins.
//! - [`kvstore`] - provides persistent key-value storage for plugins.
//! - [`library`] - provides access to music library metadata for plugins.
//! - [`scheduler`] - provides task scheduling capabilities for plugins.
//! - [`subsonicapi`] - provides access to Navidrome's Subsonic API from plugins.
//! - [`websocket`] - provides WebSocket communication capabilities for plugins.
#[path = "nd_host_artwork.rs"]
pub mod artwork;