github.com/cayleygraph/cayley@v0.7.7/query/gizmo/gizmo.go (about)

     1  // Copyright 2017 The Cayley Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gizmo
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  	"unicode"
    25  	"unicode/utf8"
    26  
    27  	"github.com/dop251/goja"
    28  
    29  	"github.com/cayleygraph/cayley/graph"
    30  	"github.com/cayleygraph/cayley/graph/iterator"
    31  	"github.com/cayleygraph/cayley/query"
    32  	"github.com/cayleygraph/cayley/schema"
    33  	"github.com/cayleygraph/quad"
    34  	"github.com/cayleygraph/quad/jsonld"
    35  	"github.com/cayleygraph/quad/voc"
    36  )
    37  
    38  const Name = "gizmo"
    39  
    40  func init() {
    41  	query.RegisterLanguage(query.Language{
    42  		Name: Name,
    43  		Session: func(qs graph.QuadStore) query.Session {
    44  			return NewSession(qs)
    45  		},
    46  		HTTP: func(qs graph.QuadStore) query.HTTP {
    47  			return NewSession(qs)
    48  		},
    49  		REPL: func(qs graph.QuadStore) query.REPLSession {
    50  			return NewSession(qs)
    51  		},
    52  	})
    53  }
    54  
    55  func NewSession(qs graph.QuadStore) *Session {
    56  	s := &Session{
    57  		ctx: context.Background(),
    58  		sch: schema.NewConfig(),
    59  		qs:  qs, limit: -1,
    60  	}
    61  	if err := s.buildEnv(); err != nil {
    62  		panic(err)
    63  	}
    64  	return s
    65  }
    66  
    67  func lcFirst(str string) string {
    68  	rune, size := utf8.DecodeRuneInString(str)
    69  	return string(unicode.ToLower(rune)) + str[size:]
    70  }
    71  
    72  type fieldNameMapper struct{}
    73  
    74  func (fieldNameMapper) FieldName(t reflect.Type, f reflect.StructField) string {
    75  	return lcFirst(f.Name)
    76  }
    77  
    78  const constructMethodPrefix = "New"
    79  const backwardsCompatibilityPrefix = "Capitalized"
    80  
    81  func (fieldNameMapper) MethodName(t reflect.Type, m reflect.Method) string {
    82  	if strings.HasPrefix(m.Name, backwardsCompatibilityPrefix) {
    83  		return strings.TrimPrefix(m.Name, backwardsCompatibilityPrefix)
    84  	}
    85  	if strings.HasPrefix(m.Name, constructMethodPrefix) {
    86  		return strings.TrimPrefix(m.Name, constructMethodPrefix)
    87  	}
    88  	return lcFirst(m.Name)
    89  }
    90  
    91  type Session struct {
    92  	qs  graph.QuadStore
    93  	vm  *goja.Runtime
    94  	ns  voc.Namespaces
    95  	sch *schema.Config
    96  	col query.Collation
    97  
    98  	last string
    99  	p    *goja.Program
   100  
   101  	out   chan *Result
   102  	ctx   context.Context
   103  	limit int
   104  	count int
   105  
   106  	err   error
   107  	shape map[string]interface{}
   108  }
   109  
   110  func (s *Session) context() context.Context {
   111  	return s.ctx
   112  }
   113  
   114  func (s *Session) buildEnv() error {
   115  	if s.vm != nil {
   116  		return nil
   117  	}
   118  	s.vm = goja.New()
   119  	s.vm.SetFieldNameMapper(fieldNameMapper{})
   120  	s.vm.Set("graph", &graphObject{s: s})
   121  	s.vm.Set("g", s.vm.Get("graph"))
   122  	for name, val := range defaultEnv {
   123  		fnc := val
   124  		s.vm.Set(name, func(call goja.FunctionCall) goja.Value {
   125  			return fnc(s.vm, call)
   126  		})
   127  	}
   128  	return nil
   129  }
   130  
   131  func (s *Session) quadValueToNative(v quad.Value) interface{} {
   132  	if v == nil {
   133  		return nil
   134  	}
   135  	if s.col == query.JSONLD {
   136  		return jsonld.FromValue(v)
   137  	}
   138  	out := v.Native()
   139  	if nv, ok := out.(quad.Value); ok && v == nv {
   140  		return quad.StringOf(v)
   141  	}
   142  	return out
   143  }
   144  
   145  func (s *Session) tagsToValueMap(m map[string]graph.Ref) map[string]interface{} {
   146  	outputMap := make(map[string]interface{})
   147  	for k, v := range m {
   148  		if o := s.quadValueToNative(s.qs.NameOf(v)); o != nil {
   149  			outputMap[k] = o
   150  		}
   151  	}
   152  	if len(outputMap) == 0 {
   153  		return nil
   154  	}
   155  	return outputMap
   156  }
   157  func (s *Session) runIteratorToArray(it graph.Iterator, limit int) ([]map[string]interface{}, error) {
   158  	ctx := s.context()
   159  
   160  	output := make([]map[string]interface{}, 0)
   161  	err := graph.Iterate(ctx, it).Limit(limit).TagEach(func(tags map[string]graph.Ref) {
   162  		tm := s.tagsToValueMap(tags)
   163  		if tm == nil {
   164  			return
   165  		}
   166  		output = append(output, tm)
   167  	})
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	return output, nil
   172  }
   173  
   174  func (s *Session) runIteratorToArrayNoTags(it graph.Iterator, limit int) ([]interface{}, error) {
   175  	ctx := s.context()
   176  
   177  	output := make([]interface{}, 0)
   178  	err := graph.Iterate(ctx, it).Paths(false).Limit(limit).EachValue(s.qs, func(v quad.Value) {
   179  		if o := s.quadValueToNative(v); o != nil {
   180  			output = append(output, o)
   181  		}
   182  	})
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  	return output, nil
   187  }
   188  
   189  func (s *Session) runIteratorWithCallback(it graph.Iterator, callback goja.Value, this goja.FunctionCall, limit int) error {
   190  	fnc, ok := goja.AssertFunction(callback)
   191  	if !ok {
   192  		return fmt.Errorf("expected js callback function")
   193  	}
   194  	ctx, cancel := context.WithCancel(s.context())
   195  	defer cancel()
   196  	var gerr error
   197  	err := graph.Iterate(ctx, it).Paths(true).Limit(limit).TagEach(func(tags map[string]graph.Ref) {
   198  		tm := s.tagsToValueMap(tags)
   199  		if tm == nil {
   200  			return
   201  		}
   202  		if _, err := fnc(this.This, s.vm.ToValue(tm)); err != nil {
   203  			gerr = err
   204  			cancel()
   205  		}
   206  	})
   207  	if gerr != nil {
   208  		err = gerr
   209  	}
   210  	return err
   211  }
   212  
   213  func (s *Session) send(ctx context.Context, r *Result) bool {
   214  	if s.limit > 0 && s.count >= s.limit {
   215  		return false
   216  	}
   217  	if s.out == nil {
   218  		return false
   219  	}
   220  	if ctx == nil {
   221  		ctx = s.ctx
   222  	}
   223  	select {
   224  	case s.out <- r:
   225  	case <-ctx.Done():
   226  		return false
   227  	}
   228  	s.count++
   229  	return s.limit <= 0 || s.count < s.limit
   230  }
   231  
   232  func (s *Session) runIterator(it graph.Iterator) error {
   233  	if s.shape != nil {
   234  		iterator.OutputQueryShapeForIterator(it, s.qs, s.shape)
   235  		return nil
   236  	}
   237  
   238  	ctx, cancel := context.WithCancel(s.context())
   239  	defer cancel()
   240  	stop := false
   241  	err := graph.Iterate(ctx, it).Paths(true).TagEach(func(tags map[string]graph.Ref) {
   242  		if !s.send(ctx, &Result{Tags: tags}) {
   243  			cancel()
   244  			stop = true
   245  		}
   246  	})
   247  	if stop {
   248  		err = nil
   249  	}
   250  	return err
   251  }
   252  
   253  func (s *Session) countResults(it graph.Iterator) (int64, error) {
   254  	if s.shape != nil {
   255  		iterator.OutputQueryShapeForIterator(it, s.qs, s.shape)
   256  		return 0, nil
   257  	}
   258  	return graph.Iterate(s.context(), it).Paths(true).Count()
   259  }
   260  
   261  type Result struct {
   262  	Meta bool
   263  	Val  interface{}
   264  	Tags map[string]graph.Ref
   265  }
   266  
   267  func (r *Result) Result() interface{} {
   268  	if r.Tags != nil {
   269  		return r.Tags
   270  	}
   271  	return r.Val
   272  }
   273  
   274  func (s *Session) compile(qu string) error {
   275  	var p *goja.Program
   276  	if s.last == qu && s.last != "" {
   277  		p = s.p
   278  	} else {
   279  		var err error
   280  		p, err = goja.Compile("", qu, false)
   281  		if err != nil {
   282  			return err
   283  		}
   284  		s.last, s.p = qu, p
   285  	}
   286  	return nil
   287  }
   288  
   289  func (s *Session) run() (goja.Value, error) {
   290  	v, err := s.vm.RunProgram(s.p)
   291  	if e, ok := err.(*goja.Exception); ok && e.Value() != nil {
   292  		if er, ok := e.Value().Export().(error); ok {
   293  			err = er
   294  		}
   295  	}
   296  	return v, err
   297  }
   298  func (s *Session) Execute(ctx context.Context, qu string, opt query.Options) (query.Iterator, error) {
   299  	switch opt.Collation {
   300  	case query.Raw, query.JSON, query.JSONLD, query.REPL:
   301  	default:
   302  		return nil, &query.ErrUnsupportedCollation{Collation: opt.Collation}
   303  	}
   304  	if err := s.compile(qu); err != nil {
   305  		return nil, err
   306  	}
   307  	s.limit = opt.Limit
   308  	s.count = 0
   309  	ctx, cancel := context.WithCancel(context.Background())
   310  	s.ctx = ctx
   311  	s.col = opt.Collation
   312  	return &results{
   313  		col: opt.Collation,
   314  		s:   s,
   315  		ctx: ctx, cancel: cancel,
   316  	}, nil
   317  }
   318  
   319  type results struct {
   320  	s      *Session
   321  	col    query.Collation
   322  	ctx    context.Context
   323  	cancel func()
   324  
   325  	running bool
   326  	errc    chan error
   327  
   328  	err error
   329  	cur *Result
   330  }
   331  
   332  func (it *results) stop(err error) {
   333  	it.cancel()
   334  	if !it.running {
   335  		return
   336  	}
   337  	it.s.vm.Interrupt(err)
   338  	it.running = false
   339  }
   340  
   341  func (it *results) Next(ctx context.Context) bool {
   342  	if it.errc == nil {
   343  		it.s.out = make(chan *Result)
   344  		it.errc = make(chan error, 1)
   345  		it.running = true
   346  		go func() {
   347  			defer close(it.errc)
   348  			v, err := it.s.run()
   349  			if err != nil {
   350  				it.errc <- err
   351  				return
   352  			}
   353  			if !goja.IsNull(v) && !goja.IsUndefined(v) {
   354  				it.s.send(it.ctx, &Result{Meta: true, Val: v.Export()})
   355  			}
   356  		}()
   357  	}
   358  	select {
   359  	case r := <-it.s.out:
   360  		it.cur = r
   361  		return true
   362  	case err := <-it.errc:
   363  		if err != nil {
   364  			it.err = err
   365  		}
   366  		return false
   367  	case <-ctx.Done():
   368  		it.err = ctx.Err()
   369  		it.stop(it.err)
   370  		return false
   371  	}
   372  }
   373  
   374  func (it *results) Result() interface{} {
   375  	if it.cur == nil {
   376  		return nil
   377  	}
   378  	switch it.col {
   379  	case query.Raw:
   380  		return it.cur
   381  	case query.JSON, query.JSONLD:
   382  		return it.jsonResult()
   383  	case query.REPL:
   384  		return it.replResult()
   385  	}
   386  	return nil
   387  }
   388  
   389  func (it *results) jsonResult() interface{} {
   390  	data := it.cur
   391  	if data.Meta {
   392  		return nil
   393  	}
   394  	if data.Val != nil {
   395  		return data.Val
   396  	}
   397  	obj := make(map[string]interface{})
   398  	tags := data.Tags
   399  	var tagKeys []string
   400  	for k := range tags {
   401  		tagKeys = append(tagKeys, k)
   402  	}
   403  	sort.Strings(tagKeys)
   404  	for _, k := range tagKeys {
   405  		if name := it.s.qs.NameOf(tags[k]); name != nil {
   406  			obj[k] = it.s.quadValueToNative(name)
   407  		} else {
   408  			delete(obj, k)
   409  		}
   410  	}
   411  	return obj
   412  }
   413  
   414  func (it *results) replResult() interface{} {
   415  	data := it.cur
   416  	if data.Meta {
   417  		if data.Val != nil {
   418  			s := data.Val
   419  			switch s.(type) {
   420  			case *pathObject, *graphObject:
   421  				s = "[internal Iterator]"
   422  			}
   423  			return fmt.Sprintln("=>", s)
   424  		}
   425  		return fmt.Sprintln("=>", nil)
   426  	}
   427  	var out string
   428  	out = fmt.Sprintln("****")
   429  	if data.Val == nil {
   430  		tags := data.Tags
   431  		tagKeys := make([]string, len(tags))
   432  		i := 0
   433  		for k := range tags {
   434  			tagKeys[i] = k
   435  			i++
   436  		}
   437  		sort.Strings(tagKeys)
   438  		for _, k := range tagKeys {
   439  			if k == "$_" {
   440  				continue
   441  			}
   442  			out += fmt.Sprintf("%s : %s\n", k, quadValueToString(it.s.qs.NameOf(tags[k])))
   443  		}
   444  	} else {
   445  		switch export := data.Val.(type) {
   446  		case map[string]string:
   447  			for k, v := range export {
   448  				out += fmt.Sprintf("%s : %s\n", k, v)
   449  			}
   450  		case map[string]interface{}:
   451  			for k, v := range export {
   452  				out += fmt.Sprintf("%s : %v\n", k, v)
   453  			}
   454  		default:
   455  			out += fmt.Sprintf("%s\n", data.Val)
   456  		}
   457  	}
   458  	return out
   459  }
   460  
   461  func (it *results) Err() error {
   462  	return it.err
   463  }
   464  
   465  func (it *results) Close() error {
   466  	it.stop(errors.New("iterator closed"))
   467  	return nil
   468  }
   469  
   470  // Web stuff
   471  
   472  func (s *Session) ShapeOf(qu string) (interface{}, error) {
   473  	s.shape = make(map[string]interface{})
   474  	err := s.compile(qu)
   475  	if err != nil {
   476  		return nil, err
   477  	}
   478  	_, err = s.run()
   479  	out := s.shape
   480  	s.shape = nil
   481  	return out, err
   482  }