Repository: Khan/genqlient (go)

Issue:
Customize field names without alias
**Is your feature request related to a problem? Please describe.**
By default ugly names will be generated if the field name contains an underscore. This can be worked around using aliases as [documented](https://github.com/Khan/genqlient/blob/main/docs/operations.md#field-names), but this has limitations. If the server operator has configured [max aliases](https://escape.tech/graphql-armor/docs/plugins/max-aliases/), queries will fail when they exceed this.

**Describe the solution you'd like**
The ability to customize the field names using a `@genqlient` directive as an alternative to aliases.



Pull request description:
Add field aliasing
<!--
Thanks for your contribution! Check out the
[contributing docs](https://github.com/Khan/genqlient/blob/main/docs/CONTRIBUTING.md)
for more on contributing to genqlient.
-->

  - Added @genqlient(alias) directive to customize field names without GraphQL aliases (fixes #367)

I have:
- [x] Written a clear PR title and description (above)
- [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla)
- [x] Added tests covering my changes, if applicable
- [x] Included a link to the issue fixed, if applicable
- [x] Included documentation, for new features
- [x] Added an entry to the changelog


Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 9c0f836..38bfa85 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -24,6 +24,8 @@ When releasing a new version:
 
 ### New features:
 
+- Added `@genqlient(alias)` directive to customize field names without requiring GraphQL aliases (fixes #367)
+
 ### Bug fixes:
 
 - fixed documentation link in `introduction.md`
diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql
index 2ba0eb6..965567b 100644
--- a/docs/genqlient_directive.graphql
+++ b/docs/genqlient_directive.graphql
@@ -151,6 +151,25 @@ directive genqlient(
   # of MyField; what if we got back the other type?).
   flatten: Boolean
 
+  # If set, this field will use the provided name as the Go field name,
+  # without creating an alias in the GraphQL query.
+  #
+  # For example, given a query like
+  #  query MyQuery {
+  #    # @genqlient(alias: "MyGreatName")
+  #    myField
+  #  }
+  # genqlient will generate:
+  #  type MyQueryResponse struct {
+  #    MyGreatName <type> `json:"myField"`
+  #  }
+  #
+  # This is similar to the GraphQL alias syntax (e.g. `myGreatName: myField`),
+  # but it only affects the Go field name, not the GraphQL query. This is
+  # especially useful when working with GraphQL servers that limit the number
+  # of aliases you can use in a query.
+  alias: String
+
   # If set, this argument or field will use the given Go type instead of a
   # genqlient-generated type.
   #
diff --git a/generate/convert.go b/generate/convert.go
index 1a31fd4..36d4d52 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -918,6 +918,10 @@ func (g *generator) convertField(
 	}
 
 	goName := upperFirst(field.Alias)
+	if fieldOptions.Alias != "" {
+		goName = upperFirst(fieldOptions.Alias)
+	}
+
 	namePrefix = nextPrefix(namePrefix, field)
 
 	fieldGoType, err := g.convertType(
diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go
index f6adfda..3a1515c 100644
--- a/generate/genqlient_directive.go
+++ b/generate/genqlient_directive.go
@@ -18,6 +18,7 @@ type genqlientDirective struct {
 	Flatten   *bool
 	Bind      string
 	TypeName  string
+	Alias     string
 	// FieldDirectives contains the directives to be
 	// applied to specific fields via the "for" option.
 	// Map from type-name -> field-name -> directive.
@@ -52,6 +53,9 @@ func (dir *genqlientDirective) argsString() string {
 	if dir.TypeName != "" {
 		parts = append(parts, fmt.Sprintf("typename: %v", dir.TypeName))
 	}
+	if dir.Alias != "" {
+		parts = append(parts, fmt.Sprintf("alias: %v", dir.Alias))
+	}
 	return strings.Join(parts, ", ")
 }
 
@@ -170,6 +174,8 @@ func (dir *genqlientDirective) add(graphQLDirective *ast.Directive, pos *ast.Pos
 			err = setString("bind", &dir.Bind, arg.Value, pos)
 		case "typename":
 			err = setString("typename", &dir.TypeName, arg.Value, pos)
+		case "alias":
+			err = setString("alias", &dir.Alias, arg.Value, pos)
 		case "for":
 			// handled above
 		default:
@@ -441,6 +447,7 @@ func (dir *genqlientDirective) mergeOperationDirective(
 	// typename isn't settable on the operation (when set there it replies to
 	// the response-type).
 	fillDefaultString(&dir.TypeName, forField.TypeName)
+	fillDefaultString(&dir.Alias, forField.Alias, operationDirective.Alias)
 }
 
 // parsePrecedingComment looks at the comment right before this node, and


Test patch:
diff --git a/generate/testdata/queries/AliasDirective.graphql b/generate/testdata/queries/AliasDirective.graphql
new file mode 100644
index 0000000..53265c3
--- /dev/null
+++ b/generate/testdata/queries/AliasDirective.graphql
@@ -0,0 +1,9 @@
+query AliasDirective {
+  # @genqlient(alias: "MyUser")
+  user {
+    # @genqlient(alias: "UserID")
+    id
+    # @genqlient(alias: "UserName")
+    name
+  }
+}
\ No newline at end of file
diff --git a/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.go b/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.go
new file mode 100644
index 0000000..6bd3480
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.go
@@ -0,0 +1,69 @@
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+package test
+
+import (
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// AliasDirectiveResponse is returned by AliasDirective on success.
+type AliasDirectiveResponse struct {
+	// user looks up a user by some stuff.
+	//
+	// See UserQueryInput for what stuff is supported.
+	// If query is null, returns the current user.
+	MyUser AliasDirectiveUser `json:"user"`
+}
+
+// GetMyUser returns AliasDirectiveResponse.MyUser, and is useful for accessing the field via an interface.
+func (v *AliasDirectiveResponse) GetMyUser() AliasDirectiveUser { return v.MyUser }
+
+// AliasDirectiveUser includes the requested fields of the GraphQL type User.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type AliasDirectiveUser struct {
+	// id is the user's ID.
+	//
+	// It is stable, unique, and opaque, like all good IDs.
+	UserID   testutil.ID `json:"id"`
+	UserName string      `json:"name"`
+}
+
+// GetUserID returns AliasDirectiveUser.UserID, and is useful for accessing the field via an interface.
+func (v *AliasDirectiveUser) GetUserID() testutil.ID { return v.UserID }
+
+// GetUserName returns AliasDirectiveUser.UserName, and is useful for accessing the field via an interface.
+func (v *AliasDirectiveUser) GetUserName() string { return v.UserName }
+
+// The query executed by AliasDirective.
+const AliasDirective_Operation = `
+query AliasDirective {
+	user {
+		id
+		name
+	}
+}
+`
+
+func AliasDirective(
+	client_ graphql.Client,
+) (data_ *AliasDirectiveResponse, err_ error) {
+	req_ := &graphql.Request{
+		OpName: "AliasDirective",
+		Query:  AliasDirective_Operation,
+	}
+
+	data_ = &AliasDirectiveResponse{}
+	resp_ := &graphql.Response{Data: data_}
+
+	err_ = client_.MakeRequest(
+		nil,
+		req_,
+		resp_,
+	)
+
+	return data_, err_
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.json b/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.json
new file mode 100644
index 0000000..583f506
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-AliasDirective.graphql-AliasDirective.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "AliasDirective",
+      "query": "\nquery AliasDirective {\n\tuser {\n\t\tid\n\t\tname\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/AliasDirective.graphql"
+    }
+  ]
+}