blob: 3ebea46c598a17faf8568070bfb156a81ed6670d [file] [log] [blame]
// Copyright ©2017 The Gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package graphql
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/encoding"
)
// Unmarshal parses the JSON-encoded data and stores the result in dst.
// Node IDs are obtained from the JSON fields identified by the uid parameter.
// UIDs obtained from the JSON encoding must map to unique node ID values
// consistently across the JSON-encoded spanning tree. graph.Node values
// returned by dst.NewNode must satisfy StringIDSetter.
func Unmarshal(data []byte, uid string, dst encoding.Builder) error {
if uid == "" {
return errors.New("graphql: invalid UID field name")
}
var src json.RawMessage
err := json.Unmarshal(data, &src)
if err != nil {
return err
}
gen := generator{dst: dst, uidName: uid, nodes: make(map[string]graph.Node)}
return gen.walk(src, nil, "")
}
// StringIDSetter is a graph node that can set its ID based on the given uid string.
type StringIDSetter interface {
SetIDFromString(uid string) error
}
// LabelSetter is a graph edge that can set its label.
type LabelSetter interface {
SetLabel(string)
}
type generator struct {
dst encoding.Builder
// uidName is the name of the UID field in the source JSON.
uidName string
// nodes maps from GraphQL UID string to graph.Node.
nodes map[string]graph.Node
}
func (g *generator) walk(src json.RawMessage, node graph.Node, attr string) error {
switch src[0] {
case '{':
var val map[string]json.RawMessage
err := json.Unmarshal(src, &val)
if err != nil {
return err
}
if next, ok := val[g.uidName]; !ok {
if node != nil {
var buf bytes.Buffer
err := json.Compact(&buf, src)
if err != nil {
panic(err)
}
return fmt.Errorf("graphql: no UID for node: `%s`", &buf)
}
} else {
var v interface{}
err = json.Unmarshal(next, &v)
if err != nil {
return err
}
value := fmt.Sprint(v)
child, ok := g.nodes[value]
if !ok {
child = g.dst.NewNode()
s, ok := child.(StringIDSetter)
if !ok {
return errors.New("graphql: cannot set UID")
}
err = s.SetIDFromString(value)
if err != nil {
return err
}
g.nodes[value] = child
g.dst.AddNode(child)
}
if node != nil {
e := g.dst.NewEdge(node, child)
if s, ok := e.(LabelSetter); ok {
s.SetLabel(attr)
}
g.dst.SetEdge(e)
}
node = child
}
for attr, src := range val {
if attr == g.uidName {
continue
}
err = g.walk(src, node, attr)
if err != nil {
return err
}
}
case '[':
var val []json.RawMessage
err := json.Unmarshal(src, &val)
if err != nil {
return err
}
for _, src := range val {
err = g.walk(src, node, attr)
if err != nil {
return err
}
}
default:
var v interface{}
err := json.Unmarshal(src, &v)
if err != nil {
return err
}
if attr == g.uidName {
value := fmt.Sprint(v)
if s, ok := node.(StringIDSetter); ok {
if _, ok := g.nodes[value]; !ok {
err = s.SetIDFromString(value)
if err != nil {
return err
}
g.nodes[value] = node
}
} else {
return errors.New("graphql: cannot set ID")
}
} else if s, ok := node.(encoding.AttributeSetter); ok {
var value string
if _, ok := v.(float64); ok {
value = string(src)
} else {
value = fmt.Sprint(v)
}
err = s.SetAttribute(encoding.Attribute{Key: attr, Value: value})
if err != nil {
return err
}
}
}
return nil
}