package internal import ( "go/format" "os" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Generator", func() { Describe("GenerateHost", func() { It("should generate valid Go code for a simple service with strings", func() { // All methods use JSON request/response types svc := Service{ Name: "SubsonicAPI", Permission: "subsonicapi", Interface: "SubsonicAPIService", Methods: []Method{ { Name: "Call", HasError: true, Params: []Param{NewParam("uri", "string")}, Returns: []Param{NewParam("response", "string")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) // Verify the code is valid Go _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for generated header Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT.")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package host")) // All methods now use request type for JSON protocol Expect(codeStr).To(ContainSubstring("type SubsonicAPICallRequest struct")) Expect(codeStr).To(ContainSubstring(`Uri string `)) // Response type with error handling Expect(codeStr).To(ContainSubstring("type SubsonicAPICallResponse struct")) Expect(codeStr).To(ContainSubstring(`Response string `)) Expect(codeStr).To(ContainSubstring(`Error string `)) // Check for registration function Expect(codeStr).To(ContainSubstring("func RegisterSubsonicAPIHostFunctions(service SubsonicAPIService)")) // Check for host function name Expect(codeStr).To(ContainSubstring(`"subsonicapi_call"`)) // Check for JSON unmarshal (all methods use JSON now) Expect(codeStr).To(ContainSubstring("json.Unmarshal")) }) It("should generate code for methods without parameters", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "NoParams", HasError: true, Returns: []Param{NewParam("result", "string")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Methods without params don't need a request type - no params to serialize Expect(codeStr).NotTo(ContainSubstring("type TestNoParamsRequest struct")) // But still uses PTR input/output for consistency Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`)) }) It("should generate code for methods without return values", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "NoReturn", HasError: true, Params: []Param{NewParam("input", "string")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) }) It("should generate code for multiple methods", func() { svc := Service{ Name: "Scheduler", Permission: "scheduler", Interface: "SchedulerService", Methods: []Method{ { Name: "ScheduleRecurring", HasError: true, Params: []Param{NewParam("cronExpression", "string")}, Returns: []Param{NewParam("scheduleID", "string")}, }, { Name: "ScheduleOneTime", HasError: true, Params: []Param{NewParam("delaySeconds", "int32")}, Returns: []Param{NewParam("scheduleID", "string")}, }, { Name: "CancelSchedule", HasError: true, Params: []Param{NewParam("scheduleID", "string")}, Returns: []Param{NewParam("canceled", "bool")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring("scheduler_schedulerecurring")) Expect(codeStr).To(ContainSubstring("scheduler_scheduleonetime")) Expect(codeStr).To(ContainSubstring("scheduler_cancelschedule")) }) It("should handle multiple simple parameters with JSON", func() { // All params use JSON - single PTR input svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "MultiParam", HasError: true, Params: []Param{ NewParam("name", "string"), NewParam("count", "int32"), NewParam("enabled", "bool"), }, Returns: []Param{NewParam("result", "string")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // All methods use request type with JSON protocol Expect(codeStr).To(ContainSubstring("type TestMultiParamRequest struct")) // Check for JSON unmarshal (all methods use JSON now) Expect(codeStr).To(ContainSubstring("json.Unmarshal")) // Check that input/output ValueType both use PTR (JSON) Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`)) }) It("should use single PTR for mixed simple and complex params", func() { // When any param needs JSON, all are bundled into one request struct svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "MixedParam", HasError: true, Params: []Param{ NewParam("id", "string"), // simple (PTR for string) NewParam("tags", "[]string"), // complex - needs JSON }, Returns: []Param{NewParam("count", "int32")}, // simple }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Request type IS needed because of complex param Expect(codeStr).To(ContainSubstring("type TestMixedParamRequest struct")) // When using request type, only ONE PTR for input (the JSON request) Expect(codeStr).To(MatchRegexp(`\[\]extism\.ValueType\{extism\.ValueTypePTR\},\s*\[\]extism\.ValueType\{extism\.ValueTypePTR\}`)) }) It("should generate proper JSON tags for complex types", func() { // Complex types (structs, slices, maps) need JSON serialization svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "Method", HasError: true, Params: []Param{NewParam("inputValue", "[]string")}, // slice needs JSON Returns: []Param{NewParam("outputValue", "map[string]string")}, // map needs JSON }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Complex params need request type with JSON tags Expect(codeStr).To(ContainSubstring(`json:"inputValue"`)) // Complex returns need response type with JSON tags Expect(codeStr).To(ContainSubstring(`json:"outputValue,omitempty"`)) }) It("should include required imports", func() { // Service with complex types needs JSON import svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "Method", HasError: true, Params: []Param{NewParam("data", "MyStruct")}, // struct needs JSON }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring(`"context"`)) Expect(codeStr).To(ContainSubstring(`"encoding/json"`)) Expect(codeStr).To(ContainSubstring(`extism "github.com/extism/go-sdk"`)) }) It("should always include json import for JSON protocol", func() { // All services use JSON protocol, so json import is always needed svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "Method", Params: []Param{NewParam("count", "int32")}, Returns: []Param{NewParam("result", "int64")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring(`"context"`)) Expect(codeStr).To(ContainSubstring(`"encoding/json"`)) Expect(codeStr).To(ContainSubstring(`extism "github.com/extism/go-sdk"`)) }) It("imports the shared types package when a method references types directly", func() { svc := Service{ Name: "Matcher", Interface: "MatcherService", Methods: []Method{ { Name: "MatchSongs", HasError: true, Params: []Param{NewParam("songs", "[]types.SongRef")}, Returns: []Param{NewParam("results", "[]*types.Track")}, }, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring(`"github.com/navidrome/navidrome/plugins/types"`)) Expect(codeStr).To(ContainSubstring("Songs []types.SongRef")) }) It("does not import the shared types package when no method references types", func() { svc := Service{ Name: "Test", Interface: "TestService", Methods: []Method{ {Name: "Method", Params: []Param{NewParam("count", "int32")}}, }, } code, err := GenerateHost(svc, "host") Expect(err).NotTo(HaveOccurred()) Expect(string(code)).NotTo(ContainSubstring(`"github.com/navidrome/navidrome/plugins/types"`)) }) }) Describe("toJSONName", func() { It("should convert to camelCase matching Rust serde behavior", func() { Expect(toJSONName("InputValue")).To(Equal("inputValue")) Expect(toJSONName("URI")).To(Equal("uri")) Expect(toJSONName("id")).To(Equal("id")) Expect(toJSONName("ID")).To(Equal("id")) Expect(toJSONName("ConnectionID")).To(Equal("connectionId")) Expect(toJSONName("NewConnectionID")).To(Equal("newConnectionId")) Expect(toJSONName("XMLHTTPRequest")).To(Equal("xmlhttpRequest")) Expect(toJSONName("APIKey")).To(Equal("apiKey")) }) It("should handle empty string", func() { Expect(toJSONName("")).To(Equal("")) }) }) Describe("NewParam", func() { It("should create param with auto-generated JSON name", func() { p := NewParam("MyParam", "string") Expect(p.Name).To(Equal("MyParam")) Expect(p.Type).To(Equal("string")) Expect(p.JSONName).To(Equal("myParam")) }) }) Describe("Method.IsOptionPattern", func() { It("should return true for (value, exists bool) pattern", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, {Name: "exists", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeTrue()) }) It("should return true for (value, ok bool) pattern", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "int64"}, {Name: "ok", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeTrue()) }) It("should return true for (value, found bool) pattern", func() { m := Method{ Returns: []Param{ {Name: "data", Type: "[]byte"}, {Name: "found", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeTrue()) }) It("should be case insensitive for bool name", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, {Name: "EXISTS", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeTrue()) }) It("should return false for single return", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) It("should return false for more than two returns", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, {Name: "count", Type: "int"}, {Name: "exists", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) It("should return false when second return is not bool", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, {Name: "count", Type: "int"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) It("should return false when bool is not named exists/ok/found", func() { m := Method{ Returns: []Param{ {Name: "value", Type: "string"}, {Name: "success", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) It("should return false for Has() pattern where first return is bool", func() { // Has(key) -> (exists bool) should NOT be treated as Option pattern m := Method{ Returns: []Param{ {Name: "exists", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) It("should return false when first return is bool (preserves Has-like methods)", func() { // Even with two returns, if first is bool, don't convert to Option m := Method{ Returns: []Param{ {Name: "result", Type: "bool"}, {Name: "exists", Type: "bool"}, }, } Expect(m.IsOptionPattern()).To(BeFalse()) }) }) Describe("ToSnakeCase", func() { It("should convert PascalCase to snake_case", func() { Expect(ToSnakeCase("ScheduleRecurring")).To(Equal("schedule_recurring")) Expect(ToSnakeCase("GetString")).To(Equal("get_string")) Expect(ToSnakeCase("simple")).To(Equal("simple")) }) It("should handle acronyms correctly", func() { Expect(ToSnakeCase("ID")).To(Equal("id")) Expect(ToSnakeCase("ScheduleID")).To(Equal("schedule_id")) Expect(ToSnakeCase("NewScheduleID")).To(Equal("new_schedule_id")) Expect(ToSnakeCase("XMLParser")).To(Equal("xml_parser")) Expect(ToSnakeCase("GetHTTPResponse")).To(Equal("get_http_response")) }) }) Describe("GenerateGoDoc", func() { It("should generate valid doc.go content for multiple services", func() { services := []Service{ { Name: "Cache", Permission: "cache", Interface: "CacheService", Doc: "CacheService provides temporary key-value storage with TTL.", }, { Name: "Scheduler", Permission: "scheduler", Interface: "SchedulerService", Doc: "SchedulerService manages scheduled tasks.", }, } code, err := GenerateGoDoc(services, "ndpdk") Expect(err).NotTo(HaveOccurred()) // Verify it's valid Go code _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for generated header Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT.")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package ndpdk")) // Check for package documentation Expect(codeStr).To(ContainSubstring("Package ndpdk provides Navidrome Plugin Development Kit wrappers")) // Check that services are listed Expect(codeStr).To(ContainSubstring("Cache:")) Expect(codeStr).To(ContainSubstring("Scheduler:")) }) }) Describe("GenerateGoMod", func() { It("should generate valid go.mod content", func() { code, err := GenerateGoMod() Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for module declaration (consolidated PDK path at pdk/go level) Expect(codeStr).To(ContainSubstring("module github.com/navidrome/navidrome/plugins/pdk/go")) // Ensure it's not the old host-specific path Expect(codeStr).NotTo(ContainSubstring("module github.com/navidrome/navidrome/plugins/pdk/go/host")) // Check for Go version Expect(codeStr).To(ContainSubstring("go 1.25")) // Check for extism-go-pdk dependency Expect(codeStr).To(ContainSubstring("github.com/extism/go-pdk")) }) }) Describe("GenerateClientGo", func() { It("should include errors import when service has methods with errors", func() { svc := Service{ Name: "Cache", Permission: "cache", Interface: "CacheService", Methods: []Method{ { Name: "Get", HasError: true, Params: []Param{NewParam("key", "string")}, Returns: []Param{NewParam("value", "string")}, }, }, } code, err := GenerateClientGo(svc, "host") Expect(err).NotTo(HaveOccurred()) // Verify the code is valid Go (can't actually compile without wasip1) codeStr := string(code) // Check for errors import when methods have errors Expect(codeStr).To(ContainSubstring(`"errors"`)) Expect(codeStr).To(ContainSubstring("errors.New")) }) It("should not include errors import when service has no methods with errors", func() { svc := Service{ Name: "Config", Permission: "config", Interface: "ConfigService", Methods: []Method{ { Name: "Get", HasError: false, Params: []Param{NewParam("key", "string")}, Returns: []Param{NewParam("value", "string"), NewParam("exists", "bool")}, }, { Name: "List", HasError: false, Params: []Param{NewParam("prefix", "string")}, Returns: []Param{NewParam("keys", "[]string")}, }, }, } code, err := GenerateClientGo(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check that errors is NOT imported when no methods have errors Expect(codeStr).NotTo(ContainSubstring(`"errors"`)) Expect(codeStr).NotTo(ContainSubstring("errors.New")) }) It("should generate valid Go code structure", func() { svc := Service{ Name: "SubsonicAPI", Permission: "subsonicapi", Interface: "SubsonicAPIService", Methods: []Method{ { Name: "Call", HasError: true, Params: []Param{NewParam("uri", "string")}, Returns: []Param{NewParam("response", "string")}, }, }, } code, err := GenerateClientGo(svc, "host") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for generated header Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT.")) // Check for build tag Expect(codeStr).To(ContainSubstring("//go:build wasip1")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package host")) // Check for wasmimport directive Expect(codeStr).To(ContainSubstring("//go:wasmimport extism:host/user")) // Check for PDK import Expect(codeStr).To(ContainSubstring("github.com/navidrome/navidrome/plugins/pdk/go/pdk")) }) }) Describe("GenerateClientGoStub", func() { It("should generate valid mock code with testify/mock", func() { svc := Service{ Name: "Cache", Permission: "cache", Interface: "CacheService", Doc: "CacheService provides caching capabilities.", Methods: []Method{ { Name: "Get", Doc: "Get retrieves a value from the cache.", Params: []Param{ {Name: "key", Type: "string"}, }, Returns: []Param{ {Name: "value", Type: "string"}, {Name: "exists", Type: "bool"}, }, }, }, } code, err := GenerateClientGoStub(svc, "ndpdk") Expect(err).NotTo(HaveOccurred()) // Verify it's valid Go code _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for build tag (non-WASM) Expect(codeStr).To(ContainSubstring("//go:build !wasip1")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package ndpdk")) // Check for mock comment Expect(codeStr).To(ContainSubstring("mock implementations for non-WASM builds")) // Check for testify/mock import Expect(codeStr).To(ContainSubstring(`"github.com/stretchr/testify/mock"`)) // Check for private mock struct Expect(codeStr).To(ContainSubstring("type mockCacheService struct")) Expect(codeStr).To(ContainSubstring("mock.Mock")) // Check for exported mock instance Expect(codeStr).To(ContainSubstring("var CacheMock = &mockCacheService{}")) // Check for mock method Expect(codeStr).To(ContainSubstring("func (m *mockCacheService) Get(key string)")) Expect(codeStr).To(ContainSubstring("m.Called(key)")) // Check for wrapper function delegating to mock Expect(codeStr).To(ContainSubstring("func CacheGet(key string)")) Expect(codeStr).To(ContainSubstring("return CacheMock.Get(key)")) // Stub files should NOT have request/response types (they're not needed) Expect(codeStr).NotTo(ContainSubstring("Request struct")) Expect(codeStr).NotTo(ContainSubstring("Response struct")) }) It("should generate correct mock return values for different types", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "GetString", Params: []Param{ {Name: "key", Type: "string"}, }, Returns: []Param{ {Name: "value", Type: "string"}, }, HasError: true, }, { Name: "GetInt64", Params: []Param{ {Name: "key", Type: "string"}, }, Returns: []Param{ {Name: "value", Type: "int64"}, {Name: "exists", Type: "bool"}, }, HasError: true, }, { Name: "GetBytes", Params: []Param{ {Name: "key", Type: "string"}, }, Returns: []Param{ {Name: "value", Type: "[]byte"}, }, HasError: true, }, }, } code, err := GenerateClientGoStub(svc, "ndpdk") Expect(err).NotTo(HaveOccurred()) // Verify it's valid Go code _, err = format.Source(code) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check string return uses args.String(0) Expect(codeStr).To(ContainSubstring("args.String(0)")) // Check int64 return uses args.Get(0).(int64) Expect(codeStr).To(ContainSubstring("args.Get(0).(int64)")) // Check bool return uses args.Bool(1) Expect(codeStr).To(ContainSubstring("args.Bool(1)")) // Check []byte return uses args.Get(0).([]byte) Expect(codeStr).To(ContainSubstring("args.Get(0).([]byte)")) // Check error returns use args.Error(N) Expect(codeStr).To(ContainSubstring("args.Error(")) }) }) Describe("Integration", func() { It("should generate compilable code from parsed source", func() { // This is an integration test that verifies the full pipeline src := `package host import "context" // TestService is a test service. //nd:hostservice name=Test permission=test type TestService interface { // DoSomething does something. //nd:hostfunc DoSomething(ctx context.Context, input string) (output string, err error) } ` // Create temporary directory tmpDir := GinkgoT().TempDir() path := tmpDir + "/test.go" err := writeFile(path, src) Expect(err).NotTo(HaveOccurred()) // Parse services, err := ParseDirectory(tmpDir) Expect(err).NotTo(HaveOccurred()) Expect(services).To(HaveLen(1)) // Generate code, err := GenerateHost(services[0], "host") Expect(err).NotTo(HaveOccurred()) // Format (validates syntax) formatted, err := format.Source(code) Expect(err).NotTo(HaveOccurred()) // Verify key elements codeStr := string(formatted) Expect(codeStr).To(ContainSubstring("RegisterTestHostFunctions")) Expect(codeStr).To(ContainSubstring(`"test_dosomething"`)) }) }) Describe("GenerateCapabilityGo", func() { It("should generate valid Go code for a non-required capability", func() { cap := Capability{ Name: "metadata", Interface: "MetadataAgent", Required: false, Doc: "MetadataAgent provides metadata retrieval.", Methods: []Export{ { Name: "GetArtistBiography", ExportName: "nd_get_artist_biography", Input: Param{Type: "ArtistInput"}, Output: Param{Type: "ArtistBiographyOutput"}, Doc: "Returns artist biography", }, { Name: "GetArtistImages", ExportName: "nd_get_artist_images", Input: Param{Type: "ArtistInput"}, Output: Param{Type: "ArtistImagesOutput"}, Doc: "Returns artist images", }, }, Structs: []StructDef{ { Name: "ArtistInput", Fields: []FieldDef{ {Name: "ID", Type: "string", JSONTag: "id"}, {Name: "Name", Type: "string", JSONTag: "name"}, }, }, { Name: "ArtistBiographyOutput", Fields: []FieldDef{ {Name: "Biography", Type: "string", JSONTag: "biography"}, }, }, { Name: "ArtistImagesOutput", Fields: []FieldDef{ {Name: "Images", Type: "[]ImageInfo", JSONTag: "images"}, }, }, { Name: "ImageInfo", Fields: []FieldDef{ {Name: "URL", Type: "string", JSONTag: "url"}, {Name: "Size", Type: "int32", JSONTag: "size"}, }, }, }, } code, err := GenerateCapabilityGo(cap, "metadata") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for build tag Expect(codeStr).To(ContainSubstring("//go:build wasip1")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package metadata")) // Check for marker interface (non-required) Expect(codeStr).To(ContainSubstring("type Metadata interface{}")) // Check for provider interfaces Expect(codeStr).To(ContainSubstring("type ArtistBiographyProvider interface")) Expect(codeStr).To(ContainSubstring("type ArtistImagesProvider interface")) // Check for Register function with type assertions Expect(codeStr).To(ContainSubstring("func Register(impl Metadata)")) Expect(codeStr).To(ContainSubstring("impl.(ArtistBiographyProvider)")) // Check for export wrappers Expect(codeStr).To(ContainSubstring("//go:wasmexport nd_get_artist_biography")) Expect(codeStr).To(ContainSubstring("func _NdGetArtistBiography()")) // Check for NotImplementedCode handling Expect(codeStr).To(ContainSubstring("NotImplementedCode")) Expect(codeStr).To(ContainSubstring("return NotImplementedCode")) // Check struct definitions Expect(codeStr).To(ContainSubstring("type ArtistInput struct")) Expect(codeStr).To(ContainSubstring("type ImageInfo struct")) }) It("should generate valid Go code for a required capability", func() { cap := Capability{ Name: "scrobbler", Interface: "Scrobbler", Required: true, Methods: []Export{ { Name: "IsAuthorized", ExportName: "nd_scrobbler_is_authorized", Input: Param{Type: "AuthInput"}, Output: Param{Type: "AuthOutput"}, }, { Name: "Scrobble", ExportName: "nd_scrobbler_scrobble", Input: Param{Type: "ScrobbleInput"}, Output: Param{Type: "ScrobblerOutput"}, }, }, Structs: []StructDef{ {Name: "AuthInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, {Name: "AuthOutput", Fields: []FieldDef{{Name: "Authorized", Type: "bool", JSONTag: "authorized"}}}, {Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, {Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "Error", Type: "*string", JSONTag: "error", OmitEmpty: true}}}, }, } code, err := GenerateCapabilityGo(cap, "scrobbler") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for full interface (required capability) Expect(codeStr).To(ContainSubstring("type Scrobbler interface {")) Expect(codeStr).To(ContainSubstring("IsAuthorized(AuthInput) (AuthOutput, error)")) Expect(codeStr).To(ContainSubstring("Scrobble(ScrobbleInput) (ScrobblerOutput, error)")) // Should NOT have provider interfaces for required capability Expect(codeStr).NotTo(ContainSubstring("AuthProvider interface")) // Register should directly assign methods Expect(codeStr).To(ContainSubstring("func Register(impl Scrobbler)")) Expect(codeStr).To(ContainSubstring("impl.IsAuthorized")) }) It("should include type aliases and consts", func() { cap := Capability{ Name: "scrobbler", Interface: "Scrobbler", Required: true, Methods: []Export{ { Name: "Scrobble", ExportName: "nd_scrobble", Input: Param{Type: "ScrobbleInput"}, Output: Param{Type: "ScrobblerOutput"}, }, }, Structs: []StructDef{ {Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, {Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "ErrorType", Type: "*ScrobblerErrorType", JSONTag: "errorType", OmitEmpty: true}}}, }, TypeAliases: []TypeAlias{ {Name: "ScrobblerErrorType", Type: "string", Doc: "ScrobblerErrorType indicates error handling."}, }, Consts: []ConstGroup{ { Type: "ScrobblerErrorType", Values: []ConstDef{ {Name: "ScrobblerErrorNone", Value: `"none"`, Doc: "No error"}, {Name: "ScrobblerErrorRetry", Value: `"retry"`, Doc: "Retry later"}, }, }, }, } code, err := GenerateCapabilityGo(cap, "scrobbler") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check type alias Expect(codeStr).To(ContainSubstring("type ScrobblerErrorType string")) // Check consts - all consts should have type annotation Expect(codeStr).To(ContainSubstring("ScrobblerErrorNone ScrobblerErrorType =")) Expect(codeStr).To(ContainSubstring(`"none"`)) Expect(codeStr).To(ContainSubstring("ScrobblerErrorRetry ScrobblerErrorType =")) Expect(codeStr).To(ContainSubstring(`"retry"`)) }) It("emits a deprecated alias and types import for a shared-aliased capability", func() { cap := Capability{ Name: "scrobbler", Interface: "Scrobbler", Required: true, Methods: []Export{{ Name: "NowPlaying", ExportName: "nd_scrobbler_now_playing", Input: Param{Name: "input", Type: "NowPlayingRequest"}, }}, Structs: []StructDef{{Name: "NowPlayingRequest", Fields: []FieldDef{ {Name: "Track", Type: "TrackInfo", JSONTag: "track"}, }}}, SharedAliases: []SharedAlias{{ Name: "TrackInfo", Target: "types.TrackInfo", Doc: "Deprecated: use types.TrackInfo.", Def: StructDef{Name: "TrackInfo", Fields: []FieldDef{{Name: "Title", Type: "string", JSONTag: "title"}}}, }}, } code, err := GenerateCapabilityGo(cap, "scrobbler") Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring(`"github.com/navidrome/navidrome/plugins/pdk/go/types"`)) Expect(out).To(ContainSubstring("// Deprecated: use types.TrackInfo.")) Expect(out).To(ContainSubstring("type TrackInfo = types.TrackInfo")) Expect(out).NotTo(ContainSubstring("type TrackInfo struct")) }) It("emits the types import for a direct types.X field with no deprecated alias", func() { cap := Capability{ Name: "scrobbler", Interface: "Scrobbler", Required: true, Methods: []Export{{ Name: "NowPlaying", ExportName: "nd_scrobbler_now_playing", Input: Param{Name: "input", Type: "NowPlayingRequest"}, }}, Structs: []StructDef{{Name: "NowPlayingRequest", Fields: []FieldDef{ {Name: "Song", Type: "types.SongRef", JSONTag: "song"}, }}}, // No SharedAliases: the field references the canonical type directly. } code, err := GenerateCapabilityGo(cap, "scrobbler") Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring(`"github.com/navidrome/navidrome/plugins/pdk/go/types"`)) Expect(out).To(ContainSubstring("types.SongRef")) }) It("emits the types import for a direct types.X method input/output", func() { cap := Capability{ Name: "demo", Interface: "Demo", Required: true, Methods: []Export{{ Name: "Lookup", ExportName: "nd_demo_lookup", Input: Param{Name: "input", Type: "types.SongRef"}, Output: Param{Name: "output", Type: "types.SongRef"}, }}, // No structs, no aliases: the method signature references the shared type directly. } code, err := GenerateCapabilityGo(cap, "demo") Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring(`"github.com/navidrome/navidrome/plugins/pdk/go/types"`)) Expect(out).To(ContainSubstring("types.SongRef")) }) }) Describe("GenerateCapabilityGoStub", func() { It("should generate valid stub code for non-WASM builds", func() { cap := Capability{ Name: "metadata", Interface: "MetadataAgent", Required: false, Methods: []Export{ { Name: "GetArtistBiography", ExportName: "nd_get_artist_biography", Input: Param{Type: "ArtistInput"}, Output: Param{Type: "ArtistBiographyOutput"}, }, }, Structs: []StructDef{ {Name: "ArtistInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "ArtistBiographyOutput", Fields: []FieldDef{{Name: "Biography", Type: "string", JSONTag: "biography"}}}, }, } code, err := GenerateCapabilityGoStub(cap, "metadata") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for non-WASM build tag Expect(codeStr).To(ContainSubstring("//go:build !wasip1")) // Check for package declaration Expect(codeStr).To(ContainSubstring("package metadata")) // Check for no-op Register Expect(codeStr).To(ContainSubstring("func Register(_ Metadata) {}")) // Check struct definitions are present Expect(codeStr).To(ContainSubstring("type ArtistInput struct")) // Check there are no export wrappers Expect(codeStr).NotTo(ContainSubstring("//go:wasmexport")) Expect(codeStr).NotTo(ContainSubstring("pdk.InputJSON")) }) }) Describe("End-to-end capability generation", func() { It("should parse and generate capability code from source", func() { src := `package capabilities // Lifecycle provides plugin lifecycle hooks. //nd:capability name=lifecycle type Lifecycle interface { // OnInit is called when the plugin is loaded. //nd:export name=nd_on_init OnInit(OnInitInput) (OnInitOutput, error) } // OnInitInput is the input for OnInit. type OnInitInput struct { } // OnInitOutput is the output for OnInit. type OnInitOutput struct { // Error is the error message if initialization failed. Error *string ` + "`json:\"error,omitempty\"`" + ` } ` // Create temporary directory tmpDir := GinkgoT().TempDir() path := tmpDir + "/lifecycle.go" err := writeFile(path, src) Expect(err).NotTo(HaveOccurred()) // Parse capabilities, err := ParseCapabilities(tmpDir) Expect(err).NotTo(HaveOccurred()) Expect(capabilities).To(HaveLen(1)) cap := capabilities[0] Expect(cap.Name).To(Equal("lifecycle")) Expect(cap.Methods).To(HaveLen(1)) // Generate WASM code code, err := GenerateCapabilityGo(cap, "lifecycle") Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring("//go:wasmexport nd_on_init")) Expect(codeStr).To(ContainSubstring("type InitProvider interface")) // Generate stub code stubCode, err := GenerateCapabilityGoStub(cap, "lifecycle") Expect(err).NotTo(HaveOccurred()) stubStr := string(stubCode) Expect(stubStr).To(ContainSubstring("//go:build !wasip1")) Expect(stubStr).To(ContainSubstring("func Register(_ Lifecycle) {}")) }) }) }) var _ = Describe("Shared Types Generation", func() { It("emits a Rust types crate root with serde derives", func() { structs := []StructDef{ {Name: "ArtistRef", Doc: "ArtistRef references an artist.", Fields: []FieldDef{ {Name: "ID", Type: "string", JSONTag: "id", OmitEmpty: true}, {Name: "Name", Type: "string", JSONTag: "name"}, }}, } code, err := GenerateSharedTypesRust(structs) Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring("use serde::{Deserialize, Serialize};")) Expect(out).To(ContainSubstring("pub struct ArtistRef {")) Expect(out).To(ContainSubstring(`#[serde(rename_all = "camelCase")]`)) Expect(out).To(ContainSubstring("pub name: String,")) }) It("emits a flat Go types package with no imports", func() { structs := []StructDef{ {Name: "ArtistRef", Doc: "ArtistRef references an artist.", Fields: []FieldDef{ {Name: "ID", Type: "string", JSONTag: "id", OmitEmpty: true}, {Name: "Name", Type: "string", JSONTag: "name"}, }}, } code, err := GenerateSharedTypesGo(structs, "types") Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring("package types")) Expect(out).To(ContainSubstring("type ArtistRef struct {")) Expect(out).To(ContainSubstring("ID string `json:\"id,omitempty\"`")) Expect(out).To(ContainSubstring("Name string `json:\"name\"`")) Expect(out).NotTo(ContainSubstring("import")) }) It("emits base64 serde for Vec fields in the Rust types crate", func() { structs := []StructDef{ {Name: "Payload", Doc: "Payload carries raw bytes.", Fields: []FieldDef{ {Name: "Data", Type: "[]byte", JSONTag: "data"}, }}, } code, err := GenerateSharedTypesRust(structs) Expect(err).NotTo(HaveOccurred()) out := string(code) Expect(out).To(ContainSubstring("mod base64_bytes")) Expect(out).To(ContainSubstring("use base64::Engine as _")) Expect(out).To(ContainSubstring(`#[serde(with = "base64_bytes")]`)) }) }) var _ = Describe("Rust Generation", func() { Describe("skipSerializingFunc", func() { It("should return Option::is_none for pointer types", func() { Expect(skipSerializingFunc("*string")).To(Equal("Option::is_none")) Expect(skipSerializingFunc("*MyStruct")).To(Equal("Option::is_none")) }) It("should return the matching emptiness predicate for slice and map types", func() { // The predicate must match the rendered Rust type: []T -> Vec, map[K]V -> HashMap. Expect(skipSerializingFunc("[]string")).To(Equal("Vec::is_empty")) Expect(skipSerializingFunc("[]int32")).To(Equal("Vec::is_empty")) Expect(skipSerializingFunc("map[string]int")).To(Equal("HashMap::is_empty")) }) It("should return String::is_empty for string type", func() { Expect(skipSerializingFunc("string")).To(Equal("String::is_empty")) }) It("should return std::ops::Not::not for bool type", func() { Expect(skipSerializingFunc("bool")).To(Equal("std::ops::Not::not")) }) It("should return is_zero_* functions for numeric types", func() { Expect(skipSerializingFunc("int32")).To(Equal("is_zero_i32")) Expect(skipSerializingFunc("uint32")).To(Equal("is_zero_u32")) Expect(skipSerializingFunc("int64")).To(Equal("is_zero_i64")) Expect(skipSerializingFunc("uint64")).To(Equal("is_zero_u64")) Expect(skipSerializingFunc("float32")).To(Equal("is_zero_f32")) Expect(skipSerializingFunc("float64")).To(Equal("is_zero_f64")) }) It("should return Option::is_none for unknown types", func() { Expect(skipSerializingFunc("CustomType")).To(Equal("Option::is_none")) }) }) Describe("rustOutputType", func() { It("should convert Go primitives to Rust primitives", func() { Expect(rustOutputType("bool")).To(Equal("bool")) Expect(rustOutputType("string")).To(Equal("String")) Expect(rustOutputType("int")).To(Equal("i32")) Expect(rustOutputType("int32")).To(Equal("i32")) Expect(rustOutputType("int64")).To(Equal("i64")) Expect(rustOutputType("float32")).To(Equal("f32")) Expect(rustOutputType("float64")).To(Equal("f64")) }) It("should strip pointer prefix", func() { // NOTE: This behavior is incorrect for pointer to primitives. // "*string" returns "string" instead of "String", which would generate // invalid Rust code. No current capability uses this pattern. // See TODO in rustOutputType function. Expect(rustOutputType("*string")).To(Equal("string")) Expect(rustOutputType("*MyStruct")).To(Equal("MyStruct")) }) It("should pass through unknown types", func() { Expect(rustOutputType("CustomType")).To(Equal("CustomType")) Expect(rustOutputType("MyStruct")).To(Equal("MyStruct")) }) }) Describe("isPrimitiveRustType", func() { It("should return true for primitive Go types", func() { Expect(isPrimitiveRustType("bool")).To(BeTrue()) Expect(isPrimitiveRustType("string")).To(BeTrue()) Expect(isPrimitiveRustType("int")).To(BeTrue()) Expect(isPrimitiveRustType("int32")).To(BeTrue()) Expect(isPrimitiveRustType("int64")).To(BeTrue()) Expect(isPrimitiveRustType("float32")).To(BeTrue()) Expect(isPrimitiveRustType("float64")).To(BeTrue()) }) It("should return false for non-primitive types", func() { Expect(isPrimitiveRustType("MyStruct")).To(BeFalse()) Expect(isPrimitiveRustType("CustomType")).To(BeFalse()) Expect(isPrimitiveRustType("[]string")).To(BeFalse()) Expect(isPrimitiveRustType("map[string]int")).To(BeFalse()) }) It("should handle pointer types by stripping prefix", func() { Expect(isPrimitiveRustType("*string")).To(BeTrue()) Expect(isPrimitiveRustType("*int64")).To(BeTrue()) Expect(isPrimitiveRustType("*MyStruct")).To(BeFalse()) }) }) Describe("GenerateCapabilityRust", func() { It("should generate valid Rust code with primitive output types", func() { cap := Capability{ Name: "test", Interface: "TestAgent", Required: true, SourceFile: "test", Methods: []Export{ { Name: "GetBool", ExportName: "nd_get_bool", Input: Param{Type: "BoolInput"}, Output: Param{Type: "bool"}, }, { Name: "GetString", ExportName: "nd_get_string", Input: Param{Type: "StrInput"}, Output: Param{Type: "string"}, }, { Name: "GetInt", ExportName: "nd_get_int", Input: Param{Type: "IntInput"}, Output: Param{Type: "int32"}, }, }, Structs: []StructDef{ {Name: "BoolInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "StrInput", Fields: []FieldDef{{Name: "Key", Type: "string", JSONTag: "key"}}}, {Name: "IntInput", Fields: []FieldDef{{Name: "Index", Type: "int32", JSONTag: "index"}}}, }, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check that primitive output types are not prefixed with $crate:: // The template should use isPrimitiveRust to determine this Expect(codeStr).To(ContainSubstring("FnResult>")) Expect(codeStr).To(ContainSubstring("FnResult>")) Expect(codeStr).To(ContainSubstring("FnResult>")) // Verify that primitive output types don't use $crate:: prefix in FnResult // The pattern "$crate::test::bool>" would indicate incorrect generation Expect(codeStr).NotTo(ContainSubstring("$crate::test::bool>")) Expect(codeStr).NotTo(ContainSubstring("$crate::test::String>")) Expect(codeStr).NotTo(ContainSubstring("$crate::test::i32>")) }) It("should generate valid Rust code with struct output types", func() { cap := Capability{ Name: "metadata", Interface: "MetadataAgent", Required: true, SourceFile: "metadata", Methods: []Export{ { Name: "GetArtist", ExportName: "nd_get_artist", Input: Param{Type: "ArtistInput"}, Output: Param{Type: "ArtistOutput"}, }, }, Structs: []StructDef{ {Name: "ArtistInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "ArtistOutput", Fields: []FieldDef{{Name: "Name", Type: "string", JSONTag: "name"}}}, }, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Non-primitive struct types should use $crate:: prefix Expect(codeStr).To(ContainSubstring("$crate::metadata::ArtistOutput")) }) It("should generate valid Rust code with pointer output types", func() { cap := Capability{ Name: "test", Interface: "TestAgent", Required: true, SourceFile: "test", Methods: []Export{ { Name: "GetOptionalStruct", ExportName: "nd_get_optional_struct", Input: Param{Type: "Input"}, Output: Param{Type: "*Output"}, }, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, {Name: "Output", Fields: []FieldDef{{Name: "Value", Type: "string", JSONTag: "value"}}}, }, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Pointer to struct should strip pointer and use struct type with $crate:: Expect(codeStr).To(ContainSubstring("$crate::test::Output>")) // Pointer output types should NOT have Option<> wrapping - Result handles optionality Expect(codeStr).NotTo(ContainSubstring("Option<")) }) It("translates a shared type used directly as a method input/output", func() { cap := Capability{ Name: "demo", Interface: "Demo", Required: true, Methods: []Export{{Name: "Echo", ExportName: "nd_demo_echo", Input: Param{Name: "input", Type: "types.SongRef"}, Output: Param{Name: "output", Type: "types.SongRef"}}}, // No structs, no aliases: the method signature references the shared type directly. } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) out := string(code) // The shared type must resolve to the canonical crate path, not pass through // as the invalid Go selector `types.SongRef`. Expect(out).To(ContainSubstring("nd_pdk_types::SongRef")) Expect(out).NotTo(ContainSubstring("types.SongRef")) }) It("routes shared macro types through $crate so umbrella-crate plugins resolve them", func() { cap := Capability{ Name: "demo", Interface: "Demo", Required: true, Methods: []Export{{Name: "Echo", ExportName: "nd_demo_echo", Input: Param{Name: "input", Type: "types.SongRef"}, Output: Param{Name: "output", Type: "types.SongRef"}}}, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) out := string(code) // Inside the export macro (expanded in the downstream plugin crate, which depends // on the umbrella nd-pdk only), the shared type must be reachable via $crate, not // by naming the transitive nd_pdk_types crate directly. Expect(out).To(ContainSubstring("extism_pdk::Json<$crate::types::SongRef>")) Expect(out).NotTo(ContainSubstring("extism_pdk::Json")) }) It("emits a deprecated Rust type alias for shared types", func() { cap := Capability{ Name: "scrobbler", Interface: "Scrobbler", Required: true, Methods: []Export{{Name: "NowPlaying", ExportName: "nd_scrobbler_now_playing", Input: Param{Name: "input", Type: "NowPlayingRequest"}}}, Structs: []StructDef{{Name: "NowPlayingRequest", Fields: []FieldDef{ {Name: "Track", Type: "TrackInfo", JSONTag: "track"}}}}, SharedAliases: []SharedAlias{{ Name: "TrackInfo", Target: "types.TrackInfo", Doc: "Deprecated: use types.TrackInfo.", }}, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) out := string(code) // Note points authors at the umbrella path (nd-pdk re-exports nd_pdk_types as `types`); // the alias target stays the real crate path so it resolves inside nd-pdk-capabilities. Expect(out).To(ContainSubstring(`#[deprecated(note = "use nd_pdk::types::TrackInfo")]`)) Expect(out).To(ContainSubstring("pub type TrackInfo = nd_pdk_types::TrackInfo;")) }) It("keeps struct-field type when the type name is a shared alias (regression: was serde_json::Value)", func() { // Wrapper has a field whose type is only in SharedAliases, not Structs. // The field must render as `pub track: nd_pdk_types::TrackInfo` (canonical // path), not as the local deprecated alias and not as serde_json::Value. cap := Capability{ Name: "test", Interface: "TestAgent", Required: true, Methods: []Export{{Name: "Submit", ExportName: "nd_test_submit", Input: Param{Name: "req", Type: "Wrapper"}}}, Structs: []StructDef{{Name: "Wrapper", Fields: []FieldDef{ {Name: "Track", Type: "TrackInfo", JSONTag: "track"}}}}, SharedAliases: []SharedAlias{{ Name: "TrackInfo", Target: "types.TrackInfo", Doc: "Deprecated: use types.TrackInfo.", }}, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) out := string(code) // Field must use the canonical nd_pdk_types:: path, not the local alias. Expect(out).To(ContainSubstring("nd_pdk_types::TrackInfo")) Expect(out).NotTo(ContainSubstring("pub track: serde_json::Value")) Expect(out).NotTo(ContainSubstring("pub track: TrackInfo,")) }) It("renders a qualified types.X field as nd_pdk_types::X and keeps the renamed re-export", func() { // The capability references the shared type by its canonical qualified // name (types.Track) while the deprecated alias keeps the old name. cap := Capability{ Name: "test", Interface: "TestAgent", Required: true, Methods: []Export{{Name: "Submit", ExportName: "nd_test_submit", Input: Param{Name: "req", Type: "Wrapper"}}}, Structs: []StructDef{{Name: "Wrapper", Fields: []FieldDef{ {Name: "Track", Type: "types.Track", JSONTag: "track"}}}}, SharedAliases: []SharedAlias{{ Name: "TrackInfo", Target: "types.Track", Doc: "Deprecated: use types.Track.", }}, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) out := string(code) // Field uses the canonical qualified path (resolved from the types. prefix). Expect(out).To(ContainSubstring("pub track: nd_pdk_types::Track,")) // The deprecated alias is still re-exported under its kept name. Expect(out).To(ContainSubstring("pub type TrackInfo = nd_pdk_types::Track;")) Expect(out).NotTo(ContainSubstring("pub track: serde_json::Value")) }) It("should include all float types correctly", func() { cap := Capability{ Name: "test", Interface: "TestAgent", Required: true, SourceFile: "test", Methods: []Export{ { Name: "GetFloat32", ExportName: "nd_get_float32", Input: Param{Type: "Input"}, Output: Param{Type: "float32"}, }, { Name: "GetFloat64", ExportName: "nd_get_float64", Input: Param{Type: "Input"}, Output: Param{Type: "float64"}, }, }, Structs: []StructDef{ {Name: "Input", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, }, } code, err := GenerateCapabilityRust(cap) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).To(ContainSubstring("FnResult>")) Expect(codeStr).To(ContainSubstring("FnResult>")) }) }) Describe("GenerateClientRust", func() { It("should generate Option for (value, exists bool) pattern", func() { svc := Service{ Name: "Config", Permission: "config", Interface: "ConfigService", Methods: []Method{ { Name: "Get", Params: []Param{ {Name: "key", Type: "string", JSONName: "key"}, }, Returns: []Param{ {Name: "value", Type: "string", JSONName: "value"}, {Name: "exists", Type: "bool", JSONName: "exists"}, }, }, }, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Should generate Option return type, not (String, bool) Expect(codeStr).To(ContainSubstring("Result, Error>")) Expect(codeStr).NotTo(ContainSubstring("Result<(String, bool), Error>")) // Should generate Some/None logic Expect(codeStr).To(ContainSubstring("Ok(Some(")) Expect(codeStr).To(ContainSubstring("Ok(None)")) }) It("should generate tuple for non-option multi-return", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "GetStats", Returns: []Param{ {Name: "count", Type: "int64", JSONName: "count"}, {Name: "size", Type: "int64", JSONName: "size"}, }, }, }, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Should generate tuple return type Expect(codeStr).To(ContainSubstring("Result<(i64, i64), Error>")) Expect(codeStr).NotTo(ContainSubstring("Option<")) }) It("should NOT generate Option for Has() pattern where first return is bool", func() { svc := Service{ Name: "Cache", Permission: "cache", Interface: "CacheService", Methods: []Method{ { Name: "Has", Params: []Param{ {Name: "key", Type: "string", JSONName: "key"}, }, Returns: []Param{ {Name: "exists", Type: "bool", JSONName: "exists"}, }, }, }, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Should generate simple bool return, not Option Expect(codeStr).To(ContainSubstring("Result")) Expect(codeStr).NotTo(ContainSubstring("Option")) }) It("should generate base64 serde for Vec fields", func() { svc := Service{ Name: "Codec", Permission: "codec", Interface: "CodecService", Methods: []Method{ { Name: "Encode", HasError: true, Params: []Param{NewParam("data", "[]byte")}, Returns: []Param{NewParam("result", "[]byte")}, }, }, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Should generate base64_bytes serde module Expect(codeStr).To(ContainSubstring("mod base64_bytes")) Expect(codeStr).To(ContainSubstring("use base64::Engine as _")) // Should add serde(with = "base64_bytes") on Vec fields Expect(codeStr).To(ContainSubstring(`#[serde(with = "base64_bytes")]`)) }) It("resolves a shared alias used in a method param/return to its canonical crate path", func() { svc := Service{ Name: "Matcher", Permission: "matcher", Interface: "MatcherService", Methods: []Method{ { Name: "MatchSongs", HasError: true, Params: []Param{NewParam("query", "string")}, // Return uses the deprecated alias name directly. Returns: []Param{NewParam("matches", "[]Track")}, }, }, SharedAliases: []SharedAlias{{ Name: "Track", Target: "types.Track", Def: StructDef{Name: "Track", Fields: []FieldDef{ {Name: "Title", Type: "string", JSONTag: "title"}}}, }}, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) out := string(code) // The alias must resolve to the shared crate type; a bare `Track` is undefined // in nd-pdk-host and would not compile. Expect(out).To(ContainSubstring("nd_pdk_types::Track")) Expect(out).NotTo(ContainSubstring("Vec")) }) It("should not generate base64 module when no byte fields", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "Call", HasError: true, Params: []Param{NewParam("uri", "string")}, Returns: []Param{NewParam("response", "string")}, }, }, } code, err := GenerateClientRust(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) Expect(codeStr).NotTo(ContainSubstring("mod base64_bytes")) Expect(codeStr).NotTo(ContainSubstring("use base64")) }) }) }) func writeFile(path, content string) error { return os.WriteFile(path, []byte(content), 0600) }