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 }