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 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())
})
It("should resolve structs defined in a sibling file of the same package", func() {
typesSrc := `package host
// Artist is a track participant.
type Artist struct {
// ID is the artist identifier.
ID string ` + "`json:\"id\"`" + `
// Name is the artist name.
Name string ` + "`json:\"name\"`" + `
}
// Track is a media file projection.
type Track struct {
// ID is the track identifier.
ID string ` + "`json:\"id\"`" + `
// Title is the track title.
Title string ` + "`json:\"title\"`" + `
// Participants maps role to artists (transitive dependency test).
Participants map[string][]Artist ` + "`json:\"participants\"`" + `
}
`
serviceSrc := `package host
import "context"
// MatcherService matches tracks.
//nd:hostservice name=Matcher permission=matcher
type MatcherService interface {
// MatchTrack finds a matching track.
//nd:hostfunc
MatchTrack(ctx context.Context, t Track) (matched bool, err error)
}
`
err := os.WriteFile(filepath.Join(tmpDir, "types.go"), []byte(typesSrc), 0600)
Expect(err).NotTo(HaveOccurred())
err = os.WriteFile(filepath.Join(tmpDir, "service.go"), []byte(serviceSrc), 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("Matcher"))
Expect(svc.Methods).To(HaveLen(1))
Expect(svc.Methods[0].Name).To(Equal("MatchTrack"))
structNames := make([]string, len(svc.Structs))
for i, s := range svc.Structs {
structNames[i] = s.Name
}
Expect(structNames).To(ConsistOf("Track", "Artist"))
})
It("returns an error when a shared-type alias cannot be resolved (no registry)", func() {
fileA := `package host
import "github.com/navidrome/navidrome/plugins/types"
type TrackInfo = types.TrackInfo
`
fileB := `package host
import "context"
//nd:hostservice name=Matcher permission=matcher
type MatcherService interface {
//nd:hostfunc
Match(ctx context.Context, t TrackInfo) (bool, error)
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "aliases.go"), []byte(fileA), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(tmpDir, "matcher.go"), []byte(fileB), 0600)).To(Succeed())
_, err := ParseDirectoryWithShared(tmpDir, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("TrackInfo"))
Expect(err.Error()).To(ContainSubstring("-shared"))
})
It("resolves shared-type aliases declared in a sibling file (package-wide alias map)", func() {
fileA := `package host
import "github.com/navidrome/navidrome/plugins/types"
// Deprecated: use types.Track.
type Track = types.Track
`
fileB := `package host
import "context"
//nd:hostservice name=Matcher permission=matcher
type MatcherService interface {
//nd:hostfunc
MatchSongs(ctx context.Context, query string) (results []Track, err error)
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "aliases.go"), []byte(fileA), 0600)).To(Succeed())
Expect(os.WriteFile(filepath.Join(tmpDir, "matcher.go"), []byte(fileB), 0600)).To(Succeed())
shared := map[string]StructDef{
"Track": {
Name: "Track",
Fields: []FieldDef{
{Name: "Title", Type: "string", JSONTag: "title"},
{Name: "Artist", Type: "string", JSONTag: "artist"},
},
},
}
services, err := ParseDirectoryWithShared(tmpDir, shared)
Expect(err).NotTo(HaveOccurred())
Expect(services).To(HaveLen(1))
byName := map[string]SharedAlias{}
for _, a := range services[0].SharedAliases {
byName[a.Name] = a
}
Expect(byName).To(HaveKey("Track"))
Expect(byName["Track"].Target).To(Equal("types.Track"))
})
})
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())
})
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"}
Expect(m.RequestTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallRequest"))
Expect(m.ResponseTypeName("SubsonicAPI")).To(Equal("SubsonicAPICallResponse"))
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"))
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]
Expect(cap.TypeAliases).To(HaveLen(1))
Expect(cap.TypeAliases[0].Name).To(Equal("ScrobblerErrorType"))
Expect(cap.TypeAliases[0].Type).To(Equal("string"))
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]
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))
Expect(capabilities[0].Methods).To(HaveLen(1))
Expect(capabilities[0].Methods[0].Name).To(Equal("ExportedMethod"))
})
It("distinguishes Go type aliases from defined types", func() {
src := `package capabilities
import "github.com/navidrome/navidrome/plugins/types"
// Deprecated: use types.ArtistRef.
type ArtistRef = types.ArtistRef
// ScrobblerError is a sentinel error string.
type ScrobblerError string
//nd:capability name=scrobbler required=true
type Scrobbler interface {
//nd:export name=nd_scrobbler_check
Check(ArtistRef) (bool, error)
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)).To(Succeed())
shared := map[string]StructDef{
"ArtistRef": {Name: "ArtistRef", Fields: []FieldDef{{Name: "Name", Type: "string", JSONTag: "name"}}},
}
caps, err := ParseCapabilitiesWithShared(tmpDir, shared)
Expect(err).NotTo(HaveOccurred())
Expect(caps).To(HaveLen(1))
sharedByName := map[string]SharedAlias{}
for _, a := range caps[0].SharedAliases {
sharedByName[a.Name] = a
}
Expect(sharedByName).To(HaveKey("ArtistRef"))
Expect(sharedByName["ArtistRef"].Target).To(Equal("types.ArtistRef"))
typeByName := map[string]TypeAlias{}
for _, a := range caps[0].TypeAliases {
typeByName[a.Name] = a
}
Expect(typeByName).To(HaveKey("ScrobblerError"))
Expect(typeByName["ScrobblerError"].IsAlias).To(BeFalse())
})
})
Describe("ParseCapabilitiesWithShared", func() {
It("returns an error when a shared-type alias cannot be resolved (no registry)", func() {
src := `package capabilities
import "github.com/navidrome/navidrome/plugins/types"
// TrackInfo is an alias for the shared type.
type TrackInfo = types.TrackInfo
//nd:capability name=nowplaying required=true
type NowPlaying interface {
//nd:export name=nd_now_playing
NowPlaying(TrackInfo) error
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "nowplaying.go"), []byte(src), 0600)).To(Succeed())
_, err := ParseCapabilitiesWithShared(tmpDir, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("TrackInfo"))
Expect(err.Error()).To(ContainSubstring("-shared"))
})
It("resolves shared-type aliases against the registry", func() {
shared := map[string]StructDef{
"ArtistRef": {Name: "ArtistRef", Fields: []FieldDef{{Name: "Name", Type: "string", JSONTag: "name"}}},
"TrackInfo": {Name: "TrackInfo", Fields: []FieldDef{
{Name: "Title", Type: "string", JSONTag: "title"},
{Name: "Artists", Type: "[]ArtistRef", JSONTag: "artists"},
}},
}
src := `package capabilities
import "github.com/navidrome/navidrome/plugins/types"
// Deprecated: use types.TrackInfo.
type TrackInfo = types.TrackInfo
// Deprecated: use types.ArtistRef.
type ArtistRef = types.ArtistRef
// NowPlayingRequest carries a track.
type NowPlayingRequest struct {
Track TrackInfo ` + "`json:\"track\"`" + `
}
//nd:capability name=scrobbler required=true
type Scrobbler interface {
//nd:export name=nd_scrobbler_now_playing
NowPlaying(NowPlayingRequest) error
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)).To(Succeed())
caps, err := ParseCapabilitiesWithShared(tmpDir, shared)
Expect(err).NotTo(HaveOccurred())
Expect(caps).To(HaveLen(1))
names := []string{}
for _, a := range caps[0].SharedAliases {
names = append(names, a.Name)
}
Expect(names).To(ContainElements("TrackInfo", "ArtistRef"))
byName := map[string]SharedAlias{}
for _, a := range caps[0].SharedAliases {
byName[a.Name] = a
}
Expect(byName["TrackInfo"].Target).To(Equal("types.TrackInfo"))
Expect(byName["TrackInfo"].Def.Fields).To(HaveLen(2))
Expect(byName["ArtistRef"].Target).To(Equal("types.ArtistRef"))
})
It("resolves shared types from qualified types.X references with a renamed alias", func() {
shared := map[string]StructDef{
"ArtistRef": {Name: "ArtistRef", Fields: []FieldDef{{Name: "Name", Type: "string", JSONTag: "name"}}},
"Track": {Name: "Track", Fields: []FieldDef{
{Name: "Title", Type: "string", JSONTag: "title"},
{Name: "Artists", Type: "[]ArtistRef", JSONTag: "artists"},
}},
}
src := `package capabilities
import "github.com/navidrome/navidrome/plugins/types"
// Deprecated: use types.Track.
type TrackInfo = types.Track
// Deprecated: use types.ArtistRef.
type ArtistRef = types.ArtistRef
// NowPlayingRequest carries a track.
type NowPlayingRequest struct {
Track types.Track ` + "`json:\"track\"`" + `
}
//nd:capability name=scrobbler required=true
type Scrobbler interface {
//nd:export name=nd_scrobbler_now_playing
NowPlaying(NowPlayingRequest) error
}
`
Expect(os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600)).To(Succeed())
caps, err := ParseCapabilitiesWithShared(tmpDir, shared)
Expect(err).NotTo(HaveOccurred())
Expect(caps).To(HaveLen(1))
byName := map[string]SharedAlias{}
for _, a := range caps[0].SharedAliases {
byName[a.Name] = a
}
Expect(byName).To(HaveKey("TrackInfo"))
Expect(byName).To(HaveKey("ArtistRef"))
Expect(byName["TrackInfo"].Target).To(Equal("types.Track"))
Expect(byName["TrackInfo"].Def.Fields).To(HaveLen(2))
Expect(byName["ArtistRef"].Target).To(Equal("types.ArtistRef"))
var nowPlaying StructDef
for _, st := range caps[0].Structs {
if st.Name == "NowPlayingRequest" {
nowPlaying = st
}
}
Expect(nowPlaying.Fields).To(HaveLen(1))
Expect(nowPlaying.Fields[0].Type).To(Equal("types.Track"))
})
})
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"))
})
})
})