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

Introduce support for Universally Unique Identifiers #2749

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions _examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions _examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions docs/content/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 23 additions & 13 deletions docs/content/reference/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ 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

```graphql
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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -180,26 +190,24 @@ 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:
MyCustomBooleanScalar:
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{
Expand All @@ -213,6 +221,7 @@ input UserInput {
}

scalar Email

input ContactDetailsInput {
email: Email!
}
Expand All @@ -221,7 +230,6 @@ input ContactDetailsInput {
... and the following variables:

```json

{
"userInput": {
"name": "George",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
24 changes: 24 additions & 0 deletions graphql/uuid.go
Original file line number Diff line number Diff line change
@@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

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

writeQuotedString(w, t.String()) ?
uuid type marsher is "00000000-0000-0000-0000-00 0000000000"

must Quoted

Copy link
Author

Choose a reason for hiding this comment

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

I don't fully understand why is that needed here. I think that might be the case for arbitrary string values that should be double-quoted because they could have escaped/control characters and non-printable ones, but the uuid.UUID will never have those.

})
}

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")
}
96 changes: 96 additions & 0 deletions graphql/uuid_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}