Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy Indexer structs example #164

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Loading