diff --git a/cmd/hasura-ndc-go/schema.go b/cmd/hasura-ndc-go/schema.go index 7bfddf4..7cfa63a 100644 --- a/cmd/hasura-ndc-go/schema.go +++ b/cmd/hasura-ndc-go/schema.go @@ -741,6 +741,8 @@ func (sp *SchemaParser) parseType(rootType *TypeInfo, ty types.Type, fieldPaths scalarSchema.Representation = schema.NewTypeRepresentationBigInteger().Encode() case "Bytes": scalarSchema.Representation = schema.NewTypeRepresentationBytes().Encode() + case "URL": + scalarSchema.Representation = schema.NewTypeRepresentationString().Encode() } } diff --git a/cmd/hasura-ndc-go/testdata/basic/expected/functions.go.tmpl b/cmd/hasura-ndc-go/testdata/basic/expected/functions.go.tmpl index 898c13c..a2457c2 100644 --- a/cmd/hasura-ndc-go/testdata/basic/expected/functions.go.tmpl +++ b/cmd/hasura-ndc-go/testdata/basic/expected/functions.go.tmpl @@ -533,6 +533,10 @@ func (j *GetTypesArguments) FromValue(input map[string]any) error { if err != nil { return err } + err = functions_Decoder.DecodeObjectValue(&j.URL, input, "URL") + if err != nil { + return err + } j.UUID, err = utils.GetUUID(input, "UUID") if err != nil { return err @@ -792,6 +796,7 @@ func (j GetTypesArguments) ToMap() map[string]any { r["TextPtr"] = j.TextPtr r["Time"] = j.Time r["TimePtr"] = j.TimePtr + r["URL"] = j.URL r["UUID"] = j.UUID r["UUIDArray"] = j.UUIDArray r["UUIDPtr"] = j.UUIDPtr diff --git a/cmd/hasura-ndc-go/testdata/basic/expected/schema.json b/cmd/hasura-ndc-go/testdata/basic/expected/schema.json index 41d4a82..1d6f41f 100644 --- a/cmd/hasura-ndc-go/testdata/basic/expected/schema.json +++ b/cmd/hasura-ndc-go/testdata/basic/expected/schema.json @@ -1287,6 +1287,12 @@ } } }, + "URL": { + "type": { + "name": "URL", + "type": "named" + } + }, "UUID": { "type": { "name": "UUID", @@ -2795,6 +2801,12 @@ } } }, + "URL": { + "type": { + "name": "URL", + "type": "named" + } + }, "UUID": { "type": { "name": "UUID", @@ -3144,7 +3156,10 @@ "aggregate_functions": {}, "comparison_operators": {}, "representation": { - "one_of": ["foo", "bar"], + "one_of": [ + "foo", + "bar" + ], "type": "enum" } }, @@ -3162,6 +3177,13 @@ "type": "timestamptz" } }, + "URL": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + }, "UUID": { "aggregate_functions": {}, "comparison_operators": {}, @@ -3170,4 +3192,4 @@ } } } -} +} \ No newline at end of file diff --git a/cmd/hasura-ndc-go/testdata/basic/source/functions/prefix.go b/cmd/hasura-ndc-go/testdata/basic/source/functions/prefix.go index c690284..bfdcc44 100644 --- a/cmd/hasura-ndc-go/testdata/basic/source/functions/prefix.go +++ b/cmd/hasura-ndc-go/testdata/basic/source/functions/prefix.go @@ -103,6 +103,7 @@ type GetTypesArguments struct { CustomScalar CommentText Enum SomeEnum BigInt scalar.BigInt + URL scalar.URL UUIDPtr *uuid.UUID BoolPtr *bool diff --git a/example/codegen/connector_test.go b/example/codegen/connector_test.go index 8f5cf9f..1483241 100644 --- a/example/codegen/connector_test.go +++ b/example/codegen/connector_test.go @@ -138,6 +138,10 @@ func TestQueryGetTypes(t *testing.T) { "type": "literal", "value": "2024-04-02" }, + "URL": { + "type": "literal", + "value": "https://example.com" + }, "UUIDPtr": { "type": "literal", "value": "b085b0b9-007c-440e-9661-0d8f2de98a5b" @@ -774,6 +778,10 @@ func TestQueryGetTypes(t *testing.T) { "column": "DatePtr", "type": "column" }, + "URL": { + "column": "URL", + "type": "column" + }, "NamedArray": { "column": "NamedArray", "fields": { @@ -1301,27 +1309,32 @@ func TestQueryGetTypes(t *testing.T) { "collection_relationships": {} }`, response: arguments.GetTypesArguments{ - UUID: uuid.MustParse("b085b0b9-007c-440e-9661-0d8f2de98a5a"), - Bool: true, - String: "hello", - Int: 1, - Int8: 2, - Int16: 3, - Int32: 4, - Int64: 5, - Uint: 6, - Uint8: 7, - Uint16: 8, - Uint32: 9, - Uint64: 10, - Float32: 1.1, - Float64: 2.2, - Time: time.Date(2024, 3, 5, 7, 0, 56, 0, time.UTC), - Text: "text", - CustomScalar: commentText, - Enum: types.SomeEnumFoo, - BigInt: 10000, - Date: *scalar.NewDate(2024, 04, 02), + UUID: uuid.MustParse("b085b0b9-007c-440e-9661-0d8f2de98a5a"), + Bool: true, + String: "hello", + Int: 1, + Int8: 2, + Int16: 3, + Int32: 4, + Int64: 5, + Uint: 6, + Uint8: 7, + Uint16: 8, + Uint32: 9, + Uint64: 10, + Float32: 1.1, + Float64: 2.2, + Time: time.Date(2024, 3, 5, 7, 0, 56, 0, time.UTC), + Text: "text", + CustomScalar: commentText, + Enum: types.SomeEnumFoo, + BigInt: 10000, + Date: *scalar.NewDate(2024, 04, 02), + URL: func() scalar.URL { + r, _ := scalar.NewURL("https://example.com") + return *r + }(), + UUIDPtr: utils.ToPtr(uuid.MustParse("b085b0b9-007c-440e-9661-0d8f2de98a5b")), BoolPtr: utils.ToPtr(true), StringPtr: utils.ToPtr("world"), diff --git a/example/codegen/schema.generated.json b/example/codegen/schema.generated.json index 5c20fa1..c888231 100644 --- a/example/codegen/schema.generated.json +++ b/example/codegen/schema.generated.json @@ -1302,6 +1302,12 @@ } } }, + "URL": { + "type": { + "name": "URL", + "type": "named" + } + }, "UUID": { "type": { "name": "UUID", @@ -2834,6 +2840,12 @@ } } }, + "URL": { + "type": { + "name": "URL", + "type": "named" + } + }, "UUID": { "type": { "name": "UUID", @@ -3202,6 +3214,13 @@ "type": "timestamptz" } }, + "URL": { + "aggregate_functions": {}, + "comparison_operators": {}, + "representation": { + "type": "string" + } + }, "UUID": { "aggregate_functions": {}, "comparison_operators": {}, diff --git a/example/codegen/types/arguments/arguments.go b/example/codegen/types/arguments/arguments.go index 903ab92..91449e9 100644 --- a/example/codegen/types/arguments/arguments.go +++ b/example/codegen/types/arguments/arguments.go @@ -31,6 +31,7 @@ type GetTypesArguments struct { Enum types.SomeEnum BigInt scalar.BigInt Date scalar.Date + URL scalar.URL UUIDPtr *uuid.UUID BoolPtr *bool diff --git a/example/codegen/types/arguments/types.generated.go b/example/codegen/types/arguments/types.generated.go index 3e4d93a..22a8938 100644 --- a/example/codegen/types/arguments/types.generated.go +++ b/example/codegen/types/arguments/types.generated.go @@ -548,6 +548,10 @@ func (j *GetTypesArguments) FromValue(input map[string]any) error { if err != nil { return err } + err = arguments_Decoder.DecodeObjectValue(&j.URL, input, "URL") + if err != nil { + return err + } j.UUID, err = utils.GetUUID(input, "UUID") if err != nil { return err @@ -760,6 +764,7 @@ func (j GetTypesArguments) ToMap() map[string]any { r["TextPtr"] = j.TextPtr r["Time"] = j.Time r["TimePtr"] = j.TimePtr + r["URL"] = j.URL r["UUID"] = j.UUID r["UUIDPtr"] = j.UUIDPtr r["Uint"] = j.Uint diff --git a/scalar/bigint.go b/scalar/bigint.go index f7d53c7..dd095bb 100644 --- a/scalar/bigint.go +++ b/scalar/bigint.go @@ -18,6 +18,11 @@ func NewBigInt(value int64) BigInt { return BigInt(value) } +// ScalarName get the schema name of the scalar +func (bi BigInt) ScalarName() string { + return "BigInt" +} + // Stringer implements fmt.Stringer interface. func (bi BigInt) String() string { return strconv.FormatInt(int64(bi), 10) diff --git a/scalar/bigint_test.go b/scalar/bigint_test.go index aca5a8c..b66464a 100644 --- a/scalar/bigint_test.go +++ b/scalar/bigint_test.go @@ -9,8 +9,10 @@ import ( func TestBigInt(t *testing.T) { expected := "9999" value := NewBigInt(0) + scalarName := value.ScalarName() + if err := json.Unmarshal([]byte(expected), &value); err != nil { - t.Errorf("failed to parse bigint: %s", err) + t.Errorf("failed to parse %s: %s", scalarName, err) t.FailNow() } if expected != value.String() { @@ -19,7 +21,7 @@ func TestBigInt(t *testing.T) { } rawValue, err := json.Marshal(value) if err != nil { - t.Errorf("failed to encode bigint: %s", err) + t.Errorf("failed to encode %s: %s", scalarName, err) t.FailNow() } @@ -30,7 +32,7 @@ func TestBigInt(t *testing.T) { var value1 BigInt if err := value1.FromValue(expected); err != nil { - t.Errorf("failed to decode bigint: %s", err) + t.Errorf("failed to decode %s: %s", scalarName, err) t.FailNow() } diff --git a/scalar/bytes.go b/scalar/bytes.go index 7abc5fc..4bde91f 100644 --- a/scalar/bytes.go +++ b/scalar/bytes.go @@ -29,6 +29,11 @@ func ParseBytes(value string) (*Bytes, error) { return &Bytes{data: bs}, nil } +// ScalarName get the schema name of the scalar +func (bs Bytes) ScalarName() string { + return "Bytes" +} + // Bytes get the inner bytes value. func (bs Bytes) Bytes() []byte { return bs.data diff --git a/scalar/bytes_test.go b/scalar/bytes_test.go index e9d79ef..47c0de1 100644 --- a/scalar/bytes_test.go +++ b/scalar/bytes_test.go @@ -15,8 +15,9 @@ func TestBytes(t *testing.T) { rawJSON := fmt.Sprintf(`"%s"`, expectedB64) var value Bytes + scalarName := value.ScalarName() if err := json.Unmarshal([]byte(rawJSON), &value); err != nil { - t.Errorf("failed to parse Bytes: %s", err) + t.Errorf("failed to parse %s: %s", scalarName, err) t.FailNow() } if plain != value.String() { @@ -25,7 +26,7 @@ func TestBytes(t *testing.T) { } rawValue, err := json.Marshal(value) if err != nil { - t.Errorf("failed to encode Bytes: %s", err) + t.Errorf("failed to encode %s: %s", scalarName, err) t.FailNow() } diff --git a/scalar/date.go b/scalar/date.go index 4a7eb98..b8dff87 100644 --- a/scalar/date.go +++ b/scalar/date.go @@ -34,6 +34,11 @@ func ParseDate(value string) (*Date, error) { return &Date{Time: t}, nil } +// ScalarName get the schema name of the scalar +func (d Date) ScalarName() string { + return "Date" +} + // Stringer implements fmt.Stringer interface. func (d Date) String() string { return d.Format(dateFormat) diff --git a/scalar/date_test.go b/scalar/date_test.go index 83ff2b0..1588e9b 100644 --- a/scalar/date_test.go +++ b/scalar/date_test.go @@ -11,8 +11,10 @@ func TestDate(t *testing.T) { expected := NewDate(2014, 04, 1).String() rawJSON := fmt.Sprintf(`"%s"`, expected) var value Date + scalarName := value.ScalarName() + if err := json.Unmarshal([]byte(rawJSON), &value); err != nil { - t.Errorf("failed to parse Date: %s", err) + t.Errorf("failed to parse %s: %s", scalarName, err) t.FailNow() } if expected != value.String() { @@ -21,7 +23,7 @@ func TestDate(t *testing.T) { } rawValue, err := json.Marshal(value) if err != nil { - t.Errorf("failed to encode Date: %s", err) + t.Errorf("failed to encode %s: %s", scalarName, err) t.FailNow() } @@ -32,7 +34,7 @@ func TestDate(t *testing.T) { var value1 Date if err := value1.FromValue(expected); err != nil { - t.Errorf("failed to decode Date: %s", err) + t.Errorf("failed to decode %s: %s", scalarName, err) t.FailNow() } diff --git a/scalar/url.go b/scalar/url.go new file mode 100644 index 0000000..904a1cd --- /dev/null +++ b/scalar/url.go @@ -0,0 +1,96 @@ +package scalar + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + + "github.com/hasura/ndc-sdk-go/utils" +) + +// URL represents a URL string +// +// @scalar URL string +type URL struct { + *url.URL +} + +// NewURL creates a URL instance +func NewURL(rawURL string) (*URL, error) { + u, err := parseURL(rawURL) + if err != nil { + return nil, err + } + return &URL{u}, nil +} + +// ScalarName get the schema name of the scalar +func (j URL) ScalarName() string { + return "URL" +} + +// Stringer implements fmt.Stringer interface. +func (u URL) String() string { + if u.URL == nil { + return "" + } + return u.URL.String() +} + +// MarshalJSON implements json.Marshaler. +func (u URL) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (u *URL) UnmarshalJSON(b []byte) error { + + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + + value, err := parseURL(str) + if err != nil { + return err + } + u.URL = value + + return nil +} + +// FromValue decode any value to URL. +func (u *URL) FromValue(value any) error { + if value == nil { + return nil + } + switch v := value.(type) { + case URL: + *u = v + case *URL: + if v != nil { + *u = *v + } + case url.URL: + u.URL = &v + case *url.URL: + u.URL = v + default: + str, err := utils.DecodeNullableString(value) + if err == nil && str != nil { + u.URL, err = parseURL(*str) + } + if err != nil { + return fmt.Errorf("invalid url: %v", v) + } + } + return nil +} + +func parseURL(rawURL string) (*url.URL, error) { + if rawURL == "" { + return nil, errors.New("invalid URL") + } + return url.Parse(rawURL) +} diff --git a/scalar/url_test.go b/scalar/url_test.go new file mode 100644 index 0000000..b35a536 --- /dev/null +++ b/scalar/url_test.go @@ -0,0 +1,112 @@ +package scalar + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + "testing" +) + +func TestURL(t *testing.T) { + expectedStr := "http://localhost:8080/foo?bar=baz" + expected, err := NewURL(expectedStr) + if err != nil { + t.Fatalf("failed to parsed expected URL") + } + + if _, err := NewURL(""); err == nil { + t.Fatal("expected error, got nil") + } + rawJSON := fmt.Sprintf(`"%s"`, expected) + var value URL + scalarName := value.ScalarName() + if value.String() != "" { + t.Fatalf("expected empty string, got %s", value.String()) + } + + if err := json.Unmarshal([]byte(`""`), &value); err == nil { + t.Fatal("expected error, got nil") + } + + if err := json.Unmarshal([]byte(rawJSON), &value); err != nil { + t.Fatalf("failed to parse %s: %s", scalarName, err) + } + if expected.String() != value.String() { + t.Fatalf("expected: %s, got: %s", expected, value) + } + rawValue, err := json.Marshal(value) + if err != nil { + t.Fatalf("failed to encode %s: %s", scalarName, err) + } + + if expectedStr != strings.Trim(string(rawValue), "\"") { + t.Fatalf("expected: %s, got: %s", expectedStr, string(rawValue)) + } + + var value1 URL + if err := value1.FromValue(expectedStr); err != nil { + t.Fatalf("failed to decode %s: %s", scalarName, err) + } + + if value1.String() != expectedStr { + t.Fatalf("expected: %s, got: %s", expected, value1) + } + + if err := value1.FromValue(""); err == nil { + t.Error("expected error, got nil") + t.FailNow() + } + + if err := value1.FromValue(0); err == nil { + t.Error("expected error, got nil") + t.FailNow() + } + + if err := value1.FromValue(nil); err != nil { + t.Errorf("expected no error, got %v", err) + t.FailNow() + } + + if err := json.Unmarshal([]byte("0"), &value1); err == nil { + t.Error("expected error, got nil") + t.FailNow() + } + + var anyStr any = "example.com" + if err := value.FromValue(any(&anyStr)); err != nil { + t.Fatalf("expected nil error, got: %s", err) + } + if value.String() != fmt.Sprint(anyStr) { + t.Fatalf("expected %s, got: %s", anyStr, value.String()) + } + + if err := value1.FromValue(value); err != nil { + t.Fatalf("expected nil error, got: %s", err) + } + if value1.String() != fmt.Sprint(anyStr) { + t.Fatalf("expected %s, got: %s", anyStr, value1.String()) + } + + var value2 URL + if err := value2.FromValue(&value); err != nil { + t.Fatalf("expected nil error, got: %s", err) + } + if value2.String() != fmt.Sprint(anyStr) { + t.Fatalf("expected %s, got: %s", anyStr, value2.String()) + } + + rawURL, _ := url.Parse("https://google.com") + if err := value1.FromValue(rawURL); err != nil { + t.Fatalf("expected nil error, got: %s", err) + } + if value1.String() != rawURL.String() { + t.Fatalf("expected %s, got: %s", rawURL.String(), value1.String()) + } + if err := value2.FromValue(*rawURL); err != nil { + t.Fatalf("expected nil error, got: %s", err) + } + if value2.String() != rawURL.String() { + t.Fatalf("expected %s, got: %s", rawURL.String(), value2.String()) + } +} diff --git a/utils/decode.go b/utils/decode.go index f5d066c..96dd628 100644 --- a/utils/decode.go +++ b/utils/decode.go @@ -462,78 +462,10 @@ func DecodeUint[T uint | uint8 | uint16 | uint32 | uint64](value any) (T, error) } func decodeNullableInt[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value any, convertFn convertFunc[T]) (*T, error) { - if IsNil(value) { + if value == nil { return nil, nil } - var result T - switch v := value.(type) { - case int: - result = T(v) - case int8: - result = T(v) - case int16: - result = T(v) - case int32: - result = T(v) - case int64: - result = T(v) - case uint: - result = T(v) - case uint8: - result = T(v) - case uint16: - result = T(v) - case uint32: - result = T(v) - case uint64: - result = T(v) - case float32: - result = T(v) - case float64: - result = T(v) - case string: - newVal, err := convertFn(v) - if err != nil { - return nil, fmt.Errorf("failed to convert integer, got: %s", v) - } - return newVal, err - case *int: - result = T(*v) - case *int8: - result = T(*v) - case *int16: - result = T(*v) - case *int32: - result = T(*v) - case *int64: - result = T(*v) - case *uint: - result = T(*v) - case *uint8: - result = T(*v) - case *uint16: - result = T(*v) - case *uint32: - result = T(*v) - case *uint64: - result = T(*v) - case *float32: - result = T(*v) - case *float64: - result = T(*v) - case *string: - newVal, err := convertFn(*v) - if err != nil { - return nil, fmt.Errorf("failed to convert integer, got: %s", *v) - } - return newVal, err - case bool, complex64, complex128, time.Time, time.Duration, time.Ticker, *bool, *complex64, *complex128, *time.Time, *time.Duration, *time.Ticker, []bool, []string, []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []float32, []float64, []complex64, []complex128, []time.Time, []time.Duration, []time.Ticker: - return nil, fmt.Errorf("failed to convert integer, got: %+v", value) - default: - return decodeNullableIntRefection(convertFn)(reflect.ValueOf(value)) - } - - return &result, nil + return decodeNullableIntRefection(convertFn)(reflect.ValueOf(value)) } func decodeNullableIntRefection[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](convertFn convertFunc[T]) convertFuncReflection[T] { @@ -584,20 +516,10 @@ func convertNullableUint[T uint | uint8 | uint16 | uint32 | uint64](v any) (*T, // DecodeNullableString tries to convert an unknown value to a string pointer func DecodeNullableString(value any) (*string, error) { - if IsNil(value) { + if value == nil { return nil, nil } - var result string - switch v := value.(type) { - case string: - result = v - case *string: - result = *v - default: - return decodeNullableStringReflection(reflect.ValueOf(value)) - } - - return &result, nil + return decodeNullableStringReflection(reflect.ValueOf(value)) } func decodeNullableStringReflection(value reflect.Value) (*string, error) { @@ -632,66 +554,10 @@ func DecodeString(value any) (string, error) { // DecodeNullableFloat tries to convert an unknown value to a float pointer func DecodeNullableFloat[T float32 | float64](value any) (*T, error) { - if IsNil(value) { + if value == nil { return nil, nil } - var result T - switch v := value.(type) { - case int: - result = T(v) - case int8: - result = T(v) - case int16: - result = T(v) - case int32: - result = T(v) - case int64: - result = T(v) - case uint: - result = T(v) - case uint8: - result = T(v) - case uint16: - result = T(v) - case uint32: - result = T(v) - case uint64: - result = T(v) - case *int: - result = T(*v) - case *int8: - result = T(*v) - case *int16: - result = T(*v) - case *int32: - result = T(*v) - case *int64: - result = T(*v) - case *uint: - result = T(*v) - case *uint8: - result = T(*v) - case *uint16: - result = T(*v) - case *uint32: - result = T(*v) - case *uint64: - result = T(*v) - case float32: - result = T(v) - case float64: - result = T(v) - case *float32: - result = T(*v) - case *float64: - result = T(*v) - case bool, string, complex64, complex128, time.Time, time.Duration, time.Ticker, *bool, *string, *complex64, *complex128, *time.Time, *time.Duration, *time.Ticker, []bool, []string, []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []float32, []float64, []complex64, []complex128, []time.Time, []time.Duration, []time.Ticker: - return nil, fmt.Errorf("failed to convert Float, got: %+v", value) - default: - return decodeNullableFloatReflection[T](reflect.ValueOf(value)) - } - - return &result, nil + return decodeNullableFloatReflection[T](reflect.ValueOf(value)) } func decodeNullableFloatReflection[T float32 | float64](value reflect.Value) (*T, error) {