diff --git a/_examples/go.mod b/_examples/go.mod index d26124f0ab8..5963ca8d088 100644 --- a/_examples/go.mod +++ b/_examples/go.mod @@ -19,6 +19,7 @@ require ( require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/hashicorp/golang-lru/v2 v2.0.3 // indirect github.com/logrusorgru/aurora/v3 v3.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/_examples/go.sum b/_examples/go.sum index 8e76230a202..35bba87cb2f 100644 --- a/_examples/go.sum +++ b/_examples/go.sum @@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/docs/content/config.md b/docs/content/config.md index 1d167e2ef0e..c2b836258ae 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -113,6 +113,9 @@ models: - github.com/99designs/gqlgen/graphql.Int - github.com/99designs/gqlgen/graphql.Int64 - github.com/99designs/gqlgen/graphql.Int32 + UUID: + model: + - github.com/99designs/gqlgen/graphql.UUID ``` Everything has defaults, so add things as you need. diff --git a/docs/content/reference/scalars.md b/docs/content/reference/scalars.md index 8241090b6a0..a6f9321a80d 100644 --- a/docs/content/reference/scalars.md +++ b/docs/content/reference/scalars.md @@ -7,7 +7,8 @@ menu: { main: { parent: "reference", weight: 10 } } ## Built-in helpers -gqlgen ships with some built-in helpers for common custom scalar use-cases, `Time`, `Any`, `Upload` and `Map`. Adding any of these to a schema will automatically add the marshalling behaviour to Go types. +gqlgen ships with some built-in helpers for common custom scalar use-cases, `Time`, `Any`, `Upload` and `Map`. +Adding any of these to a schema will automatically add the marshalling behaviour to Go types. ### Time @@ -15,7 +16,16 @@ gqlgen ships with some built-in helpers for common custom scalar use-cases, `Tim scalar Time ``` -Maps a `Time` GraphQL scalar to a Go `time.Time` struct. This scalar adheres to the [time.RFC3339Nano](https://pkg.go.dev/time#pkg-constants) format. +Maps a `Time` GraphQL scalar to a Go `time.Time` struct. +This scalar adheres to the [time.RFC3339Nano](https://pkg.go.dev/time#pkg-constants) format. + +### Universally Unique Identifier (UUID) + +```graphql +scalar UUID +``` + +Maps a `UUID` scalar value to a `uuid.UUID` type. ### Map @@ -131,7 +141,7 @@ func ParseLength(string) (Length, error) func (l Length) FormatContext(ctx context.Context) (string, error) ``` -and then wire up the type in .gqlgen.yml or via directives like normal: +and then wire up the type in `.gqlgen.yml` or via directives like normal: ```yaml models: @@ -141,8 +151,8 @@ models: ## Custom scalars with third party types -Sometimes you are unable to add add methods to a type - perhaps you don't own the type, or it is part of the standard -library (eg string or time.Time). To support this we can build an external marshaler: +Sometimes you are unable to add add methods to a type — perhaps you don't own the type, or it is part of the standard library (eg `string` or `time.Time`). +To support this we can build an external marshaler: ```go package mypkg @@ -180,7 +190,7 @@ func UnmarshalMyCustomBooleanScalar(v interface{}) (bool, error) { } ``` -Then in .gqlgen.yml point to the name without the Marshal|Unmarshal in front: +Then in `.gqlgen.yml` point to the name without the Marshal|Unmarshal in front: ```yaml models: @@ -188,18 +198,16 @@ models: model: github.com/me/mypkg.MyCustomBooleanScalar ``` -**Note:** you also can un/marshal to pointer types via this approach, simply accept a pointer in your -`Marshal...` func and return one in your `Unmarshal...` func. +**Note:** You also can (un)marshal to pointer types via this approach, simply accept a pointer in your `Marshal...` func and return one in your `Unmarshal...` func. -**Note:** you can also un/marshal with a context by having your custom marshal function return a -`graphql.ContextMarshaler` _and_ your unmarshal function take a `context.Context` as the first argument. +**Note:** You can also un/marshal with a context by having your custom marshal function return a `graphql.ContextMarshaler` _and_ your unmarshal function take a `context.Context` as the first argument. See the [_examples/scalars](https://github.com/99designs/gqlgen/tree/master/_examples/scalars) package for more examples. ## Marshaling/Unmarshaling Errors The errors that occur as part of custom scalar marshaling/unmarshaling will return a full path to the field. -For example, given the following schema ... +For example, given the following schema: ```graphql extend type Mutation{ @@ -213,6 +221,7 @@ input UserInput { } scalar Email + input ContactDetailsInput { email: Email! } @@ -221,7 +230,6 @@ input ContactDetailsInput { ... and the following variables: ```json - { "userInput": { "name": "George", @@ -235,7 +243,9 @@ input ContactDetailsInput { } ``` -... and an unmarshal function that returns an error if the email is invalid. The mutation will return an error containing the full path: +... and an unmarshal function that returns an error if the email is invalid. +The mutation will return an error containing the full path: + ```json { "message": "email invalid", diff --git a/go.mod b/go.mod index 141c03e3a37..df8e48b4209 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/99designs/gqlgen go 1.18 require ( + github.com/gofrs/uuid v4.4.0+incompatible github.com/gorilla/websocket v1.5.0 github.com/hashicorp/golang-lru/v2 v2.0.3 github.com/kevinmbeaulieu/eq-go v1.0.0 diff --git a/go.sum b/go.sum index 32203df77f1..dd74de62f49 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/graphql/uuid.go b/graphql/uuid.go new file mode 100644 index 00000000000..bbf1093d36b --- /dev/null +++ b/graphql/uuid.go @@ -0,0 +1,24 @@ +package graphql + +import ( + "errors" + "io" + + "github.com/gofrs/uuid" +) + +func MarshalUUID(t uuid.UUID) Marshaler { + if t.IsNil() { + return Null + } + return WriterFunc(func(w io.Writer) { + _, _ = io.WriteString(w, t.String()) + }) +} + +func UnmarshalUUID(v interface{}) (uuid.UUID, error) { + if str, ok := v.(string); ok { + return uuid.FromString(str) + } + return uuid.Nil, errors.New("input must be an RFC-4122 formatted string") +} diff --git a/graphql/uuid_test.go b/graphql/uuid_test.go new file mode 100644 index 00000000000..305d1848e01 --- /dev/null +++ b/graphql/uuid_test.go @@ -0,0 +1,96 @@ +package graphql + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" +) + +func TestMarshalUUID(t *testing.T) { + t.Run("Null Values", func(t *testing.T) { + var input = []uuid.UUID{uuid.Nil, uuid.FromStringOrNil("00000000-0000-0000-0000-000000000000")} + for _, v := range input { + assert.Equal(t, Null, MarshalUUID(v)) + } + }) + + t.Run("Valid Values", func(t *testing.T) { + var generator = uuid.NewGen() + var v1, _ = generator.NewV1() + var v3 = generator.NewV3(uuid.FromStringOrNil("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "gqlgen.com") + var v4, _ = generator.NewV4() + var v5 = generator.NewV5(uuid.FromStringOrNil("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "gqlgen.com") + var v6, _ = generator.NewV6() + var v7, _ = generator.NewV7() + var values = []struct { + input uuid.UUID + expected string + }{ + {v1, v1.String()}, + {v3, v3.String()}, + {v4, v4.String()}, + {v5, v5.String()}, + {v6, v6.String()}, + {v7, v7.String()}, + } + for _, v := range values { + assert.Equal(t, v.expected, m2s(MarshalUUID(v.input))) + } + }) +} + +func TestUnmarshalUUID(t *testing.T) { + t.Run("Invalid Non-String Values", func(t *testing.T) { + var values = []interface{}{123, 1.2345678901, 1.2e+20, 1.2e-20, true, false} + for _, v := range values { + result, err := UnmarshalUUID(v) + assert.Equal(t, uuid.Nil, result) + assert.ErrorContains(t, err, "input must be an RFC-4122 formatted string") + } + }) + + t.Run("Invalid String Values", func(t *testing.T) { + var values = []struct { + input string + expected string + }{ + {"x50e8400-e29b-41d4-a716-446655440000", "uuid: invalid UUID format"}, + {"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "uuid: invalid UUID format"}, + {"f50e8400-e29b-41d4-a716-44665544000", "uuid: incorrect UUID length 35 in string"}, + {"foo", "uuid: incorrect UUID length 3 in string"}, + {"", "uuid: incorrect UUID length 0 in string"}, + } + for _, v := range values { + result, err := UnmarshalUUID(v.input) + assert.Equal(t, uuid.Nil, result) + assert.ErrorContains(t, err, v.expected) + } + }) + + t.Run("Valid Values", func(t *testing.T) { + var generator = uuid.NewGen() + var v1, _ = generator.NewV1() + var v3 = generator.NewV3(uuid.FromStringOrNil("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "gqlgen.com") + var v4, _ = generator.NewV4() + var v5 = generator.NewV5(uuid.FromStringOrNil("6ba7b810-9dad-11d1-80b4-00c04fd430c8"), "gqlgen.com") + var v6, _ = generator.NewV6() + var v7, _ = generator.NewV7() + var values = []struct { + input string + expected uuid.UUID + }{ + {v1.String(), v1}, + {v3.String(), v3}, + {v4.String(), v4}, + {v5.String(), v5}, + {v6.String(), v6}, + {v7.String(), v7}, + } + for _, v := range values { + result, err := UnmarshalUUID(v.input) + assert.Equal(t, v.expected, result) + assert.Nil(t, err) + } + }) +}