stargate / stargate-grpc-go-client

A golang gRPC client for https://stargate.io
Apache License 2.0
18 stars 7 forks source link

Feature request: Go native types #22

Open SpencerC opened 2 years ago

SpencerC commented 2 years ago

Overview

It's great being able to interact with the gRPC message types directly! But sometimes it's more desirable to work with more simplified semantics, especially for testing. In addition, something like this could be used a a translation type for features like #20 if you guys want to go in that direction.

How it might look

Here's what developers currently have to do to generate test data:

id0, _ := uuid.MustParse("00000000-0000-0000-0000-000000000000").MarshalBinary()
id1, _ := uuid.MustParse("00000000-0000-0000-0000-000000000001").MarshalBinary()
res := &pb.Response{
    Result: &pb.Response_ResultSet{
        ResultSet: &pb.ResultSet{
            Columns: []*pb.ColumnSpec{
                {
                    Type: &pb.TypeSpec{
                        Spec: &pb.TypeSpec_Basic_{Basic: pb.TypeSpec_UUID},
                    },
                    Name: "id",
                },
                {
                    Type: &pb.TypeSpec{
                        Spec: &pb.TypeSpec_Basic_{Basic: pb.TypeSpec_TEXT},
                    },
                    Name: "name",
                },
                {
                    Type: &pb.TypeSpec{
                        Spec: &pb.TypeSpec_Basic_{Basic: pb.TypeSpec_INT},
                    },
                    Name: "age",
                },
            },
            Rows: []*pb.Row{
                {
                    Values: []*pb.Value{
                        {Inner: &pb.Value_Uuid{Uuid: &pb.Uuid{Value: id0}}},
                        {Inner: &pb.Value_String_{String_: "Someone"}},
                        {Inner: &pb.Value_Int{Int: 30}},
                    },
                },
                {
                    Values: []*pb.Value{
                        {Inner: &pb.Value_Uuid{Uuid: &pb.Uuid{Value: id1}}},
                        {Inner: &pb.Value_String_{String_: "Someone Else"}},
                        {Inner: &pb.Value_Int{Int: 73}},
                    },
                },
            },
        },
    },
}

Here's an approach to the same thing with native types:

id0 := uuid.MustParse("00000000-0000-0000-0000-000000000000")
id1 := uuid.MustParse("00000000-0000-0000-0000-000000000001")
sr := &StargateResponse{
    Result: &Result{
        Columns: []StargateColumnSpec{
            &BasicColumn{Name: "id", Type: &UUIDType{}},
            &BasicColumn{Name: "name", Type: &TextType{}},
            &BasicColumn{Name: "age", Type: &IntType{}},
        },
        Rows: []*Row{
            {
                &UUIDValue{id0}, &TextValue{"Someone"}, &IntValue{30},
            },
            {
                &UUIDValue{id1}, &TextValue{"Someone Else"}, &IntValue{73},
            },
        },
    },
}
dRes := sr.ToProto()

What I have so far

For my own use, I've created these types so far and intend to add more as needed. Happy to PR these, or wait and PR them once this is a bit more baked.

type StargateTypeSpec interface {
    ToProto() *pb.TypeSpec
}

type UUIDType struct{}

func (t *UUIDType) ToProto() *pb.TypeSpec {
    return &pb.TypeSpec{Spec: &pb.TypeSpec_Basic_{Basic: pb.TypeSpec_UUID}}
}

type TextType struct{}

func (t *TextType) ToProto() *pb.TypeSpec {
    return &pb.TypeSpec{Spec: &pb.TypeSpec_Basic_{Basic: pb.TypeSpec_TEXT}}
}

type StargateColumnSpec interface {
    ToProto() *pb.ColumnSpec
}

type BasicColumn struct {
    Name string
    Type StargateTypeSpec
}

func (c *BasicColumn) ToProto() *pb.ColumnSpec {
    return &pb.ColumnSpec{
        Type: c.Type.ToProto(),
        Name: c.Name,
    }
}

type ListColumn struct {
    Name string
    Type StargateTypeSpec
}

func (c *ListColumn) ToProto() *pb.ColumnSpec {
    return &pb.ColumnSpec{
        Type: &pb.TypeSpec{
            Spec: &pb.TypeSpec_List_{
                List: &pb.TypeSpec_List{Element: c.Type.ToProto()},
            },
        },
        Name: c.Name,
    }
}

type StargateValue interface {
    ToProto() *pb.Value
}

type UUIDValue struct{ uuid.UUID }

func (u *UUIDValue) ToProto() *pb.Value {
    b, _ := u.MarshalBinary()
    return &pb.Value{
        Inner: &pb.Value_Uuid{
            Uuid: &pb.Uuid{Value: b},
        },
    }
}

type TextValue struct{ string }

func (s *TextValue) ToProto() *pb.Value {
    return &pb.Value{
        Inner: &pb.Value_String_{
            String_: s.string,
        },
    }
}

type IntValue struct{ int64 }

func (i *IntValue) ToProto() *pb.Value {
    return &pb.Value{
        Inner: &pb.Value_Int{
            Int: i.int64,
        },
    }
}

type ListValue struct {
    Values []StargateValue
}

func (l *ListValue) ToProto() *pb.Value {
    values := make([]*pb.Value, len(l.Values))
    for i, v := range l.Values {
        values[i] = v.ToProto()
    }
    return &pb.Value{
        Inner: &pb.Value_Collection{
            Collection: &pb.Collection{
                Elements: values,
            },
        },
    }
}

type Row []StargateValue

func (r *Row) ToProto() *pb.Row {
    row := &pb.Row{
        Values: make([]*pb.Value, len(*r)),
    }
    for i, cell := range *r {
        row.Values[i] = cell.ToProto()
    }
    return row
}

type Result struct {
    Columns []StargateColumnSpec
    Rows    []*Row
}

func (r *Result) ToProto() *pb.ResultSet {
    result := &pb.ResultSet{
        Columns: make([]*pb.ColumnSpec, len(r.Columns)),
        Rows:    make([]*pb.Row, len(r.Rows)),
    }
    for i, col := range r.Columns {
        result.Columns[i] = col.ToProto()
    }
    for i, row := range r.Rows {
        result.Rows[i] = row.ToProto()
    }
    return result
}

type StargateResponse struct {
    Result *Result
}

func (r *StargateResponse) ToProto() *pb.Response {
    return &pb.Response{
        Result: &pb.Response_ResultSet{ResultSet: r.Result.ToProto()},
    }
}
mpenick commented 2 years ago

That looks great! Huge improvement in usability and much more concise.

Happy to PR these, or wait and PR them once this is a bit more baked.

If you're up for it a PR would be most welcome even if it's not fully "baked", but it's up to you.