Khan / Khan/genqlient

Potential improvement to generated getters for pointer-optionals

Open
#301 1 comment 3 reactions 0 assignees View on GitHub
enhancement
Dominant language
Go
Stars
1.3k
Forks
143
Avg merge
4h 50m
Merged PRs (30d)
1

Description

**Is your feature request related to a problem? Please describe.**

I have a proposal that I think would make it easier to take advantage of the useful getter functions this library generates for fields. Consider this simple example, using `optional: pointer` (or individual `pointer: true` directives on optional fields):

#### Schema
```graphql
type Food {
name: String!
# Zero is a valid value if someone really loves celery, so using value optionals isn't appropriate
calories: Int!
}

type Person {
name: String!
# If the Person doesn't have a favourite food, the API returns null for this entire field
favouriteFood: Food
}

type Query {
person(name: String): Person
}
```

#### Query
```graphql
query FindPerson($name: String!) {
person(name: $name) {
name
favouriteFood {
name
}
}
}
```

One of the things genqlient will generate is a getter method for `favouriteFood.name`:

```go
// GetName returns FindPersonPersonFavouriteFood.Name, and is useful for accessing the field via an interface.
func (v *FindPersonPersonFavouriteFood) GetName() string { return v.Name }
```

My interpretation of the comment on the getter is that over time, I may find myself with many types having a `name` field, and might want to start writing functions that can deal with any of them. So, I might want to write something like:

```go
type named interface {
GetName() string
}

func processName(f named) *string {
if f == nil { return nil }

name := f.GetName()

// Do some processing involving the name

return &name
}
```

Of course, I've now shot myself in the foot, because `f` is a typed nil; the `f == nil` comparison will return false, and I'll get a nil pointer dereference when I call `GetName`.

**Describe the solution you'd like**

I think this could be made smoother if the getters generated for fields of a pointer-optional nested type themselves performed a nil-check and returned pointers (while leaving the actual field in the struct a value, unless it itself was optional). i.e., the generated getter from above would instead be:

```go
// GetName returns FindPersonPersonFavouriteFood.Name, and is useful for accessing the field via an interface.
func (v *FindPersonPersonFavouriteFood) GetName() *string {
if v == nil { return nil }
return &v.Name
}
```

Now, the call to `f.GetName()` would return a nil pointer instead of panicking, and I could return that from my function directly. I would probably also want to remove the `f == nil` check as it never really served a purpose anyway:

```go
func processName(f named) *string {
name := f.GetName()

// Do some processing involving the name

return name
}
```

As far as I can tell, genqlient always generates separate Go types per-query per-type, so I don't believe this would have any negative effects in cases where the nested type is optional in one parent type and non-optional in another.

This would also support the common response when this behaviour of Go is questioned as a design flaw, which is that [the implementer of the methods on an interface should be responsible for handling nil pointers](https://groups.google.com/g/golang-nuts/c/wnH302gBa4I/m/qws8QeidBAAJ). Or, to say it another way, the getter says it can return a string given any pointer to `FindPersonPersonFavouriteFood`, but currently if that pointer is nil it cannot.

Of course, there's the fairly big caveat that genqclient hasn't said it implements any interfaces; I have 😁 But I believe such a use case was the intent behind the feature?

**Describe alternatives you've considered**

I appreciate that there are several reasons the maintainers may not want to implement this, for example:
* It's 100% a breaking change
* The library already supports unmarshalling nulls to a user-provided generic type, which in an ideal world is preferable to the imperfect modelling of missing values using Go nils
* Reflection can be used to check if the concrete value is nil rather than `== nil`
* I could nil-check the pointer before I pass it to my function

Nevertheless, because it's perfectly valid in Go to call a method on a (typed) nil pointer, I think my suggestion is overall more type-safe if the getters are pointer receivers rather than value receivers.

**Additional context**

While Go is not my first language, I would be happy to look into implementing this change if there is any interest in it!

Contributor guide

Open the contributing guide

Research direction

Start by tracing how genqlient generates getters for pointer-optional nested types, using the Food/Person schema and FindPerson query as the entry point. Define and test the intended nil-pointer behavior, returned type, and compatibility impact before deciding whether the proposed change is complete.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, graphql
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.