package js import ( "fmt" "io" "sync" time "github.com/echlebek/timeproxy" "github.com/robertkrimen/otto" "github.com/robertkrimen/otto/parser" ) var ottoCache *vmCache var ottoOnce sync.Once type JavascriptAssets interface { Key() string Scripts() (map[string]io.ReadCloser, error) } // SyntaxError is returned when a javascript expression could not be parsed. type SyntaxError string func (s SyntaxError) Error() string { return string(s) } // NewSyntaxError creates a new SyntaxError. func NewSyntaxError(err string, args ...interface{}) SyntaxError { return SyntaxError(fmt.Sprintf(err, args...)) } // ParseExpressions parses each JS expression and returns the first error that // is encountered, or nil. func ParseExpressions(expressions []string) error { for i, expr := range expressions { _, err := parser.ParseFile(nil, "", expr, 0) if err != nil { return NewSyntaxError("syntax error in expression %d: %s", i, err) } } return nil } func newOttoVM(assets JavascriptAssets) (*otto.Otto, error) { ottoOnce.Do(func() { ottoCache = newVMCache() }) key := "" if assets != nil { key = assets.Key() } vm := ottoCache.Acquire(key) if vm != nil { return vm, nil } vm = otto.New() if err := addTimeFuncs(vm); err != nil { return nil, err } if assets != nil { if err := addAssets(vm, assets); err != nil { return nil, err } } ottoCache.Init(key, vm) return ottoCache.Acquire(key), nil } func addAssets(vm *otto.Otto, assets JavascriptAssets) error { scripts, err := assets.Scripts() if err != nil { return err } defer func() { for _, script := range scripts { _ = script.Close() } }() for name, script := range scripts { if _, err := vm.Eval(script); err != nil { return fmt.Errorf("error evaluating %s: %s", name, err) } } return nil } func addTimeFuncs(vm *otto.Otto) error { funcs := map[string]interface{}{ // hour returns the hour within the day "hour": func(args ...interface{}) interface{} { if len(args) == 0 { return 0 } t := time.Unix(toInt64(args[0]), 0).UTC() return t.Hour() }, // weekday returns the number representation of the day of the week, where // Sunday = 0 "weekday": func(args ...interface{}) interface{} { if len(args) == 0 { return 0 } t := time.Unix(toInt64(args[0]), 0).UTC() return t.Weekday() }, } for k, v := range funcs { if err := vm.Set(k, v); err != nil { return err } } return nil } // Evaluate evaluates the javascript expression with parameters applied. // If scripts is non-nil, then the scripts will be evaluated in the // expression's runtime context before the expression is evaluated. func Evaluate(expr string, parameters interface{}, assets JavascriptAssets) (bool, error) { jsvm, err := newOttoVM(assets) if err != nil { return false, err } if params, ok := parameters.(map[string]interface{}); ok { for name, value := range params { if err := jsvm.Set(name, value); err != nil { return false, err } } } value, err := jsvm.Run(expr) if err != nil { return false, err } return value.ToBoolean() }