mirror of
https://github.com/navidrome/navidrome.git
synced 2026-02-13 08:21:09 -05:00
* feat: implement raw binary framing for host function responses Signed-off-by: Deluan <deluan@navidrome.org> * feat: add CallRaw method for Subsonic API to handle binary responses Signed-off-by: Deluan <deluan@navidrome.org> * test: add tests for raw=true methods and binary framing generation Signed-off-by: Deluan <deluan@navidrome.org> * fix: improve error message for malformed raw responses to indicate incomplete header Signed-off-by: Deluan <deluan@navidrome.org> * fix: add wasm_import_module attribute for raw methods and improve content-type handling Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
661 lines
19 KiB
Go
661 lines
19 KiB
Go
package internal
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Parser", func() {
|
|
var tmpDir string
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
tmpDir, err = os.MkdirTemp("", "ndpgen-test-*")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
|
|
AfterEach(func() {
|
|
os.RemoveAll(tmpDir)
|
|
})
|
|
|
|
Describe("ParseDirectory", func() {
|
|
It("should parse a simple host service interface", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
// SubsonicAPIService provides access to Navidrome's Subsonic API.
|
|
//nd:hostservice name=SubsonicAPI permission=subsonicapi
|
|
type SubsonicAPIService interface {
|
|
// Call executes a Subsonic API request.
|
|
//nd:hostfunc
|
|
Call(ctx context.Context, uri string) (response string, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "service.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
|
|
svc := services[0]
|
|
Expect(svc.Name).To(Equal("SubsonicAPI"))
|
|
Expect(svc.Permission).To(Equal("subsonicapi"))
|
|
Expect(svc.Interface).To(Equal("SubsonicAPIService"))
|
|
Expect(svc.Methods).To(HaveLen(1))
|
|
|
|
m := svc.Methods[0]
|
|
Expect(m.Name).To(Equal("Call"))
|
|
Expect(m.HasError).To(BeTrue())
|
|
Expect(m.Params).To(HaveLen(1))
|
|
Expect(m.Params[0].Name).To(Equal("uri"))
|
|
Expect(m.Params[0].Type).To(Equal("string"))
|
|
Expect(m.Returns).To(HaveLen(1))
|
|
Expect(m.Returns[0].Name).To(Equal("response"))
|
|
Expect(m.Returns[0].Type).To(Equal("string"))
|
|
})
|
|
|
|
It("should parse multiple methods", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
// SchedulerService provides scheduling capabilities.
|
|
//nd:hostservice name=Scheduler permission=scheduler
|
|
type SchedulerService interface {
|
|
//nd:hostfunc
|
|
ScheduleRecurring(ctx context.Context, cronExpression string) (scheduleID string, err error)
|
|
|
|
//nd:hostfunc
|
|
ScheduleOneTime(ctx context.Context, delaySeconds int32) (scheduleID string, err error)
|
|
|
|
//nd:hostfunc
|
|
CancelSchedule(ctx context.Context, scheduleID string) (canceled bool, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "scheduler.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
|
|
svc := services[0]
|
|
Expect(svc.Name).To(Equal("Scheduler"))
|
|
Expect(svc.Methods).To(HaveLen(3))
|
|
|
|
Expect(svc.Methods[0].Name).To(Equal("ScheduleRecurring"))
|
|
Expect(svc.Methods[0].Params[0].Type).To(Equal("string"))
|
|
|
|
Expect(svc.Methods[1].Name).To(Equal("ScheduleOneTime"))
|
|
Expect(svc.Methods[1].Params[0].Type).To(Equal("int32"))
|
|
|
|
Expect(svc.Methods[2].Name).To(Equal("CancelSchedule"))
|
|
Expect(svc.Methods[2].Returns[0].Type).To(Equal("bool"))
|
|
})
|
|
|
|
It("should skip methods without hostfunc annotation", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc
|
|
Exported(ctx context.Context) error
|
|
|
|
// This method is not exported
|
|
NotExported(ctx context.Context) error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
Expect(services[0].Methods).To(HaveLen(1))
|
|
Expect(services[0].Methods[0].Name).To(Equal("Exported"))
|
|
})
|
|
|
|
It("should parse raw=true annotation", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Stream permission=stream
|
|
type StreamService interface {
|
|
//nd:hostfunc raw=true
|
|
GetStream(ctx context.Context, uri string) (contentType string, data []byte, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "stream.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
|
|
m := services[0].Methods[0]
|
|
Expect(m.Name).To(Equal("GetStream"))
|
|
Expect(m.Raw).To(BeTrue())
|
|
Expect(m.HasError).To(BeTrue())
|
|
Expect(m.Returns).To(HaveLen(2))
|
|
Expect(m.Returns[0].Name).To(Equal("contentType"))
|
|
Expect(m.Returns[0].Type).To(Equal("string"))
|
|
Expect(m.Returns[1].Name).To(Equal("data"))
|
|
Expect(m.Returns[1].Type).To(Equal("[]byte"))
|
|
})
|
|
|
|
It("should set Raw=false when raw annotation is absent", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc
|
|
Call(ctx context.Context, uri string) (response string, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services[0].Methods[0].Raw).To(BeFalse())
|
|
})
|
|
|
|
It("should reject raw=true with invalid return signature", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc raw=true
|
|
BadRaw(ctx context.Context, uri string) (result string, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
_, err = ParseDirectory(tmpDir)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("raw=true"))
|
|
Expect(err.Error()).To(ContainSubstring("must return (string, []byte, error)"))
|
|
})
|
|
|
|
It("should reject raw=true without error return", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc raw=true
|
|
BadRaw(ctx context.Context, uri string) (contentType string, data []byte)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
_, err = ParseDirectory(tmpDir)
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(err.Error()).To(ContainSubstring("raw=true"))
|
|
})
|
|
|
|
It("should parse mixed raw and non-raw methods", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=API permission=api
|
|
type APIService interface {
|
|
//nd:hostfunc
|
|
Call(ctx context.Context, uri string) (responseJSON string, err error)
|
|
|
|
//nd:hostfunc raw=true
|
|
CallRaw(ctx context.Context, uri string) (contentType string, data []byte, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "api.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
Expect(services[0].Methods).To(HaveLen(2))
|
|
Expect(services[0].Methods[0].Raw).To(BeFalse())
|
|
Expect(services[0].Methods[1].Raw).To(BeTrue())
|
|
Expect(services[0].HasRawMethods()).To(BeTrue())
|
|
})
|
|
|
|
It("should handle custom export name", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc name=custom_export_name
|
|
MyMethod(ctx context.Context) error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services[0].Methods[0].ExportName).To(Equal("custom_export_name"))
|
|
Expect(services[0].Methods[0].FunctionName("test")).To(Equal("custom_export_name"))
|
|
})
|
|
|
|
It("should skip generated files", func() {
|
|
regularSrc := `package host
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc
|
|
Method(ctx context.Context) error
|
|
}
|
|
`
|
|
genSrc := `// Code generated. DO NOT EDIT.
|
|
package host
|
|
|
|
//nd:hostservice name=Generated permission=gen
|
|
type GeneratedService interface {
|
|
//nd:hostfunc
|
|
Method() error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(regularSrc), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
err = os.WriteFile(filepath.Join(tmpDir, "test_gen.go"), []byte(genSrc), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
Expect(services[0].Name).To(Equal("Test"))
|
|
})
|
|
|
|
It("should skip interfaces without hostservice annotation", func() {
|
|
src := `package host
|
|
|
|
import "context"
|
|
|
|
// Regular interface without annotation
|
|
type RegularInterface interface {
|
|
Method(ctx context.Context) error
|
|
}
|
|
|
|
//nd:hostservice name=Annotated permission=annotated
|
|
type AnnotatedService interface {
|
|
//nd:hostfunc
|
|
Method(ctx context.Context) error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
Expect(services[0].Name).To(Equal("Annotated"))
|
|
})
|
|
|
|
It("should return empty slice for directory with no host services", func() {
|
|
src := `package host
|
|
|
|
type RegularInterface interface {
|
|
Method() error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Describe("parseKeyValuePairs", func() {
|
|
It("should parse key=value pairs", func() {
|
|
result := parseKeyValuePairs("name=Test permission=test")
|
|
Expect(result).To(HaveKeyWithValue("name", "Test"))
|
|
Expect(result).To(HaveKeyWithValue("permission", "test"))
|
|
})
|
|
|
|
It("should return nil for empty input", func() {
|
|
result := parseKeyValuePairs("")
|
|
Expect(result).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("typeToString", func() {
|
|
It("should handle basic types", func() {
|
|
src := `package test
|
|
type T interface {
|
|
Method(s string, i int, b bool) ([]byte, error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "types.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
// Parse and verify type conversion works
|
|
// This is implicitly tested through ParseDirectory
|
|
})
|
|
|
|
It("should convert interface{} to any", func() {
|
|
src := `package test
|
|
|
|
import "context"
|
|
|
|
//nd:hostservice name=Test permission=test
|
|
type TestService interface {
|
|
//nd:hostfunc
|
|
GetMetadata(ctx context.Context) (data map[string]interface{}, err error)
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
services, err := ParseDirectory(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(services).To(HaveLen(1))
|
|
Expect(services[0].Methods[0].Returns[0].Type).To(Equal("map[string]any"))
|
|
})
|
|
})
|
|
|
|
Describe("Method helpers", func() {
|
|
It("should generate correct function names", func() {
|
|
m := Method{Name: "Call"}
|
|
Expect(m.FunctionName("subsonicapi")).To(Equal("subsonicapi_call"))
|
|
|
|
m.ExportName = "custom_name"
|
|
Expect(m.FunctionName("subsonicapi")).To(Equal("custom_name"))
|
|
})
|
|
|
|
It("should generate correct type names", func() {
|
|
m := Method{Name: "Call"}
|
|
// Host-side types are public
|
|
Expect(m.RequestTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallRequest"))
|
|
Expect(m.ResponseTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallResponse"))
|
|
// Client/PDK types are private
|
|
Expect(m.ClientRequestTypeName("SubsonicAPI")).To(Equal("subsonicAPICallRequest"))
|
|
Expect(m.ClientResponseTypeName("SubsonicAPI")).To(Equal("subsonicAPICallResponse"))
|
|
})
|
|
})
|
|
|
|
Describe("Service helpers", func() {
|
|
It("should generate correct output file name", func() {
|
|
s := Service{Name: "SubsonicAPI"}
|
|
Expect(s.OutputFileName()).To(Equal("subsonicapi_gen.go"))
|
|
})
|
|
|
|
It("should generate correct export prefix", func() {
|
|
s := Service{Name: "SubsonicAPI"}
|
|
Expect(s.ExportPrefix()).To(Equal("subsonicapi"))
|
|
})
|
|
})
|
|
|
|
Describe("ParseCapabilities", func() {
|
|
It("should parse a simple capability interface", func() {
|
|
src := `package capabilities
|
|
|
|
// MetadataAgent provides metadata retrieval.
|
|
//nd:capability name=metadata
|
|
type MetadataAgent interface {
|
|
// GetArtistBiography returns artist biography.
|
|
//nd:export name=nd_get_artist_biography
|
|
GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error)
|
|
}
|
|
|
|
// ArtistInput is the input for artist-related functions.
|
|
type ArtistInput struct {
|
|
// ID is the artist ID.
|
|
ID string ` + "`json:\"id\"`" + `
|
|
// Name is the artist name.
|
|
Name string ` + "`json:\"name\"`" + `
|
|
}
|
|
|
|
// ArtistBiographyOutput is the output for GetArtistBiography.
|
|
type ArtistBiographyOutput struct {
|
|
// Biography is the biography text.
|
|
Biography string ` + "`json:\"biography\"`" + `
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(HaveLen(1))
|
|
|
|
cap := capabilities[0]
|
|
Expect(cap.Name).To(Equal("metadata"))
|
|
Expect(cap.Interface).To(Equal("MetadataAgent"))
|
|
Expect(cap.Required).To(BeFalse())
|
|
Expect(cap.Doc).To(ContainSubstring("MetadataAgent provides metadata retrieval"))
|
|
Expect(cap.Methods).To(HaveLen(1))
|
|
|
|
m := cap.Methods[0]
|
|
Expect(m.Name).To(Equal("GetArtistBiography"))
|
|
Expect(m.ExportName).To(Equal("nd_get_artist_biography"))
|
|
Expect(m.Input.Type).To(Equal("ArtistInput"))
|
|
Expect(m.Output.Type).To(Equal("ArtistBiographyOutput"))
|
|
|
|
// Check structs were collected
|
|
Expect(cap.Structs).To(HaveLen(2))
|
|
})
|
|
|
|
It("should parse a required capability", func() {
|
|
src := `package capabilities
|
|
|
|
// Scrobbler requires all methods to be implemented.
|
|
//nd:capability name=scrobbler required=true
|
|
type Scrobbler interface {
|
|
//nd:export name=nd_scrobbler_is_authorized
|
|
IsAuthorized(AuthInput) (AuthOutput, error)
|
|
|
|
//nd:export name=nd_scrobbler_scrobble
|
|
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
|
}
|
|
|
|
type AuthInput struct {
|
|
UserID string ` + "`json:\"userId\"`" + `
|
|
}
|
|
|
|
type AuthOutput struct {
|
|
Authorized bool ` + "`json:\"authorized\"`" + `
|
|
}
|
|
|
|
type ScrobbleInput struct {
|
|
UserID string ` + "`json:\"userId\"`" + `
|
|
}
|
|
|
|
type ScrobblerOutput struct {
|
|
Error *string ` + "`json:\"error,omitempty\"`" + `
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(HaveLen(1))
|
|
|
|
cap := capabilities[0]
|
|
Expect(cap.Name).To(Equal("scrobbler"))
|
|
Expect(cap.Required).To(BeTrue())
|
|
Expect(cap.Methods).To(HaveLen(2))
|
|
})
|
|
|
|
It("should parse type aliases and consts", func() {
|
|
src := `package capabilities
|
|
|
|
//nd:capability name=scrobbler required=true
|
|
type Scrobbler interface {
|
|
//nd:export name=nd_scrobble
|
|
Scrobble(ScrobbleInput) (ScrobblerOutput, error)
|
|
}
|
|
|
|
type ScrobbleInput struct {
|
|
UserID string ` + "`json:\"userId\"`" + `
|
|
}
|
|
|
|
// ScrobblerErrorType indicates error handling behavior.
|
|
type ScrobblerErrorType string
|
|
|
|
const (
|
|
// ScrobblerErrorNone indicates no error.
|
|
ScrobblerErrorNone ScrobblerErrorType = "none"
|
|
// ScrobblerErrorRetry indicates retry later.
|
|
ScrobblerErrorRetry ScrobblerErrorType = "retry"
|
|
)
|
|
|
|
type ScrobblerOutput struct {
|
|
ErrorType *ScrobblerErrorType ` + "`json:\"errorType,omitempty\"`" + `
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(HaveLen(1))
|
|
|
|
cap := capabilities[0]
|
|
// Type alias should be collected
|
|
Expect(cap.TypeAliases).To(HaveLen(1))
|
|
Expect(cap.TypeAliases[0].Name).To(Equal("ScrobblerErrorType"))
|
|
Expect(cap.TypeAliases[0].Type).To(Equal("string"))
|
|
|
|
// Consts should be collected
|
|
Expect(cap.Consts).To(HaveLen(1))
|
|
Expect(cap.Consts[0].Type).To(Equal("ScrobblerErrorType"))
|
|
Expect(cap.Consts[0].Values).To(HaveLen(2))
|
|
Expect(cap.Consts[0].Values[0].Name).To(Equal("ScrobblerErrorNone"))
|
|
Expect(cap.Consts[0].Values[0].Value).To(Equal(`"none"`))
|
|
})
|
|
|
|
It("should collect nested struct dependencies", func() {
|
|
src := `package capabilities
|
|
|
|
//nd:capability name=metadata
|
|
type MetadataAgent interface {
|
|
//nd:export name=nd_get_images
|
|
GetImages(ArtistInput) (ImagesOutput, error)
|
|
}
|
|
|
|
type ArtistInput struct {
|
|
ID string ` + "`json:\"id\"`" + `
|
|
}
|
|
|
|
type ImagesOutput struct {
|
|
Images []ImageInfo ` + "`json:\"images\"`" + `
|
|
}
|
|
|
|
type ImageInfo struct {
|
|
URL string ` + "`json:\"url\"`" + `
|
|
Size int32 ` + "`json:\"size\"`" + `
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(HaveLen(1))
|
|
|
|
cap := capabilities[0]
|
|
// Should collect all 3 structs: ArtistInput, ImagesOutput, and ImageInfo
|
|
Expect(cap.Structs).To(HaveLen(3))
|
|
|
|
structNames := make([]string, len(cap.Structs))
|
|
for i, s := range cap.Structs {
|
|
structNames[i] = s.Name
|
|
}
|
|
Expect(structNames).To(ContainElements("ArtistInput", "ImagesOutput", "ImageInfo"))
|
|
})
|
|
|
|
It("should return empty slice for directory with no capabilities", func() {
|
|
src := `package capabilities
|
|
|
|
type RegularInterface interface {
|
|
Method() error
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(BeEmpty())
|
|
})
|
|
|
|
It("should ignore methods without export annotation", func() {
|
|
src := `package capabilities
|
|
|
|
//nd:capability name=test
|
|
type TestCapability interface {
|
|
//nd:export name=nd_exported
|
|
ExportedMethod(Input) (Output, error)
|
|
|
|
// This method has no export annotation
|
|
NotExportedMethod(Input) (Output, error)
|
|
}
|
|
|
|
type Input struct {
|
|
Value string ` + "`json:\"value\"`" + `
|
|
}
|
|
|
|
type Output struct {
|
|
Result string ` + "`json:\"result\"`" + `
|
|
}
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
capabilities, err := ParseCapabilities(tmpDir)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(capabilities).To(HaveLen(1))
|
|
|
|
// Only the exported method should be captured
|
|
Expect(capabilities[0].Methods).To(HaveLen(1))
|
|
Expect(capabilities[0].Methods[0].Name).To(Equal("ExportedMethod"))
|
|
})
|
|
})
|
|
|
|
Describe("Export helpers", func() {
|
|
It("should generate correct provider interface name", func() {
|
|
e := Export{Name: "GetArtistBiography"}
|
|
Expect(e.ProviderInterfaceName()).To(Equal("ArtistBiographyProvider"))
|
|
|
|
e = Export{Name: "OnInit"}
|
|
Expect(e.ProviderInterfaceName()).To(Equal("InitProvider"))
|
|
})
|
|
|
|
It("should generate correct impl variable name", func() {
|
|
e := Export{Name: "GetArtistBiography"}
|
|
Expect(e.ImplVarName()).To(Equal("artistBiographyImpl"))
|
|
|
|
e = Export{Name: "OnInit"}
|
|
Expect(e.ImplVarName()).To(Equal("initImpl"))
|
|
})
|
|
|
|
It("should generate correct export function name", func() {
|
|
e := Export{Name: "GetArtistBiography", ExportName: "nd_get_artist_biography"}
|
|
Expect(e.ExportFuncName()).To(Equal("_NdGetArtistBiography"))
|
|
})
|
|
})
|
|
})
|