Skip to content

Commit

Permalink
Add Copy/CopyIndexer methods
Browse files Browse the repository at this point in the history
  • Loading branch information
davidnewhall committed Jul 8, 2024
1 parent 441249a commit 8c4ee93
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 0 deletions.
20 changes: 20 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ func Str[I int | int64 | float64 | bool](val I) string {
}
}

// None can be used to return only an error condition. The opposite of Must().
// If the last argument is an error, that's what gets returned, otherwise nil.
func None(input ...any) error {
if len(input) > 0 {
err, _ := input[len(input)-1].(error)
return err
}

return nil
}

// Must can be used to avoid checking an error you'll never run into.
func Must[S any](input S, err error) S {
if err != nil {
panic("Must failed: " + err.Error())
}

return input
}

// Ptr returns a pointer to the provided "whatever".
func Ptr[P any](p P) *P {
return &p
Expand Down
22 changes: 22 additions & 0 deletions helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package starr_test

import (
"testing"

"github.com/stretchr/testify/assert"
"golift.io/starr"
)

//nolint:testifylint // we want to test each one and not fail on an error.
func TestNone(t *testing.T) {
t.Parallel()
assert.ErrorIs(t, starr.None(starr.ErrNilClient), starr.ErrNilClient)
assert.ErrorIs(t, starr.None("string", starr.ErrNilClient), starr.ErrNilClient)
assert.ErrorIs(t, starr.None(uint(1), starr.ErrNilClient), starr.ErrNilClient)
assert.ErrorIs(t, starr.None("string", uint(1), starr.ErrNilClient), starr.ErrNilClient)
assert.ErrorIs(t, starr.None(1.0, "string", starr.ErrNilClient), starr.ErrNilClient)
assert.NoError(t, starr.None(1.0, "string"))
assert.NoError(t, starr.None("string"))
assert.NoError(t, starr.None(1.0))
assert.NoError(t, starr.None())
}
98 changes: 98 additions & 0 deletions orbit/copier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Package orbit provides functions to modify data structures among the various starr libraries.
// These functions cannot live in the starr library without causing an import cycle.
// These are wrappers around the starr library and other sub modules.
package orbit

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"

"golift.io/starr/lidarr"
"golift.io/starr/prowlarr"
"golift.io/starr/radarr"
"golift.io/starr/readarr"
"golift.io/starr/sonarr"
)

var ErrNotPtr = errors.New("must provide a pointer to a non-nil value")

// Copy is an easy way to copy one data structure to another.
func Copy(src, dst any) error {
if src == nil || reflect.TypeOf(src).Kind() != reflect.Ptr {
return fmt.Errorf("copy source: %w", ErrNotPtr)
} else if dst == nil || reflect.TypeOf(dst).Kind() != reflect.Ptr {
return fmt.Errorf("copy destination: %w", ErrNotPtr)
}

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(src); err != nil {
return fmt.Errorf("encoding: %w", err)
}

if err := json.NewDecoder(&buf).Decode(dst); err != nil {
return fmt.Errorf("decoding: %w", err)
}

return nil
}

// IndexerInput represents all possible Indexer inputs.
type IndexerInput interface {
lidarr.IndexerInput | prowlarr.IndexerInput | radarr.IndexerInput |
readarr.IndexerInput | sonarr.IndexerInput
}

// IndexerOutput represents all possible Indexer outputs.
type IndexerOutput interface {
lidarr.IndexerOutput | prowlarr.IndexerOutput | radarr.IndexerOutput |
readarr.IndexerOutput | sonarr.IndexerOutput
}

// CopyIndexers copies a slice of indexers from one type to another, so you may copy them among instances.
// The destination must be a pointer to a slice, so it can be updated in place.
// The destination slice may be empty but the pointer to it must not be nil.
func CopyIndexers[S IndexerInput | IndexerOutput, D IndexerInput](src []*S, dst *[]*D, keepTags bool) ([]*D, error) {
if dst == nil {
return nil, ErrNotPtr
}

var err error

for idx, indexer := range src {
if len(*dst)-1 >= idx { // The destination slice location exists, so update it in place.
_, err = CopyIndexer(indexer, (*dst)[idx], keepTags)
} else { // The destination slice is shorter than the source, so append to it.
newIndexer := new(D)
newIndexer, err = CopyIndexer(indexer, newIndexer, keepTags)
*dst = append(*dst, newIndexer) // This happens before checking the error.
}

if err != nil {
break
}
}

return *dst, err
}

// CopyIndexer copies an indexer from one type to another, so you may copy them among instances.
func CopyIndexer[S IndexerInput | IndexerOutput, D IndexerInput](src *S, dst *D, keepTags bool) (*D, error) {
if err := Copy(src, dst); err != nil {
return dst, err
}

element := reflect.ValueOf(dst).Elem()
zeroField(element.FieldByName("ID"), true)
zeroField(element.FieldByName("Tags"), !keepTags)

return dst, nil
}

func zeroField(field reflect.Value, really bool) {
if really && field.CanSet() {
field.SetZero()
}
}
139 changes: 139 additions & 0 deletions orbit/copier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package orbit_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golift.io/starr"
"golift.io/starr/orbit"
"golift.io/starr/prowlarr"
"golift.io/starr/sonarr"
)

func copyData(t *testing.T) (*prowlarr.IndexerOutput, *sonarr.IndexerInput) {
t.Helper()

return &prowlarr.IndexerOutput{
ID: 2,
Priority: 3,
Name: "yes",
Protocol: "usenet",
Implementation: "core",
ConfigContract: "hancock",
Tags: []int{1, 2, 5},
Fields: []*starr.FieldOutput{
{Name: "One", Value: "one"},
{Name: "Two", Value: 2.0},
{Name: "Three", Value: uint(3)},
{Name: "Five", Value: 5},
},
},
&sonarr.IndexerInput{
// These are not part of the used input, so set them before copying.
EnableAutomaticSearch: true,
EnableInteractiveSearch: true,
EnableRss: true,
DownloadClientID: 15,
}
}

func TestCopyIndexers(t *testing.T) {
t.Parallel()
src1, dst1 := copyData(t)
src2, dst2 := copyData(t)
src3, dst3 := copyData(t)
src4, _ := copyData(t)
src5, _ := copyData(t)
// We test for these.
src1.Priority = 1
src2.Priority = 2
src3.Priority = 3
src4.Priority = 4
src5.Priority = 5
// Make two lists.
srcs := append([]*prowlarr.IndexerOutput{}, src1, src2, src3, src4, src5)
dsts := append([]*sonarr.IndexerInput{}, dst1, dst2, dst3) // Short by 2.
// Copy the lists.
dsts2, err := orbit.CopyIndexers(srcs, &dsts, true)
require.NoError(t, err)
// Make sure both outputs have a length matching the input.
assert.Len(t, dsts, len(srcs))
assert.Len(t, dsts2, len(srcs))
// Test that values got copied.
for idx, src := range srcs {
assert.Zero(t, dsts[idx].ID)
assert.Equal(t, src.Priority, dsts[idx].Priority)
assert.Equal(t, src.Tags, dsts[idx].Tags)
}
}

// TestCopyIndexersNilDest test a nil destination pointer and slice.
func TestCopyIndexersNilDest(t *testing.T) {
t.Parallel()
src1, _ := copyData(t)
src2, _ := copyData(t)
// Make two lists.
srcs := append([]*prowlarr.IndexerOutput{}, src1, src2)
dsts := new([]*sonarr.IndexerInput) // Super empty.
*dsts = nil // Nil the slice.
// Copy the lists.
dsts2, err := orbit.CopyIndexers(srcs, dsts, false)
require.NoError(t, err)
// Make sure both outputs have a length matching the input.
assert.Len(t, *dsts, len(srcs))
assert.Len(t, dsts2, len(srcs))
// Test that tags got removed.
for idx, src := range srcs {
assert.Zero(t, (*dsts)[idx].ID)
assert.Equal(t, src.Priority, (*dsts)[idx].Priority)
assert.NotEqual(t, src.Tags, (*dsts)[idx].Tags)
}

// Make an error.
dsts = nil // This is a no-no.
require.ErrorIs(t, starr.None(orbit.CopyIndexers(srcs, dsts, false)), orbit.ErrNotPtr)
}

func TestCopyIndexer(t *testing.T) {
t.Parallel()

src, dst := copyData(t)
// Verify everything copies over.
require.NoError(t, starr.None(orbit.CopyIndexer(src, dst, true)))
assert.Equal(t, src.Fields[0].Value, dst.Fields[0].Value)
assert.Equal(t, src.Fields[1].Value, dst.Fields[1].Value)
assert.EqualValues(t, src.Fields[2].Value, dst.Fields[2].Value)
assert.EqualValues(t, src.Fields[3].Value, dst.Fields[3].Value)
assert.Equal(t, src.Fields[0].Name, dst.Fields[0].Name)
assert.Equal(t, src.Fields[1].Name, dst.Fields[1].Name)
assert.Equal(t, src.Fields[2].Name, dst.Fields[2].Name)
assert.Equal(t, src.Fields[3].Name, dst.Fields[3].Name)
assert.Zero(t, dst.ID)
assert.Equal(t, src.Priority, dst.Priority)
assert.Equal(t, src.Name, dst.Name)
assert.Equal(t, src.Protocol, dst.Protocol)
assert.Equal(t, src.Implementation, dst.Implementation)
assert.Equal(t, src.ConfigContract, dst.ConfigContract)
assert.Equal(t, src.Tags[0], dst.Tags[0])
assert.Equal(t, src.Tags[1], dst.Tags[1])
assert.Equal(t, src.Tags[2], dst.Tags[2])
// Check passed in values.
assert.Equal(t, int64(15), dst.DownloadClientID)
assert.True(t, dst.EnableAutomaticSearch)
assert.True(t, dst.EnableInteractiveSearch)
assert.True(t, dst.EnableRss)
// Make sure tags get depleted.
starr.Must(orbit.CopyIndexer(src, dst, false))
assert.Zero(t, dst.Tags)
}

func TestCopy(t *testing.T) {
t.Parallel()

broken := struct{}{}
good := &prowlarr.IndexerOutput{}

require.ErrorIs(t, orbit.Copy(broken, good), orbit.ErrNotPtr)
require.ErrorIs(t, orbit.Copy(good, broken), orbit.ErrNotPtr)
}

0 comments on commit 8c4ee93

Please sign in to comment.