I wrote something similar for GraphQL:
You are free to take this and adapt it to DQL. The tag that I used for lookup is gql
. You can switch that to dgraph
.
import (
"bytes"
"encoding"
"encoding/json"
"fmt"
"reflect"
"github.com/pkg/errors"
)
type Op int
const (
Create Op = 1 << iota
Read
Update
Delete
)
type state struct {
bytes.Buffer // out
embedded bool
depth int
scratch [64]byte
tmp *bytes.Buffer
}
func newState() *state { s := &state{}; s.tmp = bytes.NewBuffer(s.scratch[:]); return s }
func (s *state) encodeUID(uid uint64) {
fmt.Fprintf(&s.Buffer, "\"%#x\"", uid)
}
func (s *state) encodeString(str string) {
fmt.Fprintf(&s.Buffer, "%q", str)
}
func (s *state) encodeUint(n uint64) { fmt.Fprintf(s, "%d", n) }
func (s *state) encodeInt(n int64) { fmt.Fprintf(s, "%d", n) }
func (s *state) encodeFloat(n float64) { fmt.Fprintf(s, "%f", n) }
func Marshal(a interface{}, op Op) ([]byte, error) {
s := newState()
aT := reflect.TypeOf(a)
switch aT.Kind() {
case reflect.Ptr:
// for root, we simply do this.
aV := reflect.ValueOf(a)
a = aV.Elem().Interface()
return s.marshalNP(a, op)
case reflect.Slice:
el := aT.Elem()
isPtr := el.Kind() == reflect.Ptr
return s.marshalS(a, op, isPtr)
}
return s.marshalNP(a, op)
}
// marshalName puts the name in a temporary buffer
func (s *state) marshalName(name string) {
s.tmp.WriteString(name)
s.tmp.WriteByte(':')
}
func (s *state) resetTmp() { s.tmp.Truncate(0) }
func (s *state) marshalS(a interface{}, op Op, isPtr bool) ([]byte, error) {
aV := reflect.ValueOf(a)
s.WriteByte('[')
for i := 0; i < aV.Len(); i++ {
iface := aV.Index(i).Interface()
if isPtr {
s.marshalP(iface, Update)
} else {
s.marshalNP(iface, op)
}
if i < aV.Len()-1 {
s.WriteByte(',')
}
}
s.WriteByte(']')
return s.Bytes(), nil
}
func checkNilPtr(a interface{}) bool {
return reflect.ValueOf(a).IsNil()
}
func checkZeroLen(a interface{}) bool {
return reflect.ValueOf(a).Len() == 0
}
func (s *state) marshalP(a interface{}, op Op) ([]byte, error) {
aV := reflect.ValueOf(a)
if aV.IsNil() {
return nil, errors.New("Nil")
}
a = aV.Elem().Interface()
return s.marshalNP(a, op)
}
func (s *state) marshalNP(a interface{}, op Op) ([]byte, error) {
aT := reflect.TypeOf(a)
aV := reflect.ValueOf(a)
fields := aT.NumField()
if !s.embedded {
s.WriteByte('{')
}
var isUpdate bool
if op == Update && !s.embedded && s.depth < 1 {
isUpdate = true
// find ID field
var v reflect.Value
for i := 0; i < fields; i++ {
field := aT.Field(i)
name := marshalledName(field)
if isID(name) {
v = aV.Field(i)
break
}
}
if !v.IsValid() {
return nil, errors.Errorf("Cannot perform Update. No ID found in %v of %T", a, a)
}
s.WriteString("filter: {id: \"")
s.encodeUID(v.Uint()) // panics if not uint64
s.WriteString("\"}, set:{")
}
emb := s.embedded
s.depth++
for i := 0; i < fields; i++ {
s.resetTmp()
field := aT.Field(i)
kind := field.Type.Kind()
name := marshalledName(field)
fieldV := aV.Field(i)
if op == Create && isID(name) {
continue
}
if isIgnored(name) {
continue
}
if name != "" {
s.marshalName(name)
}
s.embedded = false
if name == "" && field.Anonymous {
s.embedded = true
}
var tocomma bool = true
iface := fieldV.Interface()
switch t := iface.(type) {
case json.Marshaler:
mt, err := t.MarshalJSON()
if err != nil {
return nil, errors.Wrapf(err, "Unable to marshal field %v. Value is of %T.", name, iface)
}
if len(mt) > 0 {
s.Write(s.tmp.Bytes())
s.Write(mt)
}
case encoding.TextMarshaler:
mt, err := t.MarshalText()
if err != nil {
return nil, errors.Wrapf(err, "Unable to marshal field %v. Value is of %T.", name, iface)
}
if len(mt) > 0 {
s.Write(s.tmp.Bytes())
s.WriteByte('"')
s.Write(mt)
s.WriteByte('"')
}
default:
switch kind {
case reflect.String:
s.Write(s.tmp.Bytes())
s.encodeString(fieldV.String())
case reflect.Uint64:
s.Write(s.tmp.Bytes())
if isID(name) {
s.encodeUID(fieldV.Uint())
} else {
s.encodeUint(fieldV.Uint())
}
case reflect.Int:
s.Write(s.tmp.Bytes())
s.encodeInt(fieldV.Int())
case reflect.Struct:
s.Write(s.tmp.Bytes())
s.marshalNP(iface, op)
case reflect.Ptr:
tocomma = false
if !checkNilPtr(iface) {
tocomma = true
s.Write(s.tmp.Bytes())
}
s.marshalP(iface, Update)
case reflect.Slice:
s.embedded = false
isPtr := field.Type.Elem().Kind() == reflect.Ptr
tocomma = false
if !checkZeroLen(iface) {
tocomma = true
s.Write(s.tmp.Bytes())
s.marshalS(iface, op, isPtr)
}
default:
return nil, errors.Errorf("Unable to marshal field %v, with type %T", name, iface)
}
}
if i < fields-1 && tocomma {
s.WriteByte(',')
}
s.embedded = emb
}
s.depth--
// retract any stray commas
if s.Bytes()[len(s.Bytes())-1] == ',' {
s.Truncate(len(s.Bytes()) - 1)
}
if !s.embedded {
s.WriteByte('}')
}
if isUpdate {
s.WriteByte('}')
}
return s.Bytes(), nil
}
func marshalledName(field reflect.StructField) string {
name, ok := field.Tag.Lookup("json")
if !ok {
name, ok = field.Tag.Lookup("gql")
if !ok && !field.Anonymous {
name = field.Name
}
}
return name
}
func isID(a string) bool {
switch a {
case "id", "ID", "UID", "uid":
return true
default:
return false
}
}
func isIgnored(a string) bool {
return a == "-"
}
Note: marshalNP
and marshalP
handles marshalling of non-pointer (NP) and pointer ยง values. These are very specific to the usecase I was writing the code for. The code treats pointer values as mutable in graphql so the generated mutation is different.