Khan / genqlient

a truly type-safe Go GraphQL client
MIT License
1.03k stars 99 forks source link

New generic type option for nullable values #251

Closed DylanRJohnston-FZ closed 1 year ago

DylanRJohnston-FZ commented 1 year ago

Is your feature request related to a problem? Please describe. Currently working on a codebase where we don't like using pointers for null values, but small types like booleans present problems with using the zero value for when it's absent. False is a perfectly valid value for a partial update of a boolean field in a larger struct.

Describe the solution you'd like We have a custom built a generic optional value much you'd see in languages like Rust, Java, or Haskell that we'd like to use instead. I propose adding a third option to the optional configuration, something like generic, and then another configuration option much like context_type, optional_generic_type that can be used in place of pointers.

optional: generic
optional_generic_type: optional.Value

The question is whether to have the generic value without its generic parameter, or include some placeholder like optional.Value[A] or optional.Value[%].

Having this as a third option means you won't break compatibility with <1.18 unless someone opts into it.

Describe alternatives you've considered The available alternatives of zero values, or pointers each have significant drawbacks.

Additional context This is the full implementation of our generic optional type optional.Value, but the configuration options allow anyone else to sub their own in. Happen to open a Pull Request with the changes if you like the idea.

package optional

import (
    "encoding/json"
)

type Value[V any] struct {
    value V
    ok    bool
}

func Some[V any](value V) Value[V] {
    return Value[V]{value: value, ok: true}
}

func None[V any]() Value[V] {
    return Value[V]{ok: false}
}

func (v Value[V]) Unpack() (V, bool) {
    return v.value, v.ok
}

func (v Value[V]) Get(fallback V) V {
    if v.ok {
        return v.value
    }

    return fallback
}

func FromPtr[V any](ptr *V) Value[V] {
    if ptr == nil {
        return None[V]()
    }

    return Some(*ptr)
}

func (value Value[V]) MarshalJSON() ([]byte, error) {
    if value.ok {
        return json.Marshal(value.value)
    } else {
        return json.Marshal((*V)(nil))
    }
}

func (value *Value[V]) UnmarshalJSON(data []byte) error {
    v := (*V)(nil)

    err := json.Unmarshal(data, &v)
    if err != nil {
        return err
    }

    if v != nil {
        value.value = *v
        value.ok = true
    }

    return nil
}
benjaminjkraft commented 1 year ago

252