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"`)) }) }) 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("Python type and name helpers", func() { Describe("ToPythonType", func() { It("should map Go types to Python types", func() { Expect(ToPythonType("string")).To(Equal("str")) Expect(ToPythonType("int")).To(Equal("int")) Expect(ToPythonType("int32")).To(Equal("int")) Expect(ToPythonType("int64")).To(Equal("int")) Expect(ToPythonType("float32")).To(Equal("float")) Expect(ToPythonType("float64")).To(Equal("float")) Expect(ToPythonType("bool")).To(Equal("bool")) Expect(ToPythonType("[]byte")).To(Equal("bytes")) Expect(ToPythonType("unknown")).To(Equal("Any")) }) }) 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("Method.PythonFunctionName", func() { It("should generate snake_case function name with service prefix", func() { m := Method{Name: "GetString"} Expect(m.PythonFunctionName("cache")).To(Equal("cache_get_string")) }) }) Describe("Param.PythonType", func() { It("should return Python type for parameter", func() { p := NewParam("value", "string") Expect(p.PythonType()).To(Equal("str")) }) }) Describe("Param.PythonName", func() { It("should return snake_case name for parameter", func() { p := NewParam("ttlSeconds", "int64") Expect(p.PythonName()).To(Equal("ttl_seconds")) }) }) }) Describe("GenerateClientPython", func() { It("should generate valid Python code for a simple service", func() { svc := Service{ Name: "SubsonicAPI", Permission: "subsonicapi", Interface: "SubsonicAPIService", Methods: []Method{ { Name: "Call", HasError: true, Params: []Param{NewParam("uri", "string")}, Returns: []Param{NewParam("responseJSON", "string")}, }, }, } code, err := GenerateClientPython(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for generated header Expect(codeStr).To(ContainSubstring("Code generated by ndpgen. DO NOT EDIT.")) // Check for imports Expect(codeStr).To(ContainSubstring("from dataclasses import dataclass")) Expect(codeStr).To(ContainSubstring("import extism")) Expect(codeStr).To(ContainSubstring("import json")) // Check for exception class Expect(codeStr).To(ContainSubstring("class HostFunctionError(Exception):")) // Check for raw import function Expect(codeStr).To(ContainSubstring(`@extism.import_fn("extism:host/user", "subsonicapi_call")`)) Expect(codeStr).To(ContainSubstring("def _subsonicapi_call(offset: int) -> int:")) // Check for wrapper function with type hints Expect(codeStr).To(ContainSubstring("def subsonicapi_call(uri: str) -> str:")) // Check for error handling Expect(codeStr).To(ContainSubstring("raise HostFunctionError(response[")) }) It("should generate dataclass for multi-value returns", func() { svc := Service{ Name: "Cache", Permission: "cache", Interface: "CacheService", Methods: []Method{ { Name: "GetString", HasError: true, Params: []Param{NewParam("key", "string")}, Returns: []Param{ NewParam("value", "string"), NewParam("exists", "bool"), }, }, }, } code, err := GenerateClientPython(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check for dataclass Expect(codeStr).To(ContainSubstring("@dataclass")) Expect(codeStr).To(ContainSubstring("class CacheGetStringResult:")) Expect(codeStr).To(ContainSubstring("value: str")) Expect(codeStr).To(ContainSubstring("exists: bool")) // Check that function returns dataclass Expect(codeStr).To(ContainSubstring("def cache_get_string(key: str) -> CacheGetStringResult:")) Expect(codeStr).To(ContainSubstring("return CacheGetStringResult(")) }) It("should handle methods with no parameters", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "NoParams", HasError: true, Returns: []Param{NewParam("result", "string")}, }, }, } code, err := GenerateClientPython(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Function with no params Expect(codeStr).To(ContainSubstring("def test_no_params() -> str:")) // Empty request Expect(codeStr).To(ContainSubstring(`request_bytes = b"{}"`)) }) It("should handle methods with no return values", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "NoReturn", HasError: true, Params: []Param{NewParam("input", "string")}, }, }, } code, err := GenerateClientPython(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Function returns None Expect(codeStr).To(ContainSubstring("def test_no_return(input: str) -> None:")) }) It("should generate correct Python defaults for different types", func() { svc := Service{ Name: "Test", Permission: "test", Interface: "TestService", Methods: []Method{ { Name: "AllTypes", HasError: true, Returns: []Param{ NewParam("strVal", "string"), NewParam("intVal", "int64"), NewParam("floatVal", "float64"), NewParam("boolVal", "bool"), }, }, }, } code, err := GenerateClientPython(svc) Expect(err).NotTo(HaveOccurred()) codeStr := string(code) // Check defaults in response.get() calls Expect(codeStr).To(ContainSubstring(`response.get("strVal", "")`)) Expect(codeStr).To(ContainSubstring(`response.get("intVal", 0)`)) Expect(codeStr).To(ContainSubstring(`response.get("floatVal", 0.0)`)) Expect(codeStr).To(ContainSubstring(`response.get("boolVal", False)`)) }) }) 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"`)) }) }) 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("Rust Generation", func() { Describe("skipSerializingFunc", func() { It("should return Option::is_none for pointer, slice, and map types", func() { Expect(skipSerializingFunc("*string")).To(Equal("Option::is_none")) Expect(skipSerializingFunc("*MyStruct")).To(Equal("Option::is_none")) Expect(skipSerializingFunc("[]string")).To(Equal("Option::is_none")) Expect(skipSerializingFunc("[]int32")).To(Equal("Option::is_none")) Expect(skipSerializingFunc("map[string]int")).To(Equal("Option::is_none")) }) 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("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")) }) }) }) func writeFile(path, content string) error { return os.WriteFile(path, []byte(content), 0600) }