Search code examples
gogqlgen

Access Custom Type's Original Functions


How do I access the original functions of a customized type in Go?

I do not believe this is not specific to gqlgen, but my use-case heavily involves their framework. I'm trying to build a custom scalar for dates in gqlgen following their setup here: https://gqlgen.com/reference/scalars/

I'm having difficulty with the MarshalGQL function. Here is the code I have so far:

package scalars

import (
    "fmt"
    "io"
    "time"
)

type Date time.Time

// UnmarshalGQL implements the graphql.Unmarshaler interface
func (date *Date) UnmarshalGQL(value interface{}) error {
    dateAsString, ok := value.(string)
    if !ok {
        return fmt.Errorf("date must be a string")
    }

    parsedTime, err := time.Parse(time.RFC3339, dateAsString)
    if err != nil {
        return fmt.Errorf("failed to convert given date value (%s) to Time struct", dateAsString)
    }
    *date = Date(parsedTime)
    return nil
}

// MarshalGQL implements the graphql.Marshaler interface
func (date Date) MarshalGQL(w io.Writer) {
    // This line causes a compiler error
    w.Write(date.Format(time.RFC3339))
}

My issue is this line: w.Write(date.Format(time.RFC3339))

It's giving me this compiler error:

date.Format undefined (type Date has no field or method Format) compiler (MissingFieldOrMethod)

I understand why it's saying this. The Date type only has two functions as far as the compiler knows, UnmarshalGQL and MarshalGQL, both of which are declared in this file. I want to get to the time.Time type's functions though so that I can format the date being returned. How do I go about doing this?


Solution

  • You can either do as mkopriva suggested to explicitly convert Date to time.Time and then format

    or you can create Date type by embedding time.time which will give access to time.Time format method

    package main
    
    import (
        "fmt"
        "io"
        "time"
    )
    
    type Date struct {
        time.Time
    }
    
    func (date *Date) UnmarshalGQL(value interface{}) error {
        dateAsString, ok := value.(string)
        if !ok {
            return fmt.Errorf("date must be a string")
        }
    
        parsedTime, err := time.Parse(time.RFC3339, dateAsString)
        if err != nil {
            return fmt.Errorf("failed to convert given date value (%s) to Time struct", dateAsString)
        }
        *date = Date{parsedTime}
        return nil
    }
    
    // MarshalGQL implements the graphql.Marshaler interface
    func (date Date) MarshalGQL(w io.Writer) {
        // This line causes a compiler error
        w.Write([]byte(date.Format(time.RFC3339)))
    }
    
    func NewDate(v time.Time) *Date {
        return &Date{v}
    }
    
    func main() {
    }
    

    Playground

    For more understanding you can read golang-nuts post which discuss this

    You "cannot define new methods on non-local type[s]," by design.

    The best practice is to embed the non-local type into your own own local type, and extend it. Type-aliasing (type MyFoo Foo) creates a type that is (more-or-less) completely distinct from the original. I'm not aware of a straightforward/best-practice way to use type assertions to get around that.

    Peter Bourgon

    And:

    A type's methods are in the package that defines it.

    This is a logical coherence. It is a compilation virtue. It is seen as an important benefit for large-scale maintenance and multi-person development projects.

    The power you speak of is not lost, though, because you can embed the base type in a new type as described above and add whatever you want to it, in the kind of functional "is a" that you seek, with the only caveat that your new type must have a new name, and all of its fields and methods must be in its new package.

    Michael Jones