diff --git a/graph/api/api.go b/graph/api/api.go new file mode 100644 index 0000000..95fe85f --- /dev/null +++ b/graph/api/api.go @@ -0,0 +1,157 @@ +// Copyright 2014 The Cayley Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "github.com/google/cayley/graph" + "github.com/google/cayley/graph/iterator" + "github.com/google/cayley/quad" +) + +type Morphism struct { + Name string + Reversal func() Morphism + Apply graph.MorphismFunc +} + +type iteratorMorphism interface { + Apply(graph.Iterator) graph.Iterator +} + +type Path struct { + stack []Morphism + it graph.Iterator + qs graph.QuadStore +} + +func V(qs graph.QuadStore, nodes ...string) *Path { + fixed := qs.FixedIterator() + for _, n := range nodes { + fixed.Add(qs.ValueOf(n)) + } + return &Path{ + it: fixed, + qs: qs, + } +} + +func PathFromIterator(qs graph.QuadStore, it graph.Iterator) *Path { + return &Path{ + it: it, + qs: qs, + } +} + +func M(qs graph.QuadStore) *Path { + return &Path{ + it: nil, + qs: qs, + } +} + +func (p *Path) IsConcrete() bool { return p.it != nil } + +func (p *Path) Tag(tags ...string) *Path { p.stack = append(p.stack, TagMorphism(tags...)); return p } +func (p *Path) Out(via ...interface{}) *Path { + p.stack = append(p.stack, OutMorphism(p.qs, via...)) + return p +} +func (p *Path) In(via ...interface{}) *Path { + p.stack = append(p.stack, InMorphism(p.qs, via...)) + return p +} + +func (p *Path) BuildIterator() graph.Iterator { + f := p.MorphismFunc() + return f(p.it) +} + +func (p *Path) MorphismFunc() graph.MorphismFunc { + return func(it graph.Iterator) graph.Iterator { + i := it.Clone() + for _, m := range p.stack { + i = m.Apply(i) + } + return i + } +} + +func TagMorphism(tags ...string) Morphism { + return Morphism{ + "tag", + func() Morphism { return TagMorphism(tags...) }, + func(it graph.Iterator) graph.Iterator { + for _, t := range tags { + it.Tagger().Add(t) + } + return it + }} +} + +func OutMorphism(qs graph.QuadStore, via ...interface{}) Morphism { + path := buildViaPath(qs, via...) + return Morphism{ + "out", + func() Morphism { return InMorphism(qs, via...) }, + inOutIterator(path, false), + } +} + +func InMorphism(qs graph.QuadStore, via ...interface{}) Morphism { + path := buildViaPath(qs, via...) + return Morphism{ + "in", + func() Morphism { return OutMorphism(qs, via...) }, + inOutIterator(path, true), + } +} + +func inOutIterator(viaPath *Path, reverse bool) graph.MorphismFunc { + return func(base graph.Iterator) graph.Iterator { + in, out := quad.Subject, quad.Object + if reverse { + in, out = out, in + } + lto := iterator.NewLinksTo(viaPath.qs, base, in) + and := iterator.NewAnd() + and.AddSubIterator(iterator.NewLinksTo(viaPath.qs, viaPath.BuildIterator(), quad.Predicate)) + and.AddSubIterator(lto) + return iterator.NewHasA(viaPath.qs, and, out) + } +} + +func buildViaPath(qs graph.QuadStore, via ...interface{}) *Path { + if len(via) == 0 { + return PathFromIterator(qs, qs.NodesAllIterator()) + } else if len(via) == 1 { + v := via[0] + if path, ok := v.(*Path); ok { + return path + } else if str, ok := v.(string); ok { + return V(qs, str) + } else { + panic("Invalid type passed to buildViaPath.") + } + } + var strings []string + for _, s := range via { + if str, ok := s.(string); ok { + strings = append(strings, str) + } else { + panic("Non-string type passed to long Via path") + } + } + return V(qs, strings...) +} diff --git a/graph/api/api_test.go b/graph/api/api_test.go new file mode 100644 index 0000000..2b31327 --- /dev/null +++ b/graph/api/api_test.go @@ -0,0 +1,89 @@ +// Copyright 2014 The Cayley Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "reflect" + "sort" + "testing" + + "github.com/google/cayley/graph" + "github.com/google/cayley/quad" + + _ "github.com/google/cayley/graph/memstore" + _ "github.com/google/cayley/writer" +) + +var simpleGraph = []quad.Quad{ + {"A", "follows", "B", ""}, + {"C", "follows", "B", ""}, + {"C", "follows", "D", ""}, + {"D", "follows", "B", ""}, + {"B", "follows", "F", ""}, + {"F", "follows", "G", ""}, + {"D", "follows", "G", ""}, + {"E", "follows", "F", ""}, + {"B", "status", "cool", "status_graph"}, + {"D", "status", "cool", "status_graph"}, + {"G", "status", "cool", "status_graph"}, +} + +func makeTestStore(data []quad.Quad) graph.QuadStore { + qs, _ := graph.NewQuadStore("memstore", "", nil) + w, _ := graph.NewQuadWriter("single", qs, nil) + for _, t := range data { + w.AddQuad(t) + } + return qs +} + +func runTopLevel(path *Path) []string { + var out []string + it := path.BuildIterator() + it, _ = it.Optimize() + for graph.Next(it) { + v := path.qs.NameOf(it.Result()) + out = append(out, v) + } + return out +} + +type test struct { + message string + path *Path + expect []string +} + +func testSet(qs graph.QuadStore) []test { + return []test{ + { + message: "out", + path: V(qs, "A").Out("follows"), + expect: []string{"B"}, + }, + } +} + +func TestMorphisms(t *testing.T) { + qs := makeTestStore(simpleGraph) + for _, test := range testSet(qs) { + got := runTopLevel(test.path) + sort.Strings(got) + sort.Strings(test.expect) + if !reflect.DeepEqual(got, test.expect) { + t.Errorf("Failed to %s, got: %v expected: %v", test.message, got, test.expect) + } + } +} diff --git a/graph/iterator.go b/graph/iterator.go index 6585ff5..0fdc5a5 100644 --- a/graph/iterator.go +++ b/graph/iterator.go @@ -165,6 +165,9 @@ type Description struct { Iterators []Description `json:",omitempty"` } +// A curried function that can generates a new iterator based on some prior iterator. +type MorphismFunc func(Iterator) Iterator + type Nexter interface { // Next advances the iterator to the next value, which will then be available through // the Result method. It returns false if no further advancement is possible, or if an