Skip to content
Merged
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: 15 additions & 5 deletions specs/destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ func (d *Destination) SetDefaults() {
if d.Path == "" {
d.Path = d.Name
}
if d.Version == "" {
d.Version = "latest"
}
if d.Registry == RegistryGithub && !strings.Contains(d.Path, "/") {
d.Path = "cloudquery/" + d.Path
}
Expand All @@ -43,10 +40,23 @@ func (d *Destination) UnmarshalSpec(out interface{}) error {
if err != nil {
return err
}
dec := json.NewDecoder(nil)
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
dec.DisallowUnknownFields()
return json.Unmarshal(b, out)
return dec.Decode(out)
}

func (d *Destination) Validate() error {
if d.Name == "" {
return fmt.Errorf("name is required")
}
if d.Version == "" {
return fmt.Errorf("version is required")
}
if !strings.HasPrefix(d.Version, "v") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, we should do full SemVer validation as the following string vv1.0.0 passes this validation

return fmt.Errorf("version must start with v")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then change this error message to version must be a valid SemVer string)

}
return nil
}

func (m WriteMode) String() string {
Expand Down
154 changes: 137 additions & 17 deletions specs/destination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package specs

import (
"testing"

"github.com/google/go-cmp/cmp"
)

type testDestinationSpec struct {
Expand All @@ -24,23 +26,7 @@ func TestWriteModeFromString(t *testing.T) {
}
}

func TestDestinationSetDefaults(t *testing.T) {
destination := Destination{
Name: "testDestination",
}
destination.SetDefaults()
if destination.Registry != RegistryGithub {
t.Fatalf("expected RegistryGithub, got %v", destination.Registry)
}
if destination.Path != "cloudquery/testDestination" {
t.Fatalf("expected destination.Path (%s), got %s", destination.Name, destination.Path)
}
if destination.Version != "latest" {
t.Fatalf("expected latest, got %s", destination.Version)
}
}

func TestDestinationUnmarshalSpec(t *testing.T) {
func TestDestinationSpecUnmarshalSpec(t *testing.T) {
destination := Destination{
Spec: map[string]interface{}{
"connection_string": "postgres://user:pass@host:port/db",
Expand All @@ -54,3 +40,137 @@ func TestDestinationUnmarshalSpec(t *testing.T) {
t.Fatalf("expected postgres://user:pass@host:port/db, got %s", spec.ConnectionString)
}
}

var destinationUnmarshalSpecTestCases = []struct {
name string
spec string
err string
source *Source
}{
{
"invalid_kind",
`kind: nice`,
"failed to decode spec: unknown kind nice",
nil,
Comment on lines +51 to +54
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's add keys to these struct instantiations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel for this test driven use-case it reduces the boilerplate imo and makes it easier to add new tests. just opinion ofc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, though as someone reading it for the first time, I found it hard to know what all these parameters were for, and had to scroll up and down a few times. I think it improves readability if you have the keys right there

},
{
"invalid_type",
`kind: source
spec:
name: 3
`,
"failed to decode spec: json: cannot unmarshal number into Go struct field Source.name of type string",
&Source{
Name: "test",
Tables: []string{"*"},
},
},
{
"unknown_field",
`kind: source
spec:
namea: 3
`,
`failed to decode spec: json: unknown field "namea"`,
&Source{
Name: "test",
Tables: []string{"*"},
},
},
}

func TestDestinationUnmarshalSpec(t *testing.T) {
for _, tc := range destinationUnmarshalSpecTestCases {
t.Run(tc.name, func(t *testing.T) {
var err error
var spec Spec
err = SpecUnmarshalYamlStrict([]byte(tc.spec), &spec)
if err != nil {
if err.Error() != tc.err {
t.Fatalf("expected:%s got:%s", tc.err, err.Error())
}
return
}

source := spec.Spec.(*Source)
if cmp.Diff(source, tc.source) != "" {
t.Fatalf("expected:%v got:%v", tc.source, source)
}
})
}
}

var destinationUnmarshalSpecValidateTestCases = []struct {
name string
spec string
err string
destination *Destination
}{
{
"required_name",
`kind: destination
spec:`,
"name is required",
nil,
},
{
"required_version",
`kind: destination
spec:
name: test
`,
"version is required",
nil,
},
{
"required_version_format",
`kind: destination
spec:
name: test
version: 1.1.0
`,
"version must start with v",
nil,
},
{
"success",
`kind: destination
spec:
name: test
version: v1.1.0
`,
"",
&Destination{
Name: "test",
Registry: RegistryGithub,
Path: "cloudquery/test",
Version: "v1.1.0",
},
},
}

func TestDestinationUnmarshalSpecValidate(t *testing.T) {
for _, tc := range destinationUnmarshalSpecValidateTestCases {
t.Run(tc.name, func(t *testing.T) {
var err error
var spec Spec
err = SpecUnmarshalYamlStrict([]byte(tc.spec), &spec)
if err != nil {
t.Fatal(err)
}
destination := spec.Spec.(*Destination)
destination.SetDefaults()
err = destination.Validate()
if err != nil {
if err.Error() != tc.err {
t.Fatalf("expected:%s got:%s", tc.err, err.Error())
}
return
}

if cmp.Diff(destination, tc.destination) != "" {
t.Fatalf("expected:%v got:%v", tc.destination, destination)
}
})
}
}
30 changes: 21 additions & 9 deletions specs/source.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package specs

import (
"bytes"
"encoding/json"
"fmt"
"strings"

"github.com/xeipuuv/gojsonschema"
)

// Source is the spec for a source plugin
Expand Down Expand Up @@ -40,12 +40,12 @@ func (s *Source) SetDefaults() {
if s.Path == "" {
s.Path = s.Name
}
if s.Version == "" {
s.Version = "latest"
}
if s.Registry == RegistryGithub && !strings.Contains(s.Path, "/") {
s.Path = "cloudquery/" + s.Path
}
if s.Tables == nil {
s.Tables = []string{"*"}
}
}

// UnmarshalSpec unmarshals the internal spec into the given interface
Expand All @@ -54,12 +54,24 @@ func (s *Source) UnmarshalSpec(out interface{}) error {
if err != nil {
return err
}
dec := json.NewDecoder(nil)
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
dec.DisallowUnknownFields()
return json.Unmarshal(b, out)
return dec.Decode(out)
}

func (*Source) Validate() (*gojsonschema.Result, error) {
return nil, nil
func (s *Source) Validate() error {
if s.Name == "" {
return fmt.Errorf("name is required")
}
if s.Version == "" {
return fmt.Errorf("version is required")
}
if !strings.HasPrefix(s.Version, "v") {
return fmt.Errorf("version must start with v")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not support latest version at all anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope. per cloudquery/cloudquery#2054 . I think it's the right approach such as with any other package managers (go, npm) they are not supporting latest

}
if len(s.Destinations) == 0 {
return fmt.Errorf("at least one destination is required")
}
return nil
}
Loading