From f9ac0328a7efcb632ecdd84dbaf1fbc11bf9d109 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 10 Nov 2023 08:43:17 +0100 Subject: [PATCH] Implement "backends" command --- backends/interfaces.go | 93 ++++++++++++++++++++++++++++++++----- backends/interfaces_test.go | 88 +++++++++++++++++++++++++++++++++++ cmd/backends.go | 59 +++++++++++++++++++++++ 3 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 backends/interfaces_test.go create mode 100644 cmd/backends.go diff --git a/backends/interfaces.go b/backends/interfaces.go index 1498bb6..e96ac55 100644 --- a/backends/interfaces.go +++ b/backends/interfaces.go @@ -25,6 +25,7 @@ import ( "errors" "fmt" "reflect" + "strings" "time" "github.com/spf13/viper" @@ -55,35 +56,105 @@ type ImportResult struct { LastTimestamp time.Time } -func resolveBackend(config *viper.Viper) (string, Backend, error) { - backendName := config.GetString("backend") - // fmt.Printf("requested backend %s\n", backendName) - backendType := knownBackends[backendName] - if backendType == nil { - return backendName, nil, errors.New(fmt.Sprintf("Unknown backend %s", backendName)) - } - return backendName, backendType().FromConfig(config), nil +type BackendInfo struct { + Name string + ExportCapabilities []Capability + ImportCapabilities []Capability } +type Capability = string + func ResolveBackend[T interface{}](config *viper.Viper) (T, error) { - expectedInterface := reflect.TypeOf((*T)(nil)).Elem() backendName, backend, err := resolveBackend(config) var result T if err != nil { return result, err } - if backend != nil && reflect.TypeOf(backend).Implements(expectedInterface) { + implements, interfaceName := implementsInterface[T](backend) + if implements { result = backend.(T) } else { err = errors.New( - fmt.Sprintf("Backend %s does not implement %s", backendName, expectedInterface.String())) + fmt.Sprintf("Backend %s does not implement %s", backendName, interfaceName)) } return result, err } +func GetBackends() []BackendInfo { + backends := make([]BackendInfo, 0) + for name, backendFunc := range knownBackends { + backend := backendFunc() + info := BackendInfo{ + Name: name, + ExportCapabilities: getExportCapabilities(backend), + ImportCapabilities: getImportCapabilities(backend), + } + backends = append(backends, info) + } + + return backends +} + var knownBackends = map[string]func() Backend{ "dump": func() Backend { return &DumpBackend{} }, "listenbrainz-api": func() Backend { return &ListenBrainzApiBackend{} }, "maloja-api": func() Backend { return &MalojaApiBackend{} }, "scrobbler-log": func() Backend { return &ScrobblerLogBackend{} }, } + +func resolveBackend(config *viper.Viper) (string, Backend, error) { + backendName := config.GetString("backend") + backendType := knownBackends[backendName] + if backendType == nil { + return backendName, nil, errors.New(fmt.Sprintf("Unknown backend %s", backendName)) + } + return backendName, backendType().FromConfig(config), nil +} + +func implementsInterface[T interface{}](backend Backend) (bool, string) { + expectedInterface := reflect.TypeOf((*T)(nil)).Elem() + implements := backend != nil && reflect.TypeOf(backend).Implements(expectedInterface) + return implements, expectedInterface.Name() +} + +func getExportCapabilities(backend Backend) []string { + caps := make([]Capability, 0) + var name string + var found bool + name, found = checkCapability[ListenExport](backend, "export") + if found { + caps = append(caps, name) + } + name, found = checkCapability[LovesExport](backend, "export") + if found { + caps = append(caps, name) + } + return caps +} + +func getImportCapabilities(backend Backend) []Capability { + caps := make([]Capability, 0) + var name string + var found bool + name, found = checkCapability[ListenImport](backend, "import") + if found { + caps = append(caps, name) + } + name, found = checkCapability[LovesImport](backend, "import") + if found { + caps = append(caps, name) + } + return caps +} + +func checkCapability[T interface{}](backend Backend, suffix string) (string, bool) { + implements, name := implementsInterface[T](backend) + if implements { + cap, found := strings.CutSuffix(strings.ToLower(name), suffix) + if found { + return cap, found + } + } + + return "", false +} diff --git a/backends/interfaces_test.go b/backends/interfaces_test.go new file mode 100644 index 0000000..26184f8 --- /dev/null +++ b/backends/interfaces_test.go @@ -0,0 +1,88 @@ +/* +Copyright © 2023 Philipp Wolfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package backends_test + +import ( + "reflect" + "slices" + "testing" + + "github.com/spf13/viper" + "go.uploadedlobster.com/scotty/backends" +) + +func TestResolveBackend(t *testing.T) { + config := viper.New() + config.Set("backend", "dump") + backend, err := backends.ResolveBackend[backends.ListenImport](config) + if err != nil { + t.Errorf("ResolveBackend failed unexpected: %v", err) + } + realType := reflect.TypeOf(backend) + if realType.Name() != "DumpBackend" { + t.Errorf("ResolveBackend did not return a DumpBackend, unexpected type %v", realType) + } +} + +func TestResolveBackendUnknown(t *testing.T) { + config := viper.New() + config.Set("backend", "foo") + _, err := backends.ResolveBackend[backends.ListenImport](config) + if err == nil { + t.Error("Expected ResolveBackend to fail", err) + } else if err.Error() != "Unknown backend foo" { + t.Errorf("Expected \"Unknown backend foo\", got \"%s\"", err) + } +} + +func TestResolveBackendInvalidInterface(t *testing.T) { + config := viper.New() + config.Set("backend", "dump") + _, err := backends.ResolveBackend[backends.ListenExport](config) + if err == nil { + t.Error("Expected ResolveBackend to fail", err) + } else if err.Error() != "Backend dump does not implement ListenExport" { + t.Errorf("Expected \"Backend dump does not implement ListenExport\", got \"%s\"", err) + } +} + +func TestGetBackends(t *testing.T) { + backends := backends.GetBackends() + for _, info := range backends { + if info.Name == "dump" { + if !slices.Contains[[]string](info.ImportCapabilities, "listen") { + t.Error("Backend \"dump\" is expected to provide listen import capability") + } + if len(info.ExportCapabilities) > 0 { + t.Errorf( + "Backend \"dump\" is not expected to provide any export capabilities, provides %s", + info.ExportCapabilities, + ) + } + return // Finish the test + } + } + + // If we got here the "dump" backend was not included + t.Errorf("GetBackends() did not return expected bacend \"dump\"") +} diff --git a/cmd/backends.go b/cmd/backends.go new file mode 100644 index 0000000..1911eca --- /dev/null +++ b/cmd/backends.go @@ -0,0 +1,59 @@ +/* +Copyright © 2023 Philipp Wolfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "go.uploadedlobster.com/scotty/backends" +) + +// backendsCmd represents the backends command +var backendsCmd = &cobra.Command{ + Use: "backends", + Short: "List available backends", + Long: `List the names and supported capabilities of all available backends.`, + Run: func(cmd *cobra.Command, args []string) { + backends := backends.GetBackends() + for _, info := range backends { + fmt.Printf("%s:\n", info.Name) + fmt.Printf("\texport: %s\n", strings.Join(info.ExportCapabilities, ", ")) + fmt.Printf("\timport: %s\n\n", strings.Join(info.ImportCapabilities, ", ")) + } + }, +} + +func init() { + rootCmd.AddCommand(backendsCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backendsCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backendsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +}