mirror of
https://github.com/gonum/gonum.git
synced 2025-10-05 15:16:59 +08:00
graph/encoding/graphql: add new package for decoding GraphQL output
This commit is contained in:
157
graph/encoding/graphql/decode.go
Normal file
157
graph/encoding/graphql/decode.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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 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.
|
||||
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
|
||||
}
|
226
graph/encoding/graphql/decode_test.go
Normal file
226
graph/encoding/graphql/decode_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gonum.org/v1/gonum/graph"
|
||||
"gonum.org/v1/gonum/graph/encoding"
|
||||
"gonum.org/v1/gonum/graph/encoding/dot"
|
||||
"gonum.org/v1/gonum/graph/simple"
|
||||
)
|
||||
|
||||
var decodeTests = []struct {
|
||||
name string
|
||||
json string
|
||||
roots map[uint64]bool
|
||||
wantDOT string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "starwars",
|
||||
json: starwars,
|
||||
roots: map[uint64]bool{
|
||||
0xa3cff1a4c3ef3bb6: true,
|
||||
0xb39aa14d66aedad5: true,
|
||||
},
|
||||
wantDOT: `digraph {
|
||||
// Node definitions.
|
||||
0x8a10d5a2611fd03f [name="Richard Marquand"];
|
||||
0xa3cff1a4c3ef3bb6 [
|
||||
name="Star Wars: Episode V - The Empire Strikes Back"
|
||||
release_date=1980-05-21T00:00:00Z
|
||||
revenue=534000000
|
||||
running_time=124
|
||||
];
|
||||
0xb39aa14d66aedad5 [
|
||||
name="Star Wars: Episode VI - Return of the Jedi"
|
||||
release_date=1983-05-25T00:00:00Z
|
||||
revenue=572000000
|
||||
running_time=131
|
||||
];
|
||||
0x0312de17a7ee89f9 [name="Luke Skywalker"];
|
||||
0x3da8d1dcab1bb381 [name="Han Solo"];
|
||||
0x4a7d0b5fe91e78a4 [name="Irvin Kernshner"];
|
||||
0x718337b9dcbaa7d9 [name="Princess Leia"];
|
||||
|
||||
// Edge definitions.
|
||||
0xa3cff1a4c3ef3bb6 -> 0x0312de17a7ee89f9 [label=starring];
|
||||
0xa3cff1a4c3ef3bb6 -> 0x3da8d1dcab1bb381 [label=starring];
|
||||
0xa3cff1a4c3ef3bb6 -> 0x4a7d0b5fe91e78a4 [label=director];
|
||||
0xa3cff1a4c3ef3bb6 -> 0x718337b9dcbaa7d9 [label=starring];
|
||||
0xb39aa14d66aedad5 -> 0x8a10d5a2611fd03f [label=director];
|
||||
0xb39aa14d66aedad5 -> 0x0312de17a7ee89f9 [label=starring];
|
||||
0xb39aa14d66aedad5 -> 0x3da8d1dcab1bb381 [label=starring];
|
||||
0xb39aa14d66aedad5 -> 0x718337b9dcbaa7d9 [label=starring];
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "tutorial",
|
||||
json: dgraphTutorial,
|
||||
roots: map[uint64]bool{
|
||||
0xfd90205a458151f: true,
|
||||
0x52a80955d40ec819: true,
|
||||
},
|
||||
wantDOT: `digraph {
|
||||
// Node definitions.
|
||||
0x892a6da7ee1fbdec [
|
||||
age=55
|
||||
name=Sarah
|
||||
];
|
||||
0x99b74c1b5ab100ec [
|
||||
age=35
|
||||
name=Artyom
|
||||
];
|
||||
0xb9e12a67e34d6acc [
|
||||
age=19
|
||||
name=Catalina
|
||||
];
|
||||
0xbf104824c777525d [name=Perro];
|
||||
0xf590a923ea1fccaa [name=Goldie];
|
||||
0xf92d7dbe272d680b [name="Hyung Sin"];
|
||||
0x0fd90205a458151f [
|
||||
age=39
|
||||
name=Michael
|
||||
];
|
||||
0x37734fcf0a6fcc69 [name="Rammy the sheep"];
|
||||
0x52a80955d40ec819 [
|
||||
age=35
|
||||
name=Amit
|
||||
];
|
||||
0x5e9ad1cd9466228c [
|
||||
age=24
|
||||
name="Sang Hyun"
|
||||
];
|
||||
|
||||
// Edge definitions.
|
||||
0xb9e12a67e34d6acc -> 0xbf104824c777525d [label=owns_pet];
|
||||
0xb9e12a67e34d6acc -> 0x5e9ad1cd9466228c [label=friend];
|
||||
0xf92d7dbe272d680b -> 0x5e9ad1cd9466228c [label=friend];
|
||||
0x0fd90205a458151f -> 0x892a6da7ee1fbdec [label=friend];
|
||||
0x0fd90205a458151f -> 0x99b74c1b5ab100ec [label=friend];
|
||||
0x0fd90205a458151f -> 0xb9e12a67e34d6acc [label=friend];
|
||||
0x0fd90205a458151f -> 0x37734fcf0a6fcc69 [label=owns_pet];
|
||||
0x0fd90205a458151f -> 0x52a80955d40ec819 [label=friend];
|
||||
0x0fd90205a458151f -> 0x5e9ad1cd9466228c [label=friend];
|
||||
0x52a80955d40ec819 -> 0x99b74c1b5ab100ec [label=friend];
|
||||
0x52a80955d40ec819 -> 0x0fd90205a458151f [label=friend];
|
||||
0x52a80955d40ec819 -> 0x5e9ad1cd9466228c [label=friend];
|
||||
0x5e9ad1cd9466228c -> 0xb9e12a67e34d6acc [label=friend];
|
||||
0x5e9ad1cd9466228c -> 0xf590a923ea1fccaa [label=owns_pet];
|
||||
0x5e9ad1cd9466228c -> 0xf92d7dbe272d680b [label=friend];
|
||||
0x5e9ad1cd9466228c -> 0x52a80955d40ec819 [label=friend];
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "tutorial missing IDs",
|
||||
json: dgraphTutorialMissingIDs,
|
||||
wantErr: errors.New("graphql: no UID for node"), // Incomplete error string.
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
for _, test := range decodeTests {
|
||||
dst := newDirectedGraph()
|
||||
err := Unmarshal([]byte(test.json), "_uid_", dst)
|
||||
if test.wantErr == nil && err != nil {
|
||||
t.Errorf("failed to unmarshal GraphQL JSON graph for %q: %v", test.name, err)
|
||||
} else if test.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("expected error for %q: got:%v want:%v", test.name, err, test.wantErr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
b, err := dot.Marshal(dst, "", "", " ", false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to DOT marshal graph %q: %v", test.name, err)
|
||||
}
|
||||
gotDOT := string(b)
|
||||
if gotDOT != test.wantDOT {
|
||||
t.Errorf("unexpected DOT encoding for %q:\ngot:\n%s\nwant:\n%s", test.name, gotDOT, test.wantDOT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type directedGraph struct {
|
||||
*simple.DirectedGraph
|
||||
}
|
||||
|
||||
func newDirectedGraph() *directedGraph {
|
||||
return &directedGraph{DirectedGraph: simple.NewDirectedGraph(0, 0)}
|
||||
}
|
||||
|
||||
func (g *directedGraph) NewNode() graph.Node {
|
||||
return &node{attributes: make(attributes)}
|
||||
}
|
||||
|
||||
func (g *directedGraph) NewEdge(from, to graph.Node) graph.Edge {
|
||||
if e := g.Edge(from, to); e != nil {
|
||||
return e
|
||||
}
|
||||
e := &edge{Edge: simple.Edge{F: from, T: to}}
|
||||
g.SetEdge(e)
|
||||
return e
|
||||
}
|
||||
|
||||
type node struct {
|
||||
id uint64
|
||||
attributes
|
||||
}
|
||||
|
||||
func (n *node) ID() int64 { return int64(n.id) }
|
||||
func (n *node) DOTID() string { return fmt.Sprintf("0x%016x", uint64(n.id)) }
|
||||
|
||||
func (n *node) SetIDFromString(uid string) error {
|
||||
if !strings.HasPrefix(uid, "0x") {
|
||||
return fmt.Errorf("uid is not hex value: %q", uid)
|
||||
}
|
||||
var err error
|
||||
n.id, err = strconv.ParseUint(uid[2:], 16, 64)
|
||||
return err
|
||||
}
|
||||
|
||||
type edge struct {
|
||||
simple.Edge
|
||||
label string
|
||||
}
|
||||
|
||||
func (e *edge) SetLabel(l string) {
|
||||
e.label = l
|
||||
}
|
||||
|
||||
func (e *edge) Attributes() []encoding.Attribute {
|
||||
return []encoding.Attribute{{"label", e.label}}
|
||||
}
|
||||
|
||||
type attributes map[string]encoding.Attribute
|
||||
|
||||
func (a attributes) SetAttribute(attr encoding.Attribute) error {
|
||||
a[attr.Key] = attr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a attributes) Attributes() []encoding.Attribute {
|
||||
keys := make([]string, 0, len(a))
|
||||
for k := range a {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
attr := make([]encoding.Attribute, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
v := a[k]
|
||||
if strings.Contains(v.Value, " ") {
|
||||
v.Value = `"` + v.Value + `"`
|
||||
}
|
||||
attr = append(attr, v)
|
||||
}
|
||||
return attr
|
||||
}
|
7
graph/encoding/graphql/graphql.go
Normal file
7
graph/encoding/graphql/graphql.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// 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 implements JSON marshaling and unmarshaling of graph as
|
||||
// used by GraphQL
|
||||
package graphql // import "gonum.org/v1/gonum/graph/encoding/graphql"
|
1013
graph/encoding/graphql/graphs_test.go
Normal file
1013
graph/encoding/graphql/graphs_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user