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 }