mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
The ndpgen mock accessors asserted return types unconditionally, so a plugin test using Return(nil, ...) — the natural way to model an empty result or a pagination terminal page — panicked on the untyped-nil type assertion instead of returning the zero value. Guard pointer, slice, map and any returns in both the host-service client stubs and the PDK stub, and regenerate.
1799 lines
58 KiB
Go
1799 lines
58 KiB
Go
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<bool>
|
|
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)"))
|
|
|
|
// []byte is nil-able, so it gets a guarded extraction
|
|
Expect(codeStr).To(ContainSubstring("var r0 []byte"))
|
|
Expect(codeStr).To(ContainSubstring("r0 = v.([]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<u8> 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<T>, map[K]V -> HashMap<K,V>.
|
|
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<extism_pdk::Json<bool>>"))
|
|
Expect(codeStr).To(ContainSubstring("FnResult<extism_pdk::Json<String>>"))
|
|
Expect(codeStr).To(ContainSubstring("FnResult<extism_pdk::Json<i32>>"))
|
|
|
|
// 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<nd_pdk_types::SongRef>"))
|
|
})
|
|
|
|
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<extism_pdk::Json<f32>>"))
|
|
Expect(codeStr).To(ContainSubstring("FnResult<extism_pdk::Json<f64>>"))
|
|
})
|
|
})
|
|
|
|
Describe("GenerateClientRust", func() {
|
|
It("should generate Option<T> 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<String> return type, not (String, bool)
|
|
Expect(codeStr).To(ContainSubstring("Result<Option<String>, 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<bool, Error>"))
|
|
Expect(codeStr).NotTo(ContainSubstring("Option<bool>"))
|
|
})
|
|
|
|
It("should generate base64 serde for Vec<u8> 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<u8> 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<Track>"))
|
|
})
|
|
|
|
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"))
|
|
})
|
|
})
|
|
|
|
Describe("nil-safe mock accessors", func() {
|
|
Describe("GenerateClientGoStub", func() {
|
|
It("guards pointer, slice and map returns so Return(nil, ...) does not panic", func() {
|
|
svc := Service{
|
|
Name: "Paged",
|
|
Interface: "PagedService",
|
|
Methods: []Method{
|
|
{
|
|
Name: "GetPage",
|
|
HasError: true,
|
|
Params: []Param{NewParam("query", "string")},
|
|
Returns: []Param{NewParam("items", "[]Item"), NewParam("next", "*PageOptions")},
|
|
},
|
|
{
|
|
Name: "GetLabels",
|
|
HasError: true,
|
|
Params: []Param{NewParam("id", "string")},
|
|
Returns: []Param{NewParam("labels", "map[string]string")},
|
|
},
|
|
},
|
|
}
|
|
|
|
code, err := GenerateClientGoStub(svc, "host")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
_, err = format.Source(code)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
codeStr := string(code)
|
|
Expect(codeStr).To(ContainSubstring("var r0 []Item"))
|
|
Expect(codeStr).To(ContainSubstring("var r1 *PageOptions"))
|
|
Expect(codeStr).To(ContainSubstring("if v := args.Get(1); v != nil {"))
|
|
Expect(codeStr).To(ContainSubstring("r1 = v.(*PageOptions)"))
|
|
Expect(codeStr).To(ContainSubstring("return r0, r1, args.Error(2)"))
|
|
Expect(codeStr).To(ContainSubstring("var r0 map[string]string"))
|
|
Expect(codeStr).NotTo(ContainSubstring("args.Get(0).([]Item)"))
|
|
Expect(codeStr).NotTo(ContainSubstring("args.Get(1).(*PageOptions)"))
|
|
})
|
|
|
|
It("keeps non-nilable returns as inline accessors", func() {
|
|
svc := Service{
|
|
Name: "Counter",
|
|
Interface: "CounterService",
|
|
Methods: []Method{
|
|
{
|
|
Name: "Count",
|
|
HasError: true,
|
|
Params: []Param{NewParam("id", "string")},
|
|
Returns: []Param{NewParam("count", "int64"), NewParam("name", "string")},
|
|
},
|
|
},
|
|
}
|
|
|
|
code, err := GenerateClientGoStub(svc, "host")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
codeStr := string(code)
|
|
Expect(codeStr).To(ContainSubstring("return args.Get(0).(int64), args.String(1), args.Error(2)"))
|
|
Expect(codeStr).NotTo(ContainSubstring("var r0"))
|
|
})
|
|
})
|
|
|
|
Describe("GeneratePDKGoStub", func() {
|
|
It("guards pointer and slice returns", func() {
|
|
symbols := &PDKSymbols{
|
|
Functions: []PDKFunc{
|
|
{
|
|
Name: "NewHTTPRequest",
|
|
Params: []PDKParam{{Name: "method", Type: "HTTPMethod"}, {Name: "url", Type: "string"}},
|
|
Returns: []PDKReturn{{Type: "*HTTPRequest"}},
|
|
},
|
|
{
|
|
Name: "Input",
|
|
Returns: []PDKReturn{{Type: "[]byte"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
code, err := GeneratePDKGoStub(symbols)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
_, err = format.Source(code)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
codeStr := string(code)
|
|
Expect(codeStr).To(ContainSubstring("var r0 *HTTPRequest"))
|
|
Expect(codeStr).To(ContainSubstring("r0 = v.(*HTTPRequest)"))
|
|
Expect(codeStr).To(ContainSubstring("var r0 []byte"))
|
|
Expect(codeStr).NotTo(ContainSubstring("return args.Get(0).(*HTTPRequest)"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
func writeFile(path, content string) error {
|
|
return os.WriteFile(path, []byte(content), 0600)
|
|
}
|