| // 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 |
| } |