github.com/cayleygraph/cayley@v0.7.7/query/gizmo/environ.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  // Builds a new Gizmo environment pointing at a session.
    18  
    19  import (
    20  	"fmt"
    21  	"regexp"
    22  	"time"
    23  
    24  	"github.com/dop251/goja"
    25  
    26  	"github.com/cayleygraph/cayley/graph/iterator"
    27  	"github.com/cayleygraph/cayley/graph/path"
    28  	"github.com/cayleygraph/cayley/graph/shape"
    29  	"github.com/cayleygraph/quad"
    30  	"github.com/cayleygraph/quad/voc"
    31  )
    32  
    33  // graphObject is a root graph object.
    34  //
    35  // Name: `graph`, Alias: `g`
    36  //
    37  // This is the only special object in the environment, generates the query objects.
    38  // Under the hood, they're simple objects that get compiled to a Go iterator tree when executed.
    39  // Methods starting with "New" are accessible in JavaScript with a capital letter (e.g. NewV becomes V)
    40  type graphObject struct {
    41  	s *Session
    42  }
    43  
    44  // Uri creates an IRI values from a given string.
    45  func (g *graphObject) NewIRI(s string) quad.IRI {
    46  	return quad.IRI(g.s.ns.FullIRI(s))
    47  }
    48  
    49  // AddNamespace associates prefix with a given IRI namespace.
    50  func (g *graphObject) AddNamespace(pref, ns string) {
    51  	g.s.ns.Register(voc.Namespace{Prefix: pref + ":", Full: ns})
    52  }
    53  
    54  // AddDefaultNamespaces register all default namespaces for automatic IRI resolution.
    55  func (g *graphObject) AddDefaultNamespaces() {
    56  	voc.CloneTo(&g.s.ns)
    57  }
    58  
    59  // LoadNamespaces loads all namespaces saved to graph.
    60  func (g *graphObject) LoadNamespaces() error {
    61  	return g.s.sch.LoadNamespaces(g.s.ctx, g.s.qs, &g.s.ns)
    62  }
    63  
    64  // V is a shorthand for Vertex.
    65  func (g *graphObject) NewV(call goja.FunctionCall) goja.Value {
    66  	return g.NewVertex(call)
    67  }
    68  
    69  // Vertex starts a query path at the given vertex/vertices. No ids means "all vertices".
    70  // Signature: ([nodeId],[nodeId]...)
    71  //
    72  // Arguments:
    73  //
    74  // * `nodeId` (Optional): A string or list of strings representing the starting vertices.
    75  //
    76  // Returns: Path object
    77  func (g *graphObject) NewVertex(call goja.FunctionCall) goja.Value {
    78  	qv, err := toQuadValues(exportArgs(call.Arguments))
    79  	if err != nil {
    80  		return throwErr(g.s.vm, err)
    81  	}
    82  	return g.s.vm.ToValue(&pathObject{
    83  		s:      g.s,
    84  		finals: true,
    85  		path:   path.StartMorphism(qv...),
    86  	})
    87  }
    88  
    89  // M is a shorthand for Morphism.
    90  func (g *graphObject) NewM() *pathObject {
    91  	return g.NewMorphism()
    92  }
    93  
    94  // Morphism creates a morphism path object. Unqueryable on it's own, defines one end of the path.
    95  // Saving these to variables with
    96  //
    97  //	// javascript
    98  //	var shorterPath = graph.Morphism().out("foo").out("bar")
    99  //
   100  // is the common use case. See also: path.follow(), path.followR().
   101  func (g *graphObject) NewMorphism() *pathObject {
   102  	return &pathObject{
   103  		s:    g.s,
   104  		path: path.StartMorphism(),
   105  	}
   106  }
   107  
   108  // Emit adds data programmatically to the JSON result list. Can be any JSON type.
   109  //
   110  //	// javascript
   111  //	g.emit({name:"bob"}) // push {"name":"bob"} as a result
   112  func (g *graphObject) Emit(call goja.FunctionCall) goja.Value {
   113  	value := call.Argument(0)
   114  	if !goja.IsNull(value) && !goja.IsUndefined(value) {
   115  		val := exportArgs([]goja.Value{value})[0]
   116  		if val != nil {
   117  			g.s.send(nil, &Result{Val: val})
   118  		}
   119  	}
   120  	return goja.Null()
   121  }
   122  
   123  // Backwards compatibility
   124  func (g *graphObject) CapitalizedUri(s string) quad.IRI {
   125  	return g.NewIRI(s)
   126  }
   127  func (g *graphObject) CapitalizedAddNamespace(pref, ns string) {
   128  	g.AddNamespace(pref, ns)
   129  }
   130  func (g *graphObject) CapitalizedAddDefaultNamespaces() {
   131  	g.AddDefaultNamespaces()
   132  }
   133  func (g *graphObject) CapitalizedLoadNamespaces() error {
   134  	return g.LoadNamespaces()
   135  }
   136  func (g *graphObject) CapitalizedEmit(call goja.FunctionCall) goja.Value {
   137  	return g.Emit(call)
   138  }
   139  
   140  func oneStringType(fnc func(s string) quad.Value) func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   141  	return func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   142  		args := toStrings(exportArgs(call.Arguments))
   143  		if len(args) != 1 {
   144  			return throwErr(vm, errArgCount2{Expected: 1, Got: len(args)})
   145  		}
   146  		return vm.ToValue(fnc(args[0]))
   147  	}
   148  }
   149  
   150  func twoStringType(fnc func(s1, s2 string) quad.Value) func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   151  	return func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   152  		args := toStrings(exportArgs(call.Arguments))
   153  		if len(args) != 2 {
   154  			return throwErr(vm, errArgCount2{Expected: 2, Got: len(args)})
   155  		}
   156  		return vm.ToValue(fnc(args[0], args[1]))
   157  	}
   158  }
   159  
   160  func cmpOpType(op iterator.Operator) func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   161  	return func(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   162  		args := exportArgs(call.Arguments)
   163  		if len(args) != 1 {
   164  			return throwErr(vm, errArgCount2{Expected: 1, Got: len(args)})
   165  		}
   166  		qv, err := toQuadValue(args[0])
   167  		if err != nil {
   168  			return throwErr(vm, err)
   169  		}
   170  		return vm.ToValue(valFilter{f: shape.Comparison{Op: op, Val: qv}})
   171  	}
   172  }
   173  
   174  func cmpWildcard(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   175  	args := exportArgs(call.Arguments)
   176  	if len(args) != 1 {
   177  		return throwErr(vm, errArgCount2{Expected: 1, Got: len(args)})
   178  	}
   179  	pattern, ok := args[0].(string)
   180  	if !ok {
   181  		return throwErr(vm, fmt.Errorf("wildcard: unsupported type: %T", args[0]))
   182  	}
   183  	return vm.ToValue(valFilter{f: shape.Wildcard{Pattern: pattern}})
   184  }
   185  
   186  func cmpRegexp(vm *goja.Runtime, call goja.FunctionCall) goja.Value {
   187  	args := exportArgs(call.Arguments)
   188  	if len(args) != 1 && len(args) != 2 {
   189  		return throwErr(vm, errArgCount2{Expected: 1, Got: len(args)})
   190  	}
   191  	v, err := toQuadValue(args[0])
   192  	if err != nil {
   193  		return throwErr(vm, err)
   194  	}
   195  	allowRefs := false
   196  	if len(args) > 1 {
   197  		b, ok := args[1].(bool)
   198  		if !ok {
   199  			return throwErr(vm, fmt.Errorf("expected bool as second argument"))
   200  		}
   201  		allowRefs = b
   202  	}
   203  	switch vt := v.(type) {
   204  	case quad.String:
   205  		if allowRefs {
   206  			v = quad.IRI(string(vt))
   207  		}
   208  	case quad.IRI:
   209  		if !allowRefs {
   210  			return throwErr(vm, errRegexpOnIRI)
   211  		}
   212  	case quad.BNode:
   213  		if !allowRefs {
   214  			return throwErr(vm, errRegexpOnIRI)
   215  		}
   216  	default:
   217  		return throwErr(vm, fmt.Errorf("regexp: unsupported type: %T", v))
   218  	}
   219  	var (
   220  		s    string
   221  		refs bool
   222  	)
   223  	switch v := v.(type) {
   224  	case quad.String:
   225  		s = string(v)
   226  	case quad.IRI:
   227  		s, refs = string(v), true
   228  	case quad.BNode:
   229  		s, refs = string(v), true
   230  	default:
   231  		return throwErr(vm, fmt.Errorf("regexp from non-string value: %T", v))
   232  	}
   233  	re, err := regexp.Compile(string(s))
   234  	if err != nil {
   235  		return throwErr(vm, err)
   236  	}
   237  	return vm.ToValue(valFilter{f: shape.Regexp{Re: re, Refs: refs}})
   238  }
   239  
   240  type valFilter struct {
   241  	f shape.ValueFilter
   242  }
   243  
   244  var defaultEnv = map[string]func(vm *goja.Runtime, call goja.FunctionCall) goja.Value{
   245  	"iri":   oneStringType(func(s string) quad.Value { return quad.IRI(s) }),
   246  	"bnode": oneStringType(func(s string) quad.Value { return quad.BNode(s) }),
   247  	"raw":   oneStringType(func(s string) quad.Value { return quad.Raw(s) }),
   248  	"str":   oneStringType(func(s string) quad.Value { return quad.String(s) }),
   249  
   250  	"lang": twoStringType(func(s, lang string) quad.Value {
   251  		return quad.LangString{Value: quad.String(s), Lang: lang}
   252  	}),
   253  	"typed": twoStringType(func(s, typ string) quad.Value {
   254  		return quad.TypedString{Value: quad.String(s), Type: quad.IRI(typ)}
   255  	}),
   256  
   257  	"lt":    cmpOpType(iterator.CompareLT),
   258  	"lte":   cmpOpType(iterator.CompareLTE),
   259  	"gt":    cmpOpType(iterator.CompareGT),
   260  	"gte":   cmpOpType(iterator.CompareGTE),
   261  	"regex": cmpRegexp,
   262  	"like":  cmpWildcard,
   263  }
   264  
   265  func unwrap(o interface{}) interface{} {
   266  	switch v := o.(type) {
   267  	case *pathObject:
   268  		o = v.path
   269  	case []interface{}:
   270  		for i, val := range v {
   271  			v[i] = unwrap(val)
   272  		}
   273  	case map[string]interface{}:
   274  		for k, val := range v {
   275  			v[k] = unwrap(val)
   276  		}
   277  	}
   278  	return o
   279  }
   280  
   281  func exportArgs(args []goja.Value) []interface{} {
   282  	if len(args) == 0 {
   283  		return nil
   284  	}
   285  	out := make([]interface{}, 0, len(args))
   286  	for _, a := range args {
   287  		o := a.Export()
   288  		out = append(out, unwrap(o))
   289  	}
   290  	return out
   291  }
   292  
   293  func toInt(o interface{}) (int, bool) {
   294  	switch v := o.(type) {
   295  	case int:
   296  		return v, true
   297  	case int64:
   298  		return int(v), true
   299  	case float64:
   300  		return int(v), true
   301  	default:
   302  		return 0, false
   303  	}
   304  }
   305  
   306  func toQuadValue(o interface{}) (quad.Value, error) {
   307  	var qv quad.Value
   308  	switch v := o.(type) {
   309  	case quad.Value:
   310  		qv = v
   311  	case string:
   312  		qv = quad.StringToValue(v)
   313  	case bool:
   314  		qv = quad.Bool(v)
   315  	case int:
   316  		qv = quad.Int(v)
   317  	case int64:
   318  		qv = quad.Int(v)
   319  	case float64:
   320  		if float64(int(v)) == v {
   321  			qv = quad.Int(int64(v))
   322  		} else {
   323  			qv = quad.Float(v)
   324  		}
   325  	case time.Time:
   326  		qv = quad.Time(v)
   327  	default:
   328  		return nil, errNotQuadValue{Val: o}
   329  	}
   330  	return qv, nil
   331  }
   332  
   333  func toQuadValues(objs []interface{}) ([]quad.Value, error) {
   334  	if len(objs) == 0 {
   335  		return nil, nil
   336  	}
   337  	vals := make([]quad.Value, 0, len(objs))
   338  	for _, o := range objs {
   339  		qv, err := toQuadValue(o)
   340  		if err != nil {
   341  			return nil, err
   342  		}
   343  		vals = append(vals, qv)
   344  	}
   345  	return vals, nil
   346  }
   347  
   348  func toStrings(objs []interface{}) []string {
   349  	if len(objs) == 0 {
   350  		return nil
   351  	}
   352  	var out = make([]string, 0, len(objs))
   353  	for _, o := range objs {
   354  		switch v := o.(type) {
   355  		case string:
   356  			out = append(out, v)
   357  		case quad.Value:
   358  			out = append(out, quad.StringOf(v))
   359  		case []string:
   360  			out = append(out, v...)
   361  		case []interface{}:
   362  			out = append(out, toStrings(v)...)
   363  		default:
   364  			panic(fmt.Errorf("expected string, got: %T", o))
   365  		}
   366  	}
   367  	return out
   368  }
   369  
   370  func toVia(via []interface{}) []interface{} {
   371  	if len(via) == 0 {
   372  		return nil
   373  	} else if len(via) == 1 {
   374  		if via[0] == nil {
   375  			return nil
   376  		} else if v, ok := via[0].([]interface{}); ok {
   377  			return toVia(v)
   378  		} else if v, ok := via[0].([]string); ok {
   379  			arr := make([]interface{}, 0, len(v))
   380  			for _, s := range v {
   381  				arr = append(arr, s)
   382  			}
   383  			return toVia(arr)
   384  		}
   385  	}
   386  	for i := range via {
   387  		if _, ok := via[i].(*path.Path); ok {
   388  			// bypass
   389  		} else if vp, ok := via[i].(*pathObject); ok {
   390  			via[i] = vp.path
   391  		} else if qv, err := toQuadValue(via[i]); err == nil {
   392  			via[i] = qv
   393  		} else {
   394  			panic(fmt.Errorf("unsupported type: %T", via[i]))
   395  		}
   396  	}
   397  	return via
   398  }
   399  
   400  func toViaData(objs []interface{}) (predicates []interface{}, tags []string, ok bool) {
   401  	if len(objs) != 0 {
   402  		predicates = toVia([]interface{}{objs[0]})
   403  	}
   404  	if len(objs) > 1 {
   405  		tags = toStrings(objs[1:])
   406  	}
   407  	ok = true
   408  	return
   409  }
   410  
   411  func toViaDepthData(objs []interface{}) (predicates []interface{}, maxDepth int, tags []string, ok bool) {
   412  	if len(objs) != 0 {
   413  		predicates = toVia([]interface{}{objs[0]})
   414  	}
   415  	if len(objs) > 1 {
   416  		maxDepth, ok = toInt(objs[1])
   417  		if ok {
   418  			if len(objs) > 2 {
   419  				tags = toStrings(objs[2:])
   420  			}
   421  		} else {
   422  			tags = toStrings(objs[1:])
   423  		}
   424  	}
   425  	ok = true
   426  	return
   427  }
   428  
   429  func throwErr(vm *goja.Runtime, err error) goja.Value {
   430  	panic(vm.ToValue(err))
   431  }