Skip to content

Commit

Permalink
adding docs and more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dcarbone committed Aug 12, 2022
1 parent 51137b8 commit eb668fa
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 18 deletions.
24 changes: 20 additions & 4 deletions codegen/templates/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package templates

import (
"embed"
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -101,6 +102,10 @@ func TestToGoModelName(t *testing.T) {
input: [][]string{{"MyEnumName", "Value"}, {"MyEnumName", "value"}},
expected: []string{"MyEnumNameValue", "MyEnumNamevalue"},
},
{
input: [][]string{{"MyEnumName", "value"}, {"MyEnumName", "Value"}},
expected: []string{"MyEnumNameValue", "MyEnumNameValue0"},
},
{
input: [][]string{{"MyEnumName", "Value"}, {"MyEnumName", "value"}, {"MyEnumName", "vALue"}, {"MyEnumName", "VALue"}},
expected: []string{"MyEnumNameValue", "MyEnumNamevalue", "MyEnumNameVALue", "MyEnumNameVALue0"},
Expand All @@ -109,13 +114,24 @@ func TestToGoModelName(t *testing.T) {
input: [][]string{{"MyEnumName", "TitleValue"}, {"MyEnumName", "title_value"}, {"MyEnumName", "title_Value"}, {"MyEnumName", "Title_Value"}},
expected: []string{"MyEnumNameTitleValue", "MyEnumNametitle_value", "MyEnumNametitle_Value", "MyEnumNameTitle_Value"},
},
{
input: [][]string{{"MyEnumName", "TitleValue", "OtherValue"}},
expected: []string{"MyEnumNameTitleValueOtherValue"},
},
{
input: [][]string{{"MyEnumName", "TitleValue", "OtherValue"}, {"MyEnumName", "title_value", "OtherValue"}},
expected: []string{"MyEnumNameTitleValueOtherValue", "MyEnumNametitle_valueOtherValue"},
},
}

for _, at := range theTests {
for ti, at := range theTests {
resetModelNames()
for i, n := range at.input {
require.Equal(t, at.expected[i], ToGoModelName(n...))
}
t.Run(fmt.Sprintf("modelname-%d", ti), func(t *testing.T) {
at := at
for i, n := range at.input {
require.Equal(t, at.expected[i], ToGoModelName(n...))
}
})
}
}

Expand Down
188 changes: 188 additions & 0 deletions docs/content/reference/name-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: Handling type naming collisions
description: Examples of logic used to avoid type name collision
linkTitle: Name Collision
menu: { main: { parent: 'reference', weight: 10 }}
---

While most generated Golang types must have unique names by virtue of being based on their GraphQL `type` counterpart,
which themselves must be unique, there are a few edge scenarios where conflicts can occur. This document describes
how those collisions are handled.

## Enum Constants

Enum type generation is a prime example of where naming collisions can occur, as we build the const names per value
as a composite of the Enum name and each individual value.

### Example Problem

Currently, enum types are transposed as such:

```graphql
enum MyEnum {
value1
value2
value3
value4
}
```

Which will result in the following Golang:

```go
type MyEnum string

const (
MyEnumValue1 MyEnum = "value1"
MyEnumValue2 MyEnum = "value2"
MyEnumValue3 MyEnum = "value3"
MyEnumValue4 MyEnum = "value4"
)
```

However, those above enum values are just strings. What if you encounter a scenario where the following is
necessary:

```graphql
enum MyEnum {
value1
value2
value3
value4
Value4
Value_4
}
```

The `Value4` and `Value_4` enum values cannot be directly transposed into the same "pretty" naming convention as their
resulting constant names would conflict with the name for `value4`, as so:

```go
type MyEnum string

const (
MyEnumValue1 MyEnum = "value1"
MyEnumValue2 MyEnum = "value2"
MyEnumValue3 MyEnum = "value3"
MyEnumValue4 MyEnum = "value4"
MyEnumValue4 MyEnum = "Value4"
MyEnumValue4 MyEnum = "Value_4"
)
```

Which immediately leads to compilation errors as we now have three constants with the same name, but different values.

### Resolution

1. Store each name generated as part of a run for later comparison
2. Try pretty approach first. Use if no conflicts
3. If non-composite name, append integer to end of name, starting at 0 and going to `math.MaxInt`
4. If composite name, in reverse order, the pieces of the name have a less opinionated converter applied
5. If all else fails, append integer to end of name, starting at 0 and going to `math.MaxInt`

The first step to produce a name that does not conflict with an existing name succeeds.

## Examples

### Example A
GraphQL:
```graphql
enum MyEnum {
Value
value
TitleValue
title_value
}
```
Go:
```go
type MyEnum string

const (
MyEnumValue MyEnum = "Value"
MyEnumvalue MyEnum = "value"
MyEnumTitleValue MyEnum = "TitleValue"
MyEnumtitle_value MyEnum = "title_value"
)
```

### Example B
GraphQL:
```graphql
enum MyEnum {
TitleValue
title_value
title_Value
Title_Value
}
```
Go:
```go
type MyEnum string
const (
MyEnumTitleValue MyEnum = "TitleValue"
MyEnumtitle_value MyEnum = "title_value"
MyEnumtitle_Value MyEnum = "title_Value"
MyEnumTitle_Value MyEnum = "Title_Value"
)
```

### Example C
GraphQL:
```graphql
enum MyEnum {
value
Value
}
```
Go:
```go
type MyEnum string
const (
MyEnumValue = "value"
MyEnumValue0 = "Value"
)
```

## Warning

Name collision resolution is handled per-name, as they come in. If you change the order of an enum, you could very
well end up with the same constant resolving to a different value.

Lets mutate [Example C](#example-c):

### Example C - State A
GraphQL:
```graphql
enum MyEnum {
value
Value
}
```
Go:
```go
type MyEnum string
const (
MyEnumValue = "value"
MyEnumValue0 = "Value"
)
```

### Example C - State B
GraphQL:
```graphql
enum MyEnum {
Value
value
}
```
Go:
```go
type MyEnum string
const (
MyEnumValue = "Value"
MyEnumValue0 = "value"
)
```

Notice how the constant names are the same, but the value that each applies to has changed.
28 changes: 14 additions & 14 deletions plugin/modelgen/models.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@

{{- range $model := .Interfaces }}
{{ with .Description }} {{.|prefixLines "// "}} {{ end }}
type {{.Name|go }} interface {
type {{ goModelName .Name }} interface {
{{- range $impl := .Implements }}
Is{{ $impl|go }}()
Is{{ goModelName $impl }}()
{{- end }}
Is{{.Name|go }}()
Is{{ goModelName .Name }}()
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
Expand All @@ -30,7 +30,7 @@

{{ range $model := .Models }}
{{with .Description }} {{.|prefixLines "// "}} {{end}}
type {{ .Name|go }} struct {
type {{ goModelName .Name }} struct {
{{- range $field := .Fields }}
{{- with .Description }}
{{.|prefixLines "// "}}
Expand All @@ -40,7 +40,7 @@
}

{{ range .Implements }}
func ({{ $model.Name|go }}) Is{{ .|go }}() {}
func ({{ goModelName $model.Name }}) Is{{ goModelName . }}() {}
{{- with getInterfaceByName . }}
{{- range .Fields }}
{{- with .Description }}
Expand All @@ -54,48 +54,48 @@

{{ range $enum := .Enums }}
{{ with .Description }} {{.|prefixLines "// "}} {{end}}
type {{.Name|goModelName }} string
type {{ goModelName .Name }} string
const (
{{- range $value := .Values}}
{{- with .Description}}
{{.|prefixLines "// "}}
{{- end}}
{{ goModelName $enum.Name .Name }} {{ $enum.Name|goModelName }} = {{ .Name|quote }}
{{ goModelName $enum.Name .Name }} {{ goModelName $enum.Name }} = {{ .Name|quote }}
{{- end }}
)

var All{{ .Name|goModelName }} = []{{ .Name|goModelName }}{
var All{{ goModelName .Name }} = []{{ goModelName .Name }}{
{{- range $value := .Values}}
{{ goModelName $enum.Name .Name }},
{{- end }}
}

func (e {{.Name|go }}) IsValid() bool {
func (e {{ goModelName .Name }}) IsValid() bool {
switch e {
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.Name|go }}{{ $element.Name|go }}{{end}}:
case {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ goModelName $enum.Name $element.Name }}{{end}}:
return true
}
return false
}

func (e {{.Name|go }}) String() string {
func (e {{ goModelName .Name }}) String() string {
return string(e)
}

func (e *{{.Name|go }}) UnmarshalGQL(v interface{}) error {
func (e *{{ goModelName .Name }}) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}

*e = {{ .Name|go }}(str)
*e = {{ goModelName .Name }}(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid {{ .Name }}", str)
}
return nil
}

func (e {{.Name|go }}) MarshalGQL(w io.Writer) {
func (e {{ goModelName .Name }}) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

Expand Down

0 comments on commit eb668fa

Please sign in to comment.