diff --git a/go.mod b/go.mod index 6a0e2e5..eb65701 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,20 @@ module github.com/blues/jsonata-go -go 1.16 +go 1.23.0 + +toolchain go1.24.2 + +require ( + github.com/google/go-cmp v0.6.0 // indirect + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/tools v0.33.0 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect +) diff --git a/go.sum b/go.sum index e69de29..cb80434 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/latest/.claude/settings.local.json b/latest/.claude/settings.local.json new file mode 100644 index 0000000..8cb2b38 --- /dev/null +++ b/latest/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(chmod:*)", + "Bash(./run.sh:*)", + "Bash(ls:*)", + "Bash(go build:*)", + "Bash(bash:*)", + "Bash(go get:*)", + "Bash(mkdir:*)", + "Bash(go run:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/latest/.gitignore b/latest/.gitignore new file mode 100644 index 0000000..f32cf64 --- /dev/null +++ b/latest/.gitignore @@ -0,0 +1,3 @@ +# Ignore generated files +/jsonata.go +/latest \ No newline at end of file diff --git a/latest/make-latest/main.go b/latest/make-latest/main.go new file mode 100644 index 0000000..c786de6 --- /dev/null +++ b/latest/make-latest/main.go @@ -0,0 +1,218 @@ +package main + +import ( + "fmt" + "go/ast" + "go/types" + "os" + "path/filepath" + "sort" + "strings" + "text/template" + + "golang.org/x/tools/go/packages" +) + +type Symbol struct { + Name string + Type string + Doc string +} + +type ShimData struct { + ImportPath string + Version string + Types []Symbol + Vars []Symbol + Consts []Symbol + Funcs []Symbol +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: make-latest ../../v1.2.3") + fmt.Println("(creates ../jsonata.go with wrappers from the package in the v1.2.3 directory)") + os.Exit(1) + } + + sourcePath := os.Args[1] + + // Extract version from path or use the final directory name + version := extractVersionFromPath(sourcePath) + + // If version not found in path, use the final directory name + if version == "" { + // Get the last part of the path + version = filepath.Base(sourcePath) + } + + fmt.Printf("Inspecting jsonata package at: %s (version %s)\n", sourcePath, version) + + // Construct import path for the package + importPath := fmt.Sprintf("github.com/blues/jsonata-go/%s", version) + fmt.Printf("Using import path: %s\n", importPath) + + // Get exported symbols + data, err := getExportedSymbols(importPath, version) + if err != nil { + fmt.Printf("Error inspecting package: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Found %d exported symbols\n", + len(data.Types)+len(data.Vars)+len(data.Consts)+len(data.Funcs)) + + err = generateWrapper(data) + if err != nil { + fmt.Printf("Error generating wrapper: %v\n", err) + os.Exit(1) + } + + fmt.Println("Successfully generated jsonata.go wrapper") +} + +// extractVersionFromPath tries to extract the version from the path +func extractVersionFromPath(path string) string { + // Try to find a version pattern like v1.2.3 in the path + parts := strings.Split(path, "/") + for _, part := range parts { + if strings.HasPrefix(part, "v") && len(part) > 1 { + // Check if the rest of the string could be a version number + if _, err := fmt.Sscanf(part[1:], "%f", &struct{ f float64 }{}); err == nil { + return part + } + } + } + return "" +} + +func getExportedSymbols(importPath string, version string) (*ShimData, error) { + cfg := &packages.Config{ + Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports | packages.NeedName, + Env: os.Environ(), + Dir: ".", // start from current module + } + + pkgs, err := packages.Load(cfg, importPath) + if err != nil { + return nil, fmt.Errorf("error loading package: %v", err) + } + + if packages.PrintErrors(pkgs) > 0 || len(pkgs) == 0 { + return nil, fmt.Errorf("no package found for %s", importPath) + } + + pkg := pkgs[0].Types + + data := &ShimData{ + ImportPath: importPath, + Version: version, + } + + scope := pkg.Scope() + for _, name := range scope.Names() { + // Check if the name starts with an uppercase letter (exported) + if !ast.IsExported(name) { + continue + } + + obj := scope.Lookup(name) + symbol := Symbol{Name: name} + + switch obj.(type) { + case *types.TypeName: + symbol.Type = "Type" + data.Types = append(data.Types, symbol) + case *types.Var: + symbol.Type = "Var" + data.Vars = append(data.Vars, symbol) + case *types.Const: + symbol.Type = "Const" + data.Consts = append(data.Consts, symbol) + case *types.Func: + symbol.Type = "Func" + data.Funcs = append(data.Funcs, symbol) + } + } + + // Sort each category by name + sort.Slice(data.Types, func(i, j int) bool { + return data.Types[i].Name < data.Types[j].Name + }) + sort.Slice(data.Consts, func(i, j int) bool { + return data.Consts[i].Name < data.Consts[j].Name + }) + sort.Slice(data.Vars, func(i, j int) bool { + return data.Vars[i].Name < data.Vars[j].Name + }) + sort.Slice(data.Funcs, func(i, j int) bool { + return data.Funcs[i].Name < data.Funcs[j].Name + }) + + return data, nil +} + +func generateWrapper(data *ShimData) error { + // Create template functions + funcMap := template.FuncMap{ + "join": func(strs []string, sep string) string { + return strings.Join(strs, sep) + }, + } + + // Create and parse template + t := template.New("wrapper").Funcs(funcMap) + + const templateText = `// Copyright Blues Inc. All rights reserved. + +// Package jsonata is a query and transformation language for JSON. +// This is a wrapper package that provides the same interface as the v{{.Version}} package. +// Generated automatically. +package jsonata + +import ( + latest "{{.ImportPath}}" +) + +// Types +{{range .Types}}// {{.Name}} is a type from the latest JSONata package +type {{.Name}} = latest.{{.Name}} +{{end}} + +// Constants +{{range .Consts}}// {{.Name}} is a constant from the latest JSONata package +const {{.Name}} = latest.{{.Name}} +{{end}} + +// Variables +{{range .Vars}}// {{.Name}} is a variable from the latest JSONata package +var {{.Name}} = latest.{{.Name}} +{{end}} + +// Functions +{{range .Funcs}}// {{.Name}} is a function from the latest JSONata package +var {{.Name}} = latest.{{.Name}} +{{end}} +` + + t, err := t.Parse(templateText) + if err != nil { + return fmt.Errorf("error parsing template: %v", err) + } + + // Create output file + outputPath := filepath.Join("..", "jsonata.go") + file, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("error creating output file %s: %v", outputPath, err) + } + defer file.Close() + + // Execute template + err = t.Execute(file, data) + if err != nil { + return fmt.Errorf("error executing template: %v", err) + } + + return nil +} diff --git a/latest/make-latest/make-latest b/latest/make-latest/make-latest new file mode 100755 index 0000000..69b7d93 Binary files /dev/null and b/latest/make-latest/make-latest differ diff --git a/v1.5.4/.claude/settings.local.json b/v1.5.4/.claude/settings.local.json new file mode 100644 index 0000000..71100b4 --- /dev/null +++ b/v1.5.4/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(ls)", + "Bash(go:*)", + "Bash(grep:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/v1.5.4/callable.go b/v1.5.4/callable.go new file mode 100644 index 0000000..f681528 --- /dev/null +++ b/v1.5.4/callable.go @@ -0,0 +1,963 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type callableName struct { + name string +} + +func (n callableName) Name() string { + return n.name +} + +func (n *callableName) SetName(s string) { + n.name = s +} + +type callableMarshaler struct{} + +func (callableMarshaler) MarshalJSON() ([]byte, error) { + return []byte(`""`), nil +} + +type goCallableParam struct { + t reflect.Type + isOpt bool + optType *goCallableParam + isVar bool + varTypes []goCallableParam +} + +func newGoCallableParam(typ reflect.Type) goCallableParam { + + param := goCallableParam{ + t: typ, + } + + isOpt := reflect.PointerTo(typ).Implements(jtypes.TypeOptional) + if isOpt { + o := reflect.New(typ).Interface().(jtypes.Optional) + p := newGoCallableParam(o.Type()) + param.isOpt = true + param.optType = &p + } + + isVar := typ.Implements(jtypes.TypeVariant) + if isVar { + var ps []goCallableParam + types := reflect.Zero(typ).Interface().(jtypes.Variant).ValidTypes() + if n := len(types); n > 0 { + ps = make([]goCallableParam, n) + for i := range ps { + ps[i] = newGoCallableParam(types[i]) + } + } + param.isVar = true + param.varTypes = ps + } + + return param +} + +// A goCallable represents a built-in or third party Go function. +// It implements the Callable interface. +type goCallable struct { + callableName + callableMarshaler + fn reflect.Value + params []goCallableParam + isVariadic bool + undefinedHandler jtypes.ArgHandler + contextHandler jtypes.ArgHandler + context reflect.Value +} + +func newGoCallable(name string, ext Extension) (*goCallable, error) { + + if err := validateGoCallableFunc(ext.Func); err != nil { + return nil, err + } + + v := reflect.ValueOf(ext.Func) + t := v.Type() + + params := makeGoCallableParams(t) + if err := validateGoCallableParams(params, t.IsVariadic()); err != nil { + return nil, err + } + + return &goCallable{ + callableName: callableName{ + name: name, + }, + fn: v, + params: params, + isVariadic: t.IsVariadic(), + undefinedHandler: ext.UndefinedHandler, + contextHandler: ext.EvalContextHandler, + }, nil +} + +var typeError = reflect.TypeOf((*error)(nil)).Elem() + +func validateGoCallableFunc(fn interface{}) error { + + v := reflect.ValueOf(fn) + + if v.Kind() != reflect.Func { + return fmt.Errorf("func must be a Go function") + } + + t := v.Type() + switch t.NumOut() { + case 1: + case 2: + if !t.Out(1).Implements(typeError) { + return fmt.Errorf("func must return an error as its second value") + } + default: + return fmt.Errorf("func must return either 1 or 2 values") + } + + return nil +} + +func validateGoCallableParams(params []goCallableParam, isVariadic bool) error { + + var hasOptionals bool + + for i, p := range params { + + if p.isOpt && p.isVar { + return fmt.Errorf("parameters cannot be both optional and variant") + } + + if hasOptionals && !p.isOpt { + return fmt.Errorf("a non-optional parameter cannot follow an optional parameter") + } + + if p.isOpt { + if p.optType.isOpt { + return fmt.Errorf("optional parameters cannot have an optional underlying type") + } + if isVariadic && i == len(params)-1 { + return fmt.Errorf("optional parameters cannot be variadic") + } + hasOptionals = true + } + + if p.isVar { + if !jtypes.TypeValue.ConvertibleTo(p.t) { + return fmt.Errorf("variant parameter types must be derived from reflect.Value") + } + if len(p.varTypes) < 2 { + return fmt.Errorf("variant parameters must have at least two valid types") + } + for _, t := range p.varTypes { + if t.isOpt || t.isVar { + return fmt.Errorf("a variant parameter's valid types cannot be optional or variant") + } + } + } + } + + return nil +} + +func makeGoCallableParams(typ reflect.Type) []goCallableParam { + + paramCount := typ.NumIn() + if paramCount == 0 { + return nil + } + + isVariadic := typ.IsVariadic() + params := make([]goCallableParam, paramCount) + + for i := range params { + + t := typ.In(i) + if isVariadic && i == paramCount-1 { + // The type of the final parameter in a variadic + // function is a slice of the declared type. Call + // Elem to get the declared type. + t = t.Elem() + } + + params[i] = newGoCallableParam(t) + } + + return params +} + +func (c *goCallable) SetContext(context reflect.Value) { + c.context = context +} + +func (c *goCallable) ParamCount() int { + return len(c.params) +} + +func (c *goCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + var err error + + argv, err = c.validateArgCount(argv) + if err != nil { + if err == jtypes.ErrUndefined { + err = nil + } + return undefined, err + } + + argv, err = c.validateArgTypes(argv) + if err != nil { + return undefined, err + } + + results := c.fn.Call(argv) + + if len(results) == 2 && !results[1].IsNil() { + err := results[1].Interface().(error) + if err == jtypes.ErrUndefined { + err = nil + } + return undefined, err + } + + return results[0], nil +} + +func (c *goCallable) validateArgCount(argv []reflect.Value) ([]reflect.Value, error) { + + argc := len(argv) + + if c.contextHandler != nil && c.contextHandler(argv) { + // TODO: Return an error if the evaluation context + // is not the correct type. + newargv := make([]reflect.Value, 1, len(argv)+1) + newargv[0] = c.context + argv = append(newargv, argv...) + } + + if c.undefinedHandler != nil && c.undefinedHandler(argv) { + // TODO: Validate the other arguments before doing + // this. Otherwise we mask errors with the other + // arguments. + return nil, jtypes.ErrUndefined + } + + paramCount := len(c.params) + + for i := len(argv); i < paramCount; i++ { + if !c.params[i].isOpt { + break + } + argv = append(argv, undefined) + } + + if c.isVariadic && len(argv) < paramCount-1 { + return nil, newArgCountError(c, argc) + } + + if !c.isVariadic && len(argv) != paramCount { + return nil, newArgCountError(c, argc) + } + + return argv, nil +} + +func (c *goCallable) validateArgTypes(argv []reflect.Value) ([]reflect.Value, error) { + + var ok bool + paramCount := len(c.params) + + for i, v := range argv { + + v = jtypes.Resolve(v) + + // The preceding call to Resolve dereferences pointers. + // This is fine for most types but we need to restore + // pointer type Callables. + if v.Kind() == reflect.Struct && + reflect.PointerTo(v.Type()).Implements(jtypes.TypeCallable) { + if v.CanAddr() { + v = v.Addr() + } + } + + j := i + // Variadic functions can have more arguments than + // parameters. Use the type of the final parameter + // to process any extra arguments. + if j >= paramCount { + j = paramCount - 1 + } + + v, ok = processGoCallableArg(v, c.params[j]) + if !ok { + return nil, newArgTypeError(c, i+1) + } + + argv[i] = v + } + + return argv, nil +} + +var ( + typeString = reflect.TypeOf((*string)(nil)).Elem() + typeByteSlice = reflect.TypeOf((*[]byte)(nil)).Elem() +) + +func processGoCallableArg(arg reflect.Value, param goCallableParam) (reflect.Value, bool) { + + if arg == undefined { + return processUndefinedArg(param) + } + + if param.isOpt { + return processOptionalArg(arg, param) + } + + if param.isVar { + return processVariantArg(arg, param) + } + + argType := arg.Type() + paramType := param.t + + switch { + case argType == paramType: + return arg, true + case argType.AssignableTo(paramType): + return arg, true + case paramType == jtypes.TypeValue: + return reflect.ValueOf(arg), true + case argType.ConvertibleTo(paramType): + // Only allow conversion to a string if the source type + // is a byte slice. Go can convert other types (such as + // integers) to strings but this is not supported in + // JSONata. + if paramType == typeString && argType != typeByteSlice { + break + } + return arg.Convert(paramType), true + case argType.Implements(jtypes.TypeConvertible): + if arg.CanInterface() { + return arg.Interface().(jtypes.Convertible).ConvertTo(paramType) + } + } + + return undefined, false +} + +func processUndefinedArg(param goCallableParam) (reflect.Value, bool) { + + switch { + case param.isOpt, param.t == jtypes.TypeInterface, param.t == jtypes.TypeValue: + return reflect.Zero(param.t), true + default: + return undefined, false + } +} + +func processOptionalArg(arg reflect.Value, param goCallableParam) (reflect.Value, bool) { + + v, ok := processGoCallableArg(arg, *param.optType) + if !ok { + return undefined, false + } + + opt := reflect.New(param.t).Interface().(jtypes.Optional) + opt.Set(v) + + return reflect.ValueOf(opt).Elem(), true +} + +func processVariantArg(arg reflect.Value, param goCallableParam) (reflect.Value, bool) { + + for _, t := range param.varTypes { + if v, ok := processGoCallableArg(arg, t); ok { + return reflect.ValueOf(v).Convert(param.t), true + } + } + + return undefined, false +} + +// A lambdaCallable represents a user-defined JSONata function +// created with the 'function' keyword. +type lambdaCallable struct { + callableName + callableMarshaler + body jparse.Node + paramNames []string + typed bool + params []jparse.Param + env *environment + context reflect.Value +} + +func (f *lambdaCallable) ParamCount() int { + return len(f.paramNames) +} + +func (f *lambdaCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + argv, err := f.validateArgs(argv) + if err != nil { + return undefined, err + } + + // Create a local scope for this function's arguments. + env := newEnvironment(f.env, len(f.paramNames)) + + // Add the function arguments to the local scope. + // If there are fewer arguments than parameter names, + // default unset parameters to undefined. If there + // are more arguments than parameter names, ignore + // the extraneous arguments. + for i, name := range f.paramNames { + + var v reflect.Value + + if i < len(argv) { + v = argv[i] + } + + env.bind(name, v) + } + + // Evaluate the function body. + return eval(f.body, f.context, env) +} + +func (f *lambdaCallable) validateArgs(argv []reflect.Value) ([]reflect.Value, error) { + + // An untyped lambda can take any number of arguments + // of any type. No further processing is required. + if !f.typed { + return argv, nil + } + + var err error + + if argv, err = f.validateArgCount(argv); err != nil { + return nil, err + } + + if argv, err = f.validateArgTypes(argv); err != nil { + return nil, err + } + + return f.wrapVariadicArgs(argv), nil +} + +func (f *lambdaCallable) validateArgCount(argv []reflect.Value) ([]reflect.Value, error) { + + // argc is the number of arguments originally passed to + // the function. + argc := len(argv) + + // paramCount is the number of parameters specified in + // the function's type signature. + paramCount := len(f.params) + + // If there are fewer arguments than parameters and the + // first parameter is contextable, insert the evaluation + // context into the argument list. + if argc < paramCount && f.params[0].Option == jparse.ParamContextable { + argv = append([]reflect.Value{f.context}, argv...) + } + + // If there are still fewer arguments than parameters and + // the missing arguments correspond to optional parameters, + // append undefined arguments to the argument list. + for i := len(argv); i < paramCount; i++ { + if f.params[i].Option != jparse.ParamOptional { + break + } + argv = append(argv, undefined) + } + + // argCount is the final number of arguments including + // any added by this method. + argCount := len(argv) + + // isVar indicates whether the function is variadic. + isVar := paramCount > 0 && + f.params[paramCount-1].Option == jparse.ParamVariadic + + // If there are a) fewer arguments than parameters or b) + // extra arguments on a non-variadic function, return an + // error. + if argCount < paramCount || (argCount > paramCount && !isVar) { + return nil, newArgCountError(f, argc) + } + + return argv, nil +} + +func (f *lambdaCallable) validateArgTypes(argv []reflect.Value) ([]reflect.Value, error) { + + paramCount := len(f.params) + + for i, arg := range argv { + + // Don't type check undefined arguments. + if arg == undefined { + continue + } + + var param jparse.Param + + if i < paramCount { + param = f.params[i] + } else if paramCount > 0 { + param = f.params[paramCount-1] + } + + // If a parameter is an array type, force the + // corresponding argument to be an array. + if param.Type == jparse.ParamTypeArray { + arg = arrayify(arg) + argv[i] = arg + } + + if !f.validArgType(arg, param) { + return nil, newArgTypeError(f, i+1) + } + } + + return argv, nil +} + +func (f *lambdaCallable) validArgType(arg reflect.Value, p jparse.Param) bool { + + typ := p.Type + + if typ&jparse.ParamTypeAny != 0 { + return true + } + + paramTypeJSON := typ&jparse.ParamTypeJSON != 0 + + // TODO: Handle ParamTypeNull + switch { + case jtypes.IsString(arg): + return paramTypeJSON || typ&jparse.ParamTypeString != 0 + case jtypes.IsNumber(arg): + return paramTypeJSON || typ&jparse.ParamTypeNumber != 0 + case jtypes.IsBool(arg): + return paramTypeJSON || typ&jparse.ParamTypeBool != 0 + case jtypes.IsCallable(arg): + return typ&jparse.ParamTypeFunc != 0 + case jtypes.IsArray(arg): + if paramTypeJSON { + return true + } + if typ&jparse.ParamTypeArray != 0 { + if len(p.SubParams) == 0 { + return true + } + return jtypes.IsArrayOf(arg, func(v reflect.Value) bool { + return f.validArgType(v, p.SubParams[0]) + }) + } + return false + case jtypes.IsMap(arg), jtypes.IsStruct(arg): + return paramTypeJSON || typ&jparse.ParamTypeObject != 0 + } + + return false +} + +func (f *lambdaCallable) wrapVariadicArgs(argv []reflect.Value) []reflect.Value { + + paramCount := len(f.params) + + if paramCount < 1 || + f.params[paramCount-1].Option != jparse.ParamVariadic { + return argv + } + + n := len(argv) - paramCount + 1 + vars := reflect.MakeSlice(typeInterfaceSlice, n, n) + + for i := 0; i < n; i++ { + vars.Index(i).Set(argv[paramCount-1+i]) + } + + return append(argv[:paramCount-1], vars) +} + +// A partialCallable represents the partial application of +// a Callable. +type partialCallable struct { + callableName + callableMarshaler + fn jtypes.Callable + args []jparse.Node + env *environment + context reflect.Value +} + +func (f *partialCallable) ParamCount() int { + + var count int + for _, arg := range f.args { + if _, ok := arg.(*jparse.PlaceholderNode); ok { + count++ + } + } + + return count +} + +func (f *partialCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + var err error + args := make([]reflect.Value, len(f.args)) + + for i, arg := range f.args { + + var v reflect.Value + + switch arg.(type) { + case *jparse.PlaceholderNode: + if len(argv) > 0 { + v = argv[0] + argv = argv[1:] + } + default: + v, err = eval(arg, f.context, f.env) + if err != nil { + return undefined, err + } + } + + args[i] = v + } + + return f.fn.Call(args) +} + +// A transformationCallable represents JSONata's object +// transformation operator. It's a function that takes an +// object and updates and/or removes the specified keys. +type transformationCallable struct { + callableName + callableMarshaler + pattern jparse.Node + updates jparse.Node + deletes jparse.Node + env *environment +} + +func (f *transformationCallable) ParamCount() int { + return 1 +} + +func (f *transformationCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + err := f.validateArgs(argv) + if err != nil { + return undefined, err + } + + obj, err := f.clone(argv[0]) + if err != nil { + return undefined, newEvalError(ErrClone, nil, nil) + } + + if obj == undefined { + return undefined, nil + } + + items, err := eval(f.pattern, obj, f.env) + if err != nil { + return undefined, err + } + + items = arrayify(items) + + for i := 0; i < items.Len(); i++ { + + item := jtypes.Resolve(items.Index(i)) + if !jtypes.IsMap(item) { + continue + } + + if err := f.updateEntries(item); err != nil { + return undefined, err + } + + if f.deletes != nil { + if err := f.deleteEntries(item); err != nil { + return undefined, err + } + } + } + + return obj, nil +} + +func (f *transformationCallable) validateArgs(argv []reflect.Value) error { + + if argc := len(argv); argc != 1 { + return newArgCountError(f, argc) + } + + if obj := argv[0]; obj.IsValid() && + !jtypes.IsMap(obj) && !jtypes.IsStruct(obj) && !jtypes.IsArray(obj) { + return newArgTypeError(f, 1) + } + + return nil +} + +func (f *transformationCallable) updateEntries(item reflect.Value) error { + + updates, err := eval(f.updates, item, f.env) + if err != nil || updates == undefined { + return err + } + + if !jtypes.IsMap(updates) { + return newEvalError(ErrIllegalUpdate, f.updates, nil) + } + + for _, key := range updates.MapKeys() { + item.SetMapIndex(key, updates.MapIndex(key)) + } + + return nil +} + +func (f *transformationCallable) deleteEntries(item reflect.Value) error { + + deletes, err := eval(f.deletes, item, f.env) + if err != nil || deletes == undefined { + return err + } + + deletes = arrayify(deletes) + + if !jtypes.IsArrayOf(deletes, jtypes.IsString) { + return newEvalError(ErrIllegalDelete, f.deletes, nil) + } + + for i := 0; i < deletes.Len(); i++ { + key := jtypes.Resolve(deletes.Index(i)) + item.SetMapIndex(key, undefined) + } + + return nil +} + +func (f *transformationCallable) clone(v reflect.Value) (reflect.Value, error) { + + if v == undefined { + return undefined, nil + } + + s, err := jlib.String(v.Interface()) + if err != nil { + return undefined, err + } + + var dest interface{} + d := json.NewDecoder(strings.NewReader(s)) + if err = d.Decode(&dest); err != nil { + return undefined, err + } + + return reflect.ValueOf(dest), nil +} + +// A regexCallable represents a JSONata regular expression. It's +// a function that takes a string argument and returns an object +// that describes the leftmost match. The object also contains +// a Callable that returns the next leftmost match (and so on). +// A return value of undefined signifies no more matches. +type regexCallable struct { + callableName + callableMarshaler + re *regexp.Regexp +} + +func newRegexCallable(re *regexp.Regexp) *regexCallable { + return ®exCallable{ + callableName: callableName{ + name: re.String(), + }, + re: re, + } +} + +func (f *regexCallable) ParamCount() int { + return 1 +} + +func (f *regexCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + if len(argv) < 1 { + return undefined, nil + } + + s, ok := jtypes.AsString(argv[0]) + if !ok { + return undefined, nil + } + + matches, indexes := f.findMatches(s) + return newMatchCallable(f.Name(), matches, indexes).Call(nil) +} + +var typeRegexPtr = reflect.TypeOf((*regexp.Regexp)(nil)) + +func (f *regexCallable) ConvertTo(t reflect.Type) (reflect.Value, bool) { + switch t { + case typeRegexPtr: + return reflect.ValueOf(f.re), true + default: + return undefined, false + } +} + +func (f *regexCallable) findMatches(s string) ([][]string, [][]int) { + + indexes := f.re.FindAllStringSubmatchIndex(s, -1) + if indexes == nil { + return nil, nil + } + + matches := make([][]string, len(indexes)) + + for i, index := range indexes { + + matches[i] = make([]string, len(index)/2) + + for j := range matches[i] { + + if index[j*2] < 0 { + // Negative indexes indicate capturing groups + // that don't match any text. Skip them. + continue + } + matches[i][j] = s[index[j*2]:index[j*2+1]] + } + } + + return matches, indexes +} + +// A matchCallable represents a regular expression match. Its +// Call method returns an object containing the details of the +// match, plus a Callable that returns the details of the next +// match. +type matchCallable struct { + callableName + callableMarshaler + match string + start int + end int + groups []string + next jtypes.Callable +} + +func newMatchCallable(name string, matches [][]string, indexes [][]int) jtypes.Callable { + + if len(matches) < 1 { + return &undefinedCallable{ + callableName: callableName{ + name: name, + }, + } + } + + return &matchCallable{ + callableName: callableName{ + name: name, + }, + match: matches[0][0], + start: indexes[0][0], + end: indexes[0][1], + groups: matches[0][1:], + next: newMatchCallable("next", matches[1:], indexes[1:]), + } +} + +func (f *matchCallable) Call([]reflect.Value) (reflect.Value, error) { + return reflect.ValueOf(map[string]interface{}{ + "match": f.match, + "start": f.start, + "end": f.end, + "groups": f.groups, + "next": f.next, + }), nil +} + +func (*matchCallable) ParamCount() int { + return 0 +} + +// An undefinedCallable is a Callable that always returns undefined. +type undefinedCallable struct { + callableName + callableMarshaler +} + +func (*undefinedCallable) Call([]reflect.Value) (reflect.Value, error) { + return undefined, nil +} + +func (*undefinedCallable) ParamCount() int { + return 0 +} + +// A chainCallable provides function composition. +type chainCallable struct { + callableName + callableMarshaler + callables []jtypes.Callable +} + +func (f *chainCallable) ParamCount() int { + return 1 +} + +func (f *chainCallable) Call(argv []reflect.Value) (reflect.Value, error) { + + var err error + var v reflect.Value + + if len(argv) > 0 { + v = argv[0] + } + + for _, fn := range f.callables { + + v, err = fn.Call([]reflect.Value{v}) + if err != nil { + return undefined, err + } + } + + return v, nil +} diff --git a/v1.5.4/callable_test.go b/v1.5.4/callable_test.go new file mode 100644 index 0000000..1df5356 --- /dev/null +++ b/v1.5.4/callable_test.go @@ -0,0 +1,2752 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "errors" + "math" + "reflect" + "regexp" + "sort" + "strings" + "testing" + + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +var ( + typeStringRegex = reflect.TypeOf((*stringRegex)(nil)).Elem() + typeOptionalString = reflect.TypeOf((*jtypes.OptionalString)(nil)).Elem() +) + +// stringRegex is a Variant type that accepts string +// or regex arguments. +type stringRegex reflect.Value + +func (sr stringRegex) ValidTypes() []reflect.Type { + return []reflect.Type{ + typeString, + typeRegexPtr, + } +} + +// badVariant1 is a Variant that is not derived from +// reflect.Value. +type badVariant1 struct{} + +func (badVariant1) ValidTypes() []reflect.Type { + return nil +} + +// badVariant2 is a Variant that has no valid jtypes. +type badVariant2 reflect.Value + +func (badVariant2) ValidTypes() []reflect.Type { + return nil +} + +// badVariant3 is a Variant that has an Optional valid type. +type badVariant3 reflect.Value + +func (badVariant3) ValidTypes() []reflect.Type { + return []reflect.Type{ + typeString, + typeOptionalString, + } +} + +// badVariant4 is a Variant that has a Variant valid type. +type badVariant4 reflect.Value + +func (badVariant4) ValidTypes() []reflect.Type { + return []reflect.Type{ + typeString, + typeStringRegex, + } +} + +// badOptional1 is a type that implements the Optional and +// Variant interfaces. +type badOptional1 struct{} + +func (badOptional1) IsSet() bool { return false } +func (badOptional1) Set(reflect.Value) {} +func (badOptional1) Type() reflect.Type { return typeString } +func (badOptional1) ValidTypes() []reflect.Type { return nil } + +// badOptional2 is an Optional with an Optional underlying type. +type badOptional2 struct{} + +func (badOptional2) IsSet() bool { return false } +func (badOptional2) Set(reflect.Value) {} +func (badOptional2) Type() reflect.Type { return typeOptionalString } + +type newGoCallableTest struct { + Name string + Func interface{} + Result *goCallable + Fail bool +} + +func TestNewGoCallable(t *testing.T) { + + typeInt := reflect.TypeOf((*int)(nil)).Elem() + + testNewGoCallable(t, []newGoCallableTest{ + { + // Error: Func is nil. + Name: "nil", + Fail: true, + }, + { + // Error: Func is not a function. + Name: "int", + Func: 100, + Fail: true, + }, + { + // Error: Function has 0 return values. + Name: "return0", + Func: func() {}, + Fail: true, + }, + { + // Error: Function has 2 return values but the second + // value is not an error. + Name: "nonerror", + Func: func() (int, int) { return 0, 0 }, + Fail: true, + }, + { + // Error: Function has 3 return values. + Name: "return3", + Func: func() (int, int, int) { return 0, 0, 0 }, + Fail: true, + }, + { + // Error: Parameter is an Optional and a Variant. + Name: "badOptional1", + Func: func(badOptional1) int { return 0 }, + Fail: true, + }, + { + // Error: Optional parameter has an Optional subtype. + Name: "badOptional2", + Func: func(badOptional2) int { return 0 }, + Fail: true, + }, + { + // Error: Optional parameter followed by a non-optional + // parameter. + Name: "optional_nonoptional", + Func: func(jtypes.OptionalString, string) int { return 0 }, + Fail: true, + }, + { + // Error: Variadic optional parameter. + Name: "variadic_optional", + Func: func(...jtypes.OptionalString) int { return 0 }, + Fail: true, + }, + { + // Error: Variant type not derived from reflect.Value. + Name: "badvariant1", + Func: func(badVariant1) int { return 0 }, + Fail: true, + }, + { + // Error: Variant does not return any valid jtypes. + Name: "badvariant2", + Func: func(badVariant2) int { return 0 }, + Fail: true, + }, + { + // Error: Variant has an Optional valid type. + Name: "badvariant3", + Func: func(badVariant3) int { return 0 }, + Fail: true, + }, + { + // Error: Variant has a Variant valid type. + Name: "badvariant4", + Func: func(badVariant4) int { return 0 }, + Fail: true, + }, + { + // Function with 1 return value. + Name: "return1", + Func: func() int { return 0 }, + Result: &goCallable{ + callableName: callableName{ + name: "return1", + }, + }, + }, + { + // Function with 2 return values. + Name: "return2", + Func: func() (int, error) { return 0, nil }, + Result: &goCallable{ + callableName: callableName{ + name: "return2", + }, + }, + }, + { + // Standard function. + Name: "standard", + Func: func(string, int) int { return 0 }, + Result: &goCallable{ + callableName: callableName{ + name: "standard", + }, + params: []goCallableParam{ + { + t: typeString, + }, + { + t: typeInt, + }, + }, + }, + }, + { + // Variadic function. + Name: "variadic", + Func: func(string, ...int) int { return 0 }, + Result: &goCallable{ + callableName: callableName{ + name: "variadic", + }, + params: []goCallableParam{ + { + t: typeString, + }, + { + t: typeInt, + }, + }, + isVariadic: true, + }, + }, + { + // Function with an Optional parameter. + Name: "optional", + Func: func(string, jtypes.OptionalString) int { return 0 }, + Result: &goCallable{ + callableName: callableName{ + name: "optional", + }, + params: []goCallableParam{ + { + t: typeString, + }, + { + t: typeOptionalString, + isOpt: true, + optType: &goCallableParam{ + t: typeString, + }, + }, + }, + }, + }, + { + // Func with a Variant parameter. + Name: "variant", + Func: func(string, stringRegex) int { return 0 }, + Result: &goCallable{ + callableName: callableName{ + name: "variant", + }, + params: []goCallableParam{ + { + t: typeString, + }, + { + t: typeStringRegex, + isVar: true, + varTypes: []goCallableParam{ + { + t: typeString, + }, + { + t: typeRegexPtr, + }, + }, + }, + }, + }, + }, + }) +} + +func testNewGoCallable(t *testing.T, tests []newGoCallableTest) { + + for _, test := range tests { + + res, err := newGoCallable(test.Name, Extension{ + Func: test.Func, + }) + + if res != nil { + res.fn = undefined + } + + if (err != nil) != test.Fail { + t.Errorf("%s: expected error %v, got %v", test.Name, test.Fail, err) + } + + if !reflect.DeepEqual(res, test.Result) { + t.Errorf("%s: expected %v, got %v", test.Name, test.Result, res) + } + } +} + +type goCallableTest struct { + Name string + Ext Extension + Context interface{} + Args []interface{} + Output interface{} + Error error + Undefined bool +} + +func TestGoCallable(t *testing.T) { + testGoCallable(t, []goCallableTest{ + { + // Error: Not enough arguments + Name: "argCount1", + Ext: Extension{ + Func: func(string, int) int { return 0 }, + }, + Args: []interface{}{ + "hello", + }, + Error: &ArgCountError{ + Func: "argCount1", + Expected: 2, + Received: 1, + }, + }, + { + // Error: Not enough arguments (variadic) + Name: "argCount2", + Ext: Extension{ + Func: func(string, ...int) int { return 0 }, + }, + Error: &ArgCountError{ + Func: "argCount2", + Expected: 2, + Received: 0, + }, + }, + { + // Error: Too many arguments + Name: "argCount3", + Ext: Extension{ + Func: func(string) int { return 0 }, + }, + Args: []interface{}{ + "hello", + "world", + }, + Error: &ArgCountError{ + Func: "argCount3", + Expected: 1, + Received: 2, + }, + }, + { + // Error: Bad type + Name: "argType1", + Ext: Extension{ + Func: func(string) int { return 0 }, + }, + Args: []interface{}{ + 65, + }, + Error: &ArgTypeError{ + Func: "argType1", + Which: 1, + }, + }, + { + // Error: Bad type (variadic) + Name: "argType2", + Ext: Extension{ + Func: func(...string) int { return 0 }, + }, + Args: []interface{}{ + "hello", + "world", + 3.14159, + }, + Error: &ArgTypeError{ + Func: "argType2", + Which: 3, + }, + }, + { + // Function returns an error + Name: "error", + Ext: Extension{ + Func: func() (int, error) { + return 0, errors.New("test error") + }, + }, + Error: errors.New("test error"), + }, + { + // Function returns jtypes.ErrUndefined + Name: "errUndefined", + Ext: Extension{ + Func: func() (int, error) { + return 0, jtypes.ErrUndefined + }, + }, + Undefined: true, + }, + { + // Extension with UndefinedHandler + Name: "undefinedHandler", + Ext: Extension{ + Func: func() int { return 0 }, + UndefinedHandler: func([]reflect.Value) bool { + return true + }, + }, + Undefined: true, + }, + { + // Standard Extension + Name: "repeat", + Ext: Extension{ + Func: func(s string, n int) string { + return strings.Repeat(s, n) + }, + }, + Args: []interface{}{ + "*", + 5, + }, + Output: "*****", + }, + { + // Extension with ContextHandler + Name: "contextHandler", + Ext: Extension{ + Func: func(s string, n int) string { + return strings.Repeat(s, n) + }, + EvalContextHandler: func(argv []reflect.Value) bool { + return len(argv) < 2 + }, + }, + Context: "x", + Args: []interface{}{ + 3, + }, + Output: "xxx", + }, + { + // Variadic function + Name: "variadic1", + Ext: Extension{ + Func: func(nums ...int) []int { + sort.Ints(nums) + return nums + }, + }, + Args: []interface{}{ + 3, + 0, + 2, + -1, + }, + Output: []int{ + -1, + 0, + 2, + 3, + }, + }, + { + // Variadic function (no variadic arguments) + Name: "variadic2", + Ext: Extension{ + Func: func(nums ...int) int { + return len(nums) + }, + }, + Output: 0, + }, + { + // Optional parameter (set) + Name: "optional_set", + Ext: Extension{ + Func: func(in jtypes.OptionalInt) interface{} { + return map[string]interface{}{ + "set": in.IsSet(), + "value": in.Int, + } + }, + }, + Args: []interface{}{ + 100.0, + }, + Output: map[string]interface{}{ + "set": true, + "value": 100, + }, + }, + { + // Optional parameter (not set) + Name: "optional_notset", + Ext: Extension{ + Func: func(in jtypes.OptionalInt) interface{} { + return map[string]interface{}{ + "set": in.IsSet(), + "value": in.Int, + } + }, + }, + Output: map[string]interface{}{ + "set": false, + "value": 0, + }, + }, + { + // Callable parameter + Name: "callable", + Ext: Extension{ + Func: func(f jtypes.Callable) string { + return f.Name() + }, + }, + Args: []interface{}{ + &undefinedCallable{ + callableName: callableName{ + name: "test", + }, + }, + }, + Output: "test", + }, + }) +} + +func testGoCallable(t *testing.T, tests []goCallableTest) { + + for _, test := range tests { + + var output interface{} + var argv []reflect.Value + + fn, err := newGoCallable(test.Name, test.Ext) + if err != nil { + t.Errorf("%s: newGoCallable returned %v", test.Name, err) + continue + } + + if test.Context != nil { + fn.SetContext(reflect.ValueOf(test.Context)) + } + + if argc := len(test.Args); argc > 0 { + argv = make([]reflect.Value, argc) + for i := range argv { + argv[i] = reflect.ValueOf(test.Args[i]) + } + } + + res, err := fn.Call(argv) + + if res.IsValid() && res.CanInterface() { + output = res.Interface() + } + + if test.Undefined { + if res != undefined { + t.Errorf("%s: expected undefined, got %v", test.Name, res) + } + } else { + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("%s: expected %v, got %v", test.Name, test.Output, output) + } + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: expected error %v, got %v", test.Name, test.Error, err) + } + } +} + +type goCallableArgTest struct { + Input interface{} + Param goCallableParam + Output interface{} + Compare func(interface{}, interface{}) bool + Fail bool +} + +func TestProcessGoCallableArg(t *testing.T) { + + re := regexp.MustCompile("ab+") + + typeInt := reflect.TypeOf((*int)(nil)).Elem() + typeFloat64 := reflect.TypeOf((*float64)(nil)).Elem() + typeBool := reflect.TypeOf((*bool)(nil)).Elem() + typeString := reflect.TypeOf((*string)(nil)).Elem() + typeByteSlice := reflect.TypeOf((*[]byte)(nil)).Elem() + + testProcessGoCallableArgs(t, []goCallableArgTest{ + + // int + + { + // int to int + Input: -1, + Param: goCallableParam{ + t: typeInt, + }, + Output: -1, + }, + { + // uint to int + Input: uint(100), + Param: goCallableParam{ + t: typeInt, + }, + Output: 100, + }, + { + // int64 to int + Input: int64(1e6), + Param: goCallableParam{ + t: typeInt, + }, + Output: 1000000, + }, + { + // float64 to int + Input: float64(1e12), + Param: goCallableParam{ + t: typeInt, + }, + Output: 1000000000000, + }, + { + // float64 to int (truncated) + Input: 3.141592, + Param: goCallableParam{ + t: typeInt, + }, + Output: 3, + }, + { + // undefined to int + Param: goCallableParam{ + t: typeInt, + }, + Fail: true, + }, + { + // invalid type to int + Input: struct{}{}, + Param: goCallableParam{ + t: typeInt, + }, + Fail: true, + }, + + // float64 + + { + // float64 to float64 + Input: 3.141592, + Param: goCallableParam{ + t: typeFloat64, + }, + Output: 3.141592, + }, + { + // int to float64 + Input: 100, + Param: goCallableParam{ + t: typeFloat64, + }, + Output: 100.0, + }, + { + // undefined to float64 + Param: goCallableParam{ + t: typeFloat64, + }, + Fail: true, + }, + { + // invalid type to float64 + Input: struct{}{}, + Param: goCallableParam{ + t: typeFloat64, + }, + Fail: true, + }, + + // bool + + { + // bool to bool + Input: true, + Param: goCallableParam{ + t: typeBool, + }, + Output: true, + }, + { + // undefined to bool + Param: goCallableParam{ + t: typeBool, + }, + Fail: true, + }, + { + // invalid type to bool + Input: struct{}{}, + Param: goCallableParam{ + t: typeBool, + }, + Fail: true, + }, + + // string + + { + // string to string + Input: "hello", + Param: goCallableParam{ + t: typeString, + }, + Output: "hello", + }, + { + // byte slice to string + Input: []byte("hello"), + Param: goCallableParam{ + t: typeString, + }, + Output: "hello", + }, + { + // int to string + // Note: Conversion from int to string (e.g. string(65)) + // is valid in Go but it isn't permitted in JSONata. + Input: 65, + Param: goCallableParam{ + t: typeString, + }, + Fail: true, + }, + { + // rune to string + // Note: Conversion from rune to string (e.g. string('a')) + // is valid in Go but it isn't permitted in JSONata. + Input: 'a', + Param: goCallableParam{ + t: typeString, + }, + Fail: true, + }, + { + // undefined to string + Param: goCallableParam{ + t: typeString, + }, + Fail: true, + }, + { + // invalid type to string + Input: struct{}{}, + Param: goCallableParam{ + t: typeString, + }, + Fail: true, + }, + + // byte slice + + { + // byte slice to byte slice + Input: []byte("hello"), + Param: goCallableParam{ + t: typeByteSlice, + }, + Output: []byte("hello"), + }, + { + // string to byte slice + Input: "hello", + Param: goCallableParam{ + t: typeByteSlice, + }, + Output: []byte("hello"), + }, + { + // undefined to byte slice + Param: goCallableParam{ + t: typeByteSlice, + }, + Fail: true, + }, + { + // invalid type to byte slice + Input: struct{}{}, + Param: goCallableParam{ + t: typeByteSlice, + }, + Fail: true, + }, + + // reflect.Value + // A Value parameter can accept any type of argument. + + { + Input: "hello", + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf("hello"), + }, + { + Input: -100, + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf(-100), + }, + { + Input: 3.141592, + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf(3.141592), + }, + { + Input: false, + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf(false), + }, + { + Input: []interface{}{}, + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf([]interface{}{}), + }, + { + Input: map[string]interface{}{}, + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Compare: equalReflectValue, + Output: reflect.ValueOf(map[string]interface{}{}), + }, + { + Param: goCallableParam{ + t: jtypes.TypeValue, + }, + Output: undefined, + }, + + // jtypes.Callable + + { + // Callable to Callable + Input: newRegexCallable(re), + Param: goCallableParam{ + t: jtypes.TypeCallable, + }, + Output: ®exCallable{ + callableName: callableName{ + name: "ab+", + }, + re: re, + }, + }, + + // jtypes.Optional + + { + // underlying type to Optional + Input: "hello", + Param: goCallableParam{ + t: typeOptionalString, + isOpt: true, + optType: &goCallableParam{ + t: typeString, + }, + }, + Output: jtypes.NewOptionalString("hello"), + }, + { + // undefined to Optional + Param: goCallableParam{ + t: typeOptionalString, + isOpt: true, + optType: &goCallableParam{ + t: typeString, + }, + }, + Output: jtypes.OptionalString{}, + }, + { + // invalid type to Optional + Input: struct{}{}, + Param: goCallableParam{ + t: typeOptionalString, + isOpt: true, + optType: &goCallableParam{ + t: typeString, + }, + }, + Fail: true, + }, + + // jtypes.Variant + + { + // supported type to Variant + Input: "hello", + Param: goCallableParam{ + t: typeStringRegex, + isVar: true, + varTypes: []goCallableParam{ + { + t: typeString, + }, + { + t: typeRegexPtr, + }, + }, + }, + Compare: equalStringRegex, + Output: stringRegex(reflect.ValueOf("hello")), + }, + { + // supported type to Variant + Input: re, + Param: goCallableParam{ + t: typeStringRegex, + isVar: true, + varTypes: []goCallableParam{ + { + t: typeString, + }, + { + t: typeRegexPtr, + }, + }, + }, + Compare: equalStringRegex, + Output: stringRegex(reflect.ValueOf(re)), + }, + { + // unsupported type to Variant + Input: 65, + Param: goCallableParam{ + t: typeStringRegex, + isVar: true, + varTypes: []goCallableParam{ + { + t: typeString, + }, + { + t: typeRegexPtr, + }, + }, + }, + Fail: true, + }, + { + // undefined to Variant + Param: goCallableParam{ + t: typeStringRegex, + isVar: true, + varTypes: []goCallableParam{ + { + t: typeString, + }, + { + t: typeRegexPtr, + }, + }, + }, + Fail: true, + }, + + // jtypes.Convertible + + { + // Convertible to supported type + Input: newRegexCallable(re), + Param: goCallableParam{ + t: typeRegexPtr, + }, + Output: re, + }, + { + // Convertible to unsupported type + Input: newRegexCallable(re), + Param: goCallableParam{ + t: typeString, + }, + Fail: true, + }, + }) +} + +func testProcessGoCallableArgs(t *testing.T, tests []goCallableArgTest) { + + for _, test := range tests { + + res, ok := processGoCallableArg(reflect.ValueOf(test.Input), test.Param) + + var output interface{} + if res.IsValid() && res.CanInterface() { + output = res.Interface() + } + + isEqual := test.Compare + if isEqual == nil { + isEqual = reflect.DeepEqual + } + + if !isEqual(output, test.Output) { + t.Errorf("%v => %s: expected %v, got %v", test.Input, test.Param.t, test.Output, res) + } + + if ok == test.Fail { + t.Errorf("%v => %s: expected OK %v, got %v", test.Input, test.Param.t, !test.Fail, ok) + } + } +} + +func equalStringRegex(in1, in2 interface{}) bool { + + v1, ok := in1.(stringRegex) + if !ok { + return false + } + + v2, ok := in2.(stringRegex) + if !ok { + return false + } + + return reflect.DeepEqual(reflect.Value(v1).Interface(), reflect.Value(v2).Interface()) +} + +func equalReflectValue(in1, in2 interface{}) bool { + + v1, ok := in1.(reflect.Value) + if !ok { + return false + } + + v2, ok := in2.(reflect.Value) + if !ok { + return false + } + + return reflect.DeepEqual(v1.Interface(), v2.Interface()) +} + +type lambdaCallableTest struct { + Name string + Typed bool + Params []jparse.Param + Body jparse.Node + ParamNames []string + Args []interface{} + Vars map[string]interface{} + Context interface{} + Output interface{} + Error error + Undefined bool +} + +func TestLambdaCallable(t *testing.T) { + testLambdaCallable(t, []lambdaCallableTest{ + { + Name: "multiply", + Body: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "y", + }, + }, + ParamNames: []string{ + "x", + "y", + }, + Args: []interface{}{ + 3.6, + 100, + }, + Output: float64(360), + }, + { + // Lambdas can access values from the containing scope. + Name: "global", + Body: &jparse.VariableNode{ + Name: "x", + }, + Vars: map[string]interface{}{ + "x": "marks the spot", + }, + Output: "marks the spot", + }, + { + // Lambdas also have their own local scope. Lambda + // arguments shadow values from the containing scope. + Name: "local", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Args: []interface{}{ + 100, + }, + Vars: map[string]interface{}{ + "x": "marks the spot", + }, + Output: 100, + }, + { + // Shadowing occurs even when the argument is not + // defined by the caller. + Name: "undefined", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Vars: map[string]interface{}{ + "x": "marks the spot", + }, + Undefined: true, + }, + { + // Typed lambda, no params, no args. + Name: "typed", + Body: &jparse.NumberNode{}, + Typed: true, + Output: float64(0), + }, + { + // Typed lambda, no params, some args. + Name: "typed", + Body: &jparse.NumberNode{}, + Typed: true, + Args: []interface{}{ + 1, + 2, + }, + Error: &ArgCountError{ + Func: "typed", + Expected: 0, + Received: 2, + }, + }, + { + // Typed lambda, contextable first argument. + Name: "context", + Body: &jparse.StringConcatenationNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "y", + }, + }, + ParamNames: []string{ + "x", + "y", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeAny, + Option: jparse.ParamContextable, + }, + { + Type: jparse.ParamTypeAny, + }, + }, + Args: []interface{}{ + "world", + }, + Context: "hello", + Output: "helloworld", + }, + { + // Typed lambda, optional argument. + Name: "argcount", + Body: &jparse.VariableNode{ + Name: "y", + }, + ParamNames: []string{ + "x", + "y", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeAny, + }, + { + Type: jparse.ParamTypeAny, + Option: jparse.ParamOptional, + }, + }, + Args: []interface{}{ + 100, + }, + Undefined: true, + }, + { + // Typed lambda, not enough arguments. + Name: "fewer", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + "y", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeAny, + }, + { + Type: jparse.ParamTypeAny, + }, + }, + Args: []interface{}{ + 0, + }, + Error: &ArgCountError{ + Func: "fewer", + Expected: 2, + Received: 1, + }, + }, + { + // Typed lambda, too many arguments. + Name: "greater", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + "y", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeAny, + }, + { + Type: jparse.ParamTypeAny, + }, + }, + Args: []interface{}{ + 0, + 1, + 2, + }, + Error: &ArgCountError{ + Func: "greater", + Expected: 2, + Received: 3, + }, + }, + { + // Typed lambda, valid string argument. + Name: "string1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + }, + Args: []interface{}{ + "hello", + }, + Output: "hello", + }, + { + // Typed lambda, invalid string argument. + Name: "string2", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + }, + Args: []interface{}{ + 0, + }, + Error: &ArgTypeError{ + Func: "string2", + Which: 1, + }, + }, + { + // Typed lambda, valid number argument. + Name: "number1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeNumber, + }, + }, + Args: []interface{}{ + 100, + }, + Output: 100, + }, + { + // Typed lambda, invalid number argument. + Name: "number2", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeNumber, + }, + }, + Args: []interface{}{ + false, + }, + Error: &ArgTypeError{ + Func: "number2", + Which: 1, + }, + }, + { + // Typed lambda, valid boolean argument. + Name: "boolean1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeBool, + }, + }, + Args: []interface{}{ + true, + }, + Output: true, + }, + { + // Typed lambda, invalid boolean argument. + Name: "boolean2", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeBool, + }, + }, + Args: []interface{}{ + null, + }, + Error: &ArgTypeError{ + Func: "boolean2", + Which: 1, + }, + }, + { + // Typed lambda, valid Callable argument. + Name: "callable1", + Body: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "x", + }, + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeFunc, + }, + }, + Args: []interface{}{ + &undefinedCallable{}, + }, + Undefined: true, + }, + { + // Typed lambda, invalid Callable argument. + Name: "callable2", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeFunc, + }, + }, + Args: []interface{}{ + "hello", + }, + Error: &ArgTypeError{ + Func: "callable2", + Which: 1, + }, + }, + { + // Typed lambda, valid array argument. + Name: "array1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeArray, + }, + }, + Args: []interface{}{ + []interface{}{ + 1, + 2, + 3, + }, + }, + Output: []interface{}{ + 1, + 2, + 3, + }, + }, + { + // Typed lambda, non-array argument converted to + // an array. + Name: "array2", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeArray, + }, + }, + Args: []interface{}{ + "hello", + }, + Output: []interface{}{ + "hello", + }, + }, + { + // Typed lambda, valid typed array argument. + Name: "array3", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeArray, + SubParams: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + }, + }, + }, + Args: []interface{}{ + []interface{}{ + "hello", + "world", + }, + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + { + // Typed lambda, invalid typed array argument. + Name: "array4", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeArray, + SubParams: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + }, + }, + }, + Args: []interface{}{ + []interface{}{ + 0, + 1, + }, + }, + Error: &ArgTypeError{ + Func: "array4", + Which: 1, + }, + }, + { + // Typed lambda, invalid array argument. + Name: "array5", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + }, + Args: []interface{}{ + []interface{}{ + "hello", + "world", + }, + }, + Error: &ArgTypeError{ + Func: "array5", + Which: 1, + }, + }, + { + // Typed lambda, valid object (map) argument. + Name: "object1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeObject, + }, + }, + Args: []interface{}{ + map[string]interface{}{ + "x": "marks the spot", + }, + }, + Output: map[string]interface{}{ + "x": "marks the spot", + }, + }, + { + // Typed lambda, valid object (struct) argument. + Name: "object2", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeObject, + }, + }, + Args: []interface{}{ + struct { + x string + y int + z bool + }{ + x: "hello", + y: 100, + z: true, + }, + }, + Output: struct { + x string + y int + z bool + }{ + x: "hello", + y: 100, + z: true, + }, + }, + { + // Typed lambda, invalid object argument. + Name: "object3", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeObject, + }, + }, + Args: []interface{}{ + "hello", + }, + Error: &ArgTypeError{ + Func: "object3", + Which: 1, + }, + }, + { + // Typed lambda, valid JSON arguments. + Name: "json1", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "s", + "n", + "b", + "a", + "o", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeJSON, + }, + { + Type: jparse.ParamTypeJSON, + }, + { + Type: jparse.ParamTypeJSON, + }, + { + Type: jparse.ParamTypeJSON, + }, + { + Type: jparse.ParamTypeJSON, + }, + }, + Args: []interface{}{ + "hello", + 100, + true, + []interface{}{}, + map[string]interface{}{}, + }, + Output: float64(0), + }, + { + // Typed lambda, invalid JSON argument. + Name: "json2", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeJSON, + }, + }, + Args: []interface{}{ + undefinedCallable{}, + }, + Error: &ArgTypeError{ + Func: "json2", + Which: 1, + }, + }, + { + // Typed lambda, valid variadic argument. + Name: "variadic1", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + Option: jparse.ParamVariadic, + }, + }, + Args: []interface{}{ + "john", + "paul", + "ringo", + "george", + }, + Output: []interface{}{ + "john", + "paul", + "ringo", + "george", + }, + }, + { + // Typed lambda, valid variadic argument. + Name: "variadic2", + Body: &jparse.VariableNode{ + Name: "x", + }, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeNumber, + Option: jparse.ParamVariadic, + }, + }, + Args: []interface{}{ + 100, + }, + Output: []interface{}{ + 100, + }, + }, + { + // Typed lambda, invalid variadic argument. + Name: "variadic3", + Body: &jparse.NumberNode{}, + ParamNames: []string{ + "x", + }, + Typed: true, + Params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + Option: jparse.ParamVariadic, + }, + }, + Args: []interface{}{ + "john", + "paul", + "ringo", + false, + }, + Error: &ArgTypeError{ + Func: "variadic3", + Which: 4, + }, + }, + }) +} + +func testLambdaCallable(t *testing.T, tests []lambdaCallableTest) { + + for i, test := range tests { + + env := newEnvironment(nil, len(test.Vars)) + for name, v := range test.Vars { + env.bind(name, reflect.ValueOf(v)) + } + + f := &lambdaCallable{ + callableName: callableName{ + name: test.Name, + }, + body: test.Body, + paramNames: test.ParamNames, + typed: test.Typed, + params: test.Params, + env: env, + context: reflect.ValueOf(test.Context), + } + + var args []reflect.Value + for _, arg := range test.Args { + args = append(args, reflect.ValueOf(arg)) + } + + v, err := f.Call(args) + + var output interface{} + if v.IsValid() && v.CanInterface() { + output = v.Interface() + } + + if test.Undefined { + if v != undefined { + t.Errorf("lambda %d: expected undefined, got %v", i+1, v) + } + } else { + if !reflect.DeepEqual(test.Output, output) { + t.Errorf("lambda %d: expected %v, got %v", i+1, test.Output, output) + } + } + + if !reflect.DeepEqual(test.Error, err) { + t.Errorf("lambda %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type partialCallableTest struct { + Name string + Func jtypes.Callable + FuncArgs []jparse.Node + Args []reflect.Value + Output interface{} + Error error +} + +func TestPartialCallableTest(t *testing.T) { + testPartialCallableTest(t, []partialCallableTest{ + { + Func: &lambdaCallable{ + body: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "y", + }, + }, + paramNames: []string{ + "x", + "y", + }, + }, + FuncArgs: []jparse.Node{ + &jparse.NumberNode{ + Value: 2, + }, + &jparse.PlaceholderNode{}, + }, + Args: []reflect.Value{ + reflect.ValueOf(6), + }, + Output: float64(12), + }, + { + // Error evaluating argument in partial definition. + // Return the error. + Func: &lambdaCallable{ + body: &jparse.NumberNode{}, + paramNames: []string{ + "x", + "y", + }, + }, + FuncArgs: []jparse.Node{ + &jparse.PlaceholderNode{}, + &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + }) +} + +func testPartialCallableTest(t *testing.T, tests []partialCallableTest) { + + for i, test := range tests { + + name := test.Name + if name == "" { + name = "partial" + } + + f := &partialCallable{ + callableName: callableName{ + name: name, + }, + fn: test.Func, + args: test.FuncArgs, + } + + v, err := f.Call(test.Args) + + var output interface{} + if v.IsValid() && v.CanInterface() { + output = v.Interface() + } + + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("partial %d: expected %v, got %v", i+1, test.Output, v) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("partial %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type transformationCallableTest struct { + Name string + Pattern jparse.Node + Updates jparse.Node + Deletes jparse.Node + Input interface{} + Output interface{} + Error error + Undefined bool +} + +func TestTransformationCallable(t *testing.T) { + + data := []map[string]interface{}{ + { + "value": 1, + "en": "one", + "es": "uno", + }, + { + "value": 2, + "en": "two", + "es": "dos", + }, + { + "value": 3, + "en": "three", + "es": "tres", + }, + { + "value": 4, + "en": "four", + "es": "cuatro", + }, + { + "value": 5, + "en": "five", + "es": "cinco", + }, + } + + testTransformationCallable(t, []transformationCallableTest{ + { + // Update only. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "en", + }, + &jparse.NameNode{ + Value: "es", + }, + }, + { + &jparse.StringNode{ + Value: "es", + }, + &jparse.NameNode{ + Value: "en", + }, + }, + }, + }, + Input: data, + Output: []interface{}{ + map[string]interface{}{ + "value": float64(1), + "es": "one", + "en": "uno", + }, + map[string]interface{}{ + "value": float64(2), + "es": "two", + "en": "dos", + }, + map[string]interface{}{ + "value": float64(3), + "es": "three", + "en": "tres", + }, + map[string]interface{}{ + "value": float64(4), + "es": "four", + "en": "cuatro", + }, + map[string]interface{}{ + "value": float64(5), + "es": "five", + "en": "cinco", + }, + }, + }, + { + // Delete only (single value). + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Deletes: &jparse.StringNode{ + Value: "es", + }, + Input: data, + Output: []interface{}{ + map[string]interface{}{ + "value": float64(1), + "en": "one", + }, + map[string]interface{}{ + "value": float64(2), + "en": "two", + }, + map[string]interface{}{ + "value": float64(3), + "en": "three", + }, + map[string]interface{}{ + "value": float64(4), + "en": "four", + }, + map[string]interface{}{ + "value": float64(5), + "en": "five", + }, + }, + }, + { + // Delete only (multiple values). + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "en", + }, + &jparse.StringNode{ + Value: "es", + }, + }, + }, + Input: data, + Output: []interface{}{ + map[string]interface{}{ + "value": float64(1), + }, + map[string]interface{}{ + "value": float64(2), + }, + map[string]interface{}{ + "value": float64(3), + }, + map[string]interface{}{ + "value": float64(4), + }, + map[string]interface{}{ + "value": float64(5), + }, + }, + }, + { + // Update and delete. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.NameNode{ + Value: "en", + }, + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + Deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "en", + }, + &jparse.StringNode{ + Value: "es", + }, + &jparse.StringNode{ + Value: "value", + }, + }, + }, + Input: data, + Output: []interface{}{ + map[string]interface{}{ + "one": float64(1), + }, + map[string]interface{}{ + "two": float64(2), + }, + map[string]interface{}{ + "three": float64(3), + }, + map[string]interface{}{ + "four": float64(4), + }, + map[string]interface{}{ + "five": float64(5), + }, + }, + }, + { + // Non-transformable input. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.StringNode{ + Value: "value", + }, + }, + }, + }, + Input: []int{ + 1, + 2, + 3, + }, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + { + // Arg count <> 1. Return error. + Name: "noinput", + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Error: &ArgCountError{ + Func: "noinput", + Expected: 1, + Received: 0, + }, + }, + { + // Non-cloneable input. Return error. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Input: []float64{math.NaN()}, + Error: &EvalError{ + Type: ErrClone, + }, + }, + { + // Non-object/array input. + Name: "nonobject", + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Input: "hello world", + Error: &ArgTypeError{ + Func: "nonobject", + Which: 1, + }, + }, + { + // Undefined input. Return undefined. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Input: undefined, + Undefined: true, + }, + { + // Error evaluating pattern. Return the error. + Pattern: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + Updates: &jparse.ObjectNode{}, + Input: data, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Error evaluating updates. Return the error. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + Input: data, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Non-object updates. Return an error. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ArrayNode{}, + Input: data, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: "[]", + }, + }, + { + // Error evaluating deletes. Return the error. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Deletes: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + Input: data, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Non-string deletes. Return an error. + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Deletes: &jparse.NullNode{}, + Input: data, + Error: &EvalError{ + Type: ErrIllegalDelete, + Token: "null", + }, + }, + }) +} + +func testTransformationCallable(t *testing.T, tests []transformationCallableTest) { + + for i, test := range tests { + + name := test.Name + if name == "" { + name = "transform" + } + + f := &transformationCallable{ + callableName: callableName{ + name: name, + }, + pattern: test.Pattern, + updates: test.Updates, + deletes: test.Deletes, + env: newEnvironment(nil, 0), + } + + var argv []reflect.Value + switch v := test.Input.(type) { + case nil: + case reflect.Value: + argv = append(argv, v) + default: + argv = append(argv, reflect.ValueOf(v)) + } + + v, err := f.Call(argv) + + var output interface{} + if v.IsValid() && v.CanInterface() { + output = v.Interface() + } + + if test.Undefined { + if v != undefined { + t.Errorf("transform %d: expected undefined, got %v", i+1, v) + } + } else { + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("transform %d: expected %v, got %v", i+1, test.Output, v) + } + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("transform %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type regexCallableTest struct { + Expr string + Input interface{} + Results interface{} + Undefined bool +} + +func TestRegexCallable(t *testing.T) { + testRegexCallable(t, []regexCallableTest{ + { + // No input. Return undefined. + Expr: "a.", + Undefined: true, + }, + { + // Non-string input. Return undefined. + Expr: "a.", + Input: 100, + Undefined: true, + }, + { + // No matches. Return undefined. + Expr: "a.", + Input: "hello world", + Undefined: true, + }, + { + // Matches with no capturing groups. + Expr: "a.?", + Input: "abracadabra", + Results: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{}, + "next": &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ac", + start: 3, + end: 5, + groups: []string{}, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ad", + start: 5, + end: 7, + groups: []string{}, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ab", + start: 7, + end: 9, + groups: []string{}, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "a", + start: 10, + end: 11, + groups: []string{}, + next: &undefinedCallable{ + callableName: callableName{ + name: "next", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Matches with capturing groups. + Expr: "a(.?)", + Input: "abracadabra", + Results: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{ + "b", + }, + "next": &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ac", + start: 3, + end: 5, + groups: []string{ + "c", + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ad", + start: 5, + end: 7, + groups: []string{ + "d", + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ab", + start: 7, + end: 9, + groups: []string{ + "b", + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "a", + start: 10, + end: 11, + groups: []string{ + "", + }, + next: &undefinedCallable{ + callableName: callableName{ + name: "next", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Matches with capturing groups (some unmatched). + // Note that capturing groups that don't match any text + // are represented by empty strings (unlike jsonata-js + // which uses undefined). + Expr: "(a.)|(a)", + Input: "abracadabra", + Results: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{ + "ab", + "", // undefined in jsonata-js + }, + "next": &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ac", + start: 3, + end: 5, + groups: []string{ + "ac", + "", // undefined in jsonata-js + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ad", + start: 5, + end: 7, + groups: []string{ + "ad", + "", // undefined in jsonata-js + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "ab", + start: 7, + end: 9, + groups: []string{ + "ab", + "", // undefined in jsonata-js + }, + next: &matchCallable{ + callableName: callableName{ + "next", + }, + match: "a", + start: 10, + end: 11, + groups: []string{ + "", // undefined in jsonata-js + "a", + }, + next: &undefinedCallable{ + callableName: callableName{ + name: "next", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Match on a non-ASCII string. + // Note that the start and end values are byte offsets. + // This means that a) they won't necessarily match the + // jsonata-js offsets (e.g. smiley face emoji are only + // 2 bytes long in JavaScript) and b) they won't play + // well with JSONata functions that use rune offsets + // such as $substring. + Expr: "😀", + Input: "😂😁😀", + Results: map[string]interface{}{ + "match": "😀", + "start": 8, // 4 in jsonata-js + "end": 12, // 6 in jsonata-js + "groups": []string{}, + "next": &undefinedCallable{ + callableName: callableName{ + name: "next", + }, + }, + }, + }, + }) +} + +func testRegexCallable(t *testing.T, tests []regexCallableTest) { + + for _, test := range tests { + + var argv []reflect.Value + if test.Input != nil { + argv = append(argv, reflect.ValueOf(test.Input)) + } + + re := regexp.MustCompile(test.Expr) + v, err := newRegexCallable(re).Call(argv) + if err != nil { + t.Errorf("%s (%q): %s", test.Expr, test.Input, err) + } + + if test.Undefined { + if v != undefined { + t.Errorf("%s: expected undefined result, got %v", test.Expr, v) + } + continue + } + + var results interface{} + if v.IsValid() && v.CanInterface() { + results = v.Interface() + } + + if !reflect.DeepEqual(results, test.Results) { + t.Errorf("%s: expected results %v, got %v", test.Expr, test.Results, results) + } + } +} + +func TestCallableParamCount(t *testing.T) { + + typeInt := reflect.TypeOf((*int)(nil)).Elem() + + tests := []struct { + Callable jtypes.Callable + Count int + }{ + { + // goCallable, 0 parameters. + Callable: &goCallable{ + callableName: callableName{ + name: "goCallable0", + }, + }, + Count: 0, + }, + { + // goCallable, 1 parameter. + Callable: &goCallable{ + callableName: callableName{ + name: "goCallable1", + }, + params: []goCallableParam{ + { + t: typeInt, + }, + }, + }, + Count: 1, + }, + { + // lambdaCallable, 0 parameters. + Callable: &lambdaCallable{ + callableName: callableName{ + name: "lambdaCallable0", + }, + body: &jparse.NumberNode{}, + }, + Count: 0, + }, + { + // lambdaCallable, 2 parameters. + Callable: &lambdaCallable{ + callableName: callableName{ + name: "lambdaCallable2", + }, + body: &jparse.NumberNode{}, + paramNames: []string{ + "x", + "y", + }, + }, + Count: 2, + }, + { + // partialCallable, 0 parameters. + Callable: &partialCallable{ + callableName: callableName{ + name: "partialCallable0", + }, + fn: &undefinedCallable{}, + }, + Count: 0, + }, + { + // partialCallable, 1 (placeholder) parameter. + Callable: &partialCallable{ + callableName: callableName{ + name: "partialCallable1", + }, + fn: &undefinedCallable{}, + args: []jparse.Node{ + &jparse.StringNode{}, + &jparse.PlaceholderNode{}, + &jparse.NumberNode{}, + }, + }, + Count: 1, + }, + { + // All transformationCallables take 1 parameter. + Callable: &transformationCallable{ + callableName: callableName{ + name: "transformationCallable", + }, + pattern: &jparse.VariableNode{}, + updates: &jparse.ObjectNode{}, + }, + Count: 1, + }, + { + // All regexCallables take 1 parameter. + Callable: ®exCallable{ + callableName: callableName{ + name: "regexCallable", + }, + re: regexp.MustCompile("ab"), + }, + Count: 1, + }, + { + // All matchCallables take 0 parameters. + Callable: &matchCallable{ + callableName: callableName{ + name: "matchCallable", + }, + match: "ab", + start: 0, + end: 2, + next: &undefinedCallable{ + callableName: callableName{ + name: "next", + }, + }, + }, + Count: 0, + }, + { + // All undefinedCallables take 0 parameters. + Callable: &undefinedCallable{ + callableName: callableName{ + name: "undefinedCallable", + }, + }, + Count: 0, + }, + { + // All chainCallables take 1 parameter. + Callable: &chainCallable{ + callableName: callableName{ + name: "chainCallable", + }, + callables: []jtypes.Callable{ + &undefinedCallable{}, + &undefinedCallable{}, + }, + }, + Count: 1, + }, + } + + for _, test := range tests { + if count := test.Callable.ParamCount(); count != test.Count { + t.Errorf("%s: expected ParamCount %d, got %d", test.Callable.Name(), test.Count, count) + } + } +} diff --git a/v1.5.4/doc.go b/v1.5.4/doc.go new file mode 100644 index 0000000..d97d1fc --- /dev/null +++ b/v1.5.4/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package jsonata is a query and transformation language for JSON. +// It's a Go port of the JavaScript library JSONata. Please use the +// official JSONata site as a language reference. +// +// http://jsonata.org/ +package jsonata diff --git a/v1.5.4/env.go b/v1.5.4/env.go new file mode 100644 index 0000000..2259d65 --- /dev/null +++ b/v1.5.4/env.go @@ -0,0 +1,539 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "errors" + "math" + "reflect" + "strings" + "unicode/utf8" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type environment struct { + parent *environment + symbols map[string]reflect.Value +} + +func newEnvironment(parent *environment, size int) *environment { + return &environment{ + parent: parent, + symbols: make(map[string]reflect.Value, size), + } +} + +func (s *environment) bind(name string, value reflect.Value) { + if s.symbols == nil { + s.symbols = make(map[string]reflect.Value) + } + s.symbols[name] = value +} + +func (s *environment) bindAll(values map[string]reflect.Value) { + + if len(values) == 0 { + return + } + + for name, value := range values { + s.bind(name, value) + } +} + +func (s *environment) lookup(name string) reflect.Value { + + if v, ok := s.symbols[name]; ok { + return v + } + if s.parent != nil { + return s.parent.lookup(name) + } + + return undefined +} + +var ( + defaultUndefinedHandler = jtypes.ArgUndefined(0) + defaultContextHandler = jtypes.ArgCountEquals(0) + + argCountEquals1 = jtypes.ArgCountEquals(1) +) + +var baseEnv = initBaseEnv(map[string]Extension{ + + // String functions + + "string": { + Func: jlib.String, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "length": { + Func: utf8.RuneCountInString, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "substring": { + Func: jlib.Substring, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerSubstring, + }, + "substringBefore": { + Func: jlib.SubstringBefore, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerSubstringBeforeAfter, + }, + "substringAfter": { + Func: jlib.SubstringAfter, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerSubstringBeforeAfter, + }, + "uppercase": { + Func: strings.ToUpper, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "lowercase": { + Func: strings.ToLower, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "pad": { + Func: jlib.Pad, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerPad, + }, + "trim": { + Func: jlib.Trim, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "contains": { + Func: jlib.Contains, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: argCountEquals1, + }, + "split": { + Func: jlib.Split, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerSplit, + }, + "join": { + Func: jlib.Join, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "match": { + Func: jlib.Match, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerMatch, + }, + "replace": { + Func: jlib.Replace, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerReplace, + }, + "formatNumber": { + Func: jlib.FormatNumber, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: contextHandlerFormatNumber, + }, + "formatBase": { + Func: jlib.FormatBase, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "base64encode": { + Func: jlib.Base64Encode, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "base64decode": { + Func: jlib.Base64Decode, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "decodeUrl": { + Func: jlib.DecodeURL, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "decodeUrlComponent": { + Func: jlib.DecodeURL, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "encodeUrl": { + Func: jlib.EncodeURL, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "encodeUrlComponent": { + Func: jlib.EncodeURLComponent, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + + // Number functions + + "number": { + Func: jlib.Number, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "abs": { + Func: math.Abs, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "floor": { + Func: math.Floor, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "ceil": { + Func: math.Ceil, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "round": { + Func: jlib.Round, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "power": { + Func: jlib.Power, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: argCountEquals1, + }, + "sqrt": { + Func: jlib.Sqrt, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "random": { + Func: jlib.Random, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, + + // Number aggregation functions + + "sum": { + Func: jlib.Sum, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "max": { + Func: jlib.Max, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "min": { + Func: jlib.Min, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "average": { + Func: jlib.Average, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + + // Boolean functions + + "boolean": { + Func: jlib.Boolean, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "not": { + Func: jlib.Not, + UndefinedHandler: nil, + EvalContextHandler: defaultContextHandler, + }, + "exists": { + Func: jlib.Exists, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, + + // Array functions + + "distinct": { + Func: jlib.Distinct, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, + "count": { + Func: jlib.Count, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, + "reverse": { + Func: jlib.Reverse, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "sort": { + Func: jlib.Sort, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "shuffle": { + Func: jlib.Shuffle, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "zip": { + Func: jlib.Zip, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, + "append": { + Func: jlib.Append, + UndefinedHandler: undefinedHandlerAppend, + EvalContextHandler: nil, + }, + "map": { + Func: jlib.Map, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "filter": { + Func: jlib.Filter, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "reduce": { + Func: jlib.Reduce, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + "single": { + Func: jlib.Single, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + + // Object functions + + "each": { + Func: jlib.Each, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "sift": { + Func: jlib.Sift, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: argCountEquals1, + }, + "keys": { + Func: jlib.Keys, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "lookup": { + Func: lookup, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "spread": { + Func: jlib.Spread, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "merge": { + Func: jlib.Merge, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: nil, + }, + + // Date functions + // The date functions $now and $millis are not included + // in the base environment because they use the current + // time. They're added to the evaluation environment at + // runtime. + + "fromMillis": { + Func: jlib.FromMillis, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + "toMillis": { + Func: jlib.ToMillis, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + + "type": { + Func: jlib.TypeOf, + UndefinedHandler: defaultUndefinedHandler, + EvalContextHandler: defaultContextHandler, + }, + + // Misc functions + + "error": { + Func: throw, + UndefinedHandler: nil, + EvalContextHandler: nil, + }, +}) + +func initBaseEnv(exts map[string]Extension) *environment { + + env := newEnvironment(nil, len(exts)) + + for name, ext := range exts { + fn := mustGoCallable(name, ext) + env.bind(name, reflect.ValueOf(fn)) + } + + return env +} + +func mustGoCallable(name string, ext Extension) *goCallable { + + callable, err := newGoCallable(name, ext) + if err != nil { + panicf("%s is not a valid function: %s", name, err) + } + + return callable +} + +// Local functions (not from external packages) + +func lookup(v reflect.Value, name string) (interface{}, error) { + + res, err := evalName(&jparse.NameNode{Value: name}, v, nil) + if err != nil { + return nil, err + } + + if seq, ok := asSequence(res); ok { + res = seq.Value() + } + + if res.IsValid() && res.CanInterface() { + return res.Interface(), nil + } + + return nil, nil +} + +func throw(msg string) (interface{}, error) { + return nil, errors.New(msg) +} + +// Undefined handlers + +func undefinedHandlerAppend(argv []reflect.Value) bool { + return len(argv) == 2 && argv[0] == undefined && argv[1] == undefined +} + +// Context handlers + +func contextHandlerSubstring(argv []reflect.Value) bool { + + // If substring() is called with one or two numeric arguments, + // use the evaluation context as the first argument. + switch len(argv) { + case 1: + return jtypes.IsNumber(argv[0]) + case 2: + return jtypes.IsNumber(argv[0]) && jtypes.IsNumber(argv[1]) + default: + return false + } +} + +func contextHandlerSubstringBeforeAfter(argv []reflect.Value) bool { + + // If subStringBefore() or subStringAfter() are called with + // one string argument, use the evaluation context as the first + // argument. + return len(argv) == 1 && jtypes.IsString(argv[0]) +} + +func contextHandlerPad(argv []reflect.Value) bool { + + // If pad() is called with a single number, or a number and + // a string, use the evaluation context as the first argument. + switch len(argv) { + case 1: + return jtypes.IsNumber(argv[0]) + case 2: + return jtypes.IsNumber(argv[0]) && jtypes.IsString(argv[1]) + default: + return false + } +} + +func contextHandlerSplit(argv []reflect.Value) bool { + + // If split() is called with a single string/regex, or a + // string/regex and a number, use the evaluation context as + // the first argument. + switch len(argv) { + case 1: + return isStringOrCallable(argv[0]) + case 2: + return isStringOrCallable(argv[0]) && jtypes.IsNumber(argv[1]) + default: + return false + } +} + +func contextHandlerMatch(argv []reflect.Value) bool { + + // If match() is called with a single regex, or a regex and + // a number, use the evaluation context as the first argument. + switch len(argv) { + case 1: + return jtypes.IsCallable(argv[0]) + case 2: + return jtypes.IsCallable(argv[0]) && jtypes.IsNumber(argv[1]) + default: + return false + } +} + +func contextHandlerReplace(argv []reflect.Value) bool { + + // If replace() is called with a string/regex and a string/Callable, + // or a string/regex, a string/Callable, and a number, use the + // evaluation context as the first argument. + switch len(argv) { + case 2: + return isStringOrCallable(argv[0]) && isStringOrCallable(argv[1]) + case 3: + return isStringOrCallable(argv[0]) && isStringOrCallable(argv[1]) && jtypes.IsNumber(argv[2]) + default: + return false + } +} + +func contextHandlerFormatNumber(argv []reflect.Value) bool { + + // If formatNumber() is called with one or two arguments, and + // the first argument is a string, use the evaluation context + // as the first argument. + switch len(argv) { + case 1, 2: + return jtypes.IsString(argv[0]) + default: + return false + } +} + +func isStringOrCallable(v reflect.Value) bool { + return jtypes.IsString(v) || jtypes.IsCallable(v) +} diff --git a/v1.5.4/error.go b/v1.5.4/error.go new file mode 100644 index 0000000..8bd1bfc --- /dev/null +++ b/v1.5.4/error.go @@ -0,0 +1,162 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "errors" + "fmt" + "regexp" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// ErrUndefined is returned by the evaluation methods when +// a JSONata expression yields no results. Unlike most errors, +// ErrUndefined does not mean that evaluation failed. +// +// The simplest way to trigger ErrUndefined is to look up a +// field that is not present in the JSON data. Many JSONata +// operators and functions also return ErrUndefined when +// called with undefined inputs. +var ErrUndefined = errors.New("no results found") + +// ErrType indicates the reason for an error. +type ErrType uint + +// Types of errors that may be encountered by JSONata. +const ( + ErrNonIntegerLHS ErrType = iota + ErrNonIntegerRHS + ErrNonNumberLHS + ErrNonNumberRHS + ErrNonComparableLHS + ErrNonComparableRHS + ErrTypeMismatch + ErrNonCallable + ErrNonCallableApply + ErrNonCallablePartial + ErrNumberInf + ErrNumberNaN + ErrMaxRangeItems + ErrIllegalKey + ErrDuplicateKey + ErrClone + ErrIllegalUpdate + ErrIllegalDelete + ErrNonSortable + ErrSortMismatch +) + +var errmsgs = map[ErrType]string{ + ErrNonIntegerLHS: `left side of the "{{value}}" operator must evaluate to an integer`, + ErrNonIntegerRHS: `right side of the "{{value}}" operator must evaluate to an integer`, + ErrNonNumberLHS: `left side of the "{{value}}" operator must evaluate to a number`, + ErrNonNumberRHS: `right side of the "{{value}}" operator must evaluate to a number`, + ErrNonComparableLHS: `left side of the "{{value}}" operator must evaluate to a number or string`, + ErrNonComparableRHS: `right side of the "{{value}}" operator must evaluate to a number or string`, + ErrTypeMismatch: `both sides of the "{{value}}" operator must have the same type`, + ErrNonCallable: `cannot call non-function {{token}}`, + ErrNonCallableApply: `cannot use function application with non-function {{token}}`, + ErrNonCallablePartial: `cannot partially apply non-function {{token}}`, + ErrNumberInf: `result of the "{{value}}" operator is out of range`, + ErrNumberNaN: `result of the "{{value}}" operator is not a valid number`, + ErrMaxRangeItems: `range operator has too many items`, + ErrIllegalKey: `object key {{token}} does not evaluate to a string`, + ErrDuplicateKey: `multiple object keys evaluate to the value "{{value}}"`, + ErrClone: `object transformation: cannot make a copy of the object`, + ErrIllegalUpdate: `the insert/update clause of an object transformation must evaluate to an object`, + ErrIllegalDelete: `the delete clause of an object transformation must evaluate to an array of strings`, + ErrNonSortable: `expressions in a sort term must evaluate to strings or numbers`, + ErrSortMismatch: `expressions in a sort term must have the same type`, +} + +var reErrMsg = regexp.MustCompile("{{(token|value)}}") + +// An EvalError represents an error during evaluation of a +// JSONata expression. +type EvalError struct { + Type ErrType + Token string + Value string +} + +func newEvalError(typ ErrType, token interface{}, value interface{}) *EvalError { + + stringify := func(v interface{}) string { + switch v := v.(type) { + case string: + return v + case fmt.Stringer: + return v.String() + default: + return "" + } + } + + return &EvalError{ + Type: typ, + Token: stringify(token), + Value: stringify(value), + } +} + +func (e EvalError) Error() string { + + s := errmsgs[e.Type] + if s == "" { + return fmt.Sprintf("EvalError: unknown error type %d", e.Type) + } + + return reErrMsg.ReplaceAllStringFunc(s, func(match string) string { + switch match { + case "{{token}}": + return e.Token + case "{{value}}": + return e.Value + default: + return match + } + }) +} + +// ArgCountError is returned by the evaluation methods when an +// expression contains a function call with the wrong number of +// arguments. +type ArgCountError struct { + Func string + Expected int + Received int +} + +func newArgCountError(f jtypes.Callable, received int) *ArgCountError { + return &ArgCountError{ + Func: f.Name(), + Expected: f.ParamCount(), + Received: received, + } +} + +func (e ArgCountError) Error() string { + return fmt.Sprintf("function %q takes %d argument(s), got %d", e.Func, e.Expected, e.Received) +} + +// ArgTypeError is returned by the evaluation methods when an +// expression contains a function call with the wrong argument +// type. +type ArgTypeError struct { + Func string + Which int +} + +func newArgTypeError(f jtypes.Callable, which int) *ArgTypeError { + return &ArgTypeError{ + Func: f.Name(), + Which: which, + } +} + +func (e ArgTypeError) Error() string { + return fmt.Sprintf("argument %d of function %q does not match function signature", e.Which, e.Func) +} diff --git a/v1.5.4/eval.go b/v1.5.4/eval.go new file mode 100644 index 0000000..a51cfea --- /dev/null +++ b/v1.5.4/eval.go @@ -0,0 +1,1363 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "fmt" + "math" + "reflect" + "sort" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +var undefined reflect.Value + +var typeInterfaceSlice = reflect.SliceOf(jtypes.TypeInterface) + +func eval(node jparse.Node, input reflect.Value, env *environment) (reflect.Value, error) { + var err error + var v reflect.Value + + switch node := node.(type) { + case *jparse.StringNode: + v, err = evalString(node, input, env) + case *jparse.NumberNode: + v, err = evalNumber(node, input, env) + case *jparse.BooleanNode: + v, err = evalBoolean(node, input, env) + case *jparse.NullNode: + v, err = evalNull(node, input, env) + case *jparse.RegexNode: + v, err = evalRegex(node, input, env) + case *jparse.VariableNode: + v, err = evalVariable(node, input, env) + case *jparse.NameNode: + v, err = evalName(node, input, env) + case *jparse.PathNode: + v, err = evalPath(node, input, env) + case *jparse.NegationNode: + v, err = evalNegation(node, input, env) + case *jparse.RangeNode: + v, err = evalRange(node, input, env) + case *jparse.ArrayNode: + v, err = evalArray(node, input, env) + case *jparse.ObjectNode: + v, err = evalObject(node, input, env) + case *jparse.BlockNode: + v, err = evalBlock(node, input, env) + case *jparse.ConditionalNode: + v, err = evalConditional(node, input, env) + case *jparse.AssignmentNode: + v, err = evalAssignment(node, input, env) + case *jparse.WildcardNode: + v, err = evalWildcard(node, input, env) + case *jparse.DescendentNode: + v, err = evalDescendent(node, input, env) + case *jparse.GroupNode: + v, err = evalGroup(node, input, env) + case *jparse.PredicateNode: + v, err = evalPredicate(node, input, env) + case *jparse.SortNode: + v, err = evalSort(node, input, env) + case *jparse.LambdaNode: + v, err = evalLambda(node, input, env) + case *jparse.TypedLambdaNode: + v, err = evalTypedLambda(node, input, env) + case *jparse.ObjectTransformationNode: + v, err = evalObjectTransformation(node, input, env) + case *jparse.PartialNode: + v, err = evalPartial(node, input, env) + case *jparse.FunctionCallNode: + v, err = evalFunctionCall(node, input, env) + case *jparse.FunctionApplicationNode: + v, err = evalFunctionApplication(node, input, env) + case *jparse.NumericOperatorNode: + v, err = evalNumericOperator(node, input, env) + case *jparse.ComparisonOperatorNode: + v, err = evalComparisonOperator(node, input, env) + case *jparse.BooleanOperatorNode: + v, err = evalBooleanOperator(node, input, env) + case *jparse.StringConcatenationNode: + v, err = evalStringConcatenation(node, input, env) + default: + panicf("eval: unexpected node type %T", node) + } + + if err != nil { + return undefined, err + } + + if seq, ok := asSequence(v); ok { + v = seq.Value() + } + + return v, nil +} + +func evalString(node *jparse.StringNode, data reflect.Value, env *environment) (reflect.Value, error) { + return reflect.ValueOf(node.Value), nil +} + +func evalNumber(node *jparse.NumberNode, data reflect.Value, env *environment) (reflect.Value, error) { + return reflect.ValueOf(node.Value), nil +} + +func evalBoolean(node *jparse.BooleanNode, data reflect.Value, env *environment) (reflect.Value, error) { + return reflect.ValueOf(node.Value), nil +} + +var null *interface{} + +func evalNull(node *jparse.NullNode, data reflect.Value, env *environment) (reflect.Value, error) { + return reflect.ValueOf(null), nil +} + +func evalRegex(node *jparse.RegexNode, data reflect.Value, env *environment) (reflect.Value, error) { + return reflect.ValueOf(newRegexCallable(node.Value)), nil +} + +func evalVariable(node *jparse.VariableNode, data reflect.Value, env *environment) (reflect.Value, error) { + if node.Name == "" { + return data, nil + } + return env.lookup(node.Name), nil +} + +func evalName(node *jparse.NameNode, data reflect.Value, env *environment) (reflect.Value, error) { + var err error + var v reflect.Value + + data = jtypes.Resolve(data) + + switch { + case jtypes.IsStruct(data): + v = data.FieldByName(node.Value) + case jtypes.IsMap(data): + v = data.MapIndex(reflect.ValueOf(node.Value)) + case jtypes.IsArray(data): + v, err = evalNameArray(node, data, env) + default: + return undefined, nil + } + + return v, err +} + +func evalNameArray(node *jparse.NameNode, data reflect.Value, env *environment) (reflect.Value, error) { + n := data.Len() + results := newSequence(n) + + for i := 0; i < n; i++ { + + v, err := evalName(node, data.Index(i), env) + if err != nil { + return undefined, err + } + + if v.IsValid() && v.CanInterface() { + results.Append(v.Interface()) + } + } + + return reflect.ValueOf(results), nil +} + +func evalPath(node *jparse.PathNode, data reflect.Value, env *environment) (reflect.Value, error) { + if len(node.Steps) == 0 { + return undefined, nil + } + + var isVar bool + switch step0 := node.Steps[0].(type) { + case (*jparse.VariableNode): + isVar = true + case (*jparse.PredicateNode): + _, isVar = step0.Expr.(*jparse.VariableNode) + } + + output := data + if isVar || !jtypes.IsArray(data) { + output = reflect.MakeSlice(typeInterfaceSlice, 1, 1) + if data.IsValid() { + output.Index(0).Set(data) + } + } + + var err error + lastIndex := len(node.Steps) - 1 + for i, step := range node.Steps { + + if step0, ok := step.(*jparse.ArrayNode); ok && i == 0 { + output, err = eval(step0, output, env) + } else { + output, err = evalPathStep(step, output, env, i == lastIndex) + } + + if err != nil || output == undefined { + return undefined, err + } + + if jtypes.IsArray(output) && jtypes.Resolve(output).Len() == 0 { + return undefined, nil + } + } + + if node.KeepArrays { + if seq, ok := asSequence(output); ok { + seq.keepSingletons = true + return reflect.ValueOf(seq), nil + } + } + + return output, nil +} + +func evalPathStep(step jparse.Node, data reflect.Value, env *environment, lastStep bool) (reflect.Value, error) { + var err error + var results []reflect.Value + + if seq, ok := asSequence(data); ok { + results, err = evalOverSequence(step, seq, env) + } else { + results, err = evalOverArray(step, data, env) + } + + if err != nil { + return undefined, err + } + + if lastStep && len(results) == 1 && jtypes.IsArray(results[0]) { + return results[0], nil + } + + _, isCons := step.(*jparse.ArrayNode) + resultSequence := newSequence(len(results)) + + for _, v := range results { + + if isCons || !jtypes.IsArray(v) { + if v.CanInterface() { + resultSequence.Append(v.Interface()) + } + continue + } + + v = arrayify(v) + for i, N := 0, v.Len(); i < N; i++ { + if vi := v.Index(i); vi.IsValid() && vi.CanInterface() { + resultSequence.Append(vi.Interface()) + } + } + } + + if resultSequence.Len() == 0 { + return undefined, nil + } + + return reflect.ValueOf(resultSequence), nil +} + +func evalOverArray(node jparse.Node, data reflect.Value, env *environment) ([]reflect.Value, error) { + var results []reflect.Value + + for i, N := 0, data.Len(); i < N; i++ { + + res, err := eval(node, data.Index(i), env) + if err != nil { + return nil, err + } + + if res.IsValid() { + if results == nil { + results = make([]reflect.Value, 0, N) + } + results = append(results, res) + } + } + + return results, nil +} + +func evalOverSequence(node jparse.Node, seq *sequence, env *environment) ([]reflect.Value, error) { + var results []reflect.Value + + for i, N := 0, len(seq.values); i < N; i++ { + + res, err := eval(node, reflect.ValueOf(seq.values[i]), env) + if err != nil { + return nil, err + } + + if res.IsValid() { + if results == nil { + results = make([]reflect.Value, 0, N) + } + results = append(results, res) + } + } + + return results, nil +} + +func evalNegation(node *jparse.NegationNode, data reflect.Value, env *environment) (reflect.Value, error) { + rhs, err := eval(node.RHS, data, env) + if err != nil || rhs == undefined { + return undefined, err + } + + n, ok := jtypes.AsNumber(rhs) + if !ok { + return undefined, newEvalError(ErrNonNumberRHS, node.RHS, "-") + } + + return reflect.ValueOf(-n), nil +} + +// maxRangeItems is the maximum array size allowed in a range +// expression. It's defined as a global so we can use it in +// the tests. +// We use the maximum value allowed by the jsonata-js library +const maxRangeItems = 10000000 + +func isInteger(x float64) bool { + return x == math.Trunc(x) +} + +func evalRange(node *jparse.RangeNode, data reflect.Value, env *environment) (reflect.Value, error) { + evaluate := func(node jparse.Node) (float64, bool, bool, error) { + + v, err := eval(node, data, env) + if err != nil || v == undefined { + return 0, false, false, err + } + + n, isNum := jtypes.AsNumber(v) + return n, true, isNum && isInteger(n), nil + } + + // Evaluate both sides and return any errors. + lhs, lhsOK, lhsInteger, err := evaluate(node.LHS) + if err != nil { + return undefined, err + } + + rhs, rhsOK, rhsInteger, err := evaluate(node.RHS) + if err != nil { + return undefined, err + } + + // If either side is not an integer, return an error. + if lhsOK && !lhsInteger { + return undefined, newEvalError(ErrNonIntegerLHS, node.LHS, "..") + } + + if rhsOK && !rhsInteger { + return undefined, newEvalError(ErrNonIntegerRHS, node.RHS, "..") + } + + // If either side is undefined or the left side is greater + // than the right, return undefined. + if !lhsOK || !rhsOK || lhs > rhs { + return undefined, nil + } + + size := int(rhs-lhs) + 1 + // Check for integer overflow or an array size that exceeds + // our upper bound. + if size < 0 || size > maxRangeItems { + return undefined, newEvalError(ErrMaxRangeItems, "..", nil) + } + + results := reflect.MakeSlice(typeInterfaceSlice, size, size) + + for i := 0; i < size; i++ { + results.Index(i).Set(reflect.ValueOf(lhs)) + lhs++ + } + + return results, nil +} + +func evalArray(node *jparse.ArrayNode, data reflect.Value, env *environment) (reflect.Value, error) { + // Create a slice with capacity equal to the number of items + // in the ArrayNode. Note that the final length of the array + // may differ because: + // + // 1. Items that evaluate to undefined are excluded, reducing + // the length of the array. + // + // 2. Items that evaluate to arrays may be flattened into their + // individual elements, increasing the length of the array. + results := make([]interface{}, 0, len(node.Items)) + + for _, item := range node.Items { + + v, err := eval(item, data, env) + if err != nil { + return undefined, err + } + + if v == undefined { + continue + } + + switch item.(type) { + case *jparse.ArrayNode: + if v.CanInterface() { + results = append(results, v.Interface()) + } + default: + v = arrayify(v) + for i, N := 0, v.Len(); i < N; i++ { + if vi := v.Index(i); vi.IsValid() && vi.CanInterface() { + results = append(results, vi.Interface()) + } + } + } + } + + return reflect.ValueOf(results), nil +} + +func evalObject(node *jparse.ObjectNode, data reflect.Value, env *environment) (reflect.Value, error) { + data = makeArray(data) + + keys, err := groupItemsByKey(node, data, env) + if err != nil { + return undefined, err + } + + nItems := data.Len() + results := make(map[string]interface{}, len(keys)) + + for key, idx := range keys { + + items := data + if n := len(idx.items); n != 0 && n != nItems { + items = reflect.MakeSlice(typeInterfaceSlice, n, n) + for i, j := range idx.items { + items.Index(i).Set(data.Index(j)) + } + } + + value, err := eval(node.Pairs[idx.pair][1], items, env) + if err != nil { + return undefined, err + } + + if value.IsValid() && value.CanInterface() { + results[key] = value.Interface() + } + } + + return reflect.ValueOf(results), nil +} + +type keyIndexes struct { + pair int + items []int +} + +func groupItemsByKey(obj *jparse.ObjectNode, items reflect.Value, env *environment) (map[string]keyIndexes, error) { + nItems := items.Len() + results := make(map[string]keyIndexes, len(obj.Pairs)) + + for i, pair := range obj.Pairs { + + keyNode := pair[0] + + if s, ok := keyNode.(*jparse.StringNode); ok { + + key := s.Value + if _, ok := results[key]; ok { + return nil, newEvalError(ErrDuplicateKey, keyNode, key) + } + + results[key] = keyIndexes{ + pair: i, + } + continue + } + + for j := 0; j < nItems; j++ { + + v, err := eval(keyNode, items.Index(j), env) + if err != nil { + return nil, err + } + + key, ok := jtypes.AsString(v) + if !ok { + return nil, newEvalError(ErrIllegalKey, keyNode, nil) + } + + idx, ok := results[key] + if !ok { + results[key] = keyIndexes{ + pair: i, + items: []int{j}, + } + continue + } + + if idx.pair != i { + return nil, newEvalError(ErrDuplicateKey, keyNode, key) + } + + idx.items = append(idx.items, j) + results[key] = idx + } + } + + return results, nil +} + +func evalBlock(node *jparse.BlockNode, data reflect.Value, env *environment) (reflect.Value, error) { + var err error + var res reflect.Value + + // Create a local environment. Any variables defined + // inside the block will be scoped to the block. + // TODO: Is it worth calculating how many variables + // are defined in the block so that we can create an + // environment of the correct size? + env = newEnvironment(env, 0) + + // Evaluate all expressions in the block. + for _, node := range node.Exprs { + res, err = eval(node, data, env) + if err != nil { + return undefined, err + } + } + + // Return the result of the last expression. + return res, nil +} + +func evalConditional(node *jparse.ConditionalNode, data reflect.Value, env *environment) (reflect.Value, error) { + v, err := eval(node.If, data, env) + if err != nil { + return undefined, err + } + + if jlib.Boolean(v) { + return eval(node.Then, data, env) + } + + if node.Else != nil { + return eval(node.Else, data, env) + } + + return undefined, nil +} + +func evalAssignment(node *jparse.AssignmentNode, data reflect.Value, env *environment) (reflect.Value, error) { + v, err := eval(node.Value, data, env) + if err != nil { + return undefined, err + } + + env.bind(node.Name, v) + return v, nil +} + +func evalWildcard(node *jparse.WildcardNode, data reflect.Value, env *environment) (reflect.Value, error) { + results := newSequence(0) + + walkObjectValues(data, func(v reflect.Value) { + appendWildcard(results, v) + }) + + return reflect.ValueOf(results), nil +} + +func appendWildcard(seq *sequence, v reflect.Value) { + switch { + case jtypes.IsArray(v): + v = flattenArray(v) + for i, N := 0, v.Len(); i < N; i++ { + if vi := v.Index(i); vi.IsValid() && vi.CanInterface() { + seq.Append(vi.Interface()) + } + } + default: + if v.IsValid() && v.CanInterface() { + seq.Append(v.Interface()) + } + } +} + +func evalDescendent(node *jparse.DescendentNode, data reflect.Value, env *environment) (reflect.Value, error) { + results := newSequence(0) + + recurseDescendents(results, data) + + return reflect.ValueOf(results), nil +} + +func recurseDescendents(seq *sequence, v reflect.Value) { + if v.IsValid() && v.CanInterface() && !jtypes.IsArray(v) { + seq.Append(v.Interface()) + } + + walkObjectValues(v, func(v reflect.Value) { + recurseDescendents(seq, v) + }) +} + +func evalGroup(node *jparse.GroupNode, data reflect.Value, env *environment) (reflect.Value, error) { + items, err := eval(node.Expr, data, env) + if err != nil { + return undefined, err + } + + return evalObject(node.ObjectNode, items, env) +} + +func evalPredicate(node *jparse.PredicateNode, data reflect.Value, env *environment) (reflect.Value, error) { + items, err := eval(node.Expr, data, env) + if err != nil || items == undefined { + return undefined, err + } + + for _, filter := range node.Filters { + + // TODO: If this filter is of type *jparse.NumberNode, + // we should access the indexed item directly instead + // of calling applyFilter. + + items, err = applyFilter(filter, arrayify(items), env) + if err != nil { + return undefined, err + } + + if items.Len() == 0 { + items = undefined + break + } + } + + return normalizeArray(items), nil +} + +func applyFilter(filter jparse.Node, items reflect.Value, env *environment) (reflect.Value, error) { + nItems := items.Len() + results := reflect.MakeSlice(typeInterfaceSlice, 0, 0) + + for i := 0; i < nItems; i++ { + + item := items.Index(i) + + res, err := eval(filter, item, env) + if err != nil { + return undefined, err + } + + if jtypes.IsNumber(res) { + res = arrayify(res) + } + + switch { + case jtypes.IsArrayOf(res, jtypes.IsNumber): + for j, N := 0, res.Len(); j < N; j++ { + + n, _ := jtypes.AsNumber(res.Index(j)) + index := int(math.Floor(n)) + if index < 0 { + index += nItems + } + + if index == i { + results = reflect.Append(results, item) + } + } + case jlib.Boolean(res): + results = reflect.Append(results, item) + } + } + + return results, nil +} + +type sortinfo struct { + index int + values []reflect.Value +} + +func buildSortInfo(items reflect.Value, terms []jparse.SortTerm, env *environment) ([]*sortinfo, error) { + info := make([]*sortinfo, items.Len()) + + isNumberTerm := make([]bool, len(terms)) + isStringTerm := make([]bool, len(terms)) + + for i, N := 0, items.Len(); i < N; i++ { + + item := items.Index(i) + values := make([]reflect.Value, len(terms)) + + for j, term := range terms { + + v, err := eval(term.Expr, item, env) + if err != nil { + return nil, err + } + + if v == undefined { + continue + } + + switch { + case jtypes.IsNumber(v): + if isStringTerm[j] { + return nil, newEvalError(ErrSortMismatch, term.Expr, nil) + } + values[j] = v + isNumberTerm[j] = true + + case jtypes.IsString(v): + if isNumberTerm[j] { + return nil, newEvalError(ErrSortMismatch, term.Expr, nil) + } + values[j] = v + isStringTerm[j] = true + + default: + return nil, newEvalError(ErrNonSortable, term.Expr, nil) + } + } + + info[i] = &sortinfo{ + index: i, + values: values, + } + } + + return info, nil +} + +func makeLessFunc(info []*sortinfo, terms []jparse.SortTerm) func(int, int) bool { + return func(i, j int) bool { + Loop: + for t, term := range terms { + + vi := info[i].values[t] + vj := info[j].values[t] + + switch { + case vi == undefined && vj == undefined: + continue Loop + case vi == undefined: + return false + case vj == undefined: + return true + } + + if eq(vi, vj) { + continue Loop + } + + if term.Dir == jparse.SortDescending { + return lt(vj, vi) + } + return lt(vi, vj) + } + + return false + } +} + +func evalSort(node *jparse.SortNode, data reflect.Value, env *environment) (reflect.Value, error) { + items, err := eval(node.Expr, data, env) + if err != nil || items == undefined { + return undefined, err + } + + items = arrayify(items) + + info, err := buildSortInfo(items, node.Terms, env) + if err != nil { + return undefined, err + } + + sort.SliceStable(info, makeLessFunc(info, node.Terms)) + + results := reflect.MakeSlice(typeInterfaceSlice, len(info), len(info)) + + for i := range info { + results.Index(i).Set(items.Index(info[i].index)) + } + + return normalizeArray(results), nil +} + +func evalLambda(node *jparse.LambdaNode, data reflect.Value, env *environment) (reflect.Value, error) { + f := &lambdaCallable{ + callableName: callableName{ + name: "lambda", + }, + paramNames: node.ParamNames, + body: node.Body, + context: data, + env: env, + } + + return reflect.ValueOf(f), nil +} + +func evalTypedLambda(node *jparse.TypedLambdaNode, data reflect.Value, env *environment) (reflect.Value, error) { + f := &lambdaCallable{ + callableName: callableName{ + name: "lambda", + }, + typed: true, + params: node.In, + paramNames: node.ParamNames, + body: node.Body, + context: data, + env: env, + } + + return reflect.ValueOf(f), nil +} + +func evalObjectTransformation(node *jparse.ObjectTransformationNode, data reflect.Value, env *environment) (reflect.Value, error) { + f := &transformationCallable{ + callableName: callableName{ + "transform", + }, + pattern: node.Pattern, + updates: node.Updates, + deletes: node.Deletes, + env: env, + } + + return reflect.ValueOf(f), nil +} + +func evalPartial(node *jparse.PartialNode, data reflect.Value, env *environment) (reflect.Value, error) { + v, err := eval(node.Func, data, env) + if err != nil { + return undefined, err + } + + fn, ok := jtypes.AsCallable(v) + if !ok { + return undefined, newEvalError(ErrNonCallablePartial, node.Func, nil) + } + + f := &partialCallable{ + callableName: callableName{ + name: fn.Name() + "_partial", + }, + fn: fn, + args: node.Args, + context: data, + env: env, + } + + return reflect.ValueOf(f), nil +} + +type nameSetter interface { + SetName(string) +} + +type contextSetter interface { + SetContext(reflect.Value) +} + +func evalFunctionCall(node *jparse.FunctionCallNode, data reflect.Value, env *environment) (reflect.Value, error) { + v, err := eval(node.Func, data, env) + if err != nil { + return undefined, err + } + + fn, ok := jtypes.AsCallable(v) + if !ok { + return undefined, newEvalError(ErrNonCallable, node.Func, nil) + } + + if setter, ok := fn.(nameSetter); ok { + if sym, ok := node.Func.(*jparse.VariableNode); ok { + setter.SetName(sym.Name) + } + } + + if setter, ok := fn.(contextSetter); ok { + setter.SetContext(data) + } + + argv := make([]reflect.Value, len(node.Args)) + for i, arg := range node.Args { + + v, err := eval(arg, data, env) + if err != nil { + return undefined, err + } + + argv[i] = v + } + + return fn.Call(argv) +} + +func evalFunctionApplication(node *jparse.FunctionApplicationNode, data reflect.Value, env *environment) (reflect.Value, error) { + // If the right hand side is a function call, insert + // the left hand side into the argument list and + // evaluate it. + if f, ok := node.RHS.(*jparse.FunctionCallNode); ok { + + f.Args = append([]jparse.Node{node.LHS}, f.Args...) + return evalFunctionCall(f, data, env) + } + + // Evaluate both sides and return any errors. + lhs, err := eval(node.LHS, data, env) + if err != nil { + return undefined, err + } + + rhs, err := eval(node.RHS, data, env) + if err != nil { + return undefined, err + } + + // Check that the right hand side is callable. + f2, ok := jtypes.AsCallable(rhs) + if !ok { + return undefined, newEvalError(ErrNonCallableApply, node.RHS, "~>") + } + + // If the left hand side is not callable, call the right + // hand side using the left hand side as the argument. + if !jtypes.IsCallable(lhs) { + return f2.Call([]reflect.Value{lhs}) + } + + // Otherwise, combine both sides into a single callable. + f1, _ := jtypes.AsCallable(lhs) + + f := &chainCallable{ + callables: []jtypes.Callable{ + f1, + f2, + }, + } + + return reflect.ValueOf(f), nil +} + +func evalNumericOperator(node *jparse.NumericOperatorNode, data reflect.Value, env *environment) (reflect.Value, error) { + evaluate := func(node jparse.Node) (float64, bool, bool, error) { + + v, err := eval(node, data, env) + if err != nil || v == undefined { + return 0, false, false, err + } + + n, isNum := jtypes.AsNumber(v) + return n, true, isNum, nil + } + + // Evaluate both sides and return any errors. + lhs, lhsOK, lhsNumber, err := evaluate(node.LHS) + if err != nil { + return undefined, err + } + + rhs, rhsOK, rhsNumber, err := evaluate(node.RHS) + if err != nil { + return undefined, err + } + + // Return an error if either side is not a number. + if lhsOK && !lhsNumber { + return undefined, newEvalError(ErrNonNumberLHS, node.LHS, node.Type) + } + + if rhsOK && !rhsNumber { + return undefined, newEvalError(ErrNonNumberRHS, node.RHS, node.Type) + } + + // Return undefined if either side is undefined. + if !lhsOK || !rhsOK { + return undefined, nil + } + + var x float64 + + switch node.Type { + case jparse.NumericAdd: + x = lhs + rhs + case jparse.NumericSubtract: + x = lhs - rhs + case jparse.NumericMultiply: + x = lhs * rhs + case jparse.NumericDivide: + x = lhs / rhs + case jparse.NumericModulo: + x = math.Mod(lhs, rhs) + default: + panicf("unrecognised numeric operator %q", node.Type) + } + + if math.IsInf(x, 0) { + return undefined, newEvalError(ErrNumberInf, nil, node.Type) + } + + if math.IsNaN(x) { + return undefined, newEvalError(ErrNumberNaN, nil, node.Type) + } + + return reflect.ValueOf(x), nil +} + +// See https://docs.jsonata.org/expressions#comparison-expressions +func evalComparisonOperator(node *jparse.ComparisonOperatorNode, data reflect.Value, env *environment) (reflect.Value, error) { + evaluate := func(node jparse.Node) (reflect.Value, bool, bool, error) { + + v, err := eval(node, data, env) + if err != nil || v == undefined { + return undefined, false, false, err + } + + return v, jtypes.IsNumber(v), jtypes.IsString(v), nil + + } + + // Evaluate both sides and return any errors. + lhs, lhsNumber, lhsString, err := evaluate(node.LHS) + if err != nil { + return undefined, err + } + + rhs, rhsNumber, rhsString, err := evaluate(node.RHS) + if err != nil { + return undefined, err + } + + // If this operator requires comparable types, return + // an error if a) either side is not comparable or b) + // left side type does not equal right side type. + if needComparableTypes(node.Type) { + if lhs != undefined && !lhsNumber && !lhsString { + return undefined, newEvalError(ErrNonComparableLHS, node.LHS, node.Type) + } + + if rhs != undefined && !rhsNumber && !rhsString { + return undefined, newEvalError(ErrNonComparableRHS, node.RHS, node.Type) + } + + if lhs != undefined && rhs != undefined && + (lhsNumber != rhsNumber || lhsString != rhsString) { + return undefined, newEvalError(ErrTypeMismatch, nil, node.Type) + } + } + + // Return undefined if either side is undefined. + if lhs == undefined || rhs == undefined { + return reflect.ValueOf(false), nil + } + + var b bool + + switch node.Type { + case jparse.ComparisonIn: + b = in(lhs, rhs) + case jparse.ComparisonEqual: + b = eq(lhs, rhs) + case jparse.ComparisonNotEqual: + b = !eq(lhs, rhs) + case jparse.ComparisonLess: + b = lt(lhs, rhs) + case jparse.ComparisonLessEqual: + b = lte(lhs, rhs) + case jparse.ComparisonGreater: + b = !lte(lhs, rhs) + case jparse.ComparisonGreaterEqual: + b = !lt(lhs, rhs) + default: + panicf("unrecognised comparison operator %q", node.Type) + } + + return reflect.ValueOf(b), nil +} + +func needComparableTypes(op jparse.ComparisonOperator) bool { + switch op { + case jparse.ComparisonEqual, jparse.ComparisonNotEqual, jparse.ComparisonIn: + return false + default: + return true + } +} + +func eq(lhs, rhs reflect.Value) bool { + // Numbers, strings, arrays, objects and booleans are compared by value. + // Two strings might be different objects in memory but + // they're still considered equal if they have the + // same value. + + if v1, ok := jtypes.AsNumber(lhs); ok { + v2, ok := jtypes.AsNumber(rhs) + return ok && v1 == v2 + } + + if v1, ok := jtypes.AsString(lhs); ok { + v2, ok := jtypes.AsString(rhs) + return ok && v1 == v2 + } + + if v1, ok := jtypes.AsBool(lhs); ok { + v2, ok := jtypes.AsBool(rhs) + return ok && v1 == v2 + } + + // Arrays and maps are compared with a deep equal + if jtypes.IsArray(lhs) && jtypes.IsArray(rhs) { + return reflect.DeepEqual(lhs.Interface(), rhs.Interface()) + } + + if jtypes.IsMap(lhs) && jtypes.IsMap(rhs) { + return reflect.DeepEqual(lhs.Interface(), rhs.Interface()) + } + + // All other types (e.g. functions) are + // compared directly. Two functions with the same contents + // are not considered equal unless they're the same + // physical object in memory. + + return lhs == rhs +} + +func lt(lhs, rhs reflect.Value) bool { + if v1, ok := jtypes.AsNumber(lhs); ok { + if v2, ok := jtypes.AsNumber(rhs); ok { + return v1 < v2 + } + } + + if v1, ok := jtypes.AsString(lhs); ok { + if v2, ok := jtypes.AsString(rhs); ok { + return v1 < v2 + } + } + + panicf("lt: invalid types: lhs %s, rhs %s", lhs.Kind(), rhs.Kind()) + return false +} + +func lte(lhs, rhs reflect.Value) bool { + return lt(lhs, rhs) || eq(lhs, rhs) +} + +func in(lhs, rhs reflect.Value) bool { + // TODO: Does not work with null, e.g. + // null in null // evaluates to false + // null in [null] // evaluates to false + + rhs = arrayify(rhs) + + for i, N := 0, rhs.Len(); i < N; i++ { + if eq(lhs, rhs.Index(i)) { + return true + } + } + + return false +} + +func evalBooleanOperator(node *jparse.BooleanOperatorNode, data reflect.Value, env *environment) (reflect.Value, error) { + // Evaluate both sides and return any errors. + lhs, err := eval(node.LHS, data, env) + if err != nil { + return undefined, err + } + + rhs, err := eval(node.RHS, data, env) + if err != nil { + return undefined, err + } + + var b bool + + switch node.Type { + case jparse.BooleanAnd: + b = jlib.Boolean(lhs) && jlib.Boolean(rhs) + case jparse.BooleanOr: + b = jlib.Boolean(lhs) || jlib.Boolean(rhs) + default: + panicf("unrecognised boolean operator %q", node.Type) + } + + return reflect.ValueOf(b), nil +} + +func evalStringConcatenation(node *jparse.StringConcatenationNode, data reflect.Value, env *environment) (reflect.Value, error) { + stringify := func(v reflect.Value) (string, error) { + + if v == undefined || !v.CanInterface() { + return "", nil + } + return jlib.String(v.Interface()) + } + + // Evaluate both sides and return any errors. + lhs, err := eval(node.LHS, data, env) + if err != nil { + return undefined, err + } + + rhs, err := eval(node.RHS, data, env) + if err != nil { + return undefined, err + } + + // Convert both sides to strings. + s1, err := stringify(lhs) + if err != nil { + return undefined, err + } + + s2, err := stringify(rhs) + if err != nil { + return undefined, err + } + + return reflect.ValueOf(s1 + s2), nil +} + +// Helper functions + +func walkObjectValues(v reflect.Value, fn func(reflect.Value)) { + switch v := jtypes.Resolve(v); { + case jtypes.IsArray(v): + for i, N := 0, v.Len(); i < N; i++ { + fn(v.Index(i)) + } + case jtypes.IsMap(v): + for _, k := range v.MapKeys() { + fn(v.MapIndex(k)) + } + case jtypes.IsStruct(v): + for i, N := 0, v.NumField(); i < N; i++ { + fn(v.Field(i)) + } + } +} + +func normalizeArray(v reflect.Value) reflect.Value { + v = jtypes.Resolve(v) + if jtypes.IsArray(v) && v.Len() == 1 { + return v.Index(0) + } + return v +} + +func flattenArray(v reflect.Value) reflect.Value { + results := reflect.MakeSlice(typeInterfaceSlice, 0, 0) + + switch { + case jtypes.IsArray(v): + v = jtypes.Resolve(v) + for i, N := 0, v.Len(); i < N; i++ { + vi := flattenArray(v.Index(i)) + if vi.IsValid() { + results = reflect.AppendSlice(results, vi) + } + } + default: + if v.IsValid() { + results = reflect.Append(results, v) + } + } + + return results +} + +func arrayify(v reflect.Value) reflect.Value { + switch { + case jtypes.IsArray(v): + return jtypes.Resolve(v) + case !v.IsValid(): + return reflect.MakeSlice(typeInterfaceSlice, 0, 0) + default: + return reflect.Append(reflect.MakeSlice(typeInterfaceSlice, 0, 1), v) + } +} + +func makeArray(v reflect.Value) reflect.Value { + switch { + case jtypes.IsArray(v): + return jtypes.Resolve(v) + default: + arr := reflect.MakeSlice(typeInterfaceSlice, 1, 1) + if v.IsValid() { + arr.Index(0).Set(v) + } + return arr + } +} + +func panicf(format string, a ...interface{}) { + panic(fmt.Sprintf(format, a...)) +} + +// Sequence handling + +type sequence struct { + values []interface{} + keepSingletons bool +} + +func newSequence(size int) *sequence { + return &sequence{ + values: make([]interface{}, 0, size), + } +} + +func (s *sequence) Len() int { + return len(s.values) +} + +func (s *sequence) Append(v interface{}) { + s.values = append(s.values, v) +} + +func (s sequence) Value() reflect.Value { + switch n := len(s.values); { + case n == 0: + return undefined + case n == 1 && !s.keepSingletons: + return reflect.ValueOf(s.values[0]) + default: + return reflect.ValueOf(s.values) + } +} + +var ( + typeSequence = reflect.TypeOf((*sequence)(nil)).Elem() + typeSequencePtr = reflect.PointerTo(typeSequence) +) + +func asSequence(v reflect.Value) (*sequence, bool) { + if !v.IsValid() || !v.CanInterface() { + return nil, false + } + + if v.Type() == typeSequencePtr { + return v.Interface().(*sequence), true + } + + if jtypes.Resolve(v).Type() == typeSequence && v.CanAddr() { + return v.Addr().Interface().(*sequence), true + } + + return nil, false +} diff --git a/v1.5.4/eval_test.go b/v1.5.4/eval_test.go new file mode 100644 index 0000000..5a4e4a4 --- /dev/null +++ b/v1.5.4/eval_test.go @@ -0,0 +1,4319 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "math" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type evalTestCase struct { + Input jparse.Node + Vars map[string]interface{} + Exts map[string]Extension + Data interface{} + Equals func(interface{}, interface{}) bool + Output interface{} + Error error +} + +func TestEvalString(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.StringNode{}, + Output: "", + }, + { + Input: &jparse.StringNode{ + Value: "hello world", + }, + Output: "hello world", + }, + }) +} + +func TestEvalNumber(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.NumberNode{}, + Output: float64(0), + }, + { + Input: &jparse.NumberNode{ + Value: 3.14159, + }, + Output: 3.14159, + }, + }) +} + +func TestEvalBoolean(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.BooleanNode{}, + Output: false, + }, + { + Input: &jparse.BooleanNode{ + Value: true, + }, + Output: true, + }, + }) +} + +func TestEvalNull(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.NullNode{}, + Output: null, + }, + }) +} + +func TestEvalRegex(t *testing.T) { + + re := regexp.MustCompile("a(b+)") + + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.RegexNode{ + Value: re, + }, + Output: ®exCallable{ + callableName: callableName{ + name: "a(b+)", + }, + re: re, + }, + }, + }) +} + +func TestEvalVariable(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Undefined variable. Return undefined. + Input: &jparse.VariableNode{ + Name: "x", + }, + Output: nil, + }, + { + // Defined variable. Return its value. + Input: &jparse.VariableNode{ + Name: "x", + }, + Vars: map[string]interface{}{ + "x": "marks the spot", + }, + Output: "marks the spot", + }, + { + // The zero VariableNode ($) returns the evaluation + // context. + Input: &jparse.VariableNode{}, + Data: map[string]interface{}{ + "x": "marks the spot", + }, + Output: map[string]interface{}{ + "x": "marks the spot", + }, + }, + }) +} + +func TestEvalNegation(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Negate a number (in practice this happens during + // parsing). + Input: &jparse.NegationNode{ + RHS: &jparse.NumberNode{ + Value: 100, + }, + }, + Output: float64(-100), + }, + { + // Negate a field. + Input: &jparse.NegationNode{ + RHS: &jparse.NameNode{ + Value: "number", + }, + }, + Data: map[string]interface{}{ + "number": -100, + }, + Output: float64(100), + }, + { + // Negate a variable. + Input: &jparse.NegationNode{ + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Vars: map[string]interface{}{ + "x": 100, + }, + Output: float64(-100), + }, + { + // Negating undefined should return undefined. + Input: &jparse.NegationNode{ + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: nil, + }, + { + // Negating a non-number should return an error. + Input: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // Negating an error should return the error. + Input: &jparse.NegationNode{ + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + }) +} + +func TestEvalRange(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Standard case. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: -2, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + Output: []interface{}{ + float64(-2), + float64(-1), + float64(0), + float64(1), + float64(2), + }, + }, + { + // Right side equals left side. Return a singleton + // array. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 10, + }, + RHS: &jparse.NumberNode{ + Value: 10, + }, + }, + Output: []interface{}{ + float64(10), + }, + }, + { + // Left side returns an error. Return the error. + Input: &jparse.RangeNode{ + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // an error on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // a non-number on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.BooleanNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // a non-integer on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.NumberNode{ + Value: 1.5, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // an undefined right side. + Input: &jparse.RangeNode{ + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Left side is not a number. Return an error. + Input: &jparse.RangeNode{ + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "false", + Value: "..", + }, + }, + { + // A non-number on the left side takes precedence + // over a non-number on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "false", + Value: "..", + }, + }, + { + // A non-number on the left side takes precedence + // over a non-integer on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NumberNode{ + Value: 1.5, + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "false", + Value: "..", + }, + }, + { + // A non-number on the left side takes precedence + // over an undefined right side. + Input: &jparse.RangeNode{ + LHS: &jparse.BooleanNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "false", + Value: "..", + }, + }, + { + // Left side is not an integer. Return an error. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1.5, + }, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "1.5", + Value: "..", + }, + }, + { + // A non-integer on the left side takes precedence + // over a non-integer on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1.5, + }, + RHS: &jparse.NumberNode{ + Value: 0.5, + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "1.5", + Value: "..", + }, + }, + { + // A non-integer on the left side takes precedence + // over a non-number on the right side. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1.5, + }, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "1.5", + Value: "..", + }, + }, + { + // A non-integer on the left side takes precedence + // over an undefined right side. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1.5, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "1.5", + Value: "..", + }, + }, + { + // Right side returns an error. Return the error. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // An error on the right side takes precedence over + // a non-number on the left side. + Input: &jparse.RangeNode{ + LHS: &jparse.NullNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // An error on the right side takes precedence over + // a non-integer on the left side. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1.5, + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // An error on the right side takes precedence over + // an undefined left side. + Input: &jparse.RangeNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // Right side is not a number. Return an error. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "null", + Value: "..", + }, + }, + { + // A non-number right side takes precedence over + // an undefined left side. + Input: &jparse.RangeNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "null", + Value: "..", + }, + }, + { + // Right side is not an integer. Return an error. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.NumberNode{ + Value: 1.5, + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "1.5", + Value: "..", + }, + }, + { + // A non-integer right side takes precedence over + // an undefined left side. + Input: &jparse.RangeNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NumberNode{ + Value: 1.5, + }, + }, + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "1.5", + Value: "..", + }, + }, + { + // If left side is undefined, return undefined. + Input: &jparse.RangeNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NumberNode{}, + }, + Output: nil, + }, + { + // If right side is undefined, return undefined. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: nil, + }, + { + // If right side is less than left side, return undefined. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 2, + }, + RHS: &jparse.NumberNode{ + Value: -2, + }, + }, + Output: nil, + }, + { + // If there are too many items in the range, return + // an error. + Input: &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 0, + }, + RHS: &jparse.NumberNode{ + Value: maxRangeItems, + }, + }, + Error: &EvalError{ + Type: ErrMaxRangeItems, + Token: "..", + }, + }, + }) +} + +func TestEvalArray(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.ArrayNode{}, + Output: []interface{}{}, + }, + { + Input: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.StringNode{ + Value: "two", + }, + &jparse.BooleanNode{ + Value: true, + }, + &jparse.NullNode{}, + }, + }, + Output: []interface{}{ + float64(1), + "two", + true, + null, + }, + }, + { + // Nested ArrayNodes are added to the containing array + // as arrays. + Input: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "two", + }, + &jparse.BooleanNode{ + Value: true, + }, + }, + }, + &jparse.NullNode{}, + }, + }, + Output: []interface{}{ + float64(1), + []interface{}{ + "two", + true, + }, + null, + }, + }, + { + // Other array data gets flattened into the containing + // array. + Input: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 2, + }, + RHS: &jparse.NumberNode{ + Value: 4, + }, + }, + &jparse.NumberNode{ + Value: 5, + }, + }, + }, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + }, + }, + { + // Undefined values are ignored. + Input: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.VariableNode{ + Name: "two", + }, + &jparse.NameNode{ + Value: "three", + }, + &jparse.StringNode{ + Value: "four", + }, + }, + }, + Output: []interface{}{ + float64(1), + "four", + }, + }, + { + // If an item returns an error, return that error. + Input: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + }) +} + +func TestEvalObject(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.ObjectNode{}, + Output: map[string]interface{}{}, + }, + { + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + { + &jparse.StringNode{ + Value: "two", + }, + &jparse.NumberNode{ + Value: 2, + }, + }, + { + &jparse.StringNode{ + Value: "three", + }, + &jparse.NumberNode{ + Value: 3, + }, + }, + }, + }, + Output: map[string]interface{}{ + "one": float64(1), + "two": float64(2), + "three": float64(3), + }, + }, + { + // If a key evaluates to an error, return that error. + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.NegationNode{ + RHS: &jparse.StringNode{}, + }, + &jparse.NumberNode{}, + }, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: `""`, + Value: "-", + }, + }, + { + // If a key is not a string, return an error. + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.NullNode{}, + &jparse.NumberNode{}, + }, + }, + }, + Error: &EvalError{ + Type: ErrIllegalKey, + Token: "null", + }, + }, + { + // If a key is duplicated, return an error. + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.NumberNode{}, + }, + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.BooleanNode{}, + }, + }, + }, + Error: &EvalError{ + Type: ErrDuplicateKey, + Token: `"key"`, + Value: "key", + }, + }, + { + // Values that evaluate to undefined are ignored. + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + { + &jparse.StringNode{ + Value: "two", + }, + &jparse.VariableNode{ + Name: "x", + }, + }, + { + &jparse.StringNode{ + Value: "three", + }, + &jparse.NameNode{ + Value: "Field", + }, + }, + }, + }, + Output: map[string]interface{}{ + "one": float64(1), + }, + }, + { + // If a value evaluates to an error, return that error. + Input: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{}, + &jparse.NegationNode{ + RHS: &jparse.StringNode{}, + }, + }, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: `""`, + Value: "-", + }, + }, + }) +} + +func TestEvalGroup(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.GroupNode{ + Expr: &jparse.VariableNode{}, + ObjectNode: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.NameNode{ + Value: "name", + }, + &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "sum", + }, + Args: []jparse.Node{ + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + }, + }, + }, + Data: []interface{}{ + map[string]interface{}{ + "name": "zero", + }, + map[string]interface{}{ + "name": "one", + "value": 1, + }, + map[string]interface{}{ + "name": "two", + "value": 2, + }, + map[string]interface{}{ + "name": "two", + "value": 12, + }, + map[string]interface{}{ + "name": "three", + "value": 3, + }, + map[string]interface{}{ + "name": "three", + "value": 13, + }, + map[string]interface{}{ + "name": "three", + "value": 23, + }, + }, + Exts: map[string]Extension{ + "sum": { + Func: jlib.Sum, + UndefinedHandler: func(argv []reflect.Value) bool { + return len(argv) > 0 && argv[0] == undefined + }, + }, + }, + Output: map[string]interface{}{ + "one": float64(1), + "two": float64(14), + "three": float64(39), + }, + }, + { + // Expression being grouped returns an error. Return the error. + Input: &jparse.GroupNode{ + Expr: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + }) +} + +func TestEvalAssignment(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Assignment returns the value being assigned. + Input: &jparse.AssignmentNode{ + Name: "x", + Value: &jparse.StringNode{ + Value: "marks the spot", + }, + }, + Output: "marks the spot", + }, + { + // Assignment modifies the environment. + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "x", + Value: &jparse.StringNode{ + Value: "marks the spot", + }, + }, + &jparse.VariableNode{ + Name: "x", + }, + }, + }, + Output: "marks the spot", + }, + { + // If the value evaluates to an error, return + // that error. + Input: &jparse.AssignmentNode{ + Name: "x", + Value: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + }) +} + +func TestEvalBlock(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.BlockNode{}, + Output: nil, + }, + { + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "pi", + Value: &jparse.NumberNode{ + Value: 3.14159, + }, + }, + &jparse.VariableNode{ + Name: "pi", + }, + }, + }, + Output: 3.14159, + }, + { + // Variables defined in a block should be scoped + // to that block. + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "pi", + Value: &jparse.NumberNode{ + Value: 100, + }, + }, + }, + }, + &jparse.VariableNode{ + Name: "pi", + }, + }, + }, + Output: nil, + }, + { + // If an expression evaluates to an error, return + // that error. + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + }) +} + +func TestEvalConditional(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Condition is true, no else. Return true value. + Input: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: true, + }, + Then: &jparse.StringNode{ + Value: "true", + }, + }, + Output: "true", + }, + { + // Condition is false, no else. Return undefined. + Input: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: false, + }, + Then: &jparse.StringNode{ + Value: "true", + }, + }, + Output: nil, + }, + { + // Condition is undefined, no else. Return undefined. + Input: &jparse.ConditionalNode{ + If: &jparse.VariableNode{ + Name: "x", + }, + Then: &jparse.StringNode{ + Value: "true", + }, + }, + Output: nil, + }, + { + // Condition is true, else. Return true value. + Input: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: true, + }, + Then: &jparse.StringNode{ + Value: "true", + }, + Else: &jparse.StringNode{ + Value: "false", + }, + }, + Output: "true", + }, + { + // Condition is false, else. Return false value. + Input: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: false, + }, + Then: &jparse.StringNode{ + Value: "true", + }, + Else: &jparse.StringNode{ + Value: "false", + }, + }, + Output: "false", + }, + { + // Condition is undefined, else. Return false value. + Input: &jparse.ConditionalNode{ + If: &jparse.VariableNode{ + Name: "x", + }, + Then: &jparse.StringNode{ + Value: "true", + }, + Else: &jparse.StringNode{ + Value: "false", + }, + }, + Output: "false", + }, + { + // Condition evaluates to an error. Return the error. + Input: &jparse.ConditionalNode{ + If: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{ + Value: false, + }, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + }) +} + +func TestEvalWildcard(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // If the input is an empty array, the wildcard + // operator evaluates to undefined. + Input: &jparse.WildcardNode{}, + Data: []interface{}{}, + Output: nil, + }, + { + // If the input is an array with one item, the + // wildcard operator evaluates to the item. + Input: &jparse.WildcardNode{}, + Data: []interface{}{ + "one", + }, + Output: "one", + }, + { + // If the input is a multi-item array, the wildcard + // operator evaluates to the items. + Input: &jparse.WildcardNode{}, + Data: []interface{}{ + "one", + "two", + "three", + }, + Output: []interface{}{ + "one", + "two", + "three", + }, + }, + { + // Nested arrays are flattened into a single array. + Input: &jparse.WildcardNode{}, + Data: []interface{}{ + "one", + []interface{}{ + "two", + []interface{}{ + "three", + []interface{}{ + "four", + []interface{}{ + "five", + }, + }, + }, + }, + }, + Output: []interface{}{ + "one", + "two", + "three", + "four", + "five", + }, + }, + { + // If the input is an empty map, the wildcard + // operator evaluates to undefined. + Input: &jparse.WildcardNode{}, + Data: map[string]interface{}{}, + Output: nil, + }, + { + // If the input is a map with one item, the wildcard + // operator evaluates to the item's value. + Input: &jparse.WildcardNode{}, + Data: map[string]interface{}{ + "one": 1, + }, + Output: 1, + }, + { + // If the input is a map with multiple items, the + // wildcard operator returns the map values in an + // array. Note that the order is non-deterministic + // because Go traverses maps in random order. + Input: &jparse.WildcardNode{}, + Data: map[string]interface{}{ + "one": 1, + "two": 2, + "three": 3, + "fourfive": []int{ + 4, + 5, + }, + "six": 6, + }, + Equals: equalArraysUnordered, + Output: []interface{}{ + 1, + 2, + 3, + 4, + 5, + 6, + }, + }, + { + // If the input is an empty struct, the wildcard + // operator evaluates to undefined. + Input: &jparse.WildcardNode{}, + Data: struct{}{}, + Output: nil, + }, + { + // If the input is a struct with one field, the + // wildcard operator evaluates to the field's value. + Input: &jparse.WildcardNode{}, + Data: struct { + Value string + }{ + Value: "hello world", + }, + Output: "hello world", + }, + { + // Unexported struct fields are ignored, The reflect + // package does not allow them to be copied into an + // array (as the wildcard operator reuires). + Input: &jparse.WildcardNode{}, + Data: struct { + value string + }{ + value: "hello world", + }, + Output: nil, + }, + { + // If the input is a struct with multiple fields, the + // wildcard operator returns the field values in an + // array. Unlike with a map, the order of the fields + // is predictable. + Input: &jparse.WildcardNode{}, + Data: struct { + One int + Two int + Three int + FourFive []int + six int + }{ + One: 1, + Two: 2, + Three: 3, + FourFive: []int{ + 4, + 5, + }, + six: 6, + }, + Output: []interface{}{ + 1, + 2, + 3, + 4, + 5, + }, + }, + }) +} + +func TestEvalDescendent(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // If the input is an empty array, the descendent + // operator evaluates to undefined. + Input: &jparse.DescendentNode{}, + Data: []interface{}{}, + Output: nil, + }, + { + // If the input is an array with one item, the + // descendent operator evaluates to the item. + Input: &jparse.DescendentNode{}, + Data: []interface{}{ + "one", + }, + Output: "one", + }, + { + // If the input is a multi-item array, the descendent + // operator evaluates to the items. + Input: &jparse.DescendentNode{}, + Data: []interface{}{ + "one", + "two", + "three", + }, + Output: []interface{}{ + "one", + "two", + "three", + }, + }, + { + // Nested arrays are flattened into a single array. + Input: &jparse.DescendentNode{}, + Data: []interface{}{ + "one", + []interface{}{ + "two", + []interface{}{ + "three", + []interface{}{ + "four", + []interface{}{ + "five", + }, + }, + }, + }, + }, + Output: []interface{}{ + "one", + "two", + "three", + "four", + "five", + }, + }, + { + // If the input is an empty map, the descendent + // operator evaluates to an empty map. + Input: &jparse.DescendentNode{}, + Data: map[string]interface{}{}, + Output: map[string]interface{}{}, + }, + { + // If the input is a map with one item, the descendent + // operator evaluates to an array containing the map + // itself and its single value. + Input: &jparse.DescendentNode{}, + Data: map[string]interface{}{ + "one": 1, + }, + Output: []interface{}{ + map[string]interface{}{ + "one": 1, + }, + 1, + }, + }, + { + // If the input is a map with multiple items, the + // descendent operator returns an array containing + // the map itself and its values. Note that the order + // of the values is non-deterministic because Go + // traverses maps in random order. + Input: &jparse.DescendentNode{}, + Data: map[string]interface{}{ + "one": 1, + "two": 2, + "three": 3, + "fourfive": []int{ + 4, + 5, + }, + "six": 6, + }, + Equals: equalArraysUnordered, + Output: []interface{}{ + map[string]interface{}{ + "one": 1, + "two": 2, + "three": 3, + "fourfive": []int{ + 4, + 5, + }, + "six": 6, + }, + 1, + 2, + 3, + 4, + 5, + 6, + }, + }, + { + // If the input is an empty struct, the descendent + // operator evaluates to an empty struct. + Input: &jparse.DescendentNode{}, + Data: struct{}{}, + Output: struct{}{}, + }, + { + // If the input is a struct with one field, the + // descendent operator evaluates to an array containing + // the struct itself and the single field's value. + Input: &jparse.DescendentNode{}, + Data: struct { + Value string + }{ + Value: "hello world", + }, + Output: []interface{}{ + struct { + Value string + }{ + Value: "hello world", + }, + "hello world", + }, + }, + { + // Unexported struct fields appear as part of the struct + // but are not added to the result array as individual + // fields. + Input: &jparse.DescendentNode{}, + Data: struct { + value string + }{ + value: "hello world", + }, + Output: struct { + value string + }{ + value: "hello world", + }, + }, + { + // If the input is a struct with multiple fields, the + // descendent operator returns an array containing the + // struct itself plus the field values. Unlike with a + // map, the order of the individual values is predictable. + Input: &jparse.DescendentNode{}, + Data: struct { + One int + Two int + Three int + FourFive []int + six int + }{ + One: 1, + Two: 2, + Three: 3, + FourFive: []int{ + 4, + 5, + }, + six: 6, + }, + Output: []interface{}{ + struct { + One int + Two int + Three int + FourFive []int + six int + }{ + One: 1, + Two: 2, + Three: 3, + FourFive: []int{ + 4, + 5, + }, + six: 6, + }, + 1, + 2, + 3, + 4, + 5, + }, + }, + }) +} + +func TestEvalPredicate(t *testing.T) { + + makeRange := func(from, to float64) *jparse.ArrayNode { + return &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: from, + }, + RHS: &jparse.NumberNode{ + Value: to, + }, + }, + }, + } + } + + testEvalTestCases(t, []evalTestCase{ + { + // Positive indexes are zero-based. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 3, + }, + }, + }, + Output: float64(4), + }, + { + // Negative indexes count from the end. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: -3, + }, + }, + }, + Output: float64(8), + }, + { + // Non-integer indexes round down. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 0.9, + }, + }, + }, + Output: float64(1), + }, + { + // Non-integer indexes round down (i.e. away from + // zero for negative values). + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: -0.9, + }, + }, + }, + Output: float64(10), + }, + { + // Out of bounds indexes return undefined. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 20, + }, + }, + }, + Output: nil, + }, + { + // Multiple indexes return an array of values. + // Note that: + // + // 1. Values are returned in the same order as + // the source array. + // + // 2. If an index appears multiple times in the + // filter expression, the corresponding value + // will appear mutliple times in the results. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: -1, + }, + &jparse.NumberNode{ + Value: 4, + }, + &jparse.NumberNode{ + Value: 4, + }, + &jparse.NumberNode{ + Value: 10, + }, + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + }, + Output: []interface{}{ + float64(1), + float64(5), + float64(5), + float64(10), + }, + }, + { + // Filters that are not numeric indexes are tested + // for truthiness. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumericOperatorNode{ + Type: jparse.NumericModulo, + LHS: &jparse.VariableNode{}, + RHS: &jparse.NumberNode{ + Value: 3, + }, + }, + RHS: &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + Output: []interface{}{ + float64(3), + float64(6), + float64(9), + }, + }, + { + // Multiple filters are evaluated in succession. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + makeRange(3, 6), + &jparse.NumberNode{ + Value: 1, + }, + }, + }, + Output: float64(5), + }, + { + // If the predicate expression evaluates to undefined, + // the predicate should evaluate to undefined. + Input: &jparse.PredicateNode{ + Expr: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: nil, + }, + { + // If the predicate expression evaluates to an error, + // the predicate should return the error. + Input: &jparse.PredicateNode{ + Expr: &jparse.NegationNode{ + RHS: makeRange(1, 10), + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "[1..10]", + Value: "-", + }, + }, + { + // If a filter evaluates to an error, the predicate + // should return the error. + Input: &jparse.PredicateNode{ + Expr: makeRange(1, 10), + Filters: []jparse.Node{ + makeRange(1, 1e10), + }, + }, + Error: &EvalError{ + Type: ErrMaxRangeItems, + Token: "..", + }, + }, + }) +} + +func TestEvalSort(t *testing.T) { + + data := []interface{}{ + map[string]interface{}{ + "value": 0, + }, + map[string]interface{}{ + "value": 1, + "en": "one", + }, + map[string]interface{}{ + "value": 2, + "en": "two", + }, + map[string]interface{}{ + "value": 3, + "en": "three", + }, + map[string]interface{}{ + "value": 4, + "en": "four", + }, + map[string]interface{}{ + "value": 5, + "en": "five", + }, + map[string]interface{}{ + "value": 1, + "es": "uno", + }, + map[string]interface{}{ + "value": 2, + "es": "dos", + }, + map[string]interface{}{ + "value": 3, + "es": "tres", + }, + map[string]interface{}{ + "value": 4, + "es": "cuatro", + }, + map[string]interface{}{ + "value": 5, + "es": "cinco", + }, + } + + testEvalTestCases(t, []evalTestCase{ + { + // Sorting by "en" should sort the items by their + // English name. Items without an English name should + // appear at the end of the list in the same order as + // the original array. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortAscending, + Expr: &jparse.NameNode{ + Value: "en", + }, + }, + }, + }, + Data: data, + Output: []interface{}{ + map[string]interface{}{ + "value": 5, + "en": "five", + }, + map[string]interface{}{ + "value": 4, + "en": "four", + }, + map[string]interface{}{ + "value": 1, + "en": "one", + }, + map[string]interface{}{ + "value": 3, + "en": "three", + }, + map[string]interface{}{ + "value": 2, + "en": "two", + }, + map[string]interface{}{ + "value": 0, + }, + map[string]interface{}{ + "value": 1, + "es": "uno", + }, + map[string]interface{}{ + "value": 2, + "es": "dos", + }, + map[string]interface{}{ + "value": 3, + "es": "tres", + }, + map[string]interface{}{ + "value": 4, + "es": "cuatro", + }, + map[string]interface{}{ + "value": 5, + "es": "cinco", + }, + }, + }, + { + // Sorting by "es" should sort the items by their + // Spanish name. Items without a Spanish name should + // appear at the end of the list in the same order as + // the original array. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDescending, + Expr: &jparse.NameNode{ + Value: "es", + }, + }, + }, + }, + Data: data, + Output: []interface{}{ + map[string]interface{}{ + "value": 1, + "es": "uno", + }, + map[string]interface{}{ + "value": 3, + "es": "tres", + }, + map[string]interface{}{ + "value": 2, + "es": "dos", + }, + map[string]interface{}{ + "value": 4, + "es": "cuatro", + }, + map[string]interface{}{ + "value": 5, + "es": "cinco", + }, + map[string]interface{}{ + "value": 0, + }, + map[string]interface{}{ + "value": 1, + "en": "one", + }, + map[string]interface{}{ + "value": 2, + "en": "two", + }, + map[string]interface{}{ + "value": 3, + "en": "three", + }, + map[string]interface{}{ + "value": 4, + "en": "four", + }, + map[string]interface{}{ + "value": 5, + "en": "five", + }, + }, + }, + { + // Sorting by "value" should sort the items by their + // numeric value. Items with the same numeric value + // should appear in the same order as the original + // array (i.e. items with English names should come + // before items with Spanish names). + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDefault, + Expr: &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + Data: data, + Output: []interface{}{ + map[string]interface{}{ + "value": 0, + }, + map[string]interface{}{ + "value": 1, + "en": "one", + }, + map[string]interface{}{ + "value": 1, + "es": "uno", + }, + map[string]interface{}{ + "value": 2, + "en": "two", + }, + map[string]interface{}{ + "value": 2, + "es": "dos", + }, + map[string]interface{}{ + "value": 3, + "en": "three", + }, + map[string]interface{}{ + "value": 3, + "es": "tres", + }, + map[string]interface{}{ + "value": 4, + "en": "four", + }, + map[string]interface{}{ + "value": 4, + "es": "cuatro", + }, + map[string]interface{}{ + "value": 5, + "en": "five", + }, + map[string]interface{}{ + "value": 5, + "es": "cinco", + }, + }, + }, + { + // Sorting by "value" and "es" should first sort the + // items by their numeric value. Then items with the + // same numeric value should be sorted according to + // their Spanish name. Items without a Spanish name + // should appear at the end of each internal sort. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDescending, + Expr: &jparse.NameNode{ + Value: "value", + }, + }, + { + Dir: jparse.SortDescending, + Expr: &jparse.NameNode{ + Value: "es", + }, + }, + }, + }, + Data: data, + Output: []interface{}{ + map[string]interface{}{ + "value": 5, + "es": "cinco", + }, + map[string]interface{}{ + "value": 5, + "en": "five", + }, + map[string]interface{}{ + "value": 4, + "es": "cuatro", + }, + map[string]interface{}{ + "value": 4, + "en": "four", + }, + map[string]interface{}{ + "value": 3, + "es": "tres", + }, + map[string]interface{}{ + "value": 3, + "en": "three", + }, + map[string]interface{}{ + "value": 2, + "es": "dos", + }, + map[string]interface{}{ + "value": 2, + "en": "two", + }, + map[string]interface{}{ + "value": 1, + "es": "uno", + }, + map[string]interface{}{ + "value": 1, + "en": "one", + }, + map[string]interface{}{ + "value": 0, + }, + }, + }, + { + // Sorting a single item should return that item. + // TODO: Find out why the latest version of jsonata-js + // returns an array for this case. If it's because of + // the recent changes to the sort function, we don't + // need to replicate the behaviour. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDefault, + Expr: &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + Data: []interface{}{ + map[string]interface{}{ + "value": 0, + }, + }, + Output: map[string]interface{}{ + "value": 0, + }, + }, + { + // Sorting an empty array should return an empty + // array. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDefault, + Expr: &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + Data: []interface{}{}, + Output: []interface{}{}, + }, + { + // If the expression being sorted evaluates to an + // error, return the error. + Input: &jparse.SortNode{ + Expr: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // If the expression being sorted evaluates to + // undefined, return undefined. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + }, + Output: nil, + }, + { + // If a sort term evaluates to an error, return + // the error. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Expr: &jparse.NegationNode{ + RHS: &jparse.VariableNode{}, + }, + }, + }, + }, + Data: []interface{}{ + "hello", + "world", + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "$", + Value: "-", + }, + }, + { + // If a sort term evaluates to anything other than + // a string or a number, return an error. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Expr: &jparse.VariableNode{}, + }, + }, + }, + Data: []interface{}{ + "hello", + "world", + false, + }, + Error: &EvalError{ + Type: ErrNonSortable, + Token: "$", + }, + }, + { + // If the sort terms evaluate to different types for + // different items in the source array, return an error. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Expr: &jparse.VariableNode{}, + }, + }, + }, + Data: []interface{}{ + "hello", + "world", + 100, + }, + Error: &EvalError{ + Type: ErrSortMismatch, + Token: "$", + }, + }, + { + // If the sort terms evaluates to different types for + // different items in the source array, return an error. + Input: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Expr: &jparse.VariableNode{}, + }, + }, + }, + Data: []interface{}{ + 100, + "string", + }, + Error: &EvalError{ + Type: ErrSortMismatch, + Token: "$", + }, + }, + }) +} + +func TestEvalLambda(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.LambdaNode{ + Body: &jparse.BooleanNode{ + Value: true, + }, + ParamNames: []string{ + "x", + "y", + }, + }, + Output: &lambdaCallable{ + callableName: callableName{ + name: "lambda", + }, + paramNames: []string{ + "x", + "y", + }, + body: &jparse.BooleanNode{ + Value: true, + }, + env: newEnvironment(nil, 0), + }, + }, + }) +} + +func TestEvalTypedLambda(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.TypedLambdaNode{ + LambdaNode: &jparse.LambdaNode{ + Body: &jparse.BooleanNode{ + Value: true, + }, + ParamNames: []string{ + "x", + "y", + }, + }, + In: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + { + Type: jparse.ParamTypeNumber, + Option: jparse.ParamOptional, + }, + }, + }, + Output: &lambdaCallable{ + callableName: callableName{ + name: "lambda", + }, + paramNames: []string{ + "x", + "y", + }, + body: &jparse.BooleanNode{ + Value: true, + }, + typed: true, + params: []jparse.Param{ + { + Type: jparse.ParamTypeString, + }, + { + Type: jparse.ParamTypeNumber, + Option: jparse.ParamOptional, + }, + }, + env: newEnvironment(nil, 0), + }, + }, + }) +} + +func TestEvalObjectTransformation(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Updates, no deletions. + Input: &jparse.ObjectTransformationNode{ + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + }, + Output: &transformationCallable{ + callableName: callableName{ + name: "transform", + }, + pattern: &jparse.VariableNode{}, + updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + env: newEnvironment(nil, 0), + }, + }, + { + // Updates and deletions. + Input: &jparse.ObjectTransformationNode{ + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + Deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "field1", + }, + &jparse.StringNode{ + Value: "field2", + }, + }, + }, + }, + Output: &transformationCallable{ + callableName: callableName{ + name: "transform", + }, + pattern: &jparse.VariableNode{}, + updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "key", + }, + &jparse.NameNode{ + Value: "value", + }, + }, + }, + }, + deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "field1", + }, + &jparse.StringNode{ + Value: "field2", + }, + }, + }, + env: newEnvironment(nil, 0), + }, + }, + }) +} + +func TestEvalPartial(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.PartialNode{ + Func: &jparse.LambdaNode{ + Body: &jparse.NullNode{}, + ParamNames: []string{ + "x", + "y", + }, + }, + Args: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.PlaceholderNode{}, + }, + }, + Output: &partialCallable{ + callableName: callableName{ + name: "lambda_partial", + }, + fn: &lambdaCallable{ + callableName: callableName{ + name: "lambda", + }, + body: &jparse.NullNode{}, + paramNames: []string{ + "x", + "y", + }, + env: newEnvironment(nil, 0), + }, + args: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.PlaceholderNode{}, + }, + env: newEnvironment(nil, 0), + }, + }, + { + // Error evaluating the embedded function. Return the error. + Input: &jparse.PartialNode{ + Func: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + Args: []jparse.Node{ + &jparse.PlaceholderNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // Embedded function is not a Callable. Return an error. + Input: &jparse.PartialNode{ + Func: &jparse.BooleanNode{}, + Args: []jparse.Node{ + &jparse.PlaceholderNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallablePartial, + Token: "false", + }, + }, + }) +} + +func TestEvalFunctionCall(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Call lambda. + Input: &jparse.FunctionCallNode{ + Func: &jparse.LambdaNode{ + Body: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "y", + }, + }, + ParamNames: []string{ + "x", + "y", + }, + }, + Args: []jparse.Node{ + &jparse.NumberNode{ + Value: 3, + }, + &jparse.NumberNode{ + Value: 18, + }, + }, + }, + Output: float64(54), + }, + { + // Call Extension. + Input: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "repeat", + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: "x", + }, + &jparse.NumberNode{ + Value: 10, + }, + }, + }, + Exts: map[string]Extension{ + "repeat": { + Func: strings.Repeat, + }, + }, + Output: "xxxxxxxxxx", + }, + { + // Call partial. + Input: &jparse.FunctionCallNode{ + Func: &jparse.PartialNode{ + Func: &jparse.VariableNode{ + Name: "repeat", + }, + Args: []jparse.Node{ + &jparse.PlaceholderNode{}, + &jparse.NumberNode{ + Value: 5, + }, + }, + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: "😅", + }, + }, + }, + Exts: map[string]Extension{ + "repeat": { + Func: strings.Repeat, + }, + }, + Output: "😅😅😅😅😅", + }, + { + // Error evaluating the callee. Return the error. + Input: &jparse.FunctionCallNode{ + Func: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // Callee is not callable. Return an error. + Input: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Argument evaluates to an error. Return the error. + Input: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "repeat", + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: "x", + }, + &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + }, + Exts: map[string]Extension{ + "repeat": { + Func: strings.Repeat, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + }) +} + +func TestEvalFunctionApplication(t *testing.T) { + + trimSpace, _ := newGoCallable("trim", Extension{ + Func: strings.TrimSpace, + }) + + toUpper, _ := newGoCallable("uppercase", Extension{ + Func: strings.ToUpper, + }) + + testEvalTestCases(t, []evalTestCase{ + { + // Pass an argument to a function call. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.StringNode{ + Value: "😂", + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "repeat", + }, + Args: []jparse.Node{ + &jparse.NumberNode{ + Value: 5, + }, + }, + }, + }, + Exts: map[string]Extension{ + "repeat": { + Func: strings.Repeat, + }, + }, + Output: "😂😂😂😂😂", + }, + { + // Pass an argument to a function. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.VariableNode{ + Name: "uppercase", + }, + }, + Exts: map[string]Extension{ + "uppercase": { + Func: strings.ToUpper, + }, + }, + Output: "HELLO", + }, + { + // Chain two functions together. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.VariableNode{ + Name: "trim", + }, + RHS: &jparse.VariableNode{ + Name: "uppercase", + }, + }, + Exts: map[string]Extension{ + "trim": { + Func: strings.TrimSpace, + }, + "uppercase": { + Func: strings.ToUpper, + }, + }, + Output: &chainCallable{ + callables: []jtypes.Callable{ + trimSpace, + toUpper, + }, + }, + }, + { + // Left side returns an error. Return the error. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.VariableNode{ + Name: "uppercase", + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // an error on the right side. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // a non-callable right side. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.BooleanNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // Right side returns an error. Return the error. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // Right side is not a Callable. Return an error. + Input: &jparse.FunctionApplicationNode{ + LHS: &jparse.NumberNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallableApply, + Token: "null", + Value: "~>", + }, + }, + }) +} + +func TestEvalNumericOperator(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Addition. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NumberNode{ + Value: 100, + }, + RHS: &jparse.NumberNode{ + Value: 3.14159, + }, + }, + Output: 103.14159, + }, + { + // Subtraction. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericSubtract, + LHS: &jparse.NumberNode{ + Value: 100, + }, + RHS: &jparse.NumberNode{ + Value: 17.5, + }, + }, + Output: 82.5, + }, + { + // Multiplication. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.NumberNode{ + Value: 10, + }, + RHS: &jparse.NumberNode{ + Value: 1.25e5, + }, + }, + Output: float64(1250000), + }, + { + // Division. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericDivide, + LHS: &jparse.NumberNode{ + Value: -99, + }, + RHS: &jparse.NumberNode{ + Value: 3, + }, + }, + Output: float64(-33), + }, + { + // Modulo. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericModulo, + LHS: &jparse.NumberNode{ + Value: -99, + }, + RHS: &jparse.NumberNode{ + Value: 6, + }, + }, + Output: float64(-3), + }, + { + // Expression evaluates to infinity. Return an error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericDivide, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 0, + }, + }, + Error: &EvalError{ + Type: ErrNumberInf, + Value: "/", + }, + }, + { + // Expression evaluates to NaN. Return an error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericDivide, + LHS: &jparse.NumberNode{ + Value: 0, + }, + RHS: &jparse.NumberNode{ + Value: 0, + }, + }, + Error: &EvalError{ + Type: ErrNumberNaN, + Value: "/", + }, + }, + { + // Left side returns an error. Return the error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // an error on the right side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // a non-number on the right side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.BooleanNode{}, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the left side takes precedence over + // an undefined right side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Left side is not a number. Return an error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberLHS, + Token: "false", + Value: "+", + }, + }, + { + // A non-number on the left side takes precedence + // over a non-number on the right side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberLHS, + Token: "false", + Value: "+", + }, + }, + { + // A non-number on the left side takes precedence + // over an undefined right side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonNumberLHS, + Token: "false", + Value: "+", + }, + }, + { + // Right side returns an error. Return the error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NumberNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // An error on the right side takes precedence over + // a non-number on the left side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NullNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // An error on the right side takes precedence over + // an undefined left side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "false", + }, + }, + { + // Right side is not a number. Return an error. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NumberNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "+", + }, + }, + { + // A non-number right side rakes precedence over + // an undefined left side. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "+", + }, + }, + { + // Left side is undefined. Return undefined. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NumberNode{}, + }, + Output: nil, + }, + { + // Right side is undefined. Return undefined. + Input: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NumberNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: nil, + }, + }) +} + +func TestEvalComparisonOperator(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // Number = Number: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumberNode{ + Value: 100, + }, + RHS: &jparse.NumberNode{ + Value: 1e2, + }, + }, + Output: true, + }, + { + // Number = Number: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumberNode{ + Value: 100, + }, + RHS: &jparse.NumberNode{ + Value: -100, + }, + }, + Output: false, + }, + { + // Number = another type: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumberNode{ + Value: 100, + }, + RHS: &jparse.StringNode{ + Value: "100", + }, + }, + Output: false, + }, + { + // String = String: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "hello", + }, + }, + Output: true, + }, + { + // String = String: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + Output: false, + }, + { + // String = another type: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.StringNode{ + Value: "null", + }, + RHS: &jparse.NullNode{}, + }, + Output: false, + }, + { + // Boolean = Boolean: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.BooleanNode{ + Value: false, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + Output: true, + }, + { + // Boolean = Boolean: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + Output: false, + }, + { + // Boolean = another type: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.NullNode{}, + }, + Output: false, + }, + { + // Null = Null: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NullNode{}, + RHS: &jparse.NullNode{}, + }, + Output: true, + }, + { + // Null = another type: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NullNode{}, + RHS: &jparse.StringNode{}, + }, + Output: false, + }, + { + // Array = Array: true + // (Note: as of jsonata 1.7 arrays are compared with a deep comparison) + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "x", + Value: &jparse.ArrayNode{}, + }, + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + }, + }, + Output: true, + }, + { + // Array = Array: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.ArrayNode{}, + RHS: &jparse.ArrayNode{}, + }, + Output: true, + }, + { + // Object = Object: true + // (Note: must be the same object in memory) + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "x", + Value: &jparse.ObjectNode{}, + }, + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + }, + }, + Output: true, + }, + { + // Object = Object: false + // (Note: as of jsonata 1.7 objects are compared with a deep comparison) + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.ObjectNode{}, + RHS: &jparse.ObjectNode{}, + }, + Output: true, + }, + { + // Lambda = Lambda: true + // (Note: must be the same object in memory) + Input: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.AssignmentNode{ + Name: "f", + Value: &jparse.LambdaNode{ + Body: &jparse.NullNode{}, + }, + }, + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "f", + }, + RHS: &jparse.VariableNode{ + Name: "f", + }, + }, + }, + }, + Output: true, + }, + { + // Lambda = Lambda: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.LambdaNode{ + Body: &jparse.NullNode{}, + }, + RHS: &jparse.LambdaNode{ + Body: &jparse.NullNode{}, + }, + }, + Output: false, + }, + { + // Go Callable = Go Callable: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "f1", + }, + RHS: &jparse.VariableNode{ + Name: "f1", + }, + }, + Exts: map[string]Extension{ + "f1": { + Func: func() interface{} { return nil }, + }, + }, + Output: true, + }, + { + // Go Callable = Go Callable: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "f1", + }, + RHS: &jparse.VariableNode{ + Name: "f2", + }, + }, + Exts: map[string]Extension{ + "f1": { + Func: func() interface{} { return nil }, + }, + "f2": { + Func: func() interface{} { return nil }, + }, + }, + Output: false, + }, + { + // Number < Number: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + Output: true, + }, + { + // Number < Number: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{ + Value: 2, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + Output: false, + }, + { + // String < String: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.StringNode{ + Value: "cats", + }, + RHS: &jparse.StringNode{ + Value: "dogs", + }, + }, + Output: true, + }, + { + // String < String: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.StringNode{ + Value: "dogs", + }, + RHS: &jparse.StringNode{ + Value: "cats", + }, + }, + Output: false, + }, + { + // x != y: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonNotEqual, + LHS: &jparse.NullNode{}, + RHS: &jparse.BooleanNode{}, + }, + Output: true, + }, + { + // x != y: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonNotEqual, + LHS: &jparse.NullNode{}, + RHS: &jparse.NullNode{}, + }, + Output: false, + }, + { + // x > y: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreater, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 0, + }, + }, + Output: true, + }, + { + // x > y: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreater, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "hello", + }, + }, + Output: false, + }, + { + // x >= y: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreaterEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + Output: true, + }, + { + // x >= y: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreaterEqual, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + Output: false, + }, + { + // x <= y: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLessEqual, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "hello", + }, + }, + Output: true, + }, + { + // x <= y: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLessEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 0, + }, + }, + Output: false, + }, + { + // x in y: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "hello", + }, + }, + Output: true, + }, + { + // x in y: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + Output: false, + }, + { + // x in another type: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + Output: false, + }, + { + // x in [y]: true + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "bonjour", + }, + &jparse.StringNode{ + Value: "hola", + }, + &jparse.StringNode{ + Value: "ciao", + }, + &jparse.StringNode{ + Value: "hello", + }, + }, + }, + }, + Output: true, + }, + { + // x in [y]: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "au revoir", + }, + &jparse.StringNode{ + Value: "adiós", + }, + &jparse.StringNode{ + Value: "ciao", + }, + &jparse.StringNode{ + Value: "goodbye", + }, + }, + }, + }, + Output: false, + }, + { + // x in []: false + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.ArrayNode{}, + }, + Output: false, + }, + { + // Left side returns an error. Return the error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // an error on the right side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // a non-comparable on the right side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.BooleanNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // an undefined right side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // Left side is non-comparable. Return an error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NumberNode{}, + }, + Error: &EvalError{ + Type: ErrNonComparableLHS, + Token: "false", + Value: "<", + }, + }, + { + // A non-comparable on the left side takes precedence + // over a non-comparable on the right side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreater, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonComparableLHS, + Token: "false", + Value: ">", + }, + }, + { + // A non-comparable on the left side takes precedence + // over an undefined right side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreaterEqual, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonComparableLHS, + Token: "false", + Value: ">=", + }, + }, + { + // Right side returns an error. Return the error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.NumberNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the right side takes precedence over + // a non-comparable on the left side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.BooleanNode{}, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // An error on the right side takes precedence over + // an undefined left side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonCallable, + Token: "null", + }, + }, + { + // Right side is non-comparable. Return an error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{}, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonComparableRHS, + Token: "null", + Value: "<", + }, + }, + { + // A non-comparable right side takes precedence over + // an undefined left side. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLessEqual, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NullNode{}, + }, + Error: &EvalError{ + Type: ErrNonComparableRHS, + Token: "null", + Value: "<=", + }, + }, + { + // Type mismatch. Return an error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{}, + RHS: &jparse.StringNode{}, + }, + Error: &EvalError{ + Type: ErrTypeMismatch, + Value: "<", + }, + }, + { + // Type mismatch in a non-comparable operation + // is not an error. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumberNode{}, + RHS: &jparse.StringNode{}, + }, + Output: false, + }, + { + // Left side is undefined. Return false. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.StringNode{}, + }, + Output: false, + }, + { + // Right side is undefined. Return false. + Input: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{}, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: false, + }, + }) +} + +func TestEvalBooleanOperator(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // true and true: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: true, + }, + { + // true and false: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + Output: false, + }, + { + // true and undefined: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: false, + }, + { + // false and true: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: false, + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: false, + }, + { + // undefined and true: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: false, + }, + { + // false and false: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: false, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + Output: false, + }, + { + // undefined and undefined: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: false, + }, + { + // true or true: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: true, + }, + { + // true or false: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + Output: true, + }, + { + // true or undefined: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: true, + }, + { + // false or true: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.BooleanNode{ + Value: false, + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: true, + }, + { + // undefined or true: true + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Output: true, + }, + { + // undefined or undefined: false + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: false, + }, + { + // Error on left side. Return the error. + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.BooleanNode{ + Value: true, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // an error on the right side. + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // An error on the left side takes precedence over + // an undefined right side. + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + { + // Error on the right side. Return the error. + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An error on the right side takes precedence over + // an undefined left side. + Input: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + }) +} + +func TestEvalStringConcatenation(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + // string & string + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + Output: "helloworld", + }, + { + // string & undefined + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.VariableNode{ + Name: "x", + }, + }, + Output: "hello", + }, + { + // undefined & string + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + Output: "world", + }, + { + // undefined & undefined + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.VariableNode{ + Name: "x", + }, + RHS: &jparse.VariableNode{ + Name: "y", + }, + }, + Output: "", + }, + { + // Left side evaluation error. Return the error. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.StringNode{}, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An evaluation error on the left side takes + // precedence over an evaluation error on the + // right side. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.BooleanNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An evaluation error on the left side takes + // precedence over a conversion error on the + // right side. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + RHS: &jparse.NumberNode{ + Value: math.NaN(), + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // Left side conversion error. Return the error. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.NumberNode{ + Value: math.NaN(), + }, + RHS: &jparse.StringNode{}, + }, + Error: &jlib.Error{ + Type: jlib.ErrNaNInf, + Func: "string", + }, + }, + { + // Right side evaluation error. Return the error. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.StringNode{}, + RHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // An evaluation error on the right side takes + // precedence over a conversion error on the left + // side. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.NumberNode{ + Value: math.NaN(), + }, + RHS: &jparse.NegationNode{ + RHS: &jparse.NullNode{}, + }, + }, + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "null", + Value: "-", + }, + }, + { + // Right side conversion error. Return the error. + Input: &jparse.StringConcatenationNode{ + LHS: &jparse.StringNode{}, + RHS: &jparse.NumberNode{ + Value: math.NaN(), + }, + }, + Error: &jlib.Error{ + Type: jlib.ErrNaNInf, + Func: "string", + }, + }, + }) +} + +func TestEvalName(t *testing.T) { + testEvalTestCases(t, []evalTestCase{ + { + Input: &jparse.NameNode{ + Value: "Field", + }, + Output: nil, + }, + { + Input: &jparse.NameNode{ + Value: "Field", + }, + Data: map[string]interface{}{ + "Field": 9.99, + }, + Output: 9.99, + }, + { + Input: &jparse.NameNode{ + Value: "Field", + }, + Data: struct { + Field float64 + }{ + Field: 9.99, + }, + Output: 9.99, + }, + { + Input: &jparse.NameNode{ + Value: "Field", + }, + Data: []interface{}{ + map[string]interface{}{ + "Field": "one", + }, + map[string]interface{}{ + "Field": 2.0, + }, + map[string]interface{}{ + "Field": true, + }, + }, + Output: []interface{}{ + "one", + 2.0, + true, + }, + }, + }) +} + +func testEvalTestCases(t *testing.T, tests []evalTestCase) { + + for _, test := range tests { + + env := newEnvironment(nil, len(test.Vars)) + + for name, v := range test.Vars { + env.bind(name, reflect.ValueOf(v)) + } + + for name, ext := range test.Exts { + f, err := newGoCallable(name, ext) + if err != nil { + t.Fatalf("newGoCallable error: %s", err) + } + env.bind(name, reflect.ValueOf(f)) + } + + v, err := eval(test.Input, reflect.ValueOf(test.Data), env) + + var output interface{} + if v.IsValid() && v.CanInterface() { + output = v.Interface() + } + + equal := test.Equals + if test.Equals == nil { + equal = reflect.DeepEqual + } + + if !equal(output, test.Output) { + t.Errorf("%s: Expected %v, got %v", test.Input, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", test.Input, test.Error, err) + } + } +} diff --git a/v1.5.4/example_eval_test.go b/v1.5.4/example_eval_test.go new file mode 100644 index 0000000..8e8d015 --- /dev/null +++ b/v1.5.4/example_eval_test.go @@ -0,0 +1,46 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata_test + +import ( + "encoding/json" + "fmt" + "log" + + jsonata "github.com/blues/jsonata-go/v1.5.4" +) + +const jsonString = ` + { + "orders": [ + {"price": 10, "quantity": 3}, + {"price": 0.5, "quantity": 10}, + {"price": 100, "quantity": 1} + ] + } +` + +func ExampleExpr_Eval() { + + var data interface{} + + // Decode JSON. + err := json.Unmarshal([]byte(jsonString), &data) + if err != nil { + log.Fatal(err) + } + + // Create expression. + e := jsonata.MustCompile("$sum(orders.(price*quantity))") + + // Evaluate. + res, err := e.Eval(data) + if err != nil { + log.Fatal(err) + } + + fmt.Println(res) + // Output: 135 +} diff --git a/v1.5.4/example_exts_test.go b/v1.5.4/example_exts_test.go new file mode 100644 index 0000000..1ffdee6 --- /dev/null +++ b/v1.5.4/example_exts_test.go @@ -0,0 +1,66 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata_test + +import ( + "fmt" + "log" + "strings" + "unicode" + + jsonata "github.com/blues/jsonata-go/v1.5.4" +) + +// +// This example demonstrates how to extend JSONata with +// custom functions. +// + +// titleCase returns a copy of the string s with all Unicode letters that begin words +// mapped to their Unicode title case. +func titleCase(s string) string { + // Split the string into words + words := strings.Fields(s) + for i, word := range words { + runes := []rune(word) + if len(runes) > 0 { + runes[0] = unicode.ToTitle(runes[0]) + } + words[i] = string(runes) + } + return strings.Join(words, " ") +} + +// exts defines a function named "titlecase" which maps to +// our custom titleCase function. Any function, +// from the standard library or otherwise, can be used to +// extend JSONata, as long as it returns either one or two +// arguments (the second argument must be an error). +var exts = map[string]jsonata.Extension{ + "titlecase": { + Func: titleCase, + }, +} + +func ExampleExpr_RegisterExts() { + + // Create an expression that uses the titlecase function. + e := jsonata.MustCompile(`$titlecase("beneath the underdog")`) + + // Register the titlecase function. + err := e.RegisterExts(exts) + if err != nil { + log.Fatal(err) + } + + // Evaluate. + res, err := e.Eval(nil) + if err != nil { + log.Fatal(err) + } + + fmt.Println(res) + // Output: Beneath The Underdog +} diff --git a/v1.5.4/jlib/aggregate.go b/v1.5.4/jlib/aggregate.go new file mode 100644 index 0000000..4cfe368 --- /dev/null +++ b/v1.5.4/jlib/aggregate.go @@ -0,0 +1,129 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "reflect" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Sum returns the total of an array of numbers. If the array is +// empty, Sum returns 0. +func Sum(v reflect.Value) (float64, error) { + + if !jtypes.IsArray(v) { + if n, ok := jtypes.AsNumber(v); ok { + return n, nil + } + return 0, fmt.Errorf("cannot call sum on a non-array type") + } + + v = jtypes.Resolve(v) + + var sum float64 + + for i := 0; i < v.Len(); i++ { + n, ok := jtypes.AsNumber(v.Index(i)) + if !ok { + return 0, fmt.Errorf("cannot call sum on an array with non-number types") + } + sum += n + } + + return sum, nil +} + +// Max returns the largest value in an array of numbers. If the +// array is empty, Max returns 0 and an undefined error. +func Max(v reflect.Value) (float64, error) { + + if !jtypes.IsArray(v) { + if n, ok := jtypes.AsNumber(v); ok { + return n, nil + } + return 0, fmt.Errorf("cannot call max on a non-array type") + } + + v = jtypes.Resolve(v) + if v.Len() == 0 { + return 0, jtypes.ErrUndefined + } + + var max float64 + + for i := 0; i < v.Len(); i++ { + n, ok := jtypes.AsNumber(v.Index(i)) + if !ok { + return 0, fmt.Errorf("cannot call max on an array with non-number types") + } + if i == 0 || n > max { + max = n + } + } + + return max, nil +} + +// Min returns the smallest value in an array of numbers. If the +// array is empty, Min returns 0 and an undefined error. +func Min(v reflect.Value) (float64, error) { + + if !jtypes.IsArray(v) { + if n, ok := jtypes.AsNumber(v); ok { + return n, nil + } + return 0, fmt.Errorf("cannot call min on a non-array type") + } + + v = jtypes.Resolve(v) + if v.Len() == 0 { + return 0, jtypes.ErrUndefined + } + + var min float64 + + for i := 0; i < v.Len(); i++ { + n, ok := jtypes.AsNumber(v.Index(i)) + if !ok { + return 0, fmt.Errorf("cannot call min on an array with non-number types") + } + if i == 0 || n < min { + min = n + } + } + + return min, nil +} + +// Average returns the mean of an array of numbers. If the array +// is empty, Average returns 0 and an undefined error. +func Average(v reflect.Value) (float64, error) { + + if !jtypes.IsArray(v) { + if n, ok := jtypes.AsNumber(v); ok { + return n, nil + } + return 0, fmt.Errorf("cannot call average on a non-array type") + } + + v = jtypes.Resolve(v) + if v.Len() == 0 { + return 0, jtypes.ErrUndefined + } + + var sum float64 + + for i := 0; i < v.Len(); i++ { + n, ok := jtypes.AsNumber(v.Index(i)) + if !ok { + return 0, fmt.Errorf("cannot call average on an array with non-number types") + } + sum += n + } + + return sum / float64(v.Len()), nil +} diff --git a/v1.5.4/jlib/array.go b/v1.5.4/jlib/array.go new file mode 100644 index 0000000..f3089a6 --- /dev/null +++ b/v1.5.4/jlib/array.go @@ -0,0 +1,353 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "math/rand" + "reflect" + "sort" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Count (golint) +func Count(v reflect.Value) int { + v = jtypes.Resolve(v) + + if !jtypes.IsArray(v) { + if v.IsValid() { + return 1 + } + return 0 + } + + return v.Len() +} + +// Distinct returns the values passed in with any duplicates removed. +func Distinct(v reflect.Value) interface{} { + v = jtypes.Resolve(v) + + // To match the behavior of jsonata-js, if this is a string we should + // return the entire string and not dedupe the individual characters + if jtypes.IsString(v) { + return v.String() + } + + if jtypes.IsArray(v) { + items := arrayify(v) + visited := make(map[interface{}]struct{}) + distinctValues := reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 0) + + for i := 0; i < items.Len(); i++ { + item := jtypes.Resolve(items.Index(i)) + + if jtypes.IsMap(item) { + // We can't hash a map, so convert it to a + // string that is hashable + mapItem := fmt.Sprint(item.Interface()) + if _, ok := visited[mapItem]; ok { + continue + } + visited[mapItem] = struct{}{} + distinctValues = reflect.Append(distinctValues, item) + + continue + } + + if _, ok := visited[item.Interface()]; ok { + continue + } + + visited[item.Interface()] = struct{}{} + distinctValues = reflect.Append(distinctValues, item) + } + return distinctValues.Interface() + } + + return nil +} + +// Append (golint) +func Append(v1, v2 reflect.Value) (interface{}, error) { + if !v2.IsValid() && v1.IsValid() && v1.CanInterface() { + return v1.Interface(), nil + } + + if !v1.IsValid() && v2.IsValid() && v2.CanInterface() { + return v2.Interface(), nil + } + + v1 = arrayify(v1) + v2 = arrayify(v2) + + len1 := v1.Len() + len2 := v2.Len() + + results := reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, len1+len2) + + appendSlice := func(vs reflect.Value, length int) { + for i := 0; i < length; i++ { + if item := vs.Index(i); item.IsValid() { + results = reflect.Append(results, item) + } + } + } + + appendSlice(v1, len1) + appendSlice(v2, len2) + + return results.Interface(), nil +} + +// Reverse (golint) +func Reverse(v reflect.Value) (interface{}, error) { + v = arrayify(v) + length := v.Len() + + results := reflect.MakeSlice(v.Type(), 0, length) + + for i := length - 1; i >= 0; i-- { + if item := v.Index(i); item.IsValid() { + results = reflect.Append(results, item) + } + } + + return results.Interface(), nil +} + +// Sort (golint) +func Sort(v reflect.Value, swap jtypes.OptionalCallable) (interface{}, error) { + v = jtypes.Resolve(v) + + switch { + case !v.IsValid(): + return nil, jtypes.ErrUndefined + case !jtypes.IsArray(v): + if v.CanInterface() { + return []interface{}{v.Interface()}, nil + } + case swap.Callable != nil: + return sortArrayFunc(v, swap.Callable) + case jtypes.IsArrayOf(v, jtypes.IsNumber): + return sortNumberArray(v), nil + case jtypes.IsArrayOf(v, jtypes.IsString): + return sortStringArray(v), nil + } + + return nil, fmt.Errorf("argument 1 of function sort must be an array of strings or numbers") +} + +func sortNumberArray(v reflect.Value) []interface{} { + size := v.Len() + results := make([]interface{}, 0, size) + + for i := 0; i < size; i++ { + if n, ok := jtypes.AsNumber(v.Index(i)); ok { + results = append(results, n) + } + } + + sort.SliceStable(results, func(i, j int) bool { + return results[i].(float64) < results[j].(float64) + }) + + return results +} + +func sortStringArray(v reflect.Value) []interface{} { + size := v.Len() + results := make([]interface{}, 0, size) + + for i := 0; i < size; i++ { + if s, ok := jtypes.AsString(v.Index(i)); ok { + results = append(results, s) + } + } + + sort.SliceStable(results, func(i, j int) bool { + return results[i].(string) < results[j].(string) + }) + + return results +} + +func sortArrayFunc(v reflect.Value, fn jtypes.Callable) (interface{}, error) { + size := v.Len() + results := make([]interface{}, 0, size) + + for i := 0; i < size; i++ { + if item := v.Index(i); item.CanInterface() { + results = append(results, item.Interface()) + } + } + + swapFunc := func(lhs, rhs interface{}) (bool, error) { + + args := []reflect.Value{ + reflect.ValueOf(lhs), + reflect.ValueOf(rhs), + } + + v, err := fn.Call(args) + if err != nil { + return false, err + } + + b, ok := jtypes.AsBool(v) + if !ok { + return false, fmt.Errorf("argument 2 of function sort must be a function that returns a boolean, got %v (%s)", v, v.Kind()) + } + + return b, nil + } + + return mergeSort(results, swapFunc) +} + +func mergeSort(values []interface{}, swapFunc func(interface{}, interface{}) (bool, error)) ([]interface{}, error) { + n := len(values) + if n < 2 { + return values, nil + } + + pos := n / 2 + lhs, err := mergeSort(values[:pos], swapFunc) + if err != nil { + return nil, err + } + rhs, err := mergeSort(values[pos:], swapFunc) + if err != nil { + return nil, err + } + + return merge(lhs, rhs, swapFunc) +} + +func merge(lhs, rhs []interface{}, swapFunc func(interface{}, interface{}) (bool, error)) ([]interface{}, error) { + results := make([]interface{}, len(lhs)+len(rhs)) + + for i := range results { + + if len(rhs) == 0 { + results = append(results[:i], lhs...) + break + } + + if len(lhs) == 0 { + results = append(results[:i], rhs...) + break + } + + swap, err := swapFunc(lhs[0], rhs[0]) + if err != nil { + return nil, err + } + + if swap { + results[i] = rhs[0] + rhs = rhs[1:] + } else { + results[i] = lhs[0] + lhs = lhs[1:] + } + } + + return results, nil +} + +// Shuffle (golint) +func Shuffle(v reflect.Value) interface{} { + v = forceArray(jtypes.Resolve(v)) + + length := arrayLen(v) + results := make([]interface{}, length) + + for i := 0; i < length; i++ { + + j := rand.Intn(i + 1) + + if i != j { + results[i] = results[j] + } + + item := v.Index(i) + if item.IsValid() && item.CanInterface() { + results[j] = item.Interface() + } + } + + return results +} + +// Zip (golint) +func Zip(vs ...reflect.Value) (interface{}, error) { + var size int + + if len(vs) == 0 { + return nil, fmt.Errorf("cannot call zip with no arguments") + } + + for i := 0; i < len(vs); i++ { + + vs[i] = forceArray(jtypes.Resolve(vs[i])) + if !vs[i].IsValid() { + return []interface{}{}, nil + } + + if i == 0 || arrayLen(vs[i]) < size { + size = arrayLen(vs[i]) + } + } + + result := make([]interface{}, size) + + for i := 0; i < size; i++ { + + inner := make([]interface{}, len(vs)) + + for j := 0; j < len(vs); j++ { + v := vs[j].Index(i) + if v.IsValid() && v.CanInterface() { + inner[j] = v.Interface() + } + } + + result[i] = inner + } + + return result, nil +} + +func forceArray(v reflect.Value) reflect.Value { + v = jtypes.Resolve(v) + if !v.IsValid() || jtypes.IsArray(v) { + return v + } + vs := reflect.MakeSlice(reflect.SliceOf(v.Type()), 0, 1) + vs = reflect.Append(vs, v) + return vs +} + +func arrayLen(v reflect.Value) int { + if jtypes.IsArray(v) { + return v.Len() + } + return 0 +} + +var typeInterface = reflect.TypeOf((*interface{})(nil)).Elem() + +func arrayify(v reflect.Value) reflect.Value { + switch { + case jtypes.IsArray(v): + return jtypes.Resolve(v) + case !v.IsValid(): + return reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 0) + default: + return reflect.Append(reflect.MakeSlice(reflect.SliceOf(typeInterface), 0, 1), v) + } +} diff --git a/v1.5.4/jlib/boolean.go b/v1.5.4/jlib/boolean.go new file mode 100644 index 0000000..f39528e --- /dev/null +++ b/v1.5.4/jlib/boolean.go @@ -0,0 +1,54 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "reflect" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Boolean (golint) +func Boolean(v reflect.Value) bool { + + v = jtypes.Resolve(v) + + if b, ok := jtypes.AsBool(v); ok { + return b + } + + if s, ok := jtypes.AsString(v); ok { + return s != "" + } + + if n, ok := jtypes.AsNumber(v); ok { + return n != 0 + } + + if jtypes.IsArray(v) { + for i := 0; i < v.Len(); i++ { + if Boolean(v.Index(i)) { + return true + } + } + return false + } + + if jtypes.IsMap(v) { + return v.Len() > 0 + } + + return false +} + +// Not (golint) +func Not(v reflect.Value) bool { + return !Boolean(v) +} + +// Exists (golint) +func Exists(v reflect.Value) bool { + return v.IsValid() +} diff --git a/v1.5.4/jlib/date.go b/v1.5.4/jlib/date.go new file mode 100644 index 0000000..f1a74ce --- /dev/null +++ b/v1.5.4/jlib/date.go @@ -0,0 +1,141 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "regexp" + "strconv" + "time" + + "github.com/blues/jsonata-go/v1.5.4/jlib/jxpath" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// 2006-01-02T15:04:05.000Z07:00 +const defaultFormatTimeLayout = "[Y]-[M01]-[D01]T[H01]:[m]:[s].[f001][Z01:01t]" + +var defaultParseTimeLayouts = []string{ + "[Y]-[M01]-[D01]T[H01]:[m]:[s][Z01:01t]", + "[Y]-[M01]-[D01]T[H01]:[m]:[s][Z0100t]", + "[Y]-[M01]-[D01]T[H01]:[m]:[s]", + "[Y]-[M01]-[D01]", + "[Y]", +} + +// FromMillis (golint) +func FromMillis(ms int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { + + t := msToTime(ms).UTC() + + if tz.String != "" { + loc, err := parseTimeZone(tz.String) + if err != nil { + return "", err + } + + t = t.In(loc) + } + + layout := picture.String + if layout == "" { + layout = defaultFormatTimeLayout + } + + return jxpath.FormatTime(t, layout) +} + +// parseTimeZone parses a JSONata timezone. +// +// The format is a "+" or "-" character, followed by four digits, the first two +// denoting the hour offset, and the last two denoting the minute offset. +func parseTimeZone(tz string) (*time.Location, error) { + // must be exactly 5 characters + if len(tz) != 5 { + return nil, fmt.Errorf("invalid timezone") + } + + plusOrMinus := string(tz[0]) + + // the first character must be a literal "+" or "-" character. + // Any other character will error. + var offsetMultiplier int + switch plusOrMinus { + case "-": + offsetMultiplier = -1 + case "+": + offsetMultiplier = 1 + default: + return nil, fmt.Errorf("invalid timezone") + } + + // take the first two digits as "HH" + hours, err := strconv.Atoi(tz[1:3]) + if err != nil { + return nil, fmt.Errorf("invalid timezone") + } + + // take the last two digits as "MM" + minutes, err := strconv.Atoi(tz[3:5]) + if err != nil { + return nil, fmt.Errorf("invalid timezone") + } + + // convert to seconds + offsetSeconds := offsetMultiplier * (60 * ((60 * hours) + minutes)) + + // construct a time.Location based on the tz string and the offset in seconds. + loc := time.FixedZone(tz, offsetSeconds) + + return loc, nil +} + +// ToMillis (golint) +func ToMillis(s string, picture jtypes.OptionalString, tz jtypes.OptionalString) (int64, error) { + layouts := defaultParseTimeLayouts + if picture.String != "" { + layouts = []string{picture.String} + } + + // TODO: How are timezones used for parsing? + + for _, l := range layouts { + if t, err := parseTime(s, l); err == nil { + return timeToMS(t), nil + } + } + + return 0, fmt.Errorf("could not parse time %q", s) +} + +var reMinus7 = regexp.MustCompile("-(0*7)") + +func parseTime(s string, picture string) (time.Time, error) { + // Go's reference time: Mon Jan 2 15:04:05 MST 2006 + refTime := time.Date(2006, time.January, 2, 15, 4, 5, 0, time.FixedZone("MST", -7*60*60)) + + layout, err := jxpath.FormatTime(refTime, picture) + if err != nil { + return time.Time{}, fmt.Errorf("the second argument of the toMillis function must be a valid date format") + } + + // Replace -07:00 with Z07:00 + layout = reMinus7.ReplaceAllString(layout, "Z$1") + + t, err := time.Parse(layout, s) + if err != nil { + return time.Time{}, fmt.Errorf("could not parse time %q", s) + } + + return t, nil +} + +func msToTime(ms int64) time.Time { + return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)) +} + +func timeToMS(t time.Time) int64 { + return t.UnixNano() / int64(time.Millisecond) +} diff --git a/v1.5.4/jlib/date_test.go b/v1.5.4/jlib/date_test.go new file mode 100644 index 0000000..dab8bba --- /dev/null +++ b/v1.5.4/jlib/date_test.go @@ -0,0 +1,119 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib_test + +import ( + "reflect" + "testing" + "time" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +func TestFromMillis(t *testing.T) { + + date := time.Date(2018, time.September, 30, 15, 58, 5, int(762*time.Millisecond), time.UTC) + input := date.UnixNano() / int64(time.Millisecond) + + data := []struct { + Picture string + TZ string + Output string + ExpectedError bool + }{ + { + Picture: "[Y0001]-[M01]-[D01]", + Output: "2018-09-30", + }, + /*{ + Picture: "[[[Y0001]-[M01]-[D01]]]", + Output: "[2018-09-30]", + },*/ + { + Picture: "[M]-[D]-[Y]", + Output: "9-30-2018", + }, + { + Picture: "[D1o] [MNn], [Y]", + Output: "30th September, 2018", + }, + { + Picture: "[D01] [MN,*-3] [Y0001]", + Output: "30 SEP 2018", + }, + { + Picture: "[h]:[m01] [PN]", + Output: "3:58 PM", + }, + { + Picture: "[h]:[m01]:[s01] [Pn]", + Output: "3:58:05 pm", + }, + { + Picture: "[h]:[m01]:[s01] [PN] [ZN,*-3]", + Output: "3:58:05 PM UTC", + }, + { + Picture: "[h]:[m01]:[s01] o'clock [PN] [ZN,*-3]", + Output: "3:58:05 o'clock PM UTC", + }, + { + Picture: "[H01]:[m01]:[s01].[f001]", + Output: "15:58:05.762", + }, + { + Picture: "[H01]:[m01]:[s01] [Z]", + TZ: "+0200", + Output: "17:58:05 +02:00", + }, + { + Picture: "[H01]:[m01]:[s01] [z]", + TZ: "-0500", + Output: "10:58:05 GMT-05:00", + }, + { + Picture: "[H01]:[m01]:[s01] [z]", + TZ: "-0630", + Output: "09:28:05 GMT-06:30", + }, + { + Picture: "[H01]:[m01]:[s01] [z]", + // Invalid TZ + TZ: "-0", + ExpectedError: true, + }, + { + Picture: "[h].[m01][Pn] on [FNn], [D1o] [MNn]", + Output: "3.58pm on Sunday, 30th September", + }, + { + Picture: "[M01]/[D01]/[Y0001] at [H01]:[m01]:[s01]", + Output: "09/30/2018 at 15:58:05", + }, + } + + for _, test := range data { + + var picture jtypes.OptionalString + var tz jtypes.OptionalString + + if test.Picture != "" { + picture.Set(reflect.ValueOf(test.Picture)) + } + + if test.TZ != "" { + tz.Set(reflect.ValueOf(test.TZ)) + } + + got, err := jlib.FromMillis(input, picture, tz) + + if test.ExpectedError && err == nil { + t.Errorf("%s: Expected error, got nil", test.Picture) + } else if got != test.Output { + t.Errorf("%s: Expected %q, got %q", test.Picture, test.Output, got) + } + } +} diff --git a/v1.5.4/jlib/error.go b/v1.5.4/jlib/error.go new file mode 100644 index 0000000..0166211 --- /dev/null +++ b/v1.5.4/jlib/error.go @@ -0,0 +1,44 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import "fmt" + +// ErrType (golint) +type ErrType uint + +// ErrNanInf (golint) +const ( + _ ErrType = iota + ErrNaNInf +) + +// Error (golint) +type Error struct { + Type ErrType + Func string +} + +// Error (golint) +func (e Error) Error() string { + + var msg string + + switch e.Type { + case ErrNaNInf: + msg = "cannot convert NaN/Infinity to string" + default: + msg = "unknown error" + } + + return fmt.Sprintf("%s: %s", e.Func, msg) +} + +func newError(name string, typ ErrType) *Error { + return &Error{ + Func: name, + Type: typ, + } +} diff --git a/v1.5.4/jlib/hof.go b/v1.5.4/jlib/hof.go new file mode 100644 index 0000000..e604b3b --- /dev/null +++ b/v1.5.4/jlib/hof.go @@ -0,0 +1,137 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "reflect" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Map (golint) +func Map(v reflect.Value, f jtypes.Callable) (interface{}, error) { + + v = forceArray(jtypes.Resolve(v)) + + var results []interface{} + + argc := clamp(f.ParamCount(), 1, 3) + + for i := 0; i < arrayLen(v); i++ { + + argv := []reflect.Value{v.Index(i), reflect.ValueOf(i), v} + + res, err := f.Call(argv[:argc]) + if err != nil { + return nil, err + } + if res.IsValid() && res.CanInterface() { + results = append(results, res.Interface()) + } + } + + return results, nil +} + +// Filter (golint) +func Filter(v reflect.Value, f jtypes.Callable) (interface{}, error) { + + v = forceArray(jtypes.Resolve(v)) + + var results []interface{} + + argc := clamp(f.ParamCount(), 1, 3) + + for i := 0; i < arrayLen(v); i++ { + + item := v.Index(i) + argv := []reflect.Value{item, reflect.ValueOf(i), v} + + res, err := f.Call(argv[:argc]) + if err != nil { + return nil, err + } + if Boolean(res) && item.IsValid() && item.CanInterface() { + results = append(results, item.Interface()) + } + } + + return results, nil +} + +// Reduce (golint) +func Reduce(v reflect.Value, f jtypes.Callable, init jtypes.OptionalValue) (interface{}, error) { + + v = forceArray(jtypes.Resolve(v)) + + var res reflect.Value + + if f.ParamCount() != 2 { + return nil, fmt.Errorf("second argument of function \"reduce\" must be a function that takes two arguments") + } + + i := 0 + switch { + case init.IsSet(): + res = jtypes.Resolve(init.Value) + case arrayLen(v) > 0: + res = v.Index(0) + i = 1 + } + + var err error + for ; i < arrayLen(v); i++ { + res, err = f.Call([]reflect.Value{res, v.Index(i)}) + if err != nil { + return nil, err + } + } + + if !res.IsValid() || !res.CanInterface() { + return nil, jtypes.ErrUndefined + } + + return res.Interface(), nil +} + +// Single returns the one and only one value in the array parameter that satisfy +// the function predicate (i.e. function returns Boolean true when passed the +// value). Returns an error if the number of matching values is not exactly +// one. +// https://docs.jsonata.org/higher-order-functions#single +func Single(v reflect.Value, f jtypes.Callable) (interface{}, error) { + filteredValue, err := Filter(v, f) + if err != nil { + return nil, err + } + + switch reflect.TypeOf(filteredValue).Kind() { + case reflect.Slice: + // Since Filter() returned a slice, if there is either zero or + // more than one item in the slice, return a error, otherwise + // return the item + s := reflect.ValueOf(filteredValue) + if s.Len() != 1 { + return nil, fmt.Errorf("number of matching values returned by single() must be 1, got: %d", s.Len()) + } + return s.Index(0).Interface(), nil + + default: + // Filter returned a single value, so use that + return reflect.ValueOf(filteredValue).Interface(), nil + } +} + +func clamp(n, min, max int) int { + switch { + case n < min: + return min + case n > max: + return max + default: + return n + } +} diff --git a/v1.5.4/jlib/jlib.go b/v1.5.4/jlib/jlib.go new file mode 100644 index 0000000..96eb319 --- /dev/null +++ b/v1.5.4/jlib/jlib.go @@ -0,0 +1,83 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package jlib implements the JSONata function library. +package jlib + +import ( + "fmt" + "reflect" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Random numbers for Random() and Shuffle() functions are automatically seeded +// in Go 1.20+ without needing to call rand.Seed + +var typeBool = reflect.TypeOf((*bool)(nil)).Elem() +var typeCallable = reflect.TypeOf((*jtypes.Callable)(nil)).Elem() +var typeString = reflect.TypeOf((*string)(nil)).Elem() +var typeNumber = reflect.TypeOf((*float64)(nil)).Elem() + +// StringNumberBool (golint) +type StringNumberBool reflect.Value + +// ValidTypes (golint) +func (StringNumberBool) ValidTypes() []reflect.Type { + return []reflect.Type{ + typeBool, + typeString, + typeNumber, + } +} + +// StringCallable (golint) +type StringCallable reflect.Value + +// ValidTypes (golint) +func (StringCallable) ValidTypes() []reflect.Type { + return []reflect.Type{ + typeString, + typeCallable, + } +} + +func (s StringCallable) toInterface() interface{} { + if v := reflect.Value(s); v.IsValid() && v.CanInterface() { + return v.Interface() + } + return nil +} + +// TypeOf implements the jsonata $type function that returns the data type of +// the argument +func TypeOf(x interface{}) (string, error) { + v := reflect.ValueOf(x) + if jtypes.IsCallable(v) { + return "function", nil + } + if jtypes.IsString(v) { + return "string", nil + } + if jtypes.IsNumber(v) { + return "number", nil + } + if jtypes.IsArray(v) { + return "array", nil + } + if jtypes.IsBool(v) { + return "boolean", nil + } + if jtypes.IsMap(v) { + return "object", nil + } + + switch x.(type) { + case *interface{}: + return "null", nil + } + + xType := reflect.TypeOf(x).String() + return "", fmt.Errorf("unknown type %s", xType) +} diff --git a/v1.5.4/jlib/jxpath/formatdate.go b/v1.5.4/jlib/jxpath/formatdate.go new file mode 100644 index 0000000..942fc48 --- /dev/null +++ b/v1.5.4/jlib/jxpath/formatdate.go @@ -0,0 +1,932 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jxpath + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +type dateComponent rune + +const ( + dateYear dateComponent = 'Y' + dateMonth dateComponent = 'M' + dateDay dateComponent = 'D' + dateDayOfYear dateComponent = 'd' + dateDayOfWeek dateComponent = 'F' + dateWeekOfYear dateComponent = 'W' + dateWeekOfMonth dateComponent = 'w' + dateHour24 dateComponent = 'H' + dateHour12 dateComponent = 'h' + dateAMPM dateComponent = 'P' + dateMinute dateComponent = 'm' + dateSecond dateComponent = 's' + dateNanosecond dateComponent = 'f' + dateTZ dateComponent = 'Z' + dateTZPrefixed dateComponent = 'z' + dateCalendar dateComponent = 'C' + dateEra dateComponent = 'E' +) + +var defaultDateFormats = map[dateComponent]string{ + dateYear: "1", + dateMonth: "1", + dateDay: "1", + dateDayOfYear: "1", + dateDayOfWeek: "n", + dateWeekOfYear: "1", + dateWeekOfMonth: "1", + dateHour24: "1", + dateHour12: "1", + dateAMPM: "n", + dateMinute: "01", + dateSecond: "01", + dateNanosecond: "1", + dateTZ: "01:01", + dateTZPrefixed: "01:01", + dateCalendar: "n", + dateEra: "n", +} + +type formatModifier uint8 + +const ( + _ formatModifier = iota + modOrdinal + modCardinal + modAlphabetic + modTraditional +) + +type variableMarker struct { + format string + modifier formatModifier + minWidth int + maxWidth int +} + +var errUnsupported = errors.New("unsupported date format") + +// FormatTime converts a time to a string, formatted according +// to the given picture string. +// +// See the XPath documentation for the syntax of the picture +// string. +// +// https://www.w3.org/TR/xpath-functions-31/#rules-for-datetime-formatting +func FormatTime(t time.Time, picture string) (string, error) { + var start int + var inMarker, doubleClosingBracket, expanded bool + + result := make([]byte, 0, 128) + + for current, r := range picture { + if r == '[' { + if inMarker { + if current != start { + return "", fmt.Errorf("open bracket inside variable marker") + } + inMarker = false + } else { + result = append(result, picture[start:current]...) + start = current + 1 + inMarker = true + } + + continue + } + + if r == ']' { + if inMarker { + if current == start { + return "", fmt.Errorf("empty variable marker") + } + s, err := expandVariableMarker(t, picture[start:current]) + if err != nil { + return "", err + } + result = append(result, s...) + start = current + 1 + inMarker = false + expanded = true + } else { + if doubleClosingBracket { + doubleClosingBracket = false + continue + } + next := current + 1 + if next >= len(picture) || picture[next] != ']' { + return "", fmt.Errorf("closing bracket outside variable marker") + } + doubleClosingBracket = true + result = append(result, picture[start:current]...) + start = next + } + + continue + } + } + + if inMarker { + return "", fmt.Errorf("unterminated variable marker") + } + + if !expanded { + return "", fmt.Errorf("no variable markers found") + } + + result = append(result, picture[start:]...) + return string(result), nil +} + +func expandVariableMarker(t time.Time, s string) (string, error) { + + component, marker, err := parseVariableMarker(s) + if err != nil { + return "", err + } + + var isDefaultFormat bool + + if marker.format == "" { + marker.modifier = 0 + marker.format = defaultDateFormats[component] + isDefaultFormat = true + } + + repl, err := expandDateComponent(t, component, &marker) + + if err == errUnsupported && !isDefaultFormat { + marker.modifier = 0 + marker.format = defaultDateFormats[component] + repl, err = expandDateComponent(t, component, &marker) + } + + return repl, err +} + +var zeroVariableMarker variableMarker + +func parseVariableMarker(s string) (dateComponent, variableMarker, error) { + + var format string + var modifier formatModifier + var minWidth, maxWidth int + + s = stripSpace(s) + if s == "" { + return 0, zeroVariableMarker, fmt.Errorf("empty variable marker") + } + + if len(s) == 1 { + return dateComponent(s[0]), zeroVariableMarker, nil + } + + presentationModifiers, widthModifier, err := parseVariableMarkerModifiers(s[1:]) + if err != nil { + return 0, zeroVariableMarker, err + } + + if presentationModifiers != "" { + format, modifier = parsePresentationModifiers(presentationModifiers) + } + + if widthModifier != "" { + minWidth, maxWidth, err = parseWidthModifier(widthModifier) + if err != nil { + return 0, zeroVariableMarker, err + } + } + + return dateComponent(s[0]), variableMarker{ + format: format, + modifier: modifier, + minWidth: minWidth, + maxWidth: maxWidth, + }, nil +} + +func parseVariableMarkerModifiers(s string) (string, string, error) { + + pos := strings.LastIndexByte(s, ',') + if pos < 0 { + return s, "", nil + } + + presentationModifiers := s[:pos] + widthModifier := s[pos+1:] + + if widthModifier == "" { + return "", "", fmt.Errorf("empty width modifier") + } + + return presentationModifiers, widthModifier, nil +} + +func parsePresentationModifiers(s string) (string, formatModifier) { + + var format string + var modifier formatModifier + + switch len := len(s); len { + case 0: + case 1: + format = s + default: + last := len - 1 + format = s[:last] + switch s[last] { + case 'a': + modifier = modAlphabetic + case 't': + modifier = modTraditional + case 'c': + modifier = modCardinal + case 'o': + modifier = modOrdinal + default: + format = s + } + } + + return format, modifier +} + +func parseWidthModifier(s string) (int, int, error) { + + var err error + var min, max int + + parts := strings.Split(s, "-") + switch len(parts) { + case 1: + min, err = parseWidth(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("invalid width %q: %s", parts[0], err) + } + case 2: + min, err = parseWidth(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("invalid minimum width %q: %s", parts[0], err) + } + max, err = parseWidth(parts[1]) + if err != nil { + return 0, 0, fmt.Errorf("invalid maximum width %q: %s", parts[1], err) + } + if max < min { + return 0, 0, fmt.Errorf("invalid width modifier %q: maximum width cannot be less than minimum width", s) + } + default: + return 0, 0, fmt.Errorf("invalid width modifier %q", s) + } + + return min, max, nil +} + +func parseWidth(s string) (int, error) { + + if s == "*" { + return 0, nil + } + + if !isAllDigits(s) { + return 0, fmt.Errorf("width contains illegal characters") + } + + n, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("width is not an integer") + } + + if n < 1 { + return 0, fmt.Errorf("width cannot be less than 1") + } + + return n, nil +} + +func expandDateComponent(t time.Time, component dateComponent, marker *variableMarker) (string, error) { + switch component { + case dateYear: + return formatYear(t, marker) + case dateMonth: + return formatMonth(t, marker) + case dateDay: + return formatDay(t, marker) + case dateDayOfYear: + return formatDayInYear(t, marker) + case dateDayOfWeek: + return formatDayOfWeek(t, marker) + case dateWeekOfYear: + return formatWeekInYear(t, marker) + case dateWeekOfMonth: + return formatWeekInMonth(t, marker) + case dateHour24: + return formatHour24(t, marker) + case dateHour12: + return formatHour12(t, marker) + case dateAMPM: + return formatAMPM(t, marker) + case dateMinute: + return formatMinute(t, marker) + case dateSecond: + return formatSecond(t, marker) + case dateNanosecond: + return formatNanosecond(t, marker) + case dateTZ: + return formatTimezoneUnprefixed(t, marker) + case dateTZPrefixed: + return formatTimezonePrefixed(t, marker) + case dateCalendar: + return formatCalendar(t, marker) + case dateEra: + return formatEra(t, marker) + default: + return "", fmt.Errorf("unknown component specifier %c", component) + } +} + +func formatYear(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + + size := marker.maxWidth + if size <= 0 { + if n := countDigits(marker.format); n >= 2 { + size = n + } + } + + y := t.Year() + if size > 0 { + y = y % pow10(size) + } + + return formatIntegerComponent(y, marker) +} + +func formatMonth(t time.Time, marker *variableMarker) (string, error) { + + month := t.Month() + + if isNameFormat(marker.format) { + names := defaultLanguage.months[month] + return formatNameComponent(names, marker) + } + + if isDecimalFormat(marker.format) { + return formatIntegerComponent(int(month), marker) + } + + return "", errUnsupported +} + +func formatDay(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + return formatIntegerComponent(t.Day(), marker) +} + +func formatDayInYear(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + return formatIntegerComponent(t.YearDay(), marker) +} + +func formatDayOfWeek(t time.Time, marker *variableMarker) (string, error) { + + day := t.Weekday() + + if isNameFormat(marker.format) { + names := defaultLanguage.days[day] + return formatNameComponent(names, marker) + } + + if isDecimalFormat(marker.format) { + return formatIntegerComponent(int(day)+1, marker) + } + + return "", errUnsupported +} + +func formatWeekInYear(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + + _, w := t.ISOWeek() + return formatIntegerComponent(w, marker) +} + +func formatWeekInMonth(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + return formatIntegerComponent(daysToWeeks(t.Day()), marker) +} + +func formatHour24(t time.Time, marker *variableMarker) (string, error) { + return formatHour(t, marker, false) +} + +func formatHour12(t time.Time, marker *variableMarker) (string, error) { + return formatHour(t, marker, true) +} + +func formatHour(t time.Time, marker *variableMarker, hour12 bool) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + + h := t.Hour() + if hour12 && h > 12 { + h -= 12 + } + return formatIntegerComponent(h, marker) +} + +func formatAMPM(t time.Time, marker *variableMarker) (string, error) { + + if !isNameFormat(marker.format) { + return "", errUnsupported + } + + names := defaultLanguage.am + if t.Hour() >= 12 { + names = defaultLanguage.pm + } + + return formatNameComponent(names, marker) +} + +func formatMinute(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + return formatIntegerComponent(t.Minute(), marker) +} + +func formatSecond(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + return formatIntegerComponent(t.Second(), marker) +} + +func formatNanosecond(t time.Time, marker *variableMarker) (string, error) { + + if !isDecimalFormat(marker.format) { + return "", errUnsupported + } + + l := utf8.RuneCountInString(marker.format) + + if l == 1 || !isAllDigits(marker.format) { + return formatNano(t.Nanosecond(), 9), nil + } + + return formatNano(t.Nanosecond(), l), nil +} + +func formatNano(n, maxlen int) string { + + var buf [9]byte + for start := len(buf); start > 0; { + start-- + buf[start] = byte(n%10 + '0') + n /= 10 + } + + if maxlen > 9 { + maxlen = 9 + } + + return string(buf[:maxlen]) +} + +type tzStyle uint + +const ( + _ tzStyle = iota + tzShort // -07 + tzLong // -0700 + tzSplit // -07:00 + tzMilitary // T + tzName // MST +) + +type tzSplitLayout struct { + hours string + minutes string + separator string +} + +var militaryOffsets = map[int]string{ + 1: "A", + 2: "B", + 3: "C", + 4: "D", + 5: "E", + 6: "F", + 7: "G", + 8: "H", + 9: "I", + // "J" is reserved for times with no timezone (i.e. local time). + 10: "K", + 11: "L", + 12: "M", + -1: "N", + -2: "O", + -3: "P", + -4: "Q", + -5: "R", + -6: "S", + -7: "T", + -8: "U", + -9: "V", + -10: "W", + -11: "X", + -12: "Y", + 0: "Z", +} + +var reTZSplit = regexp.MustCompile("^([0-9]+)([^0-9A-Za-z])([0-9]+)$") + +func getTimezoneStyle(s string) (tzStyle, *tzSplitLayout) { + + if s == "Z" { + return tzMilitary, nil + } + + if isNameFormat(s) { + return tzName, nil + } + + if isAllDigits(s) { + switch len(s) { + case 1, 2: + return tzShort, nil + case 3, 4: + return tzLong, nil + default: + return 0, nil + } + } + + matches := reTZSplit.FindAllStringSubmatch(s, -1) + if len(matches) == 1 && len(matches[0]) == 4 { + return tzSplit, &tzSplitLayout{ + hours: matches[0][1], + minutes: matches[0][3], + separator: matches[0][2], + } + } + + return 0, nil +} + +func formatTimezoneUnprefixed(t time.Time, marker *variableMarker) (string, error) { + return formatTimezone(t, marker, false) +} + +func formatTimezonePrefixed(t time.Time, marker *variableMarker) (string, error) { + return formatTimezone(t, marker, true) +} + +func formatTimezone(t time.Time, marker *variableMarker, prefixed bool) (string, error) { + + var tz string + var err error + + style, split := getTimezoneStyle(marker.format) + isNumeric := style == tzShort || style == tzLong || style == tzSplit + + name, hours, minutes := getTimezoneInfo(t) + + switch { + case marker.modifier == modTraditional && isNumeric && hours == 0 && minutes == 0: + tz = "Z" + isNumeric = false + + case style == tzShort: + tz, err = formatTimezoneShort(hours, minutes, marker.format) + + case style == tzLong: + tz, err = formatTimezoneLong(hours, minutes, marker.format) + + case style == tzSplit: + tz, err = formatTimezoneSplit(hours, split.hours, minutes, split.minutes, split.separator) + + case style == tzName && name != "": + tz, err = formatNameComponent([]string{name}, &variableMarker{ + format: marker.format, + }) + + case style == tzMilitary && minutes == 0 && hours >= -12 && hours <= 12: + tz = militaryOffsets[hours] + + default: + return "", errUnsupported + } + + if err != nil { + return "", err + } + + if prefixed && isNumeric { + tz = defaultLanguage.tzPrefix + tz + } + + if marker.minWidth > 0 { + padding := marker.minWidth - utf8.RuneCountInString(tz) + if padding > 0 { + tz += strings.Repeat(" ", padding) + } + } + + return tz, nil +} + +func formatTimezoneShort(h int, m int, layout string) (string, error) { + + tz, err := formatInteger(h, layout) + if err != nil { + return "", err + } + + if h >= 0 { + tz = "+" + tz + } + + if m != 0 { + tz += fmt.Sprintf(":%02d", abs(m)) + } + + return tz, nil +} + +func formatTimezoneLong(h int, m int, layout string) (string, error) { + + tz, err := formatInteger(h*100+m, layout) + if err != nil { + return "", err + } + + if h >= 0 { + tz = "+" + tz + } + + return tz, nil +} + +func formatTimezoneSplit(h int, layoutH string, m int, layoutM string, separator string) (string, error) { + + hh, err := formatInteger(h, layoutH) + if err != nil { + return "", err + } + + mm, err := formatInteger(abs(m), layoutM) + if err != nil { + return "", err + } + + tz := hh + separator + mm + + if h >= 0 { + tz = "+" + tz + } + + return tz, nil +} + +var calendars = []string{"AD"} + +func formatCalendar(t time.Time, marker *variableMarker) (string, error) { + + if !isNameFormat(marker.format) { + return "", errUnsupported + } + return formatNameComponent(calendars, marker) +} + +var eras = []string{"CE"} + +func formatEra(t time.Time, marker *variableMarker) (string, error) { + + if !isNameFormat(marker.format) { + return "", errUnsupported + } + return formatNameComponent(eras, marker) +} + +func formatIntegerComponent(n int, marker *variableMarker) (string, error) { + + s, err := formatInteger(n, marker.format) + if err != nil { + return "", err + } + + switch marker.modifier { + case modOrdinal: + s += ordinalSuffix(n) + } + + return s, nil +} + +var defaultDecimalFormat = NewDecimalFormat() + +func formatInteger(n int, layout string) (string, error) { + return FormatNumber(float64(n), layout, defaultDecimalFormat) +} + +func formatNameComponent(names []string, marker *variableMarker) (string, error) { + + s := bestFittingString(names, marker.maxWidth) + if s == "" { + return "", fmt.Errorf("no name exists for max length %d", marker.maxWidth) + } + + switch marker.format { + case "N": + s = strings.ToUpper(s) + case "n": + s = strings.ToLower(s) + case "Nn": + s = toTitle(s) + } + + if marker.minWidth > 0 { + padding := marker.minWidth - utf8.RuneCountInString(s) + if padding > 0 { + s += strings.Repeat(" ", padding) + } + } + + return s, nil +} + +func bestFittingString(values []string, maxlen int) string { + + if len(values) == 0 { + return "" + } + + if maxlen <= 0 { + return values[0] + } + + for _, s := range values { + if utf8.RuneCountInString(s) <= maxlen { + return s + } + } + + pos := positionOfNthRune(values[0], maxlen) + return values[0][:pos] +} + +const secondsPerMinute = 60 +const secondsPerHour = secondsPerMinute * 60 + +func getTimezoneInfo(t time.Time) (string, int, int) { + name, secs := t.Zone() + hours := secs / secondsPerHour + secs = secs % secondsPerHour + minutes := secs / secondsPerMinute + return name, hours, minutes +} + +func daysToWeeks(days int) int { + return (days / 7) + 1 +} + +func ordinalSuffix(n int) string { + + mod10 := n % 10 + mod100 := n % 100 + + switch { + case mod10 == 1 && mod100 != 11: + return "st" + case mod10 == 2 && mod100 != 12: + return "nd" + case mod10 == 3 && mod100 != 13: + return "rd" + default: + return "th" + } +} + +func stripSpace(s string) string { + return strings.Map(func(r rune) rune { + if isWhitespace(r) { + return -1 + } + return r + }, s) +} + +func toTitle(s string) string { + toUpper := true + return strings.Map(func(r rune) rune { + if isWhitespace(r) { + toUpper = true + return r + } + if toUpper { + toUpper = false + return unicode.ToUpper(r) + } + return unicode.ToLower(r) + }, s) +} + +func isWhitespace(r rune) bool { + switch r { + case ' ', '\t', '\n', '\r', '\v': + return true + default: + return false + } +} + +func isDecimalFormat(s string) bool { + return strings.ContainsAny(s, "0123456789") +} + +func isNameFormat(s string) bool { + return s == "N" || s == "n" || s == "Nn" +} + +func isAllDigits(s string) bool { + + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + + return len(s) > 0 +} + +func countDigits(s string) int { + + n := 0 + for _, r := range s { + if strings.ContainsRune("0123456789#", r) { + n++ + } + } + + return n +} + +func pow10(n int) int { + val := 1 + for i := 0; i < n; i++ { + val *= 10 + } + return val +} + +func positionOfNthRune(s string, n int) int { + + i := 0 + for pos := range s { + if i == n { + return pos + } + i++ + } + + return -1 +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} diff --git a/v1.5.4/jlib/jxpath/formatdate_test.go b/v1.5.4/jlib/jxpath/formatdate_test.go new file mode 100644 index 0000000..0ceaa9d --- /dev/null +++ b/v1.5.4/jlib/jxpath/formatdate_test.go @@ -0,0 +1,455 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jxpath + +import ( + "reflect" + "testing" + "time" +) + +func TestFormatYear(t *testing.T) { + + input := time.Date(2018, time.April, 1, 12, 0, 0, 0, time.UTC) + + data := []struct { + Picture string + Output string + Error error + }{ + { + // Default layout is 1. + Picture: "[Y]", + Output: "2018", + }, + { + Picture: "[Y1]", + Output: "2018", + }, + { + Picture: "[Y01]", + Output: "18", + }, + { + Picture: "[Y001]", + Output: "018", + }, + { + Picture: "[Y0001]", + Output: "2018", + }, + { + Picture: "[Y9,999,*]", + Output: "2,018", + }, + { + Picture: "[Y1,*-1]", + Output: "8", + }, + { + Picture: "[Y1,*-2]", + Output: "18", + }, + /*{ + Picture: "[Y1,*-3]", + Output: "018", + },*/ + { + Picture: "[Y1,*-4]", + Output: "2018", + }, + { + // Unsupported layouts should fall back to the default. + Picture: "[YNn]", + Output: "2018", + }, + } + + for _, test := range data { + + got, err := FormatTime(input, test.Picture) + + if got != test.Output { + t.Errorf("%s: Expected %q, got %q", test.Picture, test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", test.Picture, test.Error, err) + } + } +} + +func TestFormatTimezone(t *testing.T) { + + const minutes = 60 + const hours = 60 * minutes + + timezones := []struct { + Name string + Offset int + }{ + { + Name: "HST", + Offset: -10 * hours, + }, + { + Name: "EST", + Offset: -5 * hours, + }, + { + Name: "GMT", + Offset: 0, + }, + { + Name: "IST", + Offset: 5*hours + 30*minutes, + }, + { + Offset: 13 * hours, + }, + } + + times := make([]time.Time, len(timezones)) + for i, tz := range timezones { + // We're mostly interested in the timezone for these tests + // so the exact date used here doesn't matter. But the time + // must be 12:00 for the final test case (which also outputs + // the time) to work. + times[i] = time.Date(2018, time.April, 1, 12, 0, 0, 0, time.FixedZone(tz.Name, tz.Offset)) + } + + data := []struct { + Picture string + Location *time.Location + Outputs []string + }{ + { + // Default layout is 00:00. + Picture: "[Z]", + Outputs: []string{ + "-10:00", + "-05:00", + "+00:00", + "+05:30", + "+13:00", + }, + }, + { + Picture: "[Z0]", + Outputs: []string{ + "-10", + "-5", + "+0", + "+5:30", + "+13", + }, + }, + { + Picture: "[Z00]", + Outputs: []string{ + "-10", + "-05", + "+00", + "+05:30", + "+13", + }, + }, + { + Picture: "[Z00t]", + Outputs: []string{ + "-10", + "-05", + "Z", + "+05:30", + "+13", + }, + }, + { + Picture: "[Z000]", + Outputs: []string{ + "-1000", + "-500", + "+000", + "+530", + "+1300", + }, + }, + { + Picture: "[Z0000]", + Outputs: []string{ + "-1000", + "-0500", + "+0000", + "+0530", + "+1300", + }, + }, + { + Picture: "[Z0000t]", + Outputs: []string{ + "-1000", + "-0500", + "Z", + "+0530", + "+1300", + }, + }, + { + Picture: "[Z0:00]", + Outputs: []string{ + "-10:00", + "-5:00", + "+0:00", + "+5:30", + "+13:00", + }, + }, + { + Picture: "[Z00:00]", + Outputs: []string{ + "-10:00", + "-05:00", + "+00:00", + "+05:30", + "+13:00", + }, + }, + { + Picture: "[Z00:00t]", + Outputs: []string{ + "-10:00", + "-05:00", + "Z", + "+05:30", + "+13:00", + }, + }, + { + Picture: "[z]", + Outputs: []string{ + "GMT-10:00", + "GMT-05:00", + "GMT+00:00", + "GMT+05:30", + "GMT+13:00", + }, + }, + { + Picture: "[ZZ]", + Outputs: []string{ + "W", + "R", + "Z", + "+05:30", // military layout only supports whole hour offsets, fall back to the default + "+13:00", // military layout only supports offsets up to 12 hours, fall back to the default + }, + }, + { + Picture: "[ZN]", + Outputs: []string{ + "HST", + "EST", + "GMT", + "IST", + "+13:00", // timezone has no name, fall back to the default layout + }, + }, + { + Picture: "[H00]:[m00] [ZN]", + Location: time.FixedZone("EST", -5*hours), + Outputs: []string{ + "17:00 EST", // Note: The XPath docs (incorrectly) have this as 06:00 EST + "12:00 EST", + "07:00 EST", + "01:30 EST", + "18:00 EST", + }, + }, + } + + for _, test := range data { + for i, tm := range times { + + if test.Location != nil { + tm = tm.In(test.Location) + } + + got, err := FormatTime(tm, test.Picture) + + if got != test.Outputs[i] { + t.Errorf("%s: Expected %q, got %q", test.Picture, test.Outputs[i], got) + } + + if err != nil { + t.Errorf("%s: Expected nil error, got %s", test.Picture, err) + } + } + } +} + +func TestFormatDayOfWeek(t *testing.T) { + + startTime := time.Date(2018, time.April, 1, 12, 0, 0, 0, time.UTC) + + var times [7]time.Time + for i := range times { + times[i] = startTime.AddDate(0, 0, i) + } + + data := []struct { + Picture string + Outputs [7]string + }{ + { + // Default layout is n + Picture: "[F]", + Outputs: [7]string{ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + }, + }, + { + Picture: "[FNn]", + Outputs: [7]string{ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + }, + }, + { + Picture: "[FNn,*-6]", + Outputs: [7]string{ + "Sunday", + "Monday", + "Tues", + "Weds", + "Thurs", + "Friday", + "Sat", + }, + }, + { + Picture: "[FNn,6-6]", + Outputs: [7]string{ + "Sunday", + "Monday", + "Tues ", + "Weds ", + "Thurs ", + "Friday", + "Sat ", + }, + }, + { + Picture: "[FNn,*-5]", + Outputs: [7]string{ + "Sun", + "Mon", + "Tues", + "Weds", + "Thurs", + "Fri", + "Sat", + }, + }, + { + Picture: "[FNn,*-4]", + Outputs: [7]string{ + "Sun", + "Mon", + "Tues", + "Weds", + "Thur", + "Fri", + "Sat", + }, + }, + { + Picture: "[FN,*-3]", + Outputs: [7]string{ + "SUN", + "MON", + "TUE", + "WED", + "THU", + "FRI", + "SAT", + }, + }, + { + Picture: "[FNn,*-2]", + Outputs: [7]string{ + "Su", + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa", + }, + }, + { + Picture: "Day [F01]: [FNn]", + Outputs: [7]string{ + "Day 01: Sunday", + "Day 02: Monday", + "Day 03: Tuesday", + "Day 04: Wednesday", + "Day 05: Thursday", + "Day 06: Friday", + "Day 07: Saturday", + }, + }, + { + Picture: "[FNn] is the [F1o] day of the week", + Outputs: [7]string{ + "Sunday is the 1st day of the week", + "Monday is the 2nd day of the week", + "Tuesday is the 3rd day of the week", + "Wednesday is the 4th day of the week", + "Thursday is the 5th day of the week", + "Friday is the 6th day of the week", + "Saturday is the 7th day of the week", + }, + }, + { + // Unsupported layouts should fall back to the default. + Picture: "[FI]", + Outputs: [7]string{ + "sunday", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + }, + }, + } + + for _, test := range data { + for i, tm := range times { + + got, err := FormatTime(tm, test.Picture) + + if got != test.Outputs[i] { + t.Errorf("%s: Expected %q, got %q", test.Picture, test.Outputs[i], got) + } + + if err != nil { + t.Errorf("%s: Expected nil error, got %s", test.Picture, err) + } + } + } +} diff --git a/v1.5.4/jlib/jxpath/formatnumber.go b/v1.5.4/jlib/jxpath/formatnumber.go new file mode 100644 index 0000000..875a159 --- /dev/null +++ b/v1.5.4/jlib/jxpath/formatnumber.go @@ -0,0 +1,788 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jxpath + +import ( + "bytes" + "fmt" + "math" + "strconv" + "strings" + "unicode/utf8" +) + +// A DecimalFormat defines the symbols used in a FormatNumber +// picture string. +// +// See the XPath documentation for background. +// +// https://www.w3.org/TR/xpath-functions-31/#defining-decimal-format +type DecimalFormat struct { + DecimalSeparator rune + GroupSeparator rune + ExponentSeparator rune + MinusSign rune + Infinity string + NaN string + Percent string + PerMille string + ZeroDigit rune + OptionalDigit rune + PatternSeparator rune +} + +// NewDecimalFormat returns a new DecimalFormat object with +// the default number formatting settings. +func NewDecimalFormat() DecimalFormat { + return DecimalFormat{ + DecimalSeparator: '.', + GroupSeparator: ',', + ExponentSeparator: 'e', + MinusSign: '-', + Infinity: "Infinity", + NaN: "NaN", + Percent: "%", + PerMille: "‰", + ZeroDigit: '0', + OptionalDigit: '#', + PatternSeparator: ';', + } +} + +// The following helper methods are designed for use with +// functions like strings.IndexFunc. Note that: +// +// 1. The methods have pointer receivers. There's significant +// performance overhead in using value receivers as these +// methods are potentially called once for every rune in +// a string. +// +// 2. The methods are not passed directly to the string +// functions as declaring method values causes the entire +// DecimalFormat object to be allocated on the heap. This, +// for example, is perfectly valid Go code: +// +// df := NewDecimalFormat() +// pos := strings.IndexFunc(s, df.isDigit) +// +// But the method value df.isDigit causes an allocation +// (which is more work for the garbage collector). We can +// avoid that performance hit by using an anonymous wrapper +// function instead: +// +// df := NewDecimalFormat() +// pos := strings.IndexFunc(s, func(r rune) bool { +// return df.isDigit(r) +// }) +// +// This code may not be as readable but it runs ~40% faster +// in Go v1.10.3. See issue #27557 for updates: +// +// https://github.com/golang/go/issues/27557 + +func (format *DecimalFormat) isZeroDigit(r rune) bool { + return r == format.ZeroDigit +} + +func (format *DecimalFormat) isDecimalDigit(r rune) bool { + r -= format.ZeroDigit + return r >= 0 && r <= 9 +} + +func (format *DecimalFormat) isDigit(r rune) bool { + return r == format.OptionalDigit || format.isDecimalDigit(r) +} + +func (format *DecimalFormat) isActive(r rune) bool { + switch r { + case + format.DecimalSeparator, + format.ExponentSeparator, + format.GroupSeparator, + format.PatternSeparator, + format.OptionalDigit: + return true + default: + return format.isDecimalDigit(r) + } +} + +// FormatNumber converts a number to a string, formatted according +// to the given picture string and decimal format. +// +// See the XPath function format-number for the syntax of the +// picture string. +// +// https://www.w3.org/TR/xpath-functions-31/#formatting-numbers +func FormatNumber(value float64, picture string, format DecimalFormat) (string, error) { + if picture == "" { + return "", fmt.Errorf("picture string cannot be empty") + } + + vars, err := processPicture(picture, &format, value < 0) + if err != nil { + return "", err + } + + if math.IsNaN(value) { + return vars.Prefix + format.NaN + vars.Suffix, nil + } + if math.IsInf(value, 0) { + return vars.Prefix + format.Infinity + vars.Suffix, nil + } + + switch vars.NumberType { + case typePercent: + value *= 100 + case typePermille: + value *= 1000 + } + + exponent := 0 + if vars.MinExponentSize != 0 { + + maxMantissa := math.Pow(10, float64(vars.ScalingFactor)) + minMantissa := math.Pow(10, float64(vars.ScalingFactor-1)) + + for value < minMantissa { + value *= 10 + exponent-- + } + + for value > maxMantissa { + value /= 10 + exponent++ + } + } + + var integerPart, fractionalPart, exponentPart string + + value = round(value, vars.MaxFractionalSize) + s := makeNumberString(value, vars.MaxFractionalSize, &format) + sint, sfrac := splitStringAtByte(s, '.') + if sint != "" { + integerPart = formatIntegerPart(sint, &vars, &format) + } + if sfrac != "" { + fractionalPart = formatFractionalPart(sfrac, &vars, &format) + } + + if vars.MinExponentSize != 0 { + s := makeNumberString(float64(exponent), 0, &format) + exponentPart = formatExponentPart(s, &vars, &format) + } + + buf := make([]byte, 0, 128) + + buf = append(buf, vars.Prefix...) + buf = append(buf, integerPart...) + + if len(fractionalPart) > 0 { + buf = append(buf, string(format.DecimalSeparator)...) + buf = append(buf, fractionalPart...) + } + + if len(exponentPart) > 0 { + buf = append(buf, string(format.ExponentSeparator)...) + if exponent < 0 { + buf = append(buf, string(format.MinusSign)...) + } + buf = append(buf, exponentPart...) + } + + buf = append(buf, vars.Suffix...) + + return string(buf), nil +} + +func processPicture(picture string, format *DecimalFormat, isNegative bool) (subpictureVariables, error) { + + pic1, pic2 := splitStringAtRune(picture, format.PatternSeparator) + if pic1 == "" { + return subpictureVariables{}, fmt.Errorf("picture string must contain 1 or 2 subpictures") + } + + vars1, err := processSubpicture(pic1, format) + if err != nil { + return subpictureVariables{}, err + } + + var vars2 subpictureVariables + if pic2 != "" { + vars2, err = processSubpicture(pic2, format) + if err != nil { + return subpictureVariables{}, err + } + } + + vars := vars1 + if isNegative { + if pic2 != "" { + vars = vars2 + } else { + vars.Prefix = string(format.MinusSign) + vars.Prefix + } + } + + return vars, nil +} + +func processSubpicture(subpicture string, format *DecimalFormat) (subpictureVariables, error) { + + parts := extractSubpictureParts(subpicture, format) + err := validateSubpictureParts(parts, format) + if err != nil { + return subpictureVariables{}, err + } + + return analyseSubpictureParts(parts, format), nil +} + +type subpictureParts struct { + Prefix string + Suffix string + Mantissa string + Exponent string + Integer string + Fractional string + Picture string + Active string +} + +func extractSubpictureParts(subpicture string, format *DecimalFormat) subpictureParts { + + isActive := func(r rune) bool { + return r != format.ExponentSeparator && format.isActive(r) + } + + first := strings.IndexFunc(subpicture, isActive) + if first < 0 { + first = 0 + } + + last := strings.LastIndexFunc(subpicture, isActive) + if last < 0 { + last = len(subpicture) + } else { + _, w := utf8.DecodeRuneInString(subpicture[last:]) + last += w + } + + prefix := subpicture[:first] + suffix := subpicture[last:] + activePart := subpicture[first:last] + + mantissaPart := activePart + exponentPart := "" + if pos := strings.IndexRune(activePart, format.ExponentSeparator); pos >= 0 { + w := utf8.RuneLen(format.ExponentSeparator) + mantissaPart = activePart[:pos] + exponentPart = activePart[pos+w:] + } + + integerPart := mantissaPart + fractionalPart := suffix + if pos := strings.IndexRune(mantissaPart, format.DecimalSeparator); pos >= 0 { + w := utf8.RuneLen(format.DecimalSeparator) + integerPart = mantissaPart[:pos] + fractionalPart = mantissaPart[pos+w:] + } + + return subpictureParts{ + Picture: subpicture, + Prefix: prefix, + Suffix: suffix, + Active: activePart, + Mantissa: mantissaPart, + Exponent: exponentPart, + Integer: integerPart, + Fractional: fractionalPart, + } +} + +func validateSubpictureParts(parts subpictureParts, format *DecimalFormat) error { + + if strings.Count(parts.Picture, string(format.DecimalSeparator)) > 1 { + return fmt.Errorf("a subpicture cannot contain more than one decimal separator") + } + + percents := strings.Count(parts.Picture, format.Percent) + if percents > 1 { + return fmt.Errorf("a subpicture cannot contain more than one percent character") + } + + permilles := strings.Count(parts.Picture, format.PerMille) + if permilles > 1 { + return fmt.Errorf("a subpicture cannot contain more than one per-mille character") + } + + if percents > 0 && permilles > 0 { + return fmt.Errorf("a subpicture cannot contain both percent and per-mille characters") + } + + // Passing an anonymous function to IndexFunc instead of + // a method value prevents format escaping to the heap. + if strings.IndexFunc(parts.Mantissa, func(r rune) bool { + return format.isDigit(r) + }) == -1 { + return fmt.Errorf("a mantissa part must contain at least one decimal or optional digit") + } + + isPassive := func(r rune) bool { + return !format.isActive(r) + } + if strings.IndexFunc(parts.Active, isPassive) != -1 { + return fmt.Errorf("a subpicture cannot contain a passive character that is both preceded by and followed by an active character") + } + + if lastRuneInString(parts.Integer) == format.GroupSeparator || + firstRuneInString(parts.Fractional) == format.GroupSeparator { + if strings.ContainsRune(parts.Picture, format.DecimalSeparator) { + return fmt.Errorf("a group separator cannot be adjacent to a decimal separator") + } + return fmt.Errorf("an integer part cannot end with a group separator") + } + + if strings.Contains(parts.Picture, doubleRune(format.GroupSeparator)) { + return fmt.Errorf("a subpicture cannot contain adjacent group separators") + } + + // Passing this wrapper function to IndexFunc instead of + // a method value prevents format escaping to the heap. + isDecimalDigit := func(r rune) bool { + return format.isDecimalDigit(r) + } + + pos := strings.IndexFunc(parts.Integer, isDecimalDigit) + if pos != -1 { + pos += utf8.RuneLen(format.ZeroDigit) + if strings.ContainsRune(parts.Integer[pos:], format.OptionalDigit) { + return fmt.Errorf("an integer part cannot contain a decimal digit followed by an optional digit") + } + } + + pos = strings.IndexRune(parts.Fractional, format.OptionalDigit) + if pos != -1 { + pos += utf8.RuneLen(format.OptionalDigit) + if strings.IndexFunc(parts.Fractional[pos:], isDecimalDigit) != -1 { + return fmt.Errorf("a fractional part cannot contain an optional digit followed by a decimal digit") + } + } + + exponents := strings.Count(parts.Picture, string(format.ExponentSeparator)) + if exponents > 1 { + return fmt.Errorf("a subpicture cannot contain more than one exponent separator") + } + + if exponents > 0 && (percents > 0 || permilles > 0) { + return fmt.Errorf("a subpicture cannot contain a percent/per-mille character and an exponent separator") + } + + if exponents > 0 { + isNotDecimalDigit := func(r rune) bool { + return !format.isDecimalDigit(r) + } + if strings.IndexFunc(parts.Exponent, isNotDecimalDigit) != -1 { + return fmt.Errorf("an exponent part must consist solely of one or more decimal digits") + } + } + + return nil +} + +type numberType uint8 + +const ( + _ numberType = iota + typePercent + typePermille +) + +type subpictureVariables struct { + NumberType numberType + IntegerGroupPositions []int + GroupSize int + MinIntegerSize int + ScalingFactor int + FractionalGroupPositions []int + MinFractionalSize int + MaxFractionalSize int + MinExponentSize int + Prefix string + Suffix string +} + +func analyseSubpictureParts(parts subpictureParts, format *DecimalFormat) subpictureVariables { + + var typ numberType + switch { + case strings.Contains(parts.Picture, format.Percent): + typ = typePercent + case strings.Contains(parts.Picture, format.PerMille): + typ = typePermille + } + + // Defining these wrapper functions instead of using method + // values prevents format escaping to the heap. + isDigit := func(r rune) bool { + return format.isDigit(r) + } + isDecimalDigit := func(r rune) bool { + return format.isDecimalDigit(r) + } + + integerGroupPositions := getGroupPositions(parts.Integer, format.GroupSeparator, isDigit, false) + fractionalGroupPositions := getGroupPositions(parts.Fractional, format.GroupSeparator, isDigit, true) + groupSize := getGroupSize(integerGroupPositions) + + minIntegerSize := runeCountInStringFunc(parts.Integer, isDecimalDigit) + scalingFactor := minIntegerSize + + minFractionalSize := runeCountInStringFunc(parts.Fractional, isDecimalDigit) + maxFractionalSize := runeCountInStringFunc(parts.Fractional, isDigit) + + if minIntegerSize == 0 && maxFractionalSize == 0 { + if parts.Exponent != "" { + minFractionalSize = 1 + maxFractionalSize = 1 + } else { + minIntegerSize = 1 + } + } + + if parts.Exponent != "" && minIntegerSize == 0 && + strings.ContainsRune(parts.Integer, format.OptionalDigit) { + minIntegerSize = 1 + } + + if minIntegerSize == 0 && minFractionalSize == 0 { + minFractionalSize = 1 + } + + minExponentSize := runeCountInStringFunc(parts.Exponent, isDecimalDigit) + + return subpictureVariables{ + Prefix: parts.Prefix, + Suffix: parts.Suffix, + NumberType: typ, + IntegerGroupPositions: integerGroupPositions, + GroupSize: groupSize, + MinIntegerSize: minIntegerSize, + ScalingFactor: scalingFactor, + FractionalGroupPositions: fractionalGroupPositions, + MinFractionalSize: minFractionalSize, + MaxFractionalSize: maxFractionalSize, + MinExponentSize: minExponentSize, + } +} + +func getGroupPositions(s string, sep rune, fn func(rune) bool, lookLeft bool) []int { + + var rest string + var positions []int + + length := utf8.RuneLen(sep) + + for { + pos := strings.IndexRune(s, sep) + if pos == -1 { + break + } + + if lookLeft { + rest = s[:pos] + } else { + rest = s[pos+length:] + } + + positions = append(positions, runeCountInStringFunc(rest, fn)) + + if lookLeft { + if l := len(positions); l > 1 { + positions[l-1] += positions[l-2] + } + } + + s = s[pos+length:] + } + + return positions +} + +func getGroupSize(positions []int) int { + + if len(positions) == 0 { + return 0 + } + + factor := gcdOf(positions) + for i := 0; i < len(positions); i++ { + if indexInt(positions, factor*(i+1)) == -1 { + return 0 + } + } + + return factor +} + +func formatIntegerPart(integer string, vars *subpictureVariables, format *DecimalFormat) string { + + // Passing an anonymous function to TrimLeftFunc instead + // of a method value prevents format escaping to the heap. + integer = strings.TrimLeftFunc(integer, func(r rune) bool { + return format.isZeroDigit(r) + }) + + padding := vars.MinIntegerSize - utf8.RuneCountInString(integer) + + switch { + case padding == 1: + integer = string(format.ZeroDigit) + integer + case padding > 1: + integer = strings.Repeat(string(format.ZeroDigit), padding) + integer + } + + if vars.GroupSize > 0 { + return insertSeparatorsEvery(integer, format.GroupSeparator, vars.GroupSize) + } + + if len(vars.IntegerGroupPositions) > 0 { + return insertSeparatorsAt(integer, format.GroupSeparator, vars.IntegerGroupPositions, true) + } + + return integer +} + +func formatFractionalPart(fractional string, vars *subpictureVariables, format *DecimalFormat) string { + + // Passing an anonymous function to TrimRightFunc instead + // of a method value prevents format escaping to the heap. + fractional = strings.TrimRightFunc(fractional, func(r rune) bool { + return format.isZeroDigit(r) + }) + + padding := vars.MinFractionalSize - utf8.RuneCountInString(fractional) + + switch { + case padding == 1: + fractional += string(format.ZeroDigit) + case padding > 1: + fractional += strings.Repeat(string(format.ZeroDigit), padding) + } + + if len(vars.FractionalGroupPositions) > 0 { + return insertSeparatorsAt(fractional, format.GroupSeparator, vars.FractionalGroupPositions, false) + } + + return fractional +} + +func formatExponentPart(exponent string, vars *subpictureVariables, format *DecimalFormat) string { + + padding := vars.MinExponentSize - utf8.RuneCountInString(exponent) + + switch { + case padding == 1: + exponent = string(format.ZeroDigit) + exponent + case padding > 1: + exponent = strings.Repeat(string(format.ZeroDigit), padding) + exponent + } + + return exponent +} + +func makeNumberString(value float64, dp int, format *DecimalFormat) string { + + s := strconv.AppendFloat(make([]byte, 0, 24), math.Abs(value), 'f', dp, 64) + + if format.ZeroDigit != '0' { + s = bytes.Map(func(r rune) rune { + offset := r - '0' + if offset < 0 || offset > 9 { + return r + } + return format.ZeroDigit + offset + }, s) + } + + return string(s) +} + +func insertSeparatorsEvery(s string, sep rune, interval int) string { + + l := utf8.RuneCountInString(s) + if interval <= 0 || l <= interval { + return s + } + + end := len(s) + n := (l - 1) / interval + chunks := make([]string, n+1) + + for n > 0 { + pos := 0 + for i := 0; i < interval; i++ { + _, w := utf8.DecodeLastRuneInString(s[:end]) + pos += w + } + chunks[n] = s[end-pos : end] + end -= pos + n-- + } + + chunks[n] = s[:end] + return strings.Join(chunks, string(sep)) +} + +func insertSeparatorsAt(integer string, sep rune, positions []int, fromRight bool) string { + + s := integer + chunks := make([]string, 0, len(positions)+1) + + for i := range positions { + + n := positions[i] + if fromRight { + n = utf8.RuneCountInString(s) - n + } + + pos := 0 + for n > 0 { + _, w := utf8.DecodeRuneInString(s[pos:]) + pos += w + n-- + } + + chunks = append(chunks, s[:pos]) + s = s[pos:] + } + + chunks = append(chunks, s) + return strings.Join(chunks, string(sep)) +} + +func splitStringAtRune(s string, r rune) (string, string) { + + pos := strings.IndexRune(s, r) + if pos == -1 { + return s, "" + } + + if s2 := s[pos+utf8.RuneLen(r):]; !strings.ContainsRune(s2, r) { + return s[:pos], s2 + } + + return "", "" +} + +func splitStringAtByte(s string, b byte) (string, string) { + + pos := strings.IndexByte(s, b) + if pos == -1 { + return s, "" + } + + if s2 := s[pos+1:]; strings.IndexByte(s2, b) == -1 { + return s[:pos], s2 + } + + return "", "" +} + +func firstRuneInString(s string) rune { + r, _ := utf8.DecodeRuneInString(s) + return r +} + +func lastRuneInString(s string) rune { + r, _ := utf8.DecodeLastRuneInString(s) + return r +} + +func runeCountInStringFunc(s string, f func(rune) bool) int { + + var count int + for _, c := range s { + if f(c) { + count++ + } + } + + return count +} + +func doubleRune(r rune) string { + return string([]rune{r, r}) +} + +func gcd(a, b int) int { + if b == 0 { + return a + } + return gcd(b, a%b) +} + +func gcdOf(values []int) int { + res := 0 + for _, n := range values { + res = gcd(res, n) + } + return res +} + +func indexInt(values []int, want int) int { + for i, n := range values { + if n == want { + return i + } + } + return -1 +} + +func round(x float64, prec int) float64 { + // From gonum's floats.RoundEven. + // https://github.com/gonum/gonum/tree/master/floats + if x == 0 { + // Make sure zero is returned + // without the negative bit set. + return 0 + } + // Fast path for positive precision on integers. + if prec >= 0 && x == math.Trunc(x) { + return x + } + pow := math.Pow10(prec) + intermed := x * pow + if math.IsInf(intermed, 0) { + return x + } + if isHalfway(intermed) { + correction, _ := math.Modf(math.Mod(intermed, 2)) + intermed += correction + if intermed > 0 { + x = math.Floor(intermed) + } else { + x = math.Ceil(intermed) + } + } else { + if x < 0 { + x = math.Ceil(intermed - 0.5) + } else { + x = math.Floor(intermed + 0.5) + } + } + + if x == 0 { + return 0 + } + + return x / pow +} + +func isHalfway(x float64) bool { + _, frac := math.Modf(x) + frac = math.Abs(frac) + return frac == 0.5 || (math.Nextafter(frac, math.Inf(-1)) < 0.5 && math.Nextafter(frac, math.Inf(1)) > 0.5) +} diff --git a/v1.5.4/jlib/jxpath/formatnumber_test.go b/v1.5.4/jlib/jxpath/formatnumber_test.go new file mode 100644 index 0000000..eb857a7 --- /dev/null +++ b/v1.5.4/jlib/jxpath/formatnumber_test.go @@ -0,0 +1,98 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jxpath + +import ( + "reflect" + "testing" +) + +type formatNumberTest struct { + Value float64 + Picture string + Output string + Error error +} + +func TestExamples(t *testing.T) { + + tests := []formatNumberTest{ + { + Value: 12345.6, + Picture: "#,###.00", + Output: "12,345.60", + }, + { + Value: 12345678.9, + Picture: "9,999.99", + Output: "12,345,678.90", + }, + { + Value: 123.9, + Picture: "9999", + Output: "0124", + }, + { + Value: 0.14, + Picture: "01%", + Output: "14%", + }, + { + Value: 0.14, + Picture: "001‰", + Output: "140‰", + }, + { + Value: -6, + Picture: "000", + Output: "-006", + }, + { + Value: 1234.5678, + Picture: "#,##0.00", + Output: "1,234.57", + }, + { + Value: 1234.5678, + Picture: "00.000e0", + Output: "12.346e2", + }, + { + Value: 0.234, + Picture: "0.0e0", + Output: "2.3e-1", + }, + { + Value: 0.234, + Picture: "#.00e0", + Output: "0.23e0", + }, + { + Value: 0.234, + Picture: ".00e0", + Output: ".23e0", + }, + } + + testFormatNumber(t, tests) +} + +func testFormatNumber(t *testing.T, tests []formatNumberTest) { + + df := NewDecimalFormat() + + for i, test := range tests { + + output, err := FormatNumber(test.Value, test.Picture, df) + + if output != test.Output { + t.Errorf("%d. FormatNumber(%v, %q): expected %s, got %s", i+1, test.Value, test.Picture, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%d. FormatNumber(%v, %q): expected error %v, got %v", i+1, test.Value, test.Picture, test.Error, err) + } + } +} diff --git a/v1.5.4/jlib/jxpath/language.go b/v1.5.4/jlib/jxpath/language.go new file mode 100644 index 0000000..f864f59 --- /dev/null +++ b/v1.5.4/jlib/jxpath/language.go @@ -0,0 +1,136 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jxpath + +import ( + "time" +) + +type dateLanguage struct { + days [7][]string + months [13][]string + am []string + pm []string + tzPrefix string +} + +var dateLanguages = map[string]dateLanguage{ + "en": { + days: [...][]string{ + time.Sunday: { + "Sunday", + "Sun", + "Su", + }, + time.Monday: { + "Monday", + "Mon", + "Mo", + }, + time.Tuesday: { + "Tuesday", + "Tues", + "Tue", + "Tu", + }, + time.Wednesday: { + "Wednesday", + "Weds", + "Wed", + "We", + }, + time.Thursday: { + "Thursday", + "Thurs", + "Thur", + "Thu", + "Th", + }, + time.Friday: { + "Friday", + "Fri", + "Fr", + }, + time.Saturday: { + "Saturday", + "Sat", + "Sa", + }, + }, + months: [...][]string{ + time.January: { + "January", + "Jan", + "Ja", + }, + time.February: { + "February", + "Feb", + "Fe", + }, + time.March: { + "March", + "Mar", + "Mr", + }, + time.April: { + "April", + "Apr", + "Ap", + }, + time.May: { + "May", + "My", + }, + time.June: { + "June", + "Jun", + "Jn", + }, + time.July: { + "July", + "Jul", + "Jl", + }, + time.August: { + "August", + "Aug", + "Au", + }, + time.September: { + "September", + "Sept", + "Sep", + "Se", + }, + time.October: { + "October", + "Oct", + "Oc", + }, + time.November: { + "November", + "Nov", + "No", + }, + time.December: { + "December", + "Dec", + "De", + }, + }, + am: []string{ + "am", + "a", + }, + pm: []string{ + "pm", + "p", + }, + tzPrefix: "GMT", + }, +} + +var defaultLanguage = dateLanguages["en"] diff --git a/v1.5.4/jlib/number.go b/v1.5.4/jlib/number.go new file mode 100644 index 0000000..27f5e0c --- /dev/null +++ b/v1.5.4/jlib/number.go @@ -0,0 +1,146 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "math" + "math/rand" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +var reNumber = regexp.MustCompile(`^-?(([0-9]+))(\.[0-9]+)?([Ee][-+]?[0-9]+)?$`) + +// Number converts values to numbers. Numeric values are returned +// unchanged. Strings in legal JSON number format are converted +// to the number they represent. Boooleans are converted to 0 or 1. +// All other types trigger an error. +func Number(value StringNumberBool) (float64, error) { + v := reflect.Value(value) + if b, ok := jtypes.AsBool(v); ok { + if b { + return 1, nil + } + return 0, nil + } + + if n, ok := jtypes.AsNumber(v); ok { + return n, nil + } + + s, ok := jtypes.AsString(v) + if ok && reNumber.MatchString(s) { + if n, err := strconv.ParseFloat(s, 64); err == nil { + return n, nil + } + } + + return 0, fmt.Errorf("unable to cast %q to a number", s) +} + +// Round rounds its input to the number of decimal places given +// in the optional second parameter. By default, Round rounds to +// the nearest integer. A negative precision specifies which column +// to round to on the left hand side of the decimal place. +func Round(x float64, prec jtypes.OptionalInt) float64 { + // Adapted from gonum's floats.RoundEven. + // https://github.com/gonum/gonum/tree/master/floats + + if x == 0 { + // Make sure zero is returned + // without the negative bit set. + return 0 + } + // Fast path for positive precision on integers. + if prec.Int >= 0 && x == math.Trunc(x) { + return x + } + intermed := multByPow10(x, prec.Int) + if math.IsInf(intermed, 0) { + return x + } + if isHalfway(intermed) { + correction, _ := math.Modf(math.Mod(intermed, 2)) + intermed += correction + if intermed > 0 { + x = math.Floor(intermed) + } else { + x = math.Ceil(intermed) + } + } else { + if x < 0 { + x = math.Ceil(intermed - 0.5) + } else { + x = math.Floor(intermed + 0.5) + } + } + + if x == 0 { + return 0 + } + + return multByPow10(x, -prec.Int) +} + +// Power returns x to the power of y. +func Power(x, y float64) (float64, error) { + res := math.Pow(x, y) + if math.IsInf(res, 0) || math.IsNaN(res) { + return 0, fmt.Errorf("the power function has resulted in a value that cannot be represented as a JSON number") + } + return res, nil +} + +// Sqrt returns the square root of a number. It returns an error +// if the number is less than zero. +func Sqrt(x float64) (float64, error) { + if x < 0 { + return 0, fmt.Errorf("the sqrt function cannot be applied to a negative number") + } + return math.Sqrt(x), nil +} + +// Random returns a random floating point number between 0 and 1. +func Random() float64 { + return rand.Float64() +} + +// multByPow10 multiplies a number by 10 to the power of n. +// It does this by converting back and forth to strings to +// avoid floating point rounding errors, e.g. +// +// 4.525 * math.Pow10(2) returns 452.50000000000006 +func multByPow10(x float64, n int) float64 { + if n == 0 || math.IsNaN(x) || math.IsInf(x, 0) { + return x + } + + s := fmt.Sprintf("%g", x) + + chunks := strings.Split(s, "e") + switch len(chunks) { + case 1: + s = chunks[0] + "e" + strconv.Itoa(n) + case 2: + e, _ := strconv.Atoi(chunks[1]) + s = chunks[0] + "e" + strconv.Itoa(e+n) + default: + return x + } + + x, _ = strconv.ParseFloat(s, 64) + return x +} + +func isHalfway(x float64) bool { + _, frac := math.Modf(x) + frac = math.Abs(frac) + return frac == 0.5 || (math.Nextafter(frac, math.Inf(-1)) < 0.5 && math.Nextafter(frac, math.Inf(1)) > 0.5) +} diff --git a/v1.5.4/jlib/number_test.go b/v1.5.4/jlib/number_test.go new file mode 100644 index 0000000..bf7a275 --- /dev/null +++ b/v1.5.4/jlib/number_test.go @@ -0,0 +1,143 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib_test + +import ( + "fmt" + "testing" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +func TestRound(t *testing.T) { + + data := []struct { + Value float64 + Precision jtypes.OptionalInt + Output float64 + }{ + { + Value: 11.5, + Output: 12, + }, + { + Value: -11.5, + Output: -12, + }, + { + Value: 12.5, + Output: 12, + }, + { + Value: -12.5, + Output: -12, + }, + { + Value: 594.325, + Output: 594, + }, + { + Value: -594.325, + Output: -594, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(1), + Output: 594.3, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(1), + Output: -594.3, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(2), + Output: 594.32, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(2), + Output: -594.32, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(3), + Output: 594.325, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(3), + Output: -594.325, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(4), + Output: 594.325, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(4), + Output: -594.325, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(-1), + Output: 590, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(-1), + Output: -590, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(-2), + Output: 600, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(-2), + Output: -600, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(-3), + Output: 1000, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(-3), + Output: -1000, + }, + { + Value: 594.325, + Precision: jtypes.NewOptionalInt(-4), + Output: 0, + }, + { + Value: -594.325, + Precision: jtypes.NewOptionalInt(-4), + Output: 0, + }, + } + + for _, test := range data { + + got := jlib.Round(test.Value, test.Precision) + + if got != test.Output { + + s := fmt.Sprintf("round(%g", test.Value) + if test.Precision.IsSet() { + s += fmt.Sprintf(", %d", test.Precision.Int) + } + s += ")" + + t.Errorf("%s: Expected %g, got %g", s, test.Output, got) + } + } +} diff --git a/v1.5.4/jlib/object.go b/v1.5.4/jlib/object.go new file mode 100644 index 0000000..79fe20a --- /dev/null +++ b/v1.5.4/jlib/object.go @@ -0,0 +1,626 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "fmt" + "reflect" + + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// typeInterfaceMap is the reflect.Type for map[string]interface{}. +// Go uses this type (by default) when decoding JSON objects. +// JSONata objects also use this type. As such it is a common +// map type in JSONata expressions. +var typeInterfaceMap = reflect.MapOf(typeString, jtypes.TypeInterface) + +// toInterfaceMap attempts to cast a reflect.Value to a +// map[string]interface{}. This is useful for performance +// because direct map access is significantly faster than +// the reflect functions MapKeys and MapIndex. +func toInterfaceMap(v reflect.Value) (map[string]interface{}, bool) { + if v.Type() == typeInterfaceMap && v.CanInterface() { + return v.Interface().(map[string]interface{}), true + } + return nil, false +} + +// Each applies the function fn to each name/value pair in +// the object obj and returns the results in an array. The +// order of the items in the array is undefined. +// +// obj must be a map or a struct. If it is a struct, any +// unexported fields are ignored. +// +// fn must be a Callable that takes one, two or three +// arguments. The first argument is the value of a name/value +// pair. The second and third arguments, if applicable, are +// the value and the source object respectively. +func Each(obj reflect.Value, fn jtypes.Callable) (interface{}, error) { + + var each func(reflect.Value, jtypes.Callable) ([]interface{}, error) + + obj = jtypes.Resolve(obj) + + switch { + case jtypes.IsMap(obj): + each = eachMap + case jtypes.IsStruct(obj) && !jtypes.IsCallable(obj): + each = eachStruct + default: + return nil, fmt.Errorf("argument must be an object") + } + + if argc := fn.ParamCount(); argc < 1 || argc > 3 { + return nil, fmt.Errorf("function must take 1, 2 or 3 arguments") + } + + results, err := each(obj, fn) + if err != nil { + return nil, err + } + + switch len(results) { + case 0: + return nil, jtypes.ErrUndefined + case 1: + return results[0], nil + default: + return results, nil + } +} + +func eachMap(v reflect.Value, fn jtypes.Callable) ([]interface{}, error) { + + size := v.Len() + if size == 0 { + return nil, nil + } + + var results []interface{} + + argv := make([]reflect.Value, fn.ParamCount()) + + for _, k := range v.MapKeys() { + + for i := range argv { + switch i { + case 0: + argv[i] = v.MapIndex(k) + case 1: + argv[i] = k + case 2: + argv[i] = v + } + } + + res, err := fn.Call(argv) + if err != nil { + return nil, err + } + + if res.IsValid() && res.CanInterface() { + if results == nil { + results = make([]interface{}, 0, size) + } + results = append(results, res.Interface()) + } + } + + return results, nil +} + +func eachStruct(v reflect.Value, fn jtypes.Callable) ([]interface{}, error) { + + size := v.NumField() + if size == 0 { + return nil, nil + } + + var results []interface{} + + t := v.Type() + argv := make([]reflect.Value, fn.ParamCount()) + + for i := 0; i < size; i++ { + + field := t.Field(i) + if field.PkgPath != "" { + // Skip unexported fields. + continue + } + + for j := range argv { + switch j { + case 0: + argv[j] = v.Field(i) + case 1: + argv[j] = reflect.ValueOf(field.Name) + case 2: + argv[j] = v + } + } + + res, err := fn.Call(argv) + if err != nil { + return nil, err + } + + if res.IsValid() && res.CanInterface() { + if results == nil { + results = make([]interface{}, 0, size) + } + results = append(results, res.Interface()) + } + } + + return results, nil +} + +// Sift returns a map containing name/value pairs from the +// object obj that satisfy the predicate function fn. +// +// obj must be a map or a struct. If it is a map, the keys +// must be of type string. If it is a struct, any unexported +// fields are ignored. +// +// fn must be a Callable that takes one, two or three +// arguments. The first argument is the value of a name/value +// pair. The second and third arguments, if applicable, are +// the value and the source object respectively. +func Sift(obj reflect.Value, fn jtypes.Callable) (interface{}, error) { + + var sift func(reflect.Value, jtypes.Callable) (map[string]interface{}, error) + + obj = jtypes.Resolve(obj) + + switch { + case jtypes.IsMap(obj): + sift = siftMap + case jtypes.IsStruct(obj) && !jtypes.IsCallable(obj): + sift = siftStruct + default: + return nil, fmt.Errorf("argument must be an object") + } + + if argc := fn.ParamCount(); argc < 1 || argc > 3 { + return nil, fmt.Errorf("function must take 1, 2 or 3 arguments") + } + + results, err := sift(obj, fn) + if err != nil { + return nil, err + } + + if len(results) == 0 { + return nil, jtypes.ErrUndefined + } + + return results, nil +} + +func siftMap(v reflect.Value, fn jtypes.Callable) (map[string]interface{}, error) { + + size := v.Len() + if size == 0 { + return nil, nil + } + + var results map[string]interface{} + + argv := make([]reflect.Value, fn.ParamCount()) + + for _, k := range v.MapKeys() { + + key, ok := jtypes.AsString(k) + if !ok { + return nil, fmt.Errorf("object key must evaluate to a string, got %v (%s)", k, k.Kind()) + } + + val := v.MapIndex(k) + if !val.IsValid() || !val.CanInterface() { + // Skip undefined or non-interfaceable values. We + // already know we don't want them in the results, + // so we can bypass the function call. + continue + } + + for i := range argv { + switch i { + case 0: + argv[i] = val + case 1: + argv[i] = k + case 2: + argv[i] = v + } + } + + res, err := fn.Call(argv) + if err != nil { + return nil, err + } + + if Boolean(res) { + if results == nil { + results = make(map[string]interface{}, size) + } + results[key] = val.Interface() + } + } + + return results, nil +} + +func siftStruct(v reflect.Value, fn jtypes.Callable) (map[string]interface{}, error) { + + size := v.NumField() + if size == 0 { + return nil, nil + } + + var results map[string]interface{} + + t := v.Type() + argv := make([]reflect.Value, fn.ParamCount()) + + for i := 0; i < size; i++ { + + key := t.Field(i).Name + val := v.Field(i) + if !val.IsValid() || !val.CanInterface() { + // Skip undefined or non-interfaceable values. We + // already know we don't want them in the results, + // so we can bypass the function call. This also + // filters out unexported fields (as they are + // non-interfaceable). + continue + } + + for j := range argv { + switch j { + case 0: + argv[j] = val + case 1: + argv[j] = reflect.ValueOf(key) + case 2: + argv[j] = v + } + } + + res, err := fn.Call(argv) + if err != nil { + return nil, err + } + + if Boolean(res) { + if results == nil { + results = make(map[string]interface{}, size) + } + results[key] = val.Interface() + } + } + + return results, nil +} + +// Keys returns an array of the names in the object obj. +// The order of the returned items is undefined. +// +// obj must be a map, a struct or an array. If obj is a map, +// its keys must be of type string. If obj is a struct, any +// unexported fields are ignored. And if obj is an array, +// Keys returns the unique set of names from each object +// in the array. +func Keys(obj reflect.Value) (interface{}, error) { + + results, err := keys(obj) + if err != nil { + return nil, err + } + + switch len(results) { + case 0: + return nil, jtypes.ErrUndefined + case 1: + return results[0], nil + default: + return results, nil + } +} + +func keys(v reflect.Value) ([]string, error) { + + v = jtypes.Resolve(v) + + switch { + case jtypes.IsMap(v): + return keysMap(v) + case jtypes.IsStruct(v) && !jtypes.IsCallable(v): + return keysStruct(v) + case jtypes.IsArray(v): + return keysArray(v) + default: + return nil, nil + } +} + +func keysMap(v reflect.Value) ([]string, error) { + + if v.Len() == 0 { + return nil, nil + } + + if m, ok := toInterfaceMap(v); ok { + return keysMapFast(m), nil + } + + results := make([]string, v.Len()) + + for i, k := range v.MapKeys() { + + key, ok := jtypes.AsString(k) + if !ok { + return nil, fmt.Errorf("object key must evaluate to a string, got %v (%s)", k, k.Kind()) + } + + results[i] = key + } + + return results, nil +} + +func keysMapFast(m map[string]interface{}) []string { + + results := make([]string, 0, len(m)) + for key := range m { + results = append(results, key) + } + + return results +} + +func keysStruct(v reflect.Value) ([]string, error) { + + size := v.NumField() + if size == 0 { + return nil, nil + } + + var results []string + + t := v.Type() + for i := 0; i < size; i++ { + + field := t.Field(i) + if field.PkgPath != "" { + // Skip unexported fields. + continue + } + + if results == nil { + results = make([]string, 0, size) + } + results = append(results, field.Name) + } + + return results, nil +} + +func keysArray(v reflect.Value) ([]string, error) { + + size := v.Len() + if size == 0 { + return nil, nil + } + + kresults := make([][]string, 0, size) + + for i := 0; i < size; i++ { + results, err := keys(v.Index(i)) + if err != nil { + return nil, err + } + kresults = append(kresults, results) + } + + size = 0 + for _, k := range kresults { + size += len(k) + } + + if size == 0 { + return nil, nil + } + + seen := map[string]bool{} + results := make([]string, 0, size) + + for _, k := range kresults { + for _, s := range k { + if !seen[s] { + seen[s] = true + results = append(results, s) + } + } + } + + return results, nil +} + +// Merge merges an array of objects into a single object that +// contains all of the name/value pairs from the array objects. +// If a name appears multiple times, values from objects later +// in the array override those from earlier. +// +// objs must be an array of maps or structs. Maps must have +// keys of type string. Unexported struct fields are ignored. +func Merge(objs reflect.Value) (interface{}, error) { + + var size int + var merge func(map[string]interface{}, reflect.Value) error + + objs = jtypes.Resolve(objs) + + switch { + case jtypes.IsMap(objs): + size = objs.Len() + merge = mergeMap + case jtypes.IsStruct(objs) && !jtypes.IsCallable(objs): + size = objs.NumField() + merge = mergeStruct + case jtypes.IsArray(objs): + for i := 0; i < objs.Len(); i++ { + obj := jtypes.Resolve(objs.Index(i)) + switch { + case jtypes.IsMap(obj): + size += obj.Len() + case jtypes.IsStruct(obj): + size += obj.NumField() + default: + return nil, fmt.Errorf("argument must be an object or an array of objects") + } + } + merge = mergeArray + default: + return nil, fmt.Errorf("argument must be an object or an array of objects") + } + + results := make(map[string]interface{}, size) + if err := merge(results, objs); err != nil { + return nil, err + } + + return results, nil +} + +func mergeMap(dest map[string]interface{}, src reflect.Value) error { + + if m, ok := toInterfaceMap(src); ok { + mergeMapFast(dest, m) + return nil + } + + for _, k := range src.MapKeys() { + + key, ok := jtypes.AsString(k) + if !ok { + return fmt.Errorf("object key must evaluate to a string, got %v (%s)", k, k.Kind()) + } + + if val := src.MapIndex(k); val.IsValid() && val.CanInterface() { + dest[key] = val.Interface() + } + } + + return nil +} + +func mergeMapFast(dest, src map[string]interface{}) { + for k, v := range src { + if v != nil { + dest[k] = v + } + } +} + +func mergeStruct(dest map[string]interface{}, src reflect.Value) error { + + t := src.Type() + + for i := 0; i < src.NumField(); i++ { + + field := t.Field(i) + if field.PkgPath != "" { + // Skip unexported fields. + continue + } + + if val := src.Field(i); val.IsValid() && val.CanInterface() { + dest[field.Name] = val.Interface() + } + } + + return nil +} + +func mergeArray(dest map[string]interface{}, src reflect.Value) error { + + var merge func(map[string]interface{}, reflect.Value) error + + for i := 0; i < src.Len(); i++ { + + item := jtypes.Resolve(src.Index(i)) + + switch { + case jtypes.IsMap(item): + merge = mergeMap + case jtypes.IsStruct(item) && !jtypes.IsCallable(item): + merge = mergeStruct + default: + continue + } + + if err := merge(dest, item); err != nil { + return err + } + } + + return nil +} + +// Spread (golint) +func Spread(v reflect.Value) (interface{}, error) { + + var results []interface{} + + switch { + case jtypes.IsMap(v): + v = jtypes.Resolve(v) + keys := v.MapKeys() + for _, k := range keys { + if k.Kind() != reflect.String { + return nil, fmt.Errorf("object key must evaluate to a string, got %v (%s)", k, k.Kind()) + } + if v := v.MapIndex(k); v.CanInterface() { + results = append(results, map[string]interface{}{ + k.String(): v.Interface(), + }) + } + } + case jtypes.IsStruct(v) && !jtypes.IsCallable(v): + v = jtypes.Resolve(v) + for i := 0; i < v.NumField(); i++ { + k := v.Type().Field(i).Name + v := v.FieldByIndex([]int{i}) + if v.CanInterface() { + results = append(results, map[string]interface{}{ + k: v.Interface(), + }) + } + } + case jtypes.IsArray(v): + v = jtypes.Resolve(v) + for i := 0; i < v.Len(); i++ { + res, err := Spread(v.Index(i)) + if err != nil { + return nil, err + } + switch res := res.(type) { + case []interface{}: // Check for []interface{} first because it will also match interface{}. + results = append(results, res...) + case interface{}: + results = append(results, res) + } + } + default: + if v.IsValid() && v.CanInterface() { + return v.Interface(), nil + } + } + + return results, nil +} diff --git a/v1.5.4/jlib/object_test.go b/v1.5.4/jlib/object_test.go new file mode 100644 index 0000000..c40f5ce --- /dev/null +++ b/v1.5.4/jlib/object_test.go @@ -0,0 +1,907 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib_test + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type eachTest struct { + Input interface{} + Callable jtypes.Callable + Output interface{} + Error error +} + +func TestEach(t *testing.T) { + + // repeatDots is a Callable that takes a number (N) + // and returns a string of N dots. + repeatDots := callable1(func(argv []reflect.Value) (reflect.Value, error) { + n, _ := jtypes.AsNumber(argv[0]) + + res := strings.Repeat(".", int(n)) + return reflect.ValueOf(res), nil + }) + + // repeatString is a Callable that takes a number (N) + // and a string and returns the string repeated N times. + repeatString := callable2(func(argv []reflect.Value) (reflect.Value, error) { + n, _ := jtypes.AsNumber(argv[0]) + s, _ := jtypes.AsString(argv[1]) + + res := strings.Repeat(s, int(n)) + return reflect.ValueOf(res), nil + }) + + // printLen is a Callable that takes a number, a string, + // and an object and returns the object length as a string. + // Note that the object length includes unexported struct + // fields. + printLen := callable3(func(argv []reflect.Value) (reflect.Value, error) { + var len int + switch argv[2].Kind() { + case reflect.Map: + len = argv[2].Len() + case reflect.Struct: + len = argv[2].NumField() + } + res := strconv.Itoa(len) + return reflect.ValueOf(res), nil + }) + + testEach(t, []eachTest{ + { + // Empty map. + Input: map[string]interface{}{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Empty struct. + Input: struct{}{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Struct with no exported fields. + Input: struct { + a, b, c int + }{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Callable with 1 argument. + Input: map[string]interface{}{ + "a": 5, + }, + Callable: repeatDots, + Output: ".....", + }, + { + Input: struct { + A int + }{ + A: 5, + }, + Callable: repeatDots, + Output: ".....", + }, + { + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + }, + Callable: repeatDots, + Output: []interface{}{ + ".", + "..", + "...", + "....", + ".....", + }, + }, + { + Input: struct { + A, B, C, D, e int + }{ + A: 1, + B: 2, + C: 3, + D: 4, + e: 5, // unexported struct fields are ignored. + }, + Callable: repeatDots, + Output: []interface{}{ + ".", + "..", + "...", + "....", + }, + }, + { + // Callable with 2 arguments. + Input: map[string]interface{}{ + "a": 5, + }, + Callable: repeatString, + Output: "aaaaa", + }, + { + Input: struct { + A int + }{ + A: 5, + }, + Callable: repeatString, + Output: "AAAAA", + }, + { + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + }, + Callable: repeatString, + Output: []interface{}{ + "a", + "bb", + "ccc", + "dddd", + "eeeee", + }, + }, + { + Input: struct { + A, B, C, D, e int + }{ + A: 1, + B: 2, + C: 3, + D: 4, + e: 5, // unexported struct fields are ignored. + }, + Callable: repeatString, + Output: []interface{}{ + "A", + "BB", + "CCC", + "DDDD", + }, + }, + { + // Callable with 3 arguments. + Input: map[string]interface{}{ + "a": 5, + }, + Callable: printLen, + Output: "1", + }, + { + Input: struct { + A int + }{ + A: 5, + }, + Callable: printLen, + Output: "1", + }, + { + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + }, + Callable: printLen, + Output: []interface{}{ + "5", + "5", + "5", + "5", + "5", + }, + }, + { + Input: struct { + A, B, C, D, e int + }{ + A: 1, + B: 2, + C: 3, + D: 4, + e: 5, // unexported struct fields are ignored. + }, + Callable: printLen, + Output: []interface{}{ + "5", + "5", + "5", + "5", + }, + }, + { + // Invalid input. Return an error. + // Note that we don't even get as far as validating the + // Callable in this case. + Input: "hello", + Error: fmt.Errorf("argument must be an object"), + }, + { + // Callable has too few parameters. + Input: map[string]interface{}{}, + Callable: paramCountCallable(0), + Error: fmt.Errorf("function must take 1, 2 or 3 arguments"), + }, + { + // Callable has too many parameters. + Input: struct{}{}, + Callable: paramCountCallable(4), + Error: fmt.Errorf("function must take 1, 2 or 3 arguments"), + }, + { + // If the Callable returns an error, return the error. + Input: map[string]interface{}{ + "a": 1, + }, + Callable: paramCountCallable(1), + Error: errTest, + }, + { + // If the Callable returns an error, return the error. + Input: struct { + A int + }{}, + Callable: paramCountCallable(1), + Error: errTest, + }, + }) +} + +func testEach(t *testing.T, tests []eachTest) { + + for i, test := range tests { + + output, err := jlib.Each(reflect.ValueOf(test.Input), test.Callable) + + if !equalStringArray(output, test.Output) { + t.Errorf("Test %d: expected %v, got %v", i+1, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("Test %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type siftTest struct { + Input interface{} + Callable jtypes.Callable + Output interface{} + Error error +} + +func TestSift(t *testing.T) { + + // valueIsOdd is a Callable that takes one argument and + // returns true if it is an odd number. + valueIsOdd := callable1(func(argv []reflect.Value) (reflect.Value, error) { + n, ok := jtypes.AsNumber(argv[0]) + res := ok && int(n)&1 == 1 + return reflect.ValueOf(res), nil + }) + + // valuesAreEqual is a Callable that takes two arguments + // and returns true if they are equal. + valuesAreEqual := callable2(func(argv []reflect.Value) (reflect.Value, error) { + res := reflect.DeepEqual(argv[0].Interface(), argv[1].Interface()) + return reflect.ValueOf(res), nil + }) + + // valueIsLen is a Callable that takes three arguments + // and returns true if the first argument is equal to + // the number of elements in the third argument. + valueIsLen := callable3(func(argv []reflect.Value) (reflect.Value, error) { + var len int + switch argv[2].Kind() { + case reflect.Map: + len = argv[2].Len() + case reflect.Struct: + len = argv[2].NumField() + } + n, ok := jtypes.AsNumber(argv[0]) + res := ok && int(n) == len + return reflect.ValueOf(res), nil + }) + + testSift(t, []siftTest{ + { + // Empty map. + Input: map[string]interface{}{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Empty struct. + Input: struct{}{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Struct with no exported fields. + Input: struct { + a, b, c int + }{}, + Callable: paramCountCallable(1), + Error: jtypes.ErrUndefined, + }, + { + // Callable with 1 argument. + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + }, + Callable: valueIsOdd, + Output: map[string]interface{}{ + "a": 1, + "c": 3, + }, + }, + { + Input: struct { + A, B, C, d int + }{ + A: 5, + B: 0, + C: 4, + d: 1, // unexported struct fields are ignored. + }, + Callable: valueIsOdd, + Output: map[string]interface{}{ + "A": 5, + }, + }, + { + // Callable with 2 arguments. + Input: map[string]interface{}{ + "a": 1, + "b": "b", + "c": true, + }, + Callable: valuesAreEqual, + Output: map[string]interface{}{ + "b": "b", + }, + }, + { + Input: struct { + A int + B string + c string // unexported struct fields are ignored. + }{ + A: 1, + B: "B", + c: "c", + }, + Callable: valuesAreEqual, + Output: map[string]interface{}{ + "B": "B", + }, + }, + { + // Callable with 3 arguments. + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + }, + Callable: valueIsLen, + Output: map[string]interface{}{ + "c": 3, + }, + }, + { + Input: struct { + A, B, C, d int + }{ + A: 4, + B: 2, + C: 3, + d: 4, // unexported struct fields are ignored. + }, + Callable: valueIsLen, + Output: map[string]interface{}{ + "A": 4, + }, + }, + { + // Invalid input. Return an error. + // Note that we don't even get as far as validating the + // Callable in this case. + Input: 3.141592, + Error: fmt.Errorf("argument must be an object"), + }, + { + // Invalid key type. + Input: map[bool]string{ + true: "true", + }, + Callable: paramCountCallable(1), + Error: fmt.Errorf("object key must evaluate to a string, got true (bool)"), + }, + { + // Callable has too few parameters. + Input: map[string]interface{}{}, + Callable: paramCountCallable(0), + Error: fmt.Errorf("function must take 1, 2 or 3 arguments"), + }, + { + // Callable has too many parameters. + Input: struct{}{}, + Callable: paramCountCallable(4), + Error: fmt.Errorf("function must take 1, 2 or 3 arguments"), + }, + { + // If the Callable returns an error, return the error. + Input: map[string]interface{}{ + "a": 1, + }, + Callable: paramCountCallable(1), + Error: errTest, + }, + { + // If the Callable returns an error, return the error. + Input: struct { + A int + }{}, + Callable: paramCountCallable(1), + Error: errTest, + }, + }) +} + +func testSift(t *testing.T, tests []siftTest) { + + for i, test := range tests { + + output, err := jlib.Sift(reflect.ValueOf(test.Input), test.Callable) + + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("Test %d: expected %v, got %v", i+1, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("Test %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type keysTest struct { + Input interface{} + Output interface{} + Error error +} + +func TestKeys(t *testing.T) { + testKeys(t, []keysTest{ + { + // Empty map. + Input: map[string]interface{}{}, + Error: jtypes.ErrUndefined, + }, + { + // Empty struct. + Input: struct{}{}, + Error: jtypes.ErrUndefined, + }, + { + // Struct with no exported fields. + Input: struct { + a, b, c int + }{}, + Error: jtypes.ErrUndefined, + }, + { + // Empty array. + Input: []interface{}{}, + Error: jtypes.ErrUndefined, + }, + { + Input: map[string]interface{}{ + "a": 1, + }, + Output: "a", + }, + { + Input: map[string]bool{ // non-shortcut version + "a": true, + }, + Output: "a", + }, + { + Input: struct { + A int + }{}, + Output: "A", + }, + { + Input: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + }, + Output: []string{ + "a", + "b", + "c", + }, + }, + { + Input: map[string]float64{ // non-shortcut version + "a": 1, + "b": 2, + "c": 3, + }, + Output: []string{ + "a", + "b", + "c", + }, + }, + { + Input: struct { + A int + B string + C bool + d float64 // unexported struct fields are ignored. + }{}, + Output: []string{ + "A", + "B", + "C", + }, + }, + { + Input: []interface{}{ + map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + }, + map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + }, + struct { + A, B, C, d int // unexported struct fields are ignored. + }{}, + }, + Output: []string{ + "a", + "b", + "c", + "A", + "B", + "C", + }, + }, + { + Input: "this isn't an object", + Error: jtypes.ErrUndefined, + }, + { + Input: []interface{}{ + 3.141592, + }, + Error: jtypes.ErrUndefined, + }, + { + Input: map[bool]string{ + true: "true", + }, + Error: fmt.Errorf("object key must evaluate to a string, got true (bool)"), + }, + { + Input: []interface{}{ + map[bool]string{ + false: "false", + }, + }, + Error: fmt.Errorf("object key must evaluate to a string, got false (bool)"), + }, + }) +} + +func testKeys(t *testing.T, tests []keysTest) { + + for i, test := range tests { + + output, err := jlib.Keys(reflect.ValueOf(test.Input)) + + if !equalStringArray(output, test.Output) { + t.Errorf("Test %d: expected %v, got %v", i+1, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("Test %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +type mergeTest struct { + Input interface{} + Output interface{} + Error error +} + +func TestMerge(t *testing.T) { + testMerge(t, []mergeTest{ + { + // Empty map. + Input: map[string]interface{}{}, + Output: map[string]interface{}{}, + }, + { + // Empty struct. + Input: struct{}{}, + Output: map[string]interface{}{}, + }, + { + // Struct with no exported fields. + Input: struct { + a, b, c int + }{}, + Output: map[string]interface{}{}, + }, + { + // Empty array. + Input: []interface{}{}, + Output: map[string]interface{}{}, + }, + { + Input: map[string]interface{}{ + "a": 1, + }, + Output: map[string]interface{}{ + "a": 1, + }, + }, + { + Input: struct { + Pi float64 + }{ + Pi: 3.141592, + }, + Output: map[string]interface{}{ + "Pi": 3.141592, + }, + }, + { + Input: []interface{}{ + map[string]int{ + "a": 1, + "c": 3, + "e": 5, + "g": 7, + }, + map[string]int{ + "b": 2, + "d": 4, + "f": 6, + "h": 8, + }, + }, + Output: map[string]interface{}{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + "g": 7, + "h": 8, + }, + }, + { + Input: []struct { + A, B, C, d int // unexported struct fields are ignored. + }{ + { + A: 1, + }, + { + B: 2, + }, + { + C: 3, + }, + }, + Output: map[string]interface{}{ + "A": 0, + "B": 0, + "C": 3, + }, + }, + { + Input: []interface{}{ + map[string]interface{}{ + "One": 1, + "Two": 2, + }, + map[string]interface{}{ + "Three": 3, + }, + struct { + Three string + Four string + }{ + Three: "three", + Four: "four", + }, + map[string]float64{ + "Four": 4.0, + "Five": 5.0, + }, + }, + Output: map[string]interface{}{ + "One": 1, + "Two": 2, + "Three": "three", + "Four": 4.0, + "Five": 5.0, + }, + }, + { + Input: "this isn't an object", + Error: fmt.Errorf("argument must be an object or an array of objects"), + }, + { + Input: []interface{}{ + 3.141592, + }, + Error: fmt.Errorf("argument must be an object or an array of objects"), + }, + { + Input: map[bool]string{ + true: "true", + }, + Error: fmt.Errorf("object key must evaluate to a string, got true (bool)"), + }, + { + Input: []interface{}{ + map[bool]string{ + false: "false", + }, + }, + Error: fmt.Errorf("object key must evaluate to a string, got false (bool)"), + }, + }) +} + +func testMerge(t *testing.T, tests []mergeTest) { + + for i, test := range tests { + + output, err := jlib.Merge(reflect.ValueOf(test.Input)) + + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("Test %d: expected %v, got %v", i+1, test.Output, output) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("Test %d: expected error %v, got %v", i+1, test.Error, err) + } + } +} + +var errTest = errors.New("paramCountCallable.Call not implemented") + +type paramCountCallable int + +func (f paramCountCallable) Name() string { + return "paramCountCallable" +} + +func (f paramCountCallable) ParamCount() int { + return int(f) +} + +func (f paramCountCallable) Call([]reflect.Value) (reflect.Value, error) { + return reflect.Value{}, errTest +} + +type callable1 func([]reflect.Value) (reflect.Value, error) + +func (f callable1) Name() string { return "callable1" } +func (f callable1) ParamCount() int { return 1 } + +func (f callable1) Call(argv []reflect.Value) (reflect.Value, error) { + if len(argv) != 1 { + return reflect.Value{}, fmt.Errorf("callable1.Call expected 1 argument, got %d", len(argv)) + } + return f(argv) +} + +type callable2 func([]reflect.Value) (reflect.Value, error) + +func (f callable2) Name() string { return "callable2" } +func (f callable2) ParamCount() int { return 2 } + +func (f callable2) Call(argv []reflect.Value) (reflect.Value, error) { + if len(argv) != 2 { + return reflect.Value{}, fmt.Errorf("callable2.Call expected 2 arguments, got %d", len(argv)) + } + return f(argv) +} + +type callable3 func([]reflect.Value) (reflect.Value, error) + +func (f callable3) Name() string { return "callable3" } +func (f callable3) ParamCount() int { return 3 } + +func (f callable3) Call(argv []reflect.Value) (reflect.Value, error) { + if len(argv) != 3 { + return reflect.Value{}, fmt.Errorf("callable3.Call expected 3 arguments, got %d", len(argv)) + } + return f(argv) +} + +func equalStringArray(v1, v2 interface{}) bool { + switch v1 := v1.(type) { + case nil: + return v2 == nil + case string: + v2, ok := v2.(string) + return ok && v2 == v1 + case []string: + v2, ok := v2.([]string) + return ok && reflect.DeepEqual(stringArrayCount(v1), stringArrayCount(v2)) + case []interface{}: + v2, ok := v2.([]interface{}) + return ok && reflect.DeepEqual(interfaceArrayCount(v1), interfaceArrayCount(v2)) + default: + return false + } +} + +func stringArrayCount(values []string) map[string]int { + var m map[string]int + for _, s := range values { + if m == nil { + m = make(map[string]int) + } + m[s]++ + } + return m +} + +func interfaceArrayCount(values []interface{}) map[interface{}]int { + var m map[interface{}]int + for _, v := range values { + if m == nil { + m = make(map[interface{}]int) + } + m[v]++ + } + return m +} diff --git a/v1.5.4/jlib/string.go b/v1.5.4/jlib/string.go new file mode 100644 index 0000000..3ffe032 --- /dev/null +++ b/v1.5.4/jlib/string.go @@ -0,0 +1,736 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "math" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + "github.com/blues/jsonata-go/v1.5.4/jlib/jxpath" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// String converts a JSONata value to a string. Values that are +// already strings are returned unchanged. Functions return empty +// strings. All other types return their JSON representation. +func String(value interface{}) (string, error) { + + switch v := value.(type) { + case jtypes.Callable: + return "", nil + case string: + return v, nil + case []byte: + return string(v), nil + case float64: + // Will this ever fire in real world JSONata? Out of range + // errors should be caught either at the parse stage or when + // the argument to string() is evaluated. Tempted to remove + // this test as Encode would catch the error anyway. + if math.IsNaN(v) || math.IsInf(v, 0) { + return "", newError("string", ErrNaNInf) + } + } + + // TODO: Round numbers to 13dps to match jsonata-js. + b := bytes.Buffer{} + e := json.NewEncoder(&b) + if err := e.Encode(value); err != nil { + return "", err + } + + // TrimSpace removes the newline appended by Encode. + return strings.TrimSpace(b.String()), nil +} + +// Substring returns the portion of a string starting at the +// given (zero-indexed) offset. Negative offsets count from the +// end of the string, e.g. a start position of -1 returns the +// last character. The optional third argument controls the +// maximum number of characters returned. By default, Substring +// returns all characters up to the end of the string. +func Substring(s string, start int, length jtypes.OptionalInt) string { + + if (length.IsSet() && length.Int <= 0) || start >= utf8.RuneCountInString(s) { + return "" + } + + if start < 0 { + start += utf8.RuneCountInString(s) + } + + if start > 0 { + pos := positionOfNthRune(s, start) + s = s[pos:] + } + + if length.IsSet() && length.Int < utf8.RuneCountInString(s) { + pos := positionOfNthRune(s, length.Int) + s = s[:pos] + } + + return s +} + +// SubstringBefore returns the portion of a string that precedes +// the first occurrence of the given substring. If the substring +// is not present, SubstringBefore returns the full string. +func SubstringBefore(s, substr string) string { + if i := strings.Index(s, substr); i >= 0 { + return s[:i] + } + return s +} + +// SubstringAfter returns the portion of a string that follows +// the first occurrence of the given substring. If the substring +// is not present, SubstringAfter returns the full string. +func SubstringAfter(s, substr string) string { + if i := strings.Index(s, substr); i >= 0 { + return s[i+len(substr):] + } + return s +} + +// Pad returns a string padded to the specified number of characters. +// If the width is greater than zero, the string is padded to the +// right. If the width is less than zero, the string is padded to +// the left. The optional third argument specifies the characters +// used for padding. The default padding character is a space. +func Pad(s string, width int, chars jtypes.OptionalString) string { + + padlen := abs(width) - utf8.RuneCountInString(s) + if padlen <= 0 { + return s + } + + ch := chars.String + if ch == "" { + ch = " " + } + + padding := strings.Repeat(ch, padlen) + if utf8.RuneCountInString(padding) > padlen { + pos := positionOfNthRune(padding, padlen) + padding = padding[:pos] + } + + if width < 0 { + return padding + s + } + + return s + padding +} + +var reWhitespace = regexp.MustCompile(`\s+`) + +// Trim replaces consecutive whitespace characters in a string +// with a single space and trims spaces from the ends of the +// resulting string. +func Trim(s string) string { + return strings.TrimSpace(reWhitespace.ReplaceAllString(s, " ")) +} + +// Contains returns true if the source string matches a given +// pattern. The pattern can be a string or a regular expression. +func Contains(s string, pattern StringCallable) (bool, error) { + + switch v := pattern.toInterface().(type) { + case string: + return strings.Contains(s, v), nil + case jtypes.Callable: + matches, err := extractMatches(v, s, -1) + if err != nil { + return false, err + } + return len(matches) > 0, nil + default: + return false, fmt.Errorf("function contains takes a string or a regex") + } +} + +// Split returns an array of substrings generated by splitting +// a string on the provided separator. The separator can be a +// string or a regular expression. +// +// If the separator is not present in the source string, Split +// returns a single-value array containing the source string. +// If the separator is an empty string, Split returns an array +// containing one element for each character in the source string. +// +// The optional third argument specifies the maximum number of +// substrings to return. By default, Split returns all substrings. +func Split(s string, separator StringCallable, limit jtypes.OptionalInt) ([]string, error) { + + if limit.Int < 0 { + return nil, fmt.Errorf("third argument of the split function must evaluate to a positive number") + } + + var parts []string + + switch sep := separator.toInterface().(type) { + case string: + parts = strings.Split(s, sep) + case jtypes.Callable: + matches, err := extractMatches(sep, s, -1) + if err != nil { + return nil, err + } + pos := 0 + for _, m := range matches { + parts = append(parts, s[pos:m.indexes[0]]) + pos = m.indexes[1] + } + parts = append(parts, s[pos:]) + default: + return nil, fmt.Errorf("function split takes a string or a regex") + } + + if limit.IsSet() && limit.Int < len(parts) { + parts = parts[:limit.Int] + } + + return parts, nil +} + +// Join concatenates an array of strings into a single string. +// The optional second parameter is a separator inserted between +// each pair of values. +func Join(values reflect.Value, separator jtypes.OptionalString) (string, error) { + + if !jtypes.IsArrayOf(values, jtypes.IsString) { + if s, ok := jtypes.AsString(values); ok { + return s, nil + } + return "", fmt.Errorf("function join takes an array of strings") + } + + var vs []string + values = jtypes.Resolve(values) + + for i := 0; i < values.Len(); i++ { + s, _ := jtypes.AsString(values.Index(i)) + vs = append(vs, s) + } + + return strings.Join(vs, separator.String), nil +} + +// Match returns an array of objects describing matches of a +// regular expression in the source string. Each object in the +// array has the following fields: +// +// match - the substring matched by the regex +// index - the starting offset of this match +// groups - any captured groups for this match +// +// The optional third argument specifies the maximum number +// of matches to return. By default, Match returns all matches. +func Match(s string, pattern jtypes.Callable, limit jtypes.OptionalInt) ([]map[string]interface{}, error) { + + if limit.Int < 0 { + return nil, fmt.Errorf("third argument of function match must evaluate to a positive number") + } + + max := -1 + if limit.IsSet() { + max = limit.Int + } + + matches, err := extractMatches(pattern, s, max) + if err != nil { + return nil, err + } + + result := make([]map[string]interface{}, len(matches)) + + for i, m := range matches { + result[i] = map[string]interface{}{ + "match": m.value, + "index": m.indexes[0], + "groups": m.groups, + } + } + + return result, nil +} + +// Replace returns a copy of the source string with zero or more +// instances of the given pattern replaced by the value provided. +// The pattern can be a string or a regular expression. The optional +// fourth argument specifies the maximum number of replacements +// to make. By default, all instances of pattern are replaced. +// +// If pattern is a string, the replacement must also be a string. +// If pattern is a regular expression, the replacement can be a +// string or a Callable. +// +// When replacing a regular expression with a string, the replacement +// string can refer to the matched value with $0 and any captured +// groups with $N, where N is the order of the submatch (e.g. $1 +// is the first submatch). +// +// When replacing a regular expression with a Callable, the Callable +// must take a single argument and return a string. The argument is +// an object of the same form returned by Match. +func Replace(src string, pattern StringCallable, repl StringCallable, limit jtypes.OptionalInt) (string, error) { + + if limit.Int < 0 { + return "", fmt.Errorf("fourth argument of function replace must evaluate to a positive number") + } + + max := -1 + if limit.IsSet() { + max = limit.Int + } + + switch pattern := pattern.toInterface().(type) { + case string: + return replaceString(src, pattern, repl, max) + case jtypes.Callable: + return replaceMatchFunc(src, pattern, repl, max) + default: + return "", fmt.Errorf("second argument of function replace must be a string or a regex") + } +} + +func replaceString(src string, pattern string, repl StringCallable, limit int) (string, error) { + + if pattern == "" { + return "", fmt.Errorf("second argument of function replace can't be an empty string") + } + + s, ok := repl.toInterface().(string) + if !ok { + return "", fmt.Errorf("third argument of function replace must be a string when pattern is a string") + } + + return strings.Replace(src, pattern, s, limit), nil +} + +func replaceMatchFunc(src string, fn jtypes.Callable, repl StringCallable, limit int) (string, error) { + + var f jtypes.Callable + var srepl string + var expandable bool + + switch repl := repl.toInterface().(type) { + case string: + srepl = repl + expandable = strings.ContainsRune(srepl, '$') + case jtypes.Callable: + f = repl + default: + return "", fmt.Errorf("third argument of function replace must be a string or a function") + } + + matches, err := extractMatches(fn, src, limit) + if err != nil { + return "", err + } + + for i := len(matches) - 1; i >= 0; i-- { + + var repl string + + if f != nil { + repl, err = callReplaceFunc(f, matches[i]) + if err != nil { + return "", err + } + } else { + repl = srepl + if expandable { + repl = expandReplaceString(repl, matches[i]) + } + } + + src = src[:matches[i].indexes[0]] + repl + src[matches[i].indexes[1]:] + } + + return src, nil +} + +var defaultDecimalFormat = jxpath.NewDecimalFormat() + +// FormatNumber converts a number to a string, formatted according +// to the given picture string. See the XPath function format-number +// for the syntax of the picture string. +// +// https://www.w3.org/TR/xpath-functions-31/#formatting-numbers +// +// The optional third argument defines various formatting options +// such as the decimal separator and grouping separator. See the +// XPath documentation for details. +// +// https://www.w3.org/TR/xpath-functions-31/#defining-decimal-format +func FormatNumber(value float64, picture string, options jtypes.OptionalValue) (string, error) { + + if !options.IsSet() { + return jxpath.FormatNumber(value, picture, defaultDecimalFormat) + } + + opts := jtypes.Resolve(options.Value) + if !jtypes.IsMap(opts) { + return "", fmt.Errorf("decimal format options must be a map") + } + + format, err := newDecimalFormat(opts) + if err != nil { + return "", err + } + + return jxpath.FormatNumber(value, picture, format) +} + +func newDecimalFormat(opts reflect.Value) (jxpath.DecimalFormat, error) { + + format := jxpath.NewDecimalFormat() + + for _, key := range opts.MapKeys() { + + k, ok := jtypes.AsString(key) + if !ok { + return jxpath.DecimalFormat{}, fmt.Errorf("decimal format options must be a map of strings to strings") + } + + v, ok := jtypes.AsString(opts.MapIndex(key)) + if !ok { + return jxpath.DecimalFormat{}, fmt.Errorf("decimal format options must be a map of strings to strings") + } + + if err := updateDecimalFormat(&format, k, v); err != nil { + return jxpath.DecimalFormat{}, err + } + } + + return format, nil +} + +func updateDecimalFormat(format *jxpath.DecimalFormat, key string, value string) error { + + switch key { + case "infinity": + format.Infinity = value + case "NaN": + format.NaN = value + case "percent": + format.Percent = value + case "per-mille": + format.PerMille = value + default: + r, w := utf8.DecodeRuneInString(value) + if r == utf8.RuneError || w != len(value) { + return fmt.Errorf("invalid value %q for option %q", value, key) + } + switch key { + case "decimal-separator": + format.DecimalSeparator = r + case "grouping-separator": + format.GroupSeparator = r + case "exponent-separator": + format.ExponentSeparator = r + case "minus-sign": + format.MinusSign = r + case "zero-digit": + format.ZeroDigit = r + case "digit": + format.OptionalDigit = r + case "pattern-separator": + format.PatternSeparator = r + default: + return fmt.Errorf("unknown option %q", key) + } + } + + return nil +} + +// FormatBase returns the string representation of a number in the +// optional base argument. If specified, the base must be between +// 2 and 36. By default, FormatBase uses base 10. +func FormatBase(value float64, base jtypes.OptionalFloat64) (string, error) { + + radix := 10 + if base.IsSet() { + radix = int(Round(base.Float64, jtypes.OptionalInt{})) + } + + if radix < 2 || radix > 36 { + return "", fmt.Errorf("the second argument to formatBase must be between 2 and 36") + } + + return strconv.FormatInt(int64(Round(value, jtypes.OptionalInt{})), radix), nil +} + +// Base64Encode returns the base 64 encoding of a string. +func Base64Encode(s string) (string, error) { + return base64.StdEncoding.EncodeToString([]byte(s)), nil +} + +// Base64Decode returns the string represented by a base 64 string. +func Base64Decode(s string) (string, error) { + + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + + return string(b), nil +} + +// DecodeURL decodes a Uniform Resource Locator (URL) +// See https://docs.jsonata.org/string-functions#decodeurl +// and https://docs.jsonata.org/string-functions#decodeurlcomponent +func DecodeURL(s string) (string, error) { + escaped, err := url.QueryUnescape(s) + if err != nil { + return "", err + } + return escaped, nil +} + +// EncodeURL decodes a Uniform Resource Locator (URL) +// See https://docs.jsonata.org/string-functions#encodeurl +func EncodeURL(s string) (string, error) { + // Go will encode the UTF-8 replacement character to %EF%BF%DB + // but jsonata-js expects the operation to fail, so we'll + // provide the same behavior + if s == "�" { + return "", fmt.Errorf("invalid character") + } + + baseURL, err := url.Parse(s) + if err != nil { + return "", err + } + + baseURL.RawQuery = baseURL.Query().Encode() + return baseURL.String(), nil +} + +// EncodeURLComponent decodes a component of a Uniform Resource Locator (URL) +// and https://docs.jsonata.org/string-functions#encodeurlcomponent +func EncodeURLComponent(s string) (string, error) { + // Go will encode the UTF-8 replacement character to %EF%BF%DB + // but jsonata-js expects the operation to fail, so we'll + // provide the same behavior + if s == "�" { + return "", fmt.Errorf("invalid character") + } + + return url.QueryEscape(s), nil +} + +type match struct { + value string + indexes [2]int + groups []string +} + +func extractMatches(fn jtypes.Callable, s string, limit int) ([]match, error) { + + matches, err := callMatchFunc(fn, []reflect.Value{reflect.ValueOf(s)}, nil) + if err != nil { + return nil, err + } + + if limit >= 0 && limit < len(matches) { + matches = matches[:limit] + } + + return matches, nil +} + +func callMatchFunc(fn jtypes.Callable, argv []reflect.Value, matches []match) ([]match, error) { + + res, err := fn.Call(argv) + if err != nil { + return nil, err + } + + if !res.IsValid() { + return matches, nil + } + + if !jtypes.IsMap(res) { + return nil, fmt.Errorf("match function must return an object") + } + + res = jtypes.Resolve(res) + + v := res.MapIndex(reflect.ValueOf("match")) + value, ok := jtypes.AsString(v) + if !ok { + return nil, fmt.Errorf("match function must return an object with a string value named 'match'") + } + + v = res.MapIndex(reflect.ValueOf("start")) + start, ok := jtypes.AsNumber(v) + if !ok { + return nil, fmt.Errorf("match function must return an object with a number value named 'start'") + } + + v = res.MapIndex(reflect.ValueOf("end")) + end, ok := jtypes.AsNumber(v) + if !ok { + return nil, fmt.Errorf("match function must return an object with a number value named 'end'") + } + + v = res.MapIndex(reflect.ValueOf("groups")) + if !jtypes.IsArrayOf(v, jtypes.IsString) { + return nil, fmt.Errorf("match function must return an object with a string array value named 'groups'") + } + + v = jtypes.Resolve(v) + groups := make([]string, v.Len()) + for i := range groups { + s, _ := jtypes.AsString(v.Index(i)) + groups[i] = s + } + + v = res.MapIndex(reflect.ValueOf("next")) + next, ok := jtypes.AsCallable(v) + if !ok { + return nil, fmt.Errorf("match function must return an object with a Callable value named 'next'") + } + + return callMatchFunc(next, nil, append(matches, match{ + value: value, + indexes: [2]int{ + int(start), + int(end), + }, + groups: groups, + })) +} + +func expandReplaceString(s string, m match) string { + + var result string + +Loop: + for { + pos := strings.IndexRune(s, '$') + if pos == -1 { + result += s + break + } + + result += s[:pos] + s = s[pos+1:] + + if len(s) == 0 { + result += "$" + break + } + + r, _ := utf8.DecodeRuneInString(s) + + if r == '$' || r < '0' || r > '9' { + result += "$" + if r == '$' { + s = s[1:] + } + continue + } + + if r == '0' { + // $0 represents the full matched text. + result += m.value + s = s[1:] + continue + } + + var digits []rune + + for _, r := range s { + if r < '0' || r > '9' { + break + } + digits = append(digits, r) + } + + indexes := runesToNumbers(digits) + + for i := len(indexes) - 1; i >= 0; i-- { + // $N (where N > 0) represents the captured group with + // index N-1. + index := indexes[i] - 1 + if index < len(m.groups) { + result += m.groups[index] + s = s[i+1:] + continue Loop + } + } + + s = s[1:] + } + + return result +} + +func callReplaceFunc(f jtypes.Callable, m match) (string, error) { + + data := map[string]interface{}{ + "match": m.value, + "index": m.indexes[0], + "groups": m.groups, + } + + v, err := f.Call([]reflect.Value{reflect.ValueOf(data)}) + if err != nil { + return "", err + } + + repl, ok := jtypes.AsString(v) + if !ok { + return "", fmt.Errorf("third argument of function replace must be a function that returns a string") + } + + return repl, nil +} + +func runesToNumbers(runes []rune) []int { + + nums := make([]int, len(runes)) + + for i := range runes { + for j := 0; j <= i; j++ { + nums[i] = nums[i]*10 + int(runes[j]-'0') + } + } + + return nums +} + +func positionOfNthRune(s string, n int) int { + + i := 0 + for pos := range s { + if i == n { + return pos + } + i++ + } + + return -1 +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} diff --git a/v1.5.4/jlib/string_test.go b/v1.5.4/jlib/string_test.go new file mode 100644 index 0000000..8b1186c --- /dev/null +++ b/v1.5.4/jlib/string_test.go @@ -0,0 +1,1474 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jlib_test + +import ( + "fmt" + "math" + "os" + "reflect" + "strings" + "testing" + "unicode/utf8" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +var typereplaceCallable = reflect.TypeOf((*replaceCallable)(nil)).Elem() +var typeMatchCallable = reflect.TypeOf((*matchCallable)(nil)).Elem() +var typeCallable = reflect.TypeOf((*jtypes.Callable)(nil)).Elem() + +func TestMain(m *testing.M) { + + if !typereplaceCallable.Implements(typeCallable) { + fmt.Fprintln(os.Stderr, "replaceCallable is not a Callable. Aborting...") + os.Exit(1) + } + + if !reflect.PointerTo(typeMatchCallable).Implements(typeCallable) { + fmt.Fprintln(os.Stderr, "*matchCallable is not a Callable. Aborting...") + os.Exit(1) + } + + os.Exit(m.Run()) +} + +func TestString(t *testing.T) { + + data := []struct { + Input interface{} + Output string + Error error + }{ + { + Input: "string", + Output: "string", + }, + { + Input: []byte("string"), + Output: "string", + }, + { + Input: 100, + Output: "100", + }, + { + Input: 3.14159265359, + Output: "3.14159265359", + }, + { + Input: true, + Output: "true", + }, + { + Input: false, + Output: "false", + }, + { + Input: nil, + Output: "null", + }, + { + Input: []interface{}{}, + Output: "[]", + }, + { + Input: []interface{}{ + "hello", + 100, + 3.14159265359, + false, + }, + Output: `["hello",100,3.14159265359,false]`, + }, + { + Input: map[string]interface{}{}, + Output: "{}", + }, + { + Input: map[string]interface{}{ + "hello": "world", + "one hundred": 100, + "pi": 3.14159265359, + "bool": true, + "null": nil, + }, + Output: `{"bool":true,"hello":"world","null":null,"one hundred":100,"pi":3.14159265359}`, + }, + { + Input: replaceCallable(nil), + Output: "", + }, + { + Input: &matchCallable{}, + Output: "", + }, + { + Input: math.Inf(0), + Error: &jlib.Error{ + Func: "string", + Type: jlib.ErrNaNInf, + }, + }, + { + Input: math.NaN(), + Error: &jlib.Error{ + Func: "string", + Type: jlib.ErrNaNInf, + }, + }, + } + + for _, test := range data { + + got, err := jlib.String(test.Input) + + if got != test.Output { + t.Errorf("%v: Expected %q, got %q", test.Input, test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%v: Expected error %v, got %v", test.Input, test.Error, err) + } + } +} + +func TestSubstring(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Start int + Length jtypes.OptionalInt + Output string + }{ + { + Start: 2, + Output: "emoji", + }, + { + // Start position greater than string length. + Start: 7, + Output: "", + }, + { + // Negative start position. + Start: -5, + Output: "emoji", + }, + { + // Negative start position beyond start of string. + Start: -20, + Output: "😂 emoji", + }, + { + Start: 0, + Length: jtypes.NewOptionalInt(1), + Output: "😂", + }, + { + Start: 2, + Length: jtypes.NewOptionalInt(3), + Output: "emo", + }, + { + // Length greater than string length. + Start: 2, + Length: jtypes.NewOptionalInt(30), + Output: "emoji", + }, + { + // Zero length. + Start: 2, + Length: jtypes.NewOptionalInt(0), + Output: "", + }, + { + // Negative length. + Start: 2, + Length: jtypes.NewOptionalInt(-5), + Output: "", + }, + } + + for _, test := range data { + + got := jlib.Substring(src, test.Start, test.Length) + + if got != test.Output { + + s := fmt.Sprintf("substring(%q, %d", src, test.Start) + if test.Length.IsSet() { + s += fmt.Sprintf(", %d", test.Length.Int) + } + s += ")" + + t.Errorf("%s: Expected %q, got %q", s, test.Output, got) + } + } +} + +func TestSubstringBefore(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Substr string + Output string + }{ + { + Substr: "ji", + Output: "😂 emo", + }, + { + Substr: " ", + Output: "😂", + }, + { + Substr: "😂", + Output: "", + }, + { + // The index of an empty substr is zero, so substringBefore + // returns an empty string. + Substr: "", + Output: "", + }, + { + // If substr is not present, return the full string. + Substr: "x", + Output: "😂 emoji", + }, + } + + for _, test := range data { + + got := jlib.SubstringBefore(src, test.Substr) + + if got != test.Output { + t.Errorf("substringBefore(%q, %q): Expected %q, got %q", src, test.Substr, test.Output, got) + } + } +} + +func TestSubstringAfter(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Substr string + Output string + }{ + { + Substr: "emo", + Output: "ji", + }, + { + Substr: " ", + Output: "emoji", + }, + { + Substr: "😂", + Output: " emoji", + }, + { + // The index of an empty substr is zero, so substringAfter + // returns the full string. + Substr: "", + Output: "😂 emoji", + }, + { + // If substr is not present, return the full string. + Substr: "x", + Output: "😂 emoji", + }, + } + + for _, test := range data { + + got := jlib.SubstringAfter(src, test.Substr) + + if got != test.Output { + t.Errorf("substringAfter(%q, %q): Expected %q, got %q", src, test.Substr, test.Output, got) + } + } +} + +func TestPad(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Width int + Chars jtypes.OptionalString + Output string + }{ + { + Width: 10, + Output: "😂 emoji ", + }, + { + Width: -10, + Output: " 😂 emoji", + }, + { + // Pad with custom character. + Width: 10, + Chars: jtypes.NewOptionalString("😂"), + Output: "😂 emoji😂😂😂", + }, + { + // Pad with custom character. + Width: -10, + Chars: jtypes.NewOptionalString("😂"), + Output: "😂😂😂😂 emoji", + }, + { + // Pad with multiple characters. + Width: 12, + Chars: jtypes.NewOptionalString("123"), + Output: "😂 emoji12312", + }, + { + // Pad with multiple characters. + Width: -12, + Chars: jtypes.NewOptionalString("123"), + Output: "12312😂 emoji", + }, + { + // Width less than length of string. + Width: 5, + Output: "😂 emoji", + }, + { + // Width less than length of string. + Width: -5, + Output: "😂 emoji", + }, + } + + for _, test := range data { + + got := jlib.Pad(src, test.Width, test.Chars) + + if got != test.Output { + + s := fmt.Sprintf("pad(%q, %d", src, test.Width) + if test.Chars.IsSet() { + s += fmt.Sprintf(", %q", test.Chars.String) + } + s += ")" + + t.Errorf("%s: Expected %q, got %q", s, test.Output, got) + } + } +} + +func TestTrim(t *testing.T) { + + data := []struct { + Input string + Output string + }{ + { + Input: " hello world ", + Output: "hello world", + }, + { + Input: "hello\r\nworld", + Output: "hello world", + }, + { + Input: `multiline + string + with + tabs`, + Output: "multiline string with tabs", + }, + } + + for _, test := range data { + + got := jlib.Trim(test.Input) + + if got != test.Output { + t.Errorf("trim(%q): Expected %q, got %q", test.Input, test.Output, got) + } + } +} + +func TestContains(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Pattern interface{} // pattern can be a string or a matching function + Output bool + Error error + }{ + { + Pattern: "moji", + Output: true, + }, + { + Pattern: "😂", + Output: true, + }, + { + Pattern: "muji", + Output: false, + }, + { + // Matches for regex "m.ji". + Pattern: &matchCallable{ + name: "/m.ji/", + matches: []match{ + { + Match: "moji", + Start: 6, + End: 10, + }, + }, + }, + Output: true, + }, + { + // Matches for regex "😂". + Pattern: &matchCallable{ + name: "/😂/", + matches: []match{ + { + Match: "😂", + Start: 0, + End: 4, + }, + }, + }, + Output: true, + }, + { + // No matches. + Pattern: &matchCallable{ + name: "/^m.ji/", + }, + Output: false, + }, + { + // Invalid pattern. + Pattern: 100, + Error: fmt.Errorf("function contains takes a string or a regex"), + }, + } + + for _, test := range data { + + pattern := newStringCallable(test.Pattern) + got, err := jlib.Contains(src, pattern) + + if got != test.Output { + t.Errorf("contains(%q, %s): Expected %t, got %t", src, formatStringCallable(pattern), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("contains(%q, %s): Expected error %v, got %v", src, formatStringCallable(pattern), test.Error, err) + } + } +} +func TestSplit(t *testing.T) { + + src := "😂 emoji" + + data := []struct { + Separator interface{} // separator can be a string or a matching function + Limit jtypes.OptionalInt + Output []string + Error error + }{ + { + Separator: " ", + Output: []string{ + "😂", + "emoji", + }, + }, + { + Separator: "", + Output: []string{ + "😂", + " ", + "e", + "m", + "o", + "j", + "i", + }, + }, + { + Separator: "", + Limit: jtypes.NewOptionalInt(3), + Output: []string{ + "😂", + " ", + "e", + }, + }, + { + Separator: "", + Limit: jtypes.NewOptionalInt(0), + Output: []string{}, + }, + { + Separator: "", + Limit: jtypes.NewOptionalInt(-1), + Error: fmt.Errorf("third argument of the split function must evaluate to a positive number"), + }, + { + Separator: "muji", + Output: []string{ + "😂 emoji", + }, + }, + { + // Matches for regex "\\s". + Separator: &matchCallable{ + name: "/\\s/", + matches: []match{ + { + Match: " ", + Start: 4, + End: 5, + }, + }, + }, + Output: []string{ + "😂", + "emoji", + }, + }, + { + // Matches for regex "[aeiou]". + Separator: &matchCallable{ + name: "/[aeiou]/", + matches: []match{ + { + Match: "e", + Start: 5, + End: 6, + }, + { + Match: "o", + Start: 7, + End: 8, + }, + { + Match: "i", + Start: 9, + End: 10, + }, + }, + }, + Output: []string{ + "😂 ", + "m", + "j", + "", + }, + }, + { + // No match. + Separator: &matchCallable{ + name: "/muji/", + }, + Output: []string{ + "😂 emoji", + }, + }, + { + // Invalid separator. + Separator: 100, + Error: fmt.Errorf("function split takes a string or a regex"), + }, + } + + for _, test := range data { + + separator := newStringCallable(test.Separator) + + prefix := func() string { + s := fmt.Sprintf("split(%q, %s", src, formatStringCallable(separator)) + if test.Limit.IsSet() { + s += fmt.Sprintf(", %d", test.Limit.Int) + } + return s + ")" + } + + got, err := jlib.Split(src, separator, test.Limit) + + if !reflect.DeepEqual(got, test.Output) { + t.Errorf("%s: Expected %v, got %v", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +func TestJoin(t *testing.T) { + + data := []struct { + Values interface{} + Separator jtypes.OptionalString + Output string + Error error + }{ + { + // Single values are returned unchanged. + Values: "😂 emoji", + Output: "😂 emoji", + }, + { + Values: []string{}, + Output: "", + }, + { + Values: []string{ + "😂", + "emoji", + }, + Output: "😂emoji", + }, + { + Values: []interface{}{ + "one", + "two", + "three", + "four", + "five", + }, + Separator: jtypes.NewOptionalString("😂"), + Output: "one😂two😂three😂four😂five", + }, + { + Values: []interface{}{ + "one", + "two", + "three", + "four", + 5, + }, + Error: fmt.Errorf("function join takes an array of strings"), + }, + } + + for _, test := range data { + + prefix := func() string { + s := fmt.Sprintf("join(%v", test.Values) + if test.Separator.IsSet() { + s += fmt.Sprintf(", %q", test.Separator.String) + } + return s + ")" + } + + got, err := jlib.Join(reflect.ValueOf(test.Values), test.Separator) + + if got != test.Output { + t.Errorf("%s: Expected %q, got %q", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +func TestMatch(t *testing.T) { + + src := "abracadabra" + + data := []struct { + Pattern jtypes.Callable + Limit jtypes.OptionalInt + Output []map[string]interface{} + Error error + }{ + { + // Matches for regex "a." + Pattern: abracadabraMatches0(), + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{}, + }, + { + "match": "ac", + "index": 3, + "groups": []string{}, + }, + { + "match": "ad", + "index": 5, + "groups": []string{}, + }, + { + "match": "ab", + "index": 7, + "groups": []string{}, + }, + }, + }, + { + // Matches for regex "a(.)" + Pattern: abracadabraMatches1(), + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{ + "b", + }, + }, + { + "match": "ac", + "index": 3, + "groups": []string{ + "c", + }, + }, + { + "match": "ad", + "index": 5, + "groups": []string{ + "d", + }, + }, + { + "match": "ab", + "index": 7, + "groups": []string{ + "b", + }, + }, + }, + }, + { + // Matches for regex "(a)(.)" + Pattern: abracadabraMatches2(), + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{ + "a", + "b", + }, + }, + { + "match": "ac", + "index": 3, + "groups": []string{ + "a", + "c", + }, + }, + { + "match": "ad", + "index": 5, + "groups": []string{ + "a", + "d", + }, + }, + { + "match": "ab", + "index": 7, + "groups": []string{ + "a", + "b", + }, + }, + }, + }, + { + Pattern: abracadabraMatches2(), + Limit: jtypes.NewOptionalInt(1), + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{ + "a", + "b", + }, + }, + }, + }, + { + Pattern: abracadabraMatches2(), + Limit: jtypes.NewOptionalInt(0), + Output: []map[string]interface{}{}, + }, + { + Pattern: abracadabraMatches2(), + Limit: jtypes.NewOptionalInt(-1), + Error: fmt.Errorf("third argument of function match must evaluate to a positive number"), + }, + { + Pattern: &matchCallable{ + name: "/muji/", + }, + Output: []map[string]interface{}{}, + }, + } + + for _, test := range data { + + prefix := func() string { + s := fmt.Sprintf("match(%q, %s", src, test.Pattern.Name()) + if test.Limit.IsSet() { + s += fmt.Sprintf(", %d", test.Limit.Int) + } + return s + ")" + } + + got, err := jlib.Match(src, test.Pattern, test.Limit) + + if !reflect.DeepEqual(got, test.Output) { + t.Errorf("%s: Expected %v, got %v", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +func TestReplace(t *testing.T) { + + src := "abracadabra" + + data := []struct { + Pattern interface{} // pattern can be a string or a matching function + Repl interface{} // repl can be a string or a replacement function + Limit jtypes.OptionalInt + Output string + Error error + }{ + + // String patterns + + { + Pattern: "a", + Repl: "å", + Output: "åbråcådåbrå", + }, + { + Pattern: "a", + Repl: "å", + Limit: jtypes.NewOptionalInt(3), + Output: "åbråcådabra", + }, + { + Pattern: "a", + Repl: "å", + Limit: jtypes.NewOptionalInt(0), + Output: "abracadabra", + }, + { + Pattern: "a", + Repl: "å", + Limit: jtypes.NewOptionalInt(-1), + Error: fmt.Errorf("fourth argument of function replace must evaluate to a positive number"), + }, + { + Pattern: "a", + Repl: "", + Output: "brcdbr", + }, + { + Pattern: "😂", + Repl: "", + Output: "abracadabra", + }, + { + Pattern: "", + Repl: "å", + Limit: jtypes.NewOptionalInt(0), + Error: fmt.Errorf("second argument of function replace can't be an empty string"), + }, + { + Pattern: "a", + Repl: replaceCallable(nil), + Limit: jtypes.NewOptionalInt(0), + Error: fmt.Errorf("third argument of function replace must be a string when pattern is a string"), + }, + + // Matching function patterns + + { + // Matches for regex "a." + Pattern: abracadabraMatches0(), + Repl: "åå", + Output: "åårååååååra", + }, + { + Pattern: abracadabraMatches0(), + Repl: "åå", + Limit: jtypes.NewOptionalInt(3), + Output: "åårååååabra", + }, + { + Pattern: abracadabraMatches0(), + Repl: "åå", + Limit: jtypes.NewOptionalInt(0), + Output: "abracadabra", + }, + { + Pattern: abracadabraMatches0(), + Repl: "åå", + Limit: jtypes.NewOptionalInt(-1), + Error: fmt.Errorf("fourth argument of function replace must evaluate to a positive number"), + }, + { + // $0 is replaced by the full matched string. + Pattern: abracadabraMatches0(), + Repl: "$0", + Output: "abracadabra", + }, + { + // $N is replaced by the Nth captured string. + // Matches for regex "a(.)" + Pattern: abracadabraMatches1(), + Repl: "$1", + Output: "brcdbra", + }, + { + // Matches for regex "(a)(.)" + Pattern: abracadabraMatches2(), + Repl: "$2$1", + Output: "barcadabara", + }, + { + // If N is greater than the number of captured strings, + // $N evaluates to an empty string... + Pattern: abracadabraMatches2(), + Repl: "$3", + Output: "rra", + }, + { + // ...unless N has more than one digit, in which case we + // discard the rightmost digit and retry. Discarded digits + // are copied to the output. + Pattern: abracadabraMatches2(), + Repl: "$10$200", + Output: "a0b00ra0c00a0d00a0b00ra", + }, + { + // Trailing dollar signs are treated as literal dollar signs. + Pattern: abracadabraMatches2(), + Repl: "$", + Output: "$r$$$ra", + }, + { + // Trailing dollar signs are treated as literal dollar signs. + Pattern: abracadabraMatches2(), + Repl: "$1$2$", + Output: "ab$rac$ad$ab$ra", + }, + { + // Double dollar signs are treated as literal dollar signs. + Pattern: abracadabraMatches2(), + Repl: "$$", + Output: "$r$$$ra", + }, + { + // Double dollar signs are treated as literal dollar signs. + Pattern: abracadabraMatches2(), + Repl: "$1$$$2", + Output: "a$bra$ca$da$bra", + }, + { + // Dollar signs followed by anything other than another dollar + // sign or a digit are treated as normal text. + Pattern: abracadabraMatches2(), + Repl: "$ ", + Output: "$ r$ $ $ ra", + }, + { + // Dollar signs followed by anything other than another dollar + // sign or a digit are treated as normal text. + Pattern: abracadabraMatches2(), + Repl: "$😂", + Output: "$😂r$😂$😂$😂ra", + }, + { + Pattern: abracadabraMatches0(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + match, _ := m["match"].(string) + return strings.ToUpper(match), nil + }), + Output: "ABrACADABra", + }, + { + Pattern: abracadabraMatches0(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + match, _ := m["match"].(string) + return strings.ToUpper(match), nil + }), + Limit: jtypes.NewOptionalInt(3), + Output: "ABrACADabra", + }, + { + Pattern: abracadabraMatches0(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + match, _ := m["match"].(string) + return strings.ToUpper(match), nil + }), + Limit: jtypes.NewOptionalInt(0), + Output: "abracadabra", + }, + { + Pattern: abracadabraMatches1(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + groups, _ := m["groups"].([]string) + if len(groups) != 1 { + return "", fmt.Errorf("replaceCallable expected 1 captured group, got %d", len(groups)) + } + index, _ := m["index"].(int) + return strings.Repeat(groups[0], index), nil + }), + Output: "rcccdddddbbbbbbbra", + }, + { + Pattern: abracadabraMatches2(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + groups, _ := m["groups"].([]string) + if len(groups) != 2 { + return "", fmt.Errorf("replaceCallable expected 2 captured groups, got %d", len(groups)) + } + index, _ := m["index"].(int) + c, _ := utf8.DecodeRuneInString(groups[1]) + return fmt.Sprintf("%d%c", index, 'ⓐ'+c-'a'), nil + }), + Output: "0ⓑr3ⓒ5ⓓ7ⓑra", + }, + { + Pattern: abracadabraMatches2(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + return 100, nil + }), + Error: fmt.Errorf("third argument of function replace must be a function that returns a string"), + }, + { + Pattern: abracadabraMatches2(), + Repl: replaceCallable(func(m map[string]interface{}) (interface{}, error) { + return nil, fmt.Errorf("this callable returned an error") + }), + Error: fmt.Errorf("this callable returned an error"), + }, + { + Pattern: abracadabraMatches2(), + Repl: 100, + Error: fmt.Errorf("third argument of function replace must be a string or a function"), + }, + } + + for _, test := range data { + + repl := newStringCallable(test.Repl) + pattern := newStringCallable(test.Pattern) + + prefix := func() string { + s := fmt.Sprintf("replace(%q, %s, %s", src, formatStringCallable(pattern), formatStringCallable(repl)) + if test.Limit.IsSet() { + s += fmt.Sprintf(", %d", test.Limit.Int) + } + return s + ")" + } + + got, err := jlib.Replace(src, pattern, repl, test.Limit) + + if got != test.Output { + t.Errorf("%s: Expected %q, got %q", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +func TestReplaceInvalidPattern(t *testing.T) { + + _, got := jlib.Replace("abracadabra", newStringCallable(100), newStringCallable(""), jtypes.OptionalInt{}) + exp := fmt.Errorf("second argument of function replace must be a string or a regex") + + if !reflect.DeepEqual(exp, got) { + t.Errorf("Expected error %v, got %v", exp, got) + } +} + +func TestFormatNumber(t *testing.T) { + + data := []struct { + Value float64 + Picture string + Options interface{} + Output string + Error error + }{ + { + Value: 0.0, + Picture: "0", + Output: "0", + }, + { + Value: 12345.6, + Picture: "#,###.00", + Output: "12,345.60", + }, + { + Value: 12345678.9, + Picture: "9,999.99", + Output: "12,345,678.90", + }, + { + Value: 123.9, + Picture: "9999", + Output: "0124", + }, + { + Value: 0.14, + Picture: "01%", + Output: "14%", + }, + { + Value: 0.14, + Picture: "01pc", + // Custom percent symbol. + Options: map[string]interface{}{ + "percent": "pc", + }, + Output: "14pc", + }, + { + Value: 0.014, + Picture: "01‰", + Output: "14‰", + }, + { + Value: 0.014, + Picture: "01pm", + // Custom per-mille symbol. + Options: map[string]interface{}{ + "per-mille": "pm", + }, + Output: "14pm", + }, + { + Value: -6, + Picture: "000", + Output: "-006", + }, + { + Value: 1234.5678, + Picture: "#ʹ##0·00", + // Custom grouping and decimal separators. + Options: map[string]interface{}{ + "grouping-separator": "ʹ", + "decimal-separator": "·", + }, + Output: "1ʹ234·57", + }, + { + Value: 1234.5678, + Picture: "00.000E0", + // Custom exponent separator. + Options: map[string]interface{}{ + "exponent-separator": "E", + }, + Output: "12.346E2", + }, + { + Value: 0.234, + Picture: "0.0E0", + // Custom exponent separator. + Options: map[string]interface{}{ + "exponent-separator": "E", + }, + Output: "2.3E-1", + }, + { + Value: 0.234, + Picture: "#.00E0", + // Custom exponent separator. + Options: map[string]interface{}{ + "exponent-separator": "E", + }, + Output: "0.23E0", + }, + { + Value: 0.234, + Picture: ".00E0", + // Custom exponent separator. + Options: map[string]interface{}{ + "exponent-separator": "E", + }, + Output: ".23E0", + }, + } + + for _, test := range data { + + prefix := func() string { + s := fmt.Sprintf("formatNumber(%g, %q", test.Value, test.Picture) + if test.Options != nil { + s += fmt.Sprintf(", %v", test.Options) + } + return s + ")" + } + + var options jtypes.OptionalValue + if test.Options != nil { + // TODO: This shouldn't require nested ValueOf calls! + options.Set(reflect.ValueOf(reflect.ValueOf(test.Options))) + } + + got, err := jlib.FormatNumber(test.Value, test.Picture, options) + + if got != test.Output { + t.Errorf("%s: Expected %q, got %q", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +func TestFormatBase(t *testing.T) { + + value := float64(100) + + data := []struct { + Base jtypes.OptionalFloat64 + Output string + Error error + }{ + { + Output: "100", + }, + { + Base: jtypes.NewOptionalFloat64(2), + Output: "1100100", + }, + { + Base: jtypes.NewOptionalFloat64(8), + Output: "144", + }, + { + Base: jtypes.NewOptionalFloat64(16), + Output: "64", + }, + { + Base: jtypes.NewOptionalFloat64(20), + Output: "50", + }, + { + Base: jtypes.NewOptionalFloat64(32), + Output: "34", + }, + { + Base: jtypes.NewOptionalFloat64(36), + Output: "2s", + }, + { + Base: jtypes.NewOptionalFloat64(1), + Error: fmt.Errorf("the second argument to formatBase must be between 2 and 36"), + }, + { + Base: jtypes.NewOptionalFloat64(40), + Error: fmt.Errorf("the second argument to formatBase must be between 2 and 36"), + }, + } + + for _, test := range data { + + prefix := func() string { + s := fmt.Sprintf("formatBase(%g", value) + if test.Base.IsSet() { + s += fmt.Sprintf(", %g", test.Base.Float64) + } + return s + ")" + } + + got, err := jlib.FormatBase(value, test.Base) + + if got != test.Output { + t.Errorf("%s: Expected %q, got %q", prefix(), test.Output, got) + } + + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: Expected error %v, got %v", prefix(), test.Error, err) + } + } +} + +// Callables + +type match struct { + Match string + Start int + End int + Groups []string +} + +type matchCallable struct { + name string + index int + matches []match +} + +func (f *matchCallable) Name() string { + return f.name +} + +func (f *matchCallable) ParamCount() int { + return 1 +} + +func (f *matchCallable) Call(vs []reflect.Value) (reflect.Value, error) { + + if f.index >= len(f.matches) { + return reflect.Value{}, nil + } + + obj := map[string]interface{}{ + "match": f.matches[f.index].Match, + "start": f.matches[f.index].Start, + "end": f.matches[f.index].End, + "groups": f.matches[f.index].Groups, + "next": f, + } + + f.index++ + return reflect.ValueOf(obj), nil +} + +type replaceCallable func(map[string]interface{}) (interface{}, error) + +func (f replaceCallable) Name() string { + return "" +} + +func (f replaceCallable) ParamCount() int { + return 1 +} + +func (f replaceCallable) Call(vs []reflect.Value) (reflect.Value, error) { + + if len(vs) != 1 { + return reflect.Value{}, fmt.Errorf("replaceCallable: expected 1 argument, got %d", len(vs)) + } + + if !vs[0].CanInterface() { + return reflect.Value{}, fmt.Errorf("replaceCallable: expected an interfaceable type, got %s", vs[0].Type()) + } + + m, ok := vs[0].Interface().(map[string]interface{}) + if !ok { + return reflect.Value{}, fmt.Errorf("replaceCallable: expected argument to be a map of string to empty interface, got %s", vs[0].Type()) + } + + res, err := f(m) + return reflect.ValueOf(res), err +} + +// Helpers + +func abracadabraMatches0() jtypes.Callable { + m := abracadabraMatches("/a./") + for i := range m.matches { + m.matches[i].Groups = m.matches[i].Groups[:0] + } + return m +} + +func abracadabraMatches1() jtypes.Callable { + m := abracadabraMatches("/a(.)/") + for i := range m.matches { + m.matches[i].Groups = m.matches[i].Groups[1:] + } + return m +} + +func abracadabraMatches2() jtypes.Callable { + return abracadabraMatches("/(a)(.)/") +} + +func abracadabraMatches(name string) *matchCallable { + return &matchCallable{ + name: name, + matches: []match{ + { + Match: "ab", + Start: 0, + End: 2, + Groups: []string{ + "a", + "b", + }, + }, + { + Match: "ac", + Start: 3, + End: 5, + Groups: []string{ + "a", + "c", + }, + }, + { + Match: "ad", + Start: 5, + End: 7, + Groups: []string{ + "a", + "d", + }, + }, + { + Match: "ab", + Start: 7, + End: 9, + Groups: []string{ + "a", + "b", + }, + }, + }, + } +} + +func newStringCallable(v interface{}) jlib.StringCallable { + return jlib.StringCallable(reflect.ValueOf(v)) +} + +func formatStringCallable(value jlib.StringCallable) string { + + v := reflect.Value(value).Interface() + + switch v := v.(type) { + case string: + return fmt.Sprintf("%q", v) + case jtypes.Callable: + return v.Name() + default: + return fmt.Sprintf("<%s>", reflect.ValueOf(v).Kind()) + } +} diff --git a/v1.5.4/jparse/doc.go b/v1.5.4/jparse/doc.go new file mode 100644 index 0000000..c352f34 --- /dev/null +++ b/v1.5.4/jparse/doc.go @@ -0,0 +1,14 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package jparse converts JSONata expressions to abstract +// syntax trees. Most clients will not need to work with +// this package directly. +// +// # Usage +// +// Call the Parse function, passing a JSONata expression as +// a string. If an error occurs, it will be of type Error. +// Otherwise, Parse returns the root Node of the AST. +package jparse diff --git a/v1.5.4/jparse/error.go b/v1.5.4/jparse/error.go new file mode 100644 index 0000000..8bc1d57 --- /dev/null +++ b/v1.5.4/jparse/error.go @@ -0,0 +1,121 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse + +import ( + "fmt" + "regexp" +) + +// ErrType describes the type of an error. +type ErrType uint + +// Error types returned by the parser. +const ( + _ ErrType = iota + ErrSyntaxError + ErrUnexpectedEOF + ErrUnexpectedToken + ErrMissingToken + ErrPrefix + ErrInfix + ErrUnterminatedString + ErrUnterminatedRegex + ErrUnterminatedName + ErrIllegalEscape + ErrIllegalEscapeHex + ErrInvalidNumber + ErrNumberRange + ErrEmptyRegex + ErrInvalidRegex + ErrGroupPredicate + ErrGroupGroup + ErrPathLiteral + ErrIllegalAssignment + ErrIllegalParam + ErrDuplicateParam + ErrParamCount + ErrInvalidUnionType + ErrUnmatchedOption + ErrUnmatchedSubtype + ErrInvalidSubtype + ErrInvalidParamType +) + +var errmsgs = map[ErrType]string{ + ErrSyntaxError: "syntax error: '{{token}}'", + ErrUnexpectedEOF: "unexpected end of expression", + ErrUnexpectedToken: "expected token '{{hint}}', got '{{token}}'", + ErrMissingToken: "expected token '{{hint}}' before end of expression", + ErrPrefix: "the symbol '{{token}}' cannot be used as a prefix operator", + ErrInfix: "the symbol '{{token}}' cannot be used as an infix operator", + ErrUnterminatedString: "unterminated string literal (no closing '{{hint}}')", + ErrUnterminatedRegex: "unterminated regular expression (no closing '{{hint}}')", + ErrUnterminatedName: "unterminated name (no closing '{{hint}}')", + ErrIllegalEscape: "illegal escape sequence \\{{hint}}", + ErrIllegalEscapeHex: "illegal escape sequence \\{{hint}}: \\u must be followed by a 4-digit hexadecimal code point", + ErrInvalidNumber: "invalid number literal {{token}}", + ErrNumberRange: "invalid number literal {{token}}: value out of range", + ErrEmptyRegex: "invalid regular expression: expression cannot be empty", + ErrInvalidRegex: "invalid regular expression {{token}}: {{hint}}", + ErrGroupPredicate: "a predicate cannot follow a grouping expression in a path step", + ErrGroupGroup: "a path step can only have one grouping expression", + ErrPathLiteral: "invalid path step {{hint}}: paths cannot contain nulls, strings, numbers or booleans", + ErrIllegalAssignment: "illegal assignment: {{hint}} is not a variable", + ErrIllegalParam: "illegal function parameter: {{token}} is not a variable", + ErrDuplicateParam: "duplicate function parameter: {{token}}", + ErrParamCount: "invalid type signature: number of types must match number of function parameters", + ErrInvalidUnionType: "invalid type signature: unsupported union type '{{hint}}'", + ErrUnmatchedOption: "invalid type signature: option '{{hint}}' must follow a parameter", + ErrUnmatchedSubtype: "invalid type signature: subtypes must follow a parameter", + ErrInvalidSubtype: "invalid type signature: parameter type {{hint}} does not support subtypes", + ErrInvalidParamType: "invalid type signature: unknown parameter type '{{hint}}'", +} + +var reErrMsg = regexp.MustCompile("{{(token|hint)}}") + +// Error describes an error during parsing. +type Error struct { + Type ErrType + Token string + Hint string + Position int +} + +func newError(typ ErrType, tok token) error { + return newErrorHint(typ, tok, "") +} + +func newErrorHint(typ ErrType, tok token, hint string) error { + return &Error{ + Type: typ, + Token: tok.Value, + Position: tok.Position, + Hint: hint, + } +} + +func (e Error) Error() string { + + s := errmsgs[e.Type] + if s == "" { + return fmt.Sprintf("parser.Error: unknown error type %d", e.Type) + } + + return reErrMsg.ReplaceAllStringFunc(s, func(match string) string { + switch match { + case "{{token}}": + return e.Token + case "{{hint}}": + return e.Hint + default: + return match + } + }) +} + +func panicf(format string, a ...interface{}) { + panic(fmt.Sprintf(format, a...)) +} diff --git a/v1.5.4/jparse/jparse.go b/v1.5.4/jparse/jparse.go new file mode 100644 index 0000000..01d405a --- /dev/null +++ b/v1.5.4/jparse/jparse.go @@ -0,0 +1,371 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse + +// The JSONata parser is based on Pratt's Top Down Operator +// Precededence algorithm (see https://tdop.github.io/). Given +// a series of tokens representing a JSONata expression and the +// following metadata, it converts the tokens into an abstract +// syntax tree: +// +// 1. Functions that convert tokens to nodes based on their +// type and position (see 'nud' and 'led' in Pratt). +// +// 2. Binding powers (i.e. operator precedence values) for +// infix operators (see 'lbp' in Pratt). +// +// This metadata is defined below. + +// A nud (short for null denotation) is a function that takes +// a token and returns a node representing that token's value. +// The parsing algorithm only calls the nud function for tokens +// in the prefix position. This includes simple values like +// strings and numbers, complex values like arrays and objects, +// and prefix operators like the negation operator. +type nud func(*parser, token) (Node, error) + +// An led (short for left denotation) is a function that takes +// a token and a node representing the left hand side of an +// infix operation, and returns a node representing that infix +// operation. The parsing algorithm only calls the led function +// for tokens in the infix position, e.g. the mathematical +// operators. +type led func(*parser, token, Node) (Node, error) + +// nuds defines nud functions for token types that are valid +// in the prefix position. +var nuds = [...]nud{ + typeString: parseString, + typeNumber: parseNumber, + typeBoolean: parseBoolean, + typeNull: parseNull, + typeRegex: parseRegex, + typeVariable: parseVariable, + typeName: parseName, + typeNameEsc: parseEscapedName, + typeBracketOpen: parseArray, + typeBraceOpen: parseObject, + typeParenOpen: parseBlock, + typeMult: parseWildcard, + typeMinus: parseNegation, + typeDescendent: parseDescendent, + typePipe: parseObjectTransformation, + typeIn: parseName, + typeAnd: parseName, + typeOr: parseName, +} + +// leds defines led functions for token types that are valid +// in the infix position. +var leds = [...]led{ + typeParenOpen: parseFunctionCall, + typeBracketOpen: parsePredicate, + typeBraceOpen: parseGroup, + typeCondition: parseConditional, + typeAssign: parseAssignment, + typeApply: parseFunctionApplication, + typeConcat: parseStringConcatenation, + typeSort: parseSort, + typeDot: parseDot, + typePlus: parseNumericOperator, + typeMinus: parseNumericOperator, + typeMult: parseNumericOperator, + typeDiv: parseNumericOperator, + typeMod: parseNumericOperator, + typeEqual: parseComparisonOperator, + typeNotEqual: parseComparisonOperator, + typeLess: parseComparisonOperator, + typeLessEqual: parseComparisonOperator, + typeGreater: parseComparisonOperator, + typeGreaterEqual: parseComparisonOperator, + typeIn: parseComparisonOperator, + typeAnd: parseBooleanOperator, + typeOr: parseBooleanOperator, +} + +// bps defines binding powers for token types that are valid +// in the infix position. The parsing algorithm requires that +// all infix operators (as defined by the leds variable above) +// have a non-zero binding power. +// +// Binding powers are calculated from a 2D slice of token types +// in which the outer slice is ordered by operator precedence +// (highest to lowest) and each inner slice contains token +// types of equal operator precedence. +var bps = initBindingPowers([][]tokenType{ + { + typeParenOpen, + typeBracketOpen, + }, + { + typeDot, + }, + { + typeBraceOpen, + }, + { + typeMult, + typeDiv, + typeMod, + }, + { + typePlus, + typeMinus, + typeConcat, + }, + { + typeEqual, + typeNotEqual, + typeLess, + typeLessEqual, + typeGreater, + typeGreaterEqual, + typeIn, + typeSort, + typeApply, + }, + { + typeAnd, + }, + { + typeOr, + }, + { + typeCondition, + }, + { + typeAssign, + }, +}) + +const ( + nudCount = tokenType(len(nuds)) + ledCount = tokenType(len(leds)) +) + +func lookupNud(tt tokenType) nud { + if tt >= nudCount { + return nil + } + return nuds[tt] +} + +func lookupLed(tt tokenType) led { + if tt >= ledCount { + return nil + } + return leds[tt] +} + +func lookupBp(tt tokenType) int { + if tt >= ledCount { + return 0 + } + return bps[tt] +} + +// Parse builds the abstract syntax tree for a JSONata expression +// and returns the root node. If the provided expression is not +// valid, Parse returns an error of type Error. +func Parse(expr string) (root Node, err error) { + + // Handle panics from parseExpression. + defer func() { + if r := recover(); r != nil { + if e, ok := r.(*Error); ok { + root, err = nil, e + return + } + panic(r) + } + }() + + p := newParser(expr) + node := p.parseExpression(0) + + if p.token.Type != typeEOF { + return nil, newError(ErrSyntaxError, p.token) + } + + return node.optimize() +} + +type parser struct { + lexer lexer + token token + // The following function pointers are a workaround + // for an initialisation loop compile error. See the + // comment in newParser. + lookupNud func(tokenType) nud + lookupLed func(tokenType) led + lookupBp func(tokenType) int +} + +func newParser(input string) parser { + + p := parser{ + lexer: newLexer(input), + + // Because the nuds/leds arrays refer to functions that + // call the parser methods, the parser methods cannot + // directly refer to the nuds/leds arrays. Specifically, + // calling the nud/led lookup functions from the parser + // causes an initialisation loop, e.g. + // + // nuds refers to + // parseArray refers to + // parser.parseExpression refers to + // lookupNud refers to + // nuds + // + // To avoid this, the parser accesses the nud/led lookup + // functions via function pointers set at runtime. + lookupNud: lookupNud, + lookupLed: lookupLed, + lookupBp: lookupBp, + } + + // Set current token to the first token in the expression. + p.advance(true) + return p +} + +// parseExpression is the central function of the Pratt +// algorithm. It handles dispatch to the various nud/led +// functions (which may call back into parseExpression +// and the other parser methods). +// +// Note that the parser methods, parseExpression included, +// panic instead of returning errors. Panics are caught +// by the top-level Parse function and returned to the +// caller as errors. This makes the nud/led functions +// nicer to write without sacrificing the public API. +func (p *parser) parseExpression(rbp int) Node { + + if p.token.Type == typeEOF { + panic(newError(ErrUnexpectedEOF, p.token)) + } + + t := p.token + p.advance(false) + + nud := p.lookupNud(t.Type) + if nud == nil { + panic(newError(ErrPrefix, t)) + } + + lhs, err := nud(p, t) + if err != nil { + panic(err) + } + + for rbp < p.lookupBp(p.token.Type) { + + t := p.token + p.advance(true) + + led := p.lookupLed(t.Type) + if led == nil { + panic(newError(ErrInfix, t)) + } + + lhs, err = led(p, t, lhs) + if err != nil { + panic(err) + } + } + + return lhs +} + +// advance requests the next token from the lexer and updates +// the parser's current token pointer. It panics if the lexer +// returns an error token. +func (p *parser) advance(allowRegex bool) { + p.token = p.lexer.next(allowRegex) + if p.token.Type == typeError { + panic(p.lexer.err) + } +} + +// consume is like advance except it first checks that the +// current token is of the expected type. It panics if that +// is not the case. +func (p *parser) consume(expected tokenType, allowRegex bool) { + + if p.token.Type != expected { + + typ := ErrUnexpectedToken + if p.token.Type == typeEOF { + typ = ErrMissingToken + } + + panic(newErrorHint(typ, p.token, expected.String())) + } + + p.advance(allowRegex) +} + +// bp returns the binding power for the given token type. +func (p *parser) bp(t tokenType) int { + return p.lookupBp(t) +} + +// initBindingPowers calculates binding power values for the +// given token types and returns them as an array. The specific +// values are not important. All that matters for parsing is +// whether one token's binding power is higher than another's. +// +// Token types are provided as a slice of slices. The outer +// slice is ordered by operator precedence, highest to lowest. +// Token types within each inner slice have the same operator +// precedence. +func initBindingPowers(tokenTypes [][]tokenType) [ledCount]int { + + // Binding powers must: + // + // 1. be non-zero + // 2. increase with operator precedence + // 3. be separated by more than one (because we subtract + // 1 from the binding power for right-associative + // operators). + // + // This function produces a minimum binding power of 10. + // Values increase by 10 as operator precedence increases. + + var bps [ledCount]int + + for offset, tts := range tokenTypes { + + bp := (len(tokenTypes) - offset) * 10 + + for _, tt := range tts { + if bps[tt] != 0 { + panicf("initBindingPowers: token type %d [%s] appears more than once", tt, tt) + } + bps[tt] = bp + } + } + + validateBindingPowers(bps) + + return bps +} + +// validateBindingPowers sanity checks the values calculated +// by initBindingPowers. Every token type in the leds array +// should have a binding power. No other token type should +// have a binding power. +func validateBindingPowers(bps [ledCount]int) { + + for tt := tokenType(0); tt < ledCount; tt++ { + if leds[tt] != nil && bps[tt] == 0 { + panicf("validateBindingPowers: token type %d [%s] does not have a binding power", tt, tt) + } + if leds[tt] == nil && bps[tt] != 0 { + panicf("validateBindingPowers: token type %d [%s] should not have a binding power", tt, tt) + } + } +} diff --git a/v1.5.4/jparse/jparse_test.go b/v1.5.4/jparse/jparse_test.go new file mode 100644 index 0000000..91fe085 --- /dev/null +++ b/v1.5.4/jparse/jparse_test.go @@ -0,0 +1,2346 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse_test + +import ( + "reflect" + "regexp" + "regexp/syntax" + "strings" + "testing" + "unicode/utf8" + + "github.com/blues/jsonata-go/v1.5.4/jparse" +) + +type testCase struct { + Input string + Inputs []string + Output jparse.Node + Error error +} + +func TestStringNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `""`, + Output: &jparse.StringNode{}, + }, + { + Inputs: []string{ + `"hello"`, + `"\u0068\u0065\u006c\u006c\u006f"`, + }, + Output: &jparse.StringNode{ + Value: "hello", + }, + }, + { + Input: `"hello\t"`, + Output: &jparse.StringNode{ + Value: "hello\t", + }, + }, + { + Input: `"C:\\\\windows\\temp"`, + Output: &jparse.StringNode{ + Value: "C:\\\\windows\\temp", + }, + }, + { + // Non-ASCII UTF-8 + Inputs: []string{ + `"hello 超明體繁"`, + `"hello \u8d85\u660e\u9ad4\u7e41"`, + }, + Output: &jparse.StringNode{ + Value: "hello 超明體繁", + }, + }, + { + // UTF-16 surrogate pair + Inputs: []string{ + `"😂 emoji"`, + `"\ud83d\ude02 emoji"`, + }, + Output: &jparse.StringNode{ + Value: "😂 emoji", + }, + }, + { + // Invalid escape sequence + Input: `"hello\x"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 1, + Token: "hello\\x", + Hint: "x", + }, + }, + { + // Valid escape sequence followed by invalid escape sequence + Input: `"\r\x"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 1, + Token: "\\r\\x", + Hint: "x", + }, + }, + { + // Missing hexadecimal sequence + Input: `"hello\u"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "hello\\u", + Hint: "u" + strings.Repeat(string(utf8.RuneError), 4), + }, + }, + { + // Invalid hexadecimal sequence + Input: `"hello\u123t"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "hello\\u123t", + Hint: "u123t", + }, + }, + { + // Invalid hexadecimal sequence + Input: `"hello\uworld"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "hello\\uworld", + Hint: "uworl", + }, + }, + { + // Incomplete UTF-16 surrogate pair + Input: `"\ud83d"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\ud83d", + Hint: "ud83d", + }, + }, + { + // Incomplete trailing surrogate + Input: `"\ud83d\u"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\ud83d\\u", + Hint: "ud83d", + }, + }, + { + // Trailing surrogate outside allowed range + Input: `"\ud83d\u0068"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\ud83d\\u0068", + Hint: "ud83d", + }, + }, + { + // Invalid hexadecimal trailing surrogate + Input: `"\ud83d\u123t"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\ud83d\\u123t", + Hint: "ud83d", + }, + }, + { + // Reversed UTF-16 surrogate pair + Input: `"\ude02\ud83d"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\ude02\\ud83d", + Hint: "ude02", + }, + }, + { + // Non-terminated string + Input: `"hello`, + Error: &jparse.Error{ + Type: jparse.ErrUnterminatedString, + Position: 1, + Token: "hello", + Hint: "\"", + }, + }, + { + // Non-terminated string + Input: `'world`, + Error: &jparse.Error{ + Type: jparse.ErrUnterminatedString, + Position: 1, + Token: "world", + Hint: "'", + }, + }, + }) +} + +func TestNumberNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "0", + Output: &jparse.NumberNode{}, + }, + { + Input: "100", + Output: &jparse.NumberNode{ + Value: 100, + }, + }, + { + Input: "-0.5", + Output: &jparse.NumberNode{ + Value: -0.5, + }, + }, + { + // invalid syntax + Input: "1e+", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 0, + Token: "1e+", + }, + }, + { + // overflow + Input: "1e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Position: 0, + Token: "1e1000", + }, + }, + }) +} + +func TestBooleanNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "false", + Output: &jparse.BooleanNode{}, + }, + { + Input: "true", + Output: &jparse.BooleanNode{ + Value: true, + }, + }, + }) +} + +func TestNull(t *testing.T) { + testParser(t, []testCase{ + { + Input: "null", + Output: &jparse.NullNode{}, + }, + }) +} + +func TestRegexNode(t *testing.T) { + + // well-formed regular expressions + good := map[string]string{ + `/\s+/`: `\s+`, + `/ab+/`: `ab+`, + `/(ab)+/`: `(ab)+`, + `/(ab)+/i`: `(?i)(ab)+`, + `/(ab)+/m`: `(?m)(ab)+`, + `/(ab)+/s`: `(?s)(ab)+`, + `/^[1-9][0-9]*$/`: `^[1-9][0-9]*$`, + } + + // malformed regular expressions + bad := []string{ + `?`, // repetition operator with no operand + `\C+`, // invalid escape sequence + `[9-0]`, // invalid character class range + `[a-z]{1,0}`, // invalid repeat count + } + + var data []testCase + + found := false + for input, expr := range good { + + re, err := regexp.Compile(expr) + if err != nil { + t.Logf("Good regex %q does not compile. Skipping test...", expr) + continue + } + + found = true + data = append(data, testCase{ + Input: input, + Output: &jparse.RegexNode{ + Value: re, + }, + }) + } + + if !found { + t.Errorf("No compiling regexes found") + } + + found = false + for _, expr := range bad { + + _, err := regexp.Compile(expr) + if err == nil { + t.Logf("Bad regex %q compiles. Skipping test...", expr) + continue + } + + e, ok := err.(*syntax.Error) + if !ok { + t.Logf("Bad regex %q throws a %T (expected a *syntax.Error). Skipping test...", expr, err) + continue + } + + found = true + data = append(data, testCase{ + Input: "/" + expr + "/", + Error: &jparse.Error{ + Type: jparse.ErrInvalidRegex, + Position: 1, + Token: expr, + Hint: string(e.Code), + }, + }) + } + + if !found { + t.Errorf("No non-compiling regexes found") + } + + data = append(data, testCase{ + Input: "//", + Error: &jparse.Error{ + Type: jparse.ErrEmptyRegex, + Position: 1, + }, + }) + + testParser(t, data) +} + +func TestVariableNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "$", + Output: &jparse.VariableNode{}, + }, + { + Input: "$$", + Output: &jparse.VariableNode{ + Name: "$", + }, + }, + { + Input: "$x", + Output: &jparse.VariableNode{ + Name: "x", + }, + }, + }) +} + +func TestAssignmentNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `$greeting := "hello"`, + Output: &jparse.AssignmentNode{ + Name: "greeting", + Value: &jparse.StringNode{ + Value: "hello", + }, + }, + }, + { + Input: "$trimlower := $trim ~> $lowercase", + Output: &jparse.AssignmentNode{ + Name: "trimlower", + Value: &jparse.FunctionApplicationNode{ + LHS: &jparse.VariableNode{ + Name: "trim", + }, + RHS: &jparse.VariableNode{ + Name: "lowercase", + }, + }, + }, + }, + { + Input: "$bignum := 1e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Position: 11, + Token: "1e1000", + }, + }, + { + Input: "1 := 100", + Error: &jparse.Error{ + Type: jparse.ErrIllegalAssignment, + Position: 2, + Token: ":=", + Hint: "1", + }, + }, + }) +} + +func TestNegationNode(t *testing.T) { + testParser(t, []testCase{ + { + // The parser handles negation of number literals. + Input: "-1", + Output: &jparse.NumberNode{ + Value: -1, + }, + }, + { + Input: "--0.5", + Output: &jparse.NumberNode{ + Value: 0.5, + }, + }, + { + Input: "-$i", + Output: &jparse.NegationNode{ + RHS: &jparse.VariableNode{ + Name: "i", + }, + }, + }, + { + Input: "-1e", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 1, + Token: "1e", + }, + }, + { + Input: "-", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 1, + }, + }, + }) +} + +func TestBlockNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "()", + Output: &jparse.BlockNode{}, + }, + { + Inputs: []string{ + "(1; 2; 3)", + "(1; 2; 3;)", // allow trailing semicolons + }, + Output: &jparse.BlockNode{ + Exprs: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.NumberNode{ + Value: 2, + }, + &jparse.NumberNode{ + Value: 3, + }, + }, + }, + }, + { + // Invalid value. + Input: "(1; 2; 3e)", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 7, + Token: "3e", + }, + }, + { + // Invalid delimiters. + Input: "(1, 2, 3)", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedToken, + Position: 2, + Token: ",", + Hint: ")", + }, + }, + { + // No closing bracket. + Input: "(1; 2; 3", + Error: &jparse.Error{ + Type: jparse.ErrMissingToken, + Position: 8, + Hint: ")", + }, + }, + }) +} + +func TestWildcardNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "*", + Output: &jparse.WildcardNode{}, + }, + { + Input: "*Field", + Error: &jparse.Error{ + Type: jparse.ErrSyntaxError, + Position: 1, + Token: "Field", + }, + }, + }) +} + +func TestDescendentNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "**", + Output: &jparse.DescendentNode{}, + }, + { + Input: "**Field", + Error: &jparse.Error{ + Type: jparse.ErrSyntaxError, + Position: 2, + Token: "Field", + }, + }, + }) +} + +func TestObjectTransformationNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `|$|{"one":1}|`, + Output: &jparse.ObjectTransformationNode{ + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + }, + }, + }, + }, + { + Input: `|$|{}, ["key1", "key2"]|`, + Output: &jparse.ObjectTransformationNode{ + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{}, + Deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "key1", + }, + &jparse.StringNode{ + Value: "key2", + }, + }, + }, + }, + }, + { + Input: `|$|{"one":1}, ["key1", "key2"]|`, + Output: &jparse.ObjectTransformationNode{ + Pattern: &jparse.VariableNode{}, + Updates: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + }, + }, + Deletes: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.StringNode{ + Value: "key1", + }, + &jparse.StringNode{ + Value: "key2", + }, + }, + }, + }, + }, + { + // Bad pattern + Input: "|?|{}|", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 1, + Token: "?", + }, + }, + { + // Bad update map + Input: `|$|{"one":}|`, + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 10, + Token: "}", + }, + }, + { + // Bad deletion array + Input: `|$|{},["key\1"]|`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 8, + Token: "key\\1", + Hint: "1", + }, + }, + }) +} + +func TestFunctionCallNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "$random()", + Output: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "random", + }, + }, + }, + { + Input: `$uppercase("hello")`, + Output: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "uppercase", + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: "hello", + }, + }, + }, + }, + { + Input: `$substring("hello", -3, 2)`, + Output: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "substring", + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: "hello", + }, + &jparse.NumberNode{ + Value: -3, + }, + &jparse.NumberNode{ + Value: 2, + }, + }, + }, + }, + { + // Trailing delimiter. + Input: `$trim("hello ",)`, + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: ")", + Position: 17, + }, + }, + }) +} + +func TestLambdaNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "function(){0}", + Output: &jparse.LambdaNode{ + Body: &jparse.NumberNode{ + Value: 0, + }, + }, + }, + { + Input: "function($w, $h){$w * $h}", + Output: &jparse.LambdaNode{ + ParamNames: []string{ + "w", + "h", + }, + Body: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.VariableNode{ + Name: "w", + }, + RHS: &jparse.VariableNode{ + Name: "h", + }, + }, + }, + }, + { + // No function body. + Input: "function()", + Error: &jparse.Error{ + Type: jparse.ErrMissingToken, + Position: 10, + Hint: "{", + }, + }, + { + // Empty function body. + Input: "function(){}", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 11, + Token: "}", + }, + }, + { + // Invalid function body. + Input: "function(){1e}", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 11, + Token: "1e", + }, + }, + { + // Illegal parameter. + Input: "function($w, h){$w * $h}", + Error: &jparse.Error{ + Type: jparse.ErrIllegalParam, + Position: 13, + Token: "h", + }, + }, + { + // Duplicate parameter. + Input: "function($x, $x){$x}", + Error: &jparse.Error{ + Type: jparse.ErrDuplicateParam, + Position: 14, + Token: "x", + }, + }, + { + // Lambdas cannot be partials. + Input: "function(?, 10){0}", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 9, + Token: "?", + }, + }, + { + // Trailing delimiter. + Input: "function($x, $y,){0}", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 16, + Token: ")", + }, + }, + }) +} + +func TestTypedLambdaNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "function($x, $y){0}", + Output: &jparse.TypedLambdaNode{ + LambdaNode: &jparse.LambdaNode{ + ParamNames: []string{ + "x", + "y", + }, + Body: &jparse.NumberNode{ + Value: 0, + }, + }, + In: []jparse.Param{ + { + Type: jparse.ParamTypeNumber, + }, + { + Type: jparse.ParamTypeNumber, + Option: jparse.ParamOptional, + }, + }, + }, + }, + { + Input: "function($arr)-:a>{[]}", + Output: &jparse.TypedLambdaNode{ + LambdaNode: &jparse.LambdaNode{ + ParamNames: []string{ + "arr", + }, + Body: &jparse.ArrayNode{}, + }, + In: []jparse.Param{ + { + Type: jparse.ParamTypeArray, + Option: jparse.ParamContextable, + SubParams: []jparse.Param{ + { + Type: jparse.ParamTypeNumber | jparse.ParamTypeString, + }, + }, + }, + }, + }, + }, + { + // Mismatched parameter/signature count. + Input: "λ($x, $y){0}", + Error: &jparse.Error{ + Type: jparse.ErrParamCount, + Token: "{", + Position: 18, + }, + }, + { + // Unknown parameter type in signature. + Input: "λ($x, $y){0}", + Error: &jparse.Error{ + // TODO: Add position info. + Type: jparse.ErrInvalidParamType, + Hint: "z", + }, + }, + { + // Unknown parameter type in union type. + Input: "λ($x, $y){0}", + Error: &jparse.Error{ + // TODO: Add position info. + Type: jparse.ErrInvalidUnionType, + Hint: "y", + }, + }, + { + // Option without a parameter. + Input: "λ($x, $y)<+nn?:n>{0}", + Error: &jparse.Error{ + // TODO: Add position info. + Type: jparse.ErrUnmatchedOption, + Hint: "+", + }, + }, + { + // Subtype without a parameter. + Input: "λ($x, $y)<nn?:n>{0}", + Error: &jparse.Error{ + // TODO: Add position info. + Type: jparse.ErrUnmatchedSubtype, + }, + }, + { + // Subtype on a non-array, non-function parameter. + Input: "λ($x, $y)n?:n>{0}", + Error: &jparse.Error{ + // TODO: Add position info. + Type: jparse.ErrInvalidSubtype, + Hint: "n", + }, + }, + }) +} + +func TestPartialApplicationNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "$substring(?, 0, ?)", + Output: &jparse.PartialNode{ + Func: &jparse.VariableNode{ + Name: "substring", + }, + Args: []jparse.Node{ + &jparse.PlaceholderNode{}, + &jparse.NumberNode{}, + &jparse.PlaceholderNode{}, + }, + }, + }, + }) +} + +func TestFunctionApplicationNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `$trim ~> $uppercase`, + Output: &jparse.FunctionApplicationNode{ + LHS: &jparse.VariableNode{ + Name: "trim", + }, + RHS: &jparse.VariableNode{ + Name: "uppercase", + }, + }, + }, + { + Input: `"hello world" ~> $substringBefore(" ")`, + Output: &jparse.FunctionApplicationNode{ + LHS: &jparse.StringNode{ + Value: "hello world", + }, + RHS: &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "substringBefore", + }, + Args: []jparse.Node{ + &jparse.StringNode{ + Value: " ", + }, + }, + }, + }, + }, + { + Input: `"hello world" ~> $substringBefore("\x")`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 35, + Token: "\\x", + Hint: "x", + }, + }, + { + Input: `"hello world" ~>`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 16, + }, + }, + }) +} + +func TestPredicateNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "$", + Output: &jparse.VariableNode{}, + }, + { + Input: "$[-1]", + Output: &jparse.PredicateNode{ + Expr: &jparse.VariableNode{}, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: -1, + }, + }, + }, + }, + { + Input: "$[-1][0]", + Output: &jparse.PredicateNode{ + Expr: &jparse.PredicateNode{ + Expr: &jparse.VariableNode{}, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: -1, + }, + }, + }, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + { + Input: "$[-1][0][]", + Output: &jparse.PathNode{ + KeepArrays: true, + Steps: []jparse.Node{ + &jparse.PredicateNode{ + Expr: &jparse.PredicateNode{ + Expr: &jparse.VariableNode{}, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: -1, + }, + }, + }, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + }, + }, + { + Input: "path", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "path", + }, + }, + }, + }, + { + Input: `path[type="home"]`, + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.PredicateNode{ + Expr: &jparse.NameNode{ + Value: "path", + }, + Filters: []jparse.Node{ + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "type", + }, + }, + }, + RHS: &jparse.StringNode{ + Value: "home", + }, + }, + }, + }, + }, + }, + }, + { + Input: `path[type="home"][0]`, + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.PredicateNode{ + Expr: &jparse.NameNode{ + Value: "path", + }, + Filters: []jparse.Node{ + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "type", + }, + }, + }, + RHS: &jparse.StringNode{ + Value: "home", + }, + }, + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + }, + }, + { + Input: `path[type="home"][0][]`, + Output: &jparse.PathNode{ + KeepArrays: true, + Steps: []jparse.Node{ + &jparse.PredicateNode{ + Expr: &jparse.NameNode{ + Value: "path", + }, + Filters: []jparse.Node{ + &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "type", + }, + }, + }, + RHS: &jparse.StringNode{ + Value: "home", + }, + }, + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + }, + }, + { + Input: `path[price<=1e]`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 12, + Token: "1e", + }, + }, + { + Input: `path{"one": 1}[0]`, + Error: &jparse.Error{ + // TODO: Get position. + Type: jparse.ErrGroupPredicate, + }, + }, + }) +} + +func TestConditionalNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `true ? "yes"`, + Output: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: true, + }, + Then: &jparse.StringNode{ + Value: "yes", + }, + }, + }, + { + Input: `true ? "yes" : "no"`, + Output: &jparse.ConditionalNode{ + If: &jparse.BooleanNode{ + Value: true, + }, + Then: &jparse.StringNode{ + Value: "yes", + }, + Else: &jparse.StringNode{ + Value: "no", + }, + }, + }, + { + // Missing truthy expression. + Input: `true ?`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 6, + }, + }, + { + // Bad truthy expression. + Input: `true ? 1e`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 7, + Token: "1e", + }, + }, + { + // Missing falsy expression. + Input: `true ? 1e10 :`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 13, + }, + }, + { + // Bad falsy expression. + Input: `true ? 1e10 : 1e`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 14, + Token: "1e", + }, + }, + }) +} + +func TestArrayNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "[]", + Output: &jparse.ArrayNode{}, + }, + { + Input: "[1,2,3]", + Output: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{ + Value: 1, + }, + &jparse.NumberNode{ + Value: 2, + }, + &jparse.NumberNode{ + Value: 3, + }, + }, + }, + }, + { + Input: "[1..3]", + Output: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 3, + }, + }, + }, + }, + }, + { + Input: "[-2..2,3,4,5]", + Output: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.RangeNode{ + LHS: &jparse.NumberNode{ + Value: -2, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + &jparse.NumberNode{ + Value: 3, + }, + &jparse.NumberNode{ + Value: 4, + }, + &jparse.NumberNode{ + Value: 5, + }, + }, + }, + }, + { + Input: `[0, "", false, null, [], {}]`, + Output: &jparse.ArrayNode{ + Items: []jparse.Node{ + &jparse.NumberNode{}, + &jparse.StringNode{}, + &jparse.BooleanNode{}, + &jparse.NullNode{}, + &jparse.ArrayNode{}, + &jparse.ObjectNode{}, + }, + }, + }, + { + // Invalid array member. + Input: `[1, 2, 3e]`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 7, + Token: "3e", + }, + }, + { + // Invalid range operand. + Input: `[1..1e]`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 4, + Token: "1e", + }, + }, + { + // Invalid delimiters. + Input: `[1; 2; 3]`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedToken, + Position: 2, + Token: ";", + Hint: "]", + }, + }, + { + // Trailing delimiter. + Input: `[1, 2, 3,]`, + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 9, + Token: "]", + }, + }, + }) +} + +func TestObjectNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "{}", + Output: &jparse.ObjectNode{}, + }, + { + Input: `{"one": 1, "two": 2, "three": 3}`, + Output: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + { + &jparse.StringNode{ + Value: "two", + }, + &jparse.NumberNode{ + Value: 2, + }, + }, + { + &jparse.StringNode{ + Value: "three", + }, + &jparse.NumberNode{ + Value: 3, + }, + }, + }, + }, + }, + { + // Invalid key. + Input: `{"on\e": 1}`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 2, + Token: "on\\e", + Hint: "e", + }, + }, + { + // Invalid value. + Input: `{"one": 1e}`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 8, + Token: "1e", + }, + }, + { + // Invalid delimiter. + Input: `{"one"; 1}`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedToken, + Position: 6, + Token: ";", + Hint: ":", + }, + }, + { + // Invalid delimiters. + Input: `{"one": 1; "two": 2; "three": 3}`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedToken, + Position: 9, + Token: ";", + Hint: "}", + }, + }, + { + // Trailing delimiter. + Input: `{"one": 1,}`, + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 10, + Token: "}", + }, + }, + }) +} + +func TestGroupNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `*{"one": 1}`, + Output: &jparse.GroupNode{ + Expr: &jparse.WildcardNode{}, + ObjectNode: &jparse.ObjectNode{ + Pairs: [][2]jparse.Node{ + { + &jparse.StringNode{ + Value: "one", + }, + &jparse.NumberNode{ + Value: 1, + }, + }, + }, + }, + }, + }, + { + // Invalid value. + Input: `*{"one": 1e}`, + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 9, + Token: "1e", + }, + }, + { + Input: `*{"one": 1}{"two": 2}`, + Error: &jparse.Error{ + // TODO: Get position. + Type: jparse.ErrGroupGroup, + }, + }, + }) +} + +func TestNumericOperatorNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "3.5 + 1", + Output: &jparse.NumericOperatorNode{ + Type: jparse.NumericAdd, + LHS: &jparse.NumberNode{ + Value: 3.5, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + }, + { + Input: "3.5 - 1", + Output: &jparse.NumericOperatorNode{ + Type: jparse.NumericSubtract, + LHS: &jparse.NumberNode{ + Value: 3.5, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + }, + { + Input: "3.5 * 1", + Output: &jparse.NumericOperatorNode{ + Type: jparse.NumericMultiply, + LHS: &jparse.NumberNode{ + Value: 3.5, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + }, + { + Input: "3.5 / 1", + Output: &jparse.NumericOperatorNode{ + Type: jparse.NumericDivide, + LHS: &jparse.NumberNode{ + Value: 3.5, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + }, + { + Input: "3.5 % 1", + Output: &jparse.NumericOperatorNode{ + Type: jparse.NumericModulo, + LHS: &jparse.NumberNode{ + Value: 3.5, + }, + RHS: &jparse.NumberNode{ + Value: 1, + }, + }, + }, + { + Input: "3.5e * 1", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 0, + Token: "3.5e", + }, + }, + { + Input: "3.5 * 1e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Position: 6, + Token: "1e1000", + }, + }, + { + Input: "+", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "+", + Position: 0, + }, + }, + { + Input: "-", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 1, + }, + }, + { + Input: "*", + Output: &jparse.WildcardNode{}, + }, + { + Input: "/", + Error: &jparse.Error{ + Type: jparse.ErrUnterminatedRegex, + Hint: "/", + Position: 1, + }, + }, + { + Input: "%", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "%", + Position: 0, + }, + }, + }) +} + +func TestComparisonOperatorNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "1 = 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 != 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonNotEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 > 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreater, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 >= 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonGreaterEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 < 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLess, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 <= 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonLessEqual, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + Input: "1 in 2", + Output: &jparse.ComparisonOperatorNode{ + Type: jparse.ComparisonIn, + LHS: &jparse.NumberNode{ + Value: 1, + }, + RHS: &jparse.NumberNode{ + Value: 2, + }, + }, + }, + { + // Invalid left hand side. + Input: "1e = 1", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 0, + Token: "1e", + }, + }, + { + // Missing right hand side. + Input: "1 =", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 3, + }, + }, + { + // Invalid right hand side. + Input: "1 = 1e", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 4, + Token: "1e", + }, + }, + { + Input: "=", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "=", + Position: 0, + }, + }, + { + Input: "!=", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "!=", + Position: 0, + }, + }, + { + Input: ">", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: ">", + Position: 0, + }, + }, + { + Input: ">=", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: ">=", + Position: 0, + }, + }, + { + Input: "<", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "<", + Position: 0, + }, + }, + { + Input: "<=", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Token: "<=", + Position: 0, + }, + }, + { + // Treat "in" like a name when it appears as a prefix. + Input: "in", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "in", + }, + }, + }, + }, + }) +} + +func TestBooleanOperatorNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "true and false", + Output: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanAnd, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + }, + { + Input: "true or false", + Output: &jparse.BooleanOperatorNode{ + Type: jparse.BooleanOr, + LHS: &jparse.BooleanNode{ + Value: true, + }, + RHS: &jparse.BooleanNode{ + Value: false, + }, + }, + }, + { + // Treat "and" like a name when it appears as a prefix. + Input: "and", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "and", + }, + }, + }, + }, + { + // Treat "or" like a name when it appears as a prefix. + Input: "or", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "or", + }, + }, + }, + }, + { + // Invalid left hand side. + Input: "1e or 1", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Position: 0, + Token: "1e", + }, + }, + { + // Missing right hand side. + Input: "true and", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 8, + }, + }, + { + // Invalid right hand side. + Input: "1 and 1e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Position: 6, + Token: "1e1000", + }, + }, + }) +} + +func TestConcatenationNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: `"hello" & "world"`, + Output: &jparse.StringConcatenationNode{ + LHS: &jparse.StringNode{ + Value: "hello", + }, + RHS: &jparse.StringNode{ + Value: "world", + }, + }, + }, + { + Input: `firstName & " " & lastName`, + Output: &jparse.StringConcatenationNode{ + LHS: &jparse.StringConcatenationNode{ + LHS: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "firstName", + }, + }, + }, + RHS: &jparse.StringNode{ + Value: " ", + }, + }, + RHS: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "lastName", + }, + }, + }, + }, + }, + { + // bad left hand side. + Input: `"\u000z" & " escape"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\u000z", + Hint: "u000z", + }, + }, + { + // missing right hand side. + Input: `"hello" &`, + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 9, + }, + }, + { + // bad right hand side. + Input: `"escape" & " this\x"`, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 12, + Token: " this\\x", + Hint: "x", + }, + }, + }) +} + +func TestSortNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "$^(path)", + Output: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortDefault, + Expr: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "path", + }, + }, + }, + }, + }, + }, + }, + { + Input: "$^(second)", + Output: &jparse.SortNode{ + Expr: &jparse.VariableNode{}, + Terms: []jparse.SortTerm{ + { + Dir: jparse.SortAscending, + Expr: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "first", + }, + }, + }, + }, + { + Dir: jparse.SortDescending, + Expr: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "second", + }, + }, + }, + }, + }, + }, + }, + { + // Missing sort terms. + Input: "$^", + Error: &jparse.Error{ + Type: jparse.ErrMissingToken, + Position: 2, + Hint: "(", + }, + }, + { + // Empty sort terms. + Input: "$^()", + Error: &jparse.Error{ + Type: jparse.ErrPrefix, + Position: 3, + Token: ")", + }, + }, + }) +} + +func TestPathNode(t *testing.T) { + testParser(t, []testCase{ + { + Input: "path", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "path", + }, + }, + }, + }, + { + Input: "path[]", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "path", + }, + }, + KeepArrays: true, + }, + }, + { + Input: "path[0]", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.PredicateNode{ + Expr: &jparse.NameNode{ + Value: "path", + }, + Filters: []jparse.Node{ + &jparse.NumberNode{ + Value: 0, + }, + }, + }, + }, + }, + }, + { + Input: "$.path", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.VariableNode{}, + &jparse.NameNode{ + Value: "path", + }, + }, + }, + }, + { + Input: "$.path.$uppercase()", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.VariableNode{}, + &jparse.NameNode{ + Value: "path", + }, + &jparse.FunctionCallNode{ + Func: &jparse.VariableNode{ + Name: "uppercase", + }, + }, + }, + }, + }, + { + // Incomplete path. + Input: "path.", + Error: &jparse.Error{ + Type: jparse.ErrUnexpectedEOF, + Position: 5, + }, + }, + { + // Literal on rhs of dot operator. + Input: "path.0", + Error: &jparse.Error{ + // TODO: Need position info. + Type: jparse.ErrPathLiteral, + Hint: "0", + }, + }, + { + // Literal on lhs of dot operator. + Input: `"Product Name".$uppercase()`, + Error: &jparse.Error{ + // TODO: Need position info. + Type: jparse.ErrPathLiteral, + Hint: `"Product Name"`, + }, + }, + /* + { + Input: "`escaped path`", + Output: &jparse.PathNode{ + Steps: []jparse.Node{ + &jparse.NameNode{ + Value: "escaped path", + Escaped: true, + }, + }, + }, + }, + */ + }) +} + +func TestStringers(t *testing.T) { + + data := []struct { + Input string + String string + }{ + { + Input: `"hello"`, + String: `"hello"`, + }, + { + Input: `'hello'`, + String: `"hello"`, + }, + { + Input: "100", + String: "100", + }, + { + Input: "3.14159", + String: "3.14159", + }, + { + Input: "true", + String: "true", + }, + { + Input: "false", + String: "false", + }, + { + Input: "null", + String: "null", + }, + { + Input: "/ab+/", + String: "/ab+/", + }, + { + Input: "/ab+/i", + String: "/(?i)ab+/", + }, + { + Input: "$varname", + String: "$varname", + }, + { + Input: "name", + String: "name", + }, + { + Input: "`quoted name`", + String: "`quoted name`", + }, + { + Input: "path.to.name", + String: "path.to.name", + }, + { + Input: "path.to.name[]", + String: "path.to.name[]", + }, + { + Input: "path[].to.name", + String: "path.to.name[]", + }, + { + Input: "path.to[].name", + String: "path.to.name[]", + }, + { + Input: "-1", + String: "-1", + }, + { + Input: "--1", + String: "1", + }, + { + Input: "-(1+2)", + String: "-(1 + 2)", + }, + { + Input: "[1..5]", + String: "[1..5]", + }, + { + Input: "[]", + String: "[]", + }, + { + Input: "[1,2,3]", + String: "[1, 2, 3]", + }, + { + Input: "[1..3,4,5,6]", + String: "[1..3, 4, 5, 6]", + }, + { + Input: "{}", + String: "{}", + }, + { + Input: `{ + "one": 1, + "two": 2, + 'three': 3 + }`, + String: `{"one": 1, "two": 2, "three": 3}`, + }, + { + Input: `(-1; -2; "three";)`, + String: `(-1; -2; "three")`, + }, + { + Input: "*", + String: "*", + }, + { + Input: "**", + String: "**", + }, + { + Input: `| $ | { + "one": 1, + "two": 2 + } |`, + String: `|$|{"one": 1, "two": 2}|`, + }, + { + Input: `| $ | {}, [ + "field1", + "field2" + ] |`, + String: `|$|{}, ["field1", "field2"]|`, + }, + { + Input: `$substring('hello',-3,2)`, + String: `$substring("hello", -3, 2)`, + }, + { + Input: `$substring(?, 0, ?)`, + String: `$substring(?, 0, ?)`, + }, + { + Input: "function(){0}", + String: "function(){0}", + }, + { + Input: "λ($w,$h) {$w*$h}", + String: "λ($w, $h){$w * $h}", + }, + { + Input: "λ($x,$y,$z)-nf?:a>{$w*$h}", + String: "λ($x, $y, $z)-nf?>{$w * $h}", // TODO: handle output type + }, + { + Input: "$[0]", + String: "$[0]", + }, + { + Input: "$[Price>9.99]", + String: "$[Price > 9.99]", + }, + { + Input: "$[0][Price<25]", + String: `$[0][Price < 25]`, + }, + { + Input: `Product{ + "name": Name, + "colour": Color, + "price": Price + }`, + String: `Product{"name": Name, "colour": Color, "price": Price}`, + }, + { + Input: "true ? 'yes'", + String: `true ? "yes"`, + }, + { + Input: "true ? 'yes' : 'no'", + String: `true ? "yes" : "no"`, + }, + { + Input: "$x := $x+1", + String: "$x := $x + 1", + }, + { + Input: "1+2", + String: "1 + 2", + }, + { + Input: "1-2", + String: "1 - 2", + }, + { + Input: "1*2", + String: "1 * 2", + }, + { + Input: "1/2", + String: "1 / 2", + }, + { + Input: "1%2", + String: "1 % 2", + }, + { + Input: "1=2", + String: "1 = 2", + }, + { + Input: "1!=2", + String: "1 != 2", + }, + { + Input: "1>2", + String: "1 > 2", + }, + { + Input: "1>=2", + String: "1 >= 2", + }, + { + Input: "1<2", + String: "1 < 2", + }, + { + Input: "1<=2", + String: "1 <= 2", + }, + { + Input: "1 in [1,2]", + String: "1 in [1, 2]", + }, + { + Input: "true or false", + String: "true or false", + }, + { + Input: "null and void", + String: "null and void", + }, + { + Input: "'hello'&'world'", + String: `"hello" & "world"`, + }, + { + Input: "Product^(Price)", + String: "Product^(Price)", + }, + { + Input: "Product^(Price, >Name)", + String: "Product^(Price, >Name)", + }, + { + Input: "'hello' ~> $uppercase", + String: `"hello" ~> $uppercase`, + }, + } + + for _, test := range data { + + ast, err := jparse.Parse(test.Input) + if err != nil { + t.Errorf("%s: %s", test.Input, err) + continue + } + + if got := ast.String(); got != test.String { + t.Errorf("%s: expected string %q, got %q", test.Input, test.String, got) + } + } +} + +func testParser(t *testing.T, data []testCase) { + + for _, test := range data { + + inputs := test.Inputs + if len(inputs) == 0 { + inputs = []string{test.Input} + } + + for _, input := range inputs { + + output, err := jparse.Parse(input) + + if !reflect.DeepEqual(output, test.Output) { + t.Errorf("%s: expected output %s, got %s", input, test.Output, output) + } + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("%s: expected error %s, got %s", input, test.Error, err) + } + } + } +} diff --git a/v1.5.4/jparse/lexer.go b/v1.5.4/jparse/lexer.go new file mode 100644 index 0000000..bff6df4 --- /dev/null +++ b/v1.5.4/jparse/lexer.go @@ -0,0 +1,550 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse + +import ( + "fmt" + "unicode/utf8" +) + +const eof = -1 + +type tokenType uint8 + +const ( + typeEOF tokenType = iota + typeError + + typeString // string literal, e.g. "hello" + typeNumber // number literal, e.g. 3.14159 + typeBoolean // true or false + typeNull // null + typeName // field name, e.g. Price + typeNameEsc // escaped field name, e.g. `Product Name` + typeVariable // variable, e.g. $x + typeRegex // regular expression, e.g. /ab+/ + + // Symbol operators + typeBracketOpen + typeBracketClose + typeBraceOpen + typeBraceClose + typeParenOpen + typeParenClose + typeDot + typeComma + typeColon + typeSemicolon + typeCondition + typePlus + typeMinus + typeMult + typeDiv + typeMod + typePipe + typeEqual + typeNotEqual + typeLess + typeLessEqual + typeGreater + typeGreaterEqual + typeApply + typeSort + typeConcat + typeRange + typeAssign + typeDescendent + + // Keyword operators + typeAnd + typeOr + typeIn +) + +func (tt tokenType) String() string { + switch tt { + case typeEOF: + return "(eof)" + case typeError: + return "(error)" + case typeString: + return "(string)" + case typeNumber: + return "(number)" + case typeBoolean: + return "(boolean)" + case typeName, typeNameEsc: + return "(name)" + case typeVariable: + return "(variable)" + case typeRegex: + return "(regex)" + default: + if s := symbolsAndKeywords[tt]; s != "" { + return s + } + return "(unknown)" + } +} + +// symbols1 maps 1-character symbols to the corresponding +// token types. +var symbols1 = [...]tokenType{ + '[': typeBracketOpen, + ']': typeBracketClose, + '{': typeBraceOpen, + '}': typeBraceClose, + '(': typeParenOpen, + ')': typeParenClose, + '.': typeDot, + ',': typeComma, + ';': typeSemicolon, + ':': typeColon, + '?': typeCondition, + '+': typePlus, + '-': typeMinus, + '*': typeMult, + '/': typeDiv, + '%': typeMod, + '|': typePipe, + '=': typeEqual, + '<': typeLess, + '>': typeGreater, + '^': typeSort, + '&': typeConcat, +} + +type runeTokenType struct { + r rune + tt tokenType +} + +// symbols2 maps 2-character symbols to the corresponding +// token types. +var symbols2 = [...][]runeTokenType{ + '!': {{'=', typeNotEqual}}, + '<': {{'=', typeLessEqual}}, + '>': {{'=', typeGreaterEqual}}, + '.': {{'.', typeRange}}, + '~': {{'>', typeApply}}, + ':': {{'=', typeAssign}}, + '*': {{'*', typeDescendent}}, +} + +const ( + symbol1Count = rune(len(symbols1)) + symbol2Count = rune(len(symbols2)) +) + +func lookupSymbol1(r rune) tokenType { + if r < 0 || r >= symbol1Count { + return 0 + } + return symbols1[r] +} + +func lookupSymbol2(r rune) []runeTokenType { + if r < 0 || r >= symbol2Count { + return nil + } + return symbols2[r] +} + +func lookupKeyword(s string) tokenType { + switch s { + case "and": + return typeAnd + case "or": + return typeOr + case "in": + return typeIn + case "true", "false": + return typeBoolean + case "null": + return typeNull + default: + return 0 + } +} + +// A token represents a discrete part of a JSONata expression +// such as a string, a number, a field name, or an operator. +type token struct { + Type tokenType + Value string + Position int +} + +// lexer converts a JSONata expression into a sequence of tokens. +// The implmentation is based on the technique described in Rob +// Pike's 'Lexical Scanning in Go' talk. +type lexer struct { + input string + length int + start int + current int + width int + err error +} + +// newLexer creates a new lexer from the provided input. The +// input is tokenized by successive calls to the next method. +func newLexer(input string) lexer { + return lexer{ + input: input, + length: len(input), + } +} + +// next returns the next token from the provided input. When +// the end of the input is reached, next returns EOF for all +// subsequent calls. +// +// The allowRegex argument determines how the lexer interprets +// a forward slash character. Forward slashes in JSONata can +// either be the start of a regular expression or the division +// operator depending on their position. If allowRegex is true, +// the lexer will treat a forward slash like a regular +// expression. +func (l *lexer) next(allowRegex bool) token { + + l.skipWhitespace() + + ch := l.nextRune() + if ch == eof { + return l.eof() + } + + if allowRegex && ch == '/' { + l.ignore() + return l.scanRegex(ch) + } + + if rts := lookupSymbol2(ch); rts != nil { + for _, rt := range rts { + if l.acceptRune(rt.r) { + return l.newToken(rt.tt) + } + } + } + + if tt := lookupSymbol1(ch); tt > 0 { + return l.newToken(tt) + } + + if ch == '"' || ch == '\'' { + l.ignore() + return l.scanString(ch) + } + + if ch >= '0' && ch <= '9' { + l.backup() + return l.scanNumber() + } + + if ch == '`' { + l.ignore() + return l.scanEscapedName(ch) + } + + l.backup() + return l.scanName() +} + +// scanRegex reads a regular expression from the current position +// and returns a regex token. The opening delimiter has already +// been consumed. +func (l *lexer) scanRegex(delim rune) token { + + var depth int + +Loop: + for { + switch l.nextRune() { + case delim: + if depth == 0 { + break Loop + } + case '(', '[', '{': + depth++ + case ')', ']', '}': + depth-- + case '\\': + if r := l.nextRune(); r != eof && r != '\n' { + break + } + fallthrough + case eof, '\n': + return l.error(ErrUnterminatedRegex, string(delim)) + } + } + + l.backup() + t := l.newToken(typeRegex) + l.acceptRune(delim) + l.ignore() + + // Convert JavaScript-style regex flags to Go format, + // e.g. /ab+/i becomes /(?i)ab+/. + if l.acceptAll(isRegexFlag) { + flags := l.newToken(0) + t.Value = fmt.Sprintf("(?%s)%s", flags.Value, t.Value) + } + + return t +} + +// scanString reads a string literal from the current position +// and returns a string token. The opening quote has already been +// consumed. +func (l *lexer) scanString(quote rune) token { +Loop: + for { + switch l.nextRune() { + case quote: + break Loop + case '\\': + if r := l.nextRune(); r != eof { + break + } + fallthrough + case eof: + return l.error(ErrUnterminatedString, string(quote)) + } + } + + l.backup() + t := l.newToken(typeString) + l.acceptRune(quote) + l.ignore() + return t +} + +// scanNumber reads a number literal from the current position +// and returns a number token. +func (l *lexer) scanNumber() token { + + // JSON does not support leading zeroes. The integer part of + // a number will either be a single zero, or a non-zero digit + // followed by zero or more digits. + if !l.acceptRune('0') { + l.accept(isNonZeroDigit) + l.acceptAll(isDigit) + } + if l.acceptRune('.') { + if !l.acceptAll(isDigit) { + // If there are no digits after the decimal point, + // don't treat the dot as part of the number. It + // could be part of the range operator, e.g. "1..5". + l.backup() + return l.newToken(typeNumber) + } + } + if l.acceptRunes2('e', 'E') { + l.acceptRunes2('+', '-') + l.acceptAll(isDigit) + } + return l.newToken(typeNumber) +} + +// scanEscapedName reads a field name from the current position +// and returns a name token. The opening quote has already been +// consumed. +func (l *lexer) scanEscapedName(quote rune) token { +Loop: + for { + switch l.nextRune() { + case quote: + break Loop + case eof, '\n': + return l.error(ErrUnterminatedName, string(quote)) + } + } + + l.backup() + t := l.newToken(typeNameEsc) + l.acceptRune(quote) + l.ignore() + return t +} + +// scanName reads from the current position and returns a name, +// variable, or keyword token. +func (l *lexer) scanName() token { + + isVar := l.acceptRune('$') + if isVar { + l.ignore() + } + + for { + ch := l.nextRune() + if ch == eof { + break + } + + // Stop reading if we hit whitespace... + if isWhitespace(ch) { + l.backup() + break + } + + // ...or anything that looks like an operator. + if lookupSymbol1(ch) > 0 || lookupSymbol2(ch) != nil { + l.backup() + break + } + } + + t := l.newToken(typeName) + + if isVar { + t.Type = typeVariable + } else if tt := lookupKeyword(t.Value); tt > 0 { + t.Type = tt + } + + return t +} + +func (l *lexer) eof() token { + return token{ + Type: typeEOF, + Position: l.current, + } +} + +func (l *lexer) error(typ ErrType, hint string) token { + t := l.newToken(typeError) + l.err = newErrorHint(typ, t, hint) + return t +} + +func (l *lexer) newToken(tt tokenType) token { + t := token{ + Type: tt, + Value: l.input[l.start:l.current], + Position: l.start, + } + l.width = 0 + l.start = l.current + return t +} + +func (l *lexer) nextRune() rune { + + if l.err != nil || l.current >= l.length { + l.width = 0 + return eof + } + + r, w := utf8.DecodeRuneInString(l.input[l.current:]) + l.width = w + l.current += w + /* + if r == '\n' { + l.line++ + } + */ + return r +} + +func (l *lexer) backup() { + // TODO: Support more than one backup operation. + // TODO: Store current rune so that when nextRune + // is called again, we don't need to repeat the call + // to DecodeRuneInString. + l.current -= l.width +} + +func (l *lexer) ignore() { + l.start = l.current +} + +func (l *lexer) acceptRune(r rune) bool { + return l.accept(func(c rune) bool { + return c == r + }) +} + +func (l *lexer) acceptRunes2(r1, r2 rune) bool { + return l.accept(func(c rune) bool { + return c == r1 || c == r2 + }) +} + +func (l *lexer) accept(isValid func(rune) bool) bool { + if isValid(l.nextRune()) { + return true + } + l.backup() + return false +} + +func (l *lexer) acceptAll(isValid func(rune) bool) bool { + var b bool + for l.accept(isValid) { + b = true + } + return b +} + +func (l *lexer) skipWhitespace() { + l.acceptAll(isWhitespace) + l.ignore() +} + +func isWhitespace(r rune) bool { + switch r { + case ' ', '\t', '\n', '\r', '\v': + return true + default: + return false + } +} + +func isRegexFlag(r rune) bool { + switch r { + case 'i', 'm', 's': + return true + default: + return false + } +} + +func isDigit(r rune) bool { + return r >= '0' && r <= '9' +} + +func isNonZeroDigit(r rune) bool { + return r >= '1' && r <= '9' +} + +// symbolsAndKeywords maps operator token types back to their +// string representations. It's only used by tokenType.String +// (and one test). +var symbolsAndKeywords = func() map[tokenType]string { + + m := map[tokenType]string{ + typeAnd: "and", + typeOr: "or", + typeIn: "in", + typeNull: "null", + } + + for r, tt := range symbols1 { + if tt > 0 { + m[tt] = fmt.Sprintf("%c", r) + } + } + + for r, rts := range symbols2 { + for _, rt := range rts { + m[rt.tt] = fmt.Sprintf("%c", r) + fmt.Sprintf("%c", rt.r) + } + } + + return m +}() diff --git a/v1.5.4/jparse/lexer_test.go b/v1.5.4/jparse/lexer_test.go new file mode 100644 index 0000000..8a64f00 --- /dev/null +++ b/v1.5.4/jparse/lexer_test.go @@ -0,0 +1,407 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse + +import ( + "reflect" + "testing" +) + +type lexerTestCase struct { + Input string + AllowRegex bool + Tokens []token + Error error +} + +func TestLexerWhitespace(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: "", + }, + { + Input: " ", + }, + { + Input: "\v\t\r\n", + }, + { + Input: ` + + + `, + }, + }) +} + +func TestLexerRegex(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: `//`, + Tokens: []token{ + tok(typeDiv, "/", 0), + tok(typeDiv, "/", 1), + }, + }, + { + Input: `//`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "", 1), + }, + }, + { + Input: `/ab+/`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "ab+", 1), + }, + }, + { + Input: `/(ab+/)/`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "(ab+/)", 1), + }, + }, + { + Input: `/ab+/i`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "(?i)ab+", 1), + }, + }, + { + Input: `/ab+/ i`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "ab+", 1), + tok(typeName, "i", 6), + }, + }, + { + Input: `/ab+/I`, + AllowRegex: true, + Tokens: []token{ + tok(typeRegex, "ab+", 1), + tok(typeName, "I", 5), + }, + }, + { + Input: `/ab+`, + AllowRegex: true, + Tokens: []token{ + tok(typeError, "ab+", 1), + }, + Error: &Error{ + Type: ErrUnterminatedRegex, + Token: "ab+", + Hint: "/", + Position: 1, + }, + }, + }) +} + +func TestLexerStrings(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: `""`, + Tokens: []token{ + tok(typeString, "", 1), + }, + }, + { + Input: `''`, + Tokens: []token{ + tok(typeString, "", 1), + }, + }, + { + Input: `"double quotes"`, + Tokens: []token{ + tok(typeString, "double quotes", 1), + }, + }, + { + Input: "'single quotes'", + Tokens: []token{ + tok(typeString, "single quotes", 1), + }, + }, + { + Input: `"escape\t"`, + Tokens: []token{ + tok(typeString, "escape\\t", 1), + }, + }, + { + Input: `'escape\u0036'`, + Tokens: []token{ + tok(typeString, "escape\\u0036", 1), + }, + }, + { + Input: `"超明體繁"`, + Tokens: []token{ + tok(typeString, "超明體繁", 1), + }, + }, + { + Input: `'日本語'`, + Tokens: []token{ + tok(typeString, "日本語", 1), + }, + }, + { + Input: `"No closing quote...`, + Tokens: []token{ + tok(typeError, "No closing quote...", 1), + }, + Error: &Error{ + Type: ErrUnterminatedString, + Token: "No closing quote...", + Hint: "\"", + Position: 1, + }, + }, + { + Input: `'No closing quote...`, + Tokens: []token{ + tok(typeError, "No closing quote...", 1), + }, + Error: &Error{ + Type: ErrUnterminatedString, + Token: "No closing quote...", + Hint: "'", + Position: 1, + }, + }, + }) +} + +func TestLexerNumbers(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: "1", + Tokens: []token{ + tok(typeNumber, "1", 0), + }, + }, + { + Input: "3.14159", + Tokens: []token{ + tok(typeNumber, "3.14159", 0), + }, + }, + { + Input: "1e10", + Tokens: []token{ + tok(typeNumber, "1e10", 0), + }, + }, + { + Input: "1E-10", + Tokens: []token{ + tok(typeNumber, "1E-10", 0), + }, + }, + { + // Signs are separate tokens. + Input: "-100", + Tokens: []token{ + tok(typeMinus, "-", 0), + tok(typeNumber, "100", 1), + }, + }, + { + // Leading zeroes are not supported. + Input: "007", + Tokens: []token{ + tok(typeNumber, "0", 0), + tok(typeNumber, "0", 1), + tok(typeNumber, "7", 2), + }, + }, + { + // Leading decimal points are not supported. + Input: ".5", + Tokens: []token{ + tok(typeDot, ".", 0), + tok(typeNumber, "5", 1), + }, + }, + { + // Trailing decimal points are not supported. + // TODO: Why does this require a character following the decimal point? + Input: "5. ", + Tokens: []token{ + tok(typeNumber, "5", 0), + tok(typeDot, ".", 1), + }, + }, + }) +} + +func TestLexerNames(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: "hello", + Tokens: []token{ + tok(typeName, "hello", 0), + }, + }, + { + // Names break at whitespace... + Input: "hello world", + Tokens: []token{ + tok(typeName, "hello", 0), + tok(typeName, "world", 6), + }, + }, + { + // ...and anything that looks like a symbol. + Input: "hello, world.", + Tokens: []token{ + tok(typeName, "hello", 0), + tok(typeComma, ",", 5), + tok(typeName, "world", 7), + tok(typeDot, ".", 12), + }, + }, + { + // Exclamation marks are not symbols but the != operator + // begins with one so it has the same effect on a name. + Input: "HELLO!", + Tokens: []token{ + tok(typeName, "HELLO", 0), + tok(typeName, "!", 5), + }, + }, + { + // Escaped names can contain whitespace, symbols... + Input: "`hello, world.`", + Tokens: []token{ + tok(typeNameEsc, "hello, world.", 1), + }, + }, + { + // ...and keywords. + Input: "`true or false`", + Tokens: []token{ + tok(typeNameEsc, "true or false", 1), + }, + }, + { + Input: "`no closing quote...", + Tokens: []token{ + tok(typeError, "no closing quote...", 1), + }, + Error: &Error{ + Type: ErrUnterminatedName, + Token: "no closing quote...", + Hint: "`", + Position: 1, + }, + }, + }) +} + +func TestLexerVariables(t *testing.T) { + testLexer(t, []lexerTestCase{ + { + Input: "$", + Tokens: []token{ + tok(typeVariable, "", 1), + }, + }, + { + Input: "$$", + Tokens: []token{ + tok(typeVariable, "$", 1), + }, + }, + { + Input: "$var", + Tokens: []token{ + tok(typeVariable, "var", 1), + }, + }, + { + Input: "$uppercase", + Tokens: []token{ + tok(typeVariable, "uppercase", 1), + }, + }, + }) +} + +func TestLexerSymbolsAndKeywords(t *testing.T) { + + var tests []lexerTestCase + + for tt, s := range symbolsAndKeywords { + tests = append(tests, lexerTestCase{ + Input: s, + Tokens: []token{ + tok(tt, s, 0), + }, + }) + } + + testLexer(t, tests) +} + +func testLexer(t *testing.T, data []lexerTestCase) { + + for _, test := range data { + + l := newLexer(test.Input) + eof := tok(typeEOF, "", len(test.Input)) + + for _, exp := range test.Tokens { + compareTokens(t, test.Input, exp, l.next(test.AllowRegex)) + } + + compareErrors(t, test.Input, test.Error, l.err) + + // The lexer should keep returning EOF after exhausting + // the input. Call next() a few times to make sure that + // repeated calls return EOF as expected. + for i := 0; i < 3; i++ { + compareTokens(t, test.Input, eof, l.next(test.AllowRegex)) + } + } +} + +func compareTokens(t *testing.T, prefix string, exp, got token) { + + if got.Type != exp.Type { + t.Errorf("%s: expected token with Type '%s', got '%s'", prefix, exp.Type, got.Type) + } + + if got.Value != exp.Value { + t.Errorf("%s: expected token with Value %q, got %q", prefix, exp.Value, got.Value) + } + + if got.Position != exp.Position { + t.Errorf("%s: expected token with Position %d, got %d", prefix, exp.Position, got.Position) + } +} + +func compareErrors(t *testing.T, prefix string, exp, got error) { + + if !reflect.DeepEqual(exp, got) { + t.Errorf("%s: expected error %v, got %v", prefix, exp, got) + } +} + +func tok(typ tokenType, value string, position int) token { + return token{ + Type: typ, + Value: value, + Position: position, + } +} diff --git a/v1.5.4/jparse/node.go b/v1.5.4/jparse/node.go new file mode 100644 index 0000000..6d2bbe4 --- /dev/null +++ b/v1.5.4/jparse/node.go @@ -0,0 +1,2006 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jparse + +import ( + "fmt" + "regexp" + "regexp/syntax" + "strconv" + "strings" + "unicode/utf16" + "unicode/utf8" +) + +// Node represents an individual node in a syntax tree. +type Node interface { + String() string + optimize() (Node, error) +} + +// A StringNode represents a string literal. +type StringNode struct { + Value string +} + +func parseString(p *parser, t token) (Node, error) { + + s, ok := unescape(t.Value) + if !ok { + typ := ErrIllegalEscape + if len(s) > 0 && s[0] == 'u' { + typ = ErrIllegalEscapeHex + } + + return nil, newErrorHint(typ, t, s) + } + + return &StringNode{ + Value: s, + }, nil +} + +func (n *StringNode) optimize() (Node, error) { + return n, nil +} + +func (n StringNode) String() string { + return fmt.Sprintf("%q", n.Value) +} + +// A NumberNode represents a number literal. +type NumberNode struct { + Value float64 +} + +func parseNumber(p *parser, t token) (Node, error) { + + // Number literals are promoted to type float64. + n, err := strconv.ParseFloat(t.Value, 64) + if err != nil { + typ := ErrInvalidNumber + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + typ = ErrNumberRange + } + return nil, newError(typ, t) + } + + return &NumberNode{ + Value: n, + }, nil +} + +func (n *NumberNode) optimize() (Node, error) { + return n, nil +} + +func (n NumberNode) String() string { + return fmt.Sprintf("%g", n.Value) +} + +// A BooleanNode represents the boolean constant true or false. +type BooleanNode struct { + Value bool +} + +func parseBoolean(p *parser, t token) (Node, error) { + + var b bool + + switch t.Value { + case "true": + b = true + case "false": + b = false + default: // should be unreachable + panicf("parseBoolean: unexpected value %q", t.Value) + } + + return &BooleanNode{ + Value: b, + }, nil +} + +func (n *BooleanNode) optimize() (Node, error) { + return n, nil +} + +func (n BooleanNode) String() string { + return fmt.Sprintf("%t", n.Value) +} + +// A NullNode represents the JSON null value. +type NullNode struct{} + +func parseNull(p *parser, t token) (Node, error) { + return &NullNode{}, nil +} + +func (n *NullNode) optimize() (Node, error) { + return n, nil +} + +func (NullNode) String() string { + return "null" +} + +// A RegexNode represents a regular expression. +type RegexNode struct { + Value *regexp.Regexp +} + +func parseRegex(p *parser, t token) (Node, error) { + + if t.Value == "" { + return nil, newError(ErrEmptyRegex, t) + } + + re, err := regexp.Compile(t.Value) + if err != nil { + hint := "unknown error" + if e, ok := err.(*syntax.Error); ok { + hint = string(e.Code) + } + + return nil, newErrorHint(ErrInvalidRegex, t, hint) + } + + return &RegexNode{ + Value: re, + }, nil +} + +func (n *RegexNode) optimize() (Node, error) { + return n, nil +} + +func (n RegexNode) String() string { + var expr string + if n.Value != nil { + expr = n.Value.String() + } + return fmt.Sprintf("/%s/", expr) +} + +// A VariableNode represents a JSONata variable. +type VariableNode struct { + Name string +} + +func parseVariable(p *parser, t token) (Node, error) { + return &VariableNode{ + Name: t.Value, + }, nil +} + +func (n *VariableNode) optimize() (Node, error) { + return n, nil +} + +func (n VariableNode) String() string { + return "$" + n.Name +} + +// A NameNode represents a JSON field name. +type NameNode struct { + Value string + escaped bool +} + +func parseName(p *parser, t token) (Node, error) { + return &NameNode{ + Value: t.Value, + }, nil +} + +func parseEscapedName(p *parser, t token) (Node, error) { + return &NameNode{ + Value: t.Value, + escaped: true, + }, nil +} + +func (n *NameNode) optimize() (Node, error) { + return &PathNode{ + Steps: []Node{n}, + }, nil +} + +func (n NameNode) String() string { + if n.escaped { + return fmt.Sprintf("`%s`", n.Value) + } + return n.Value +} + +// Escaped returns true for names enclosed in backticks (e.g. +// `Product Name`), and false otherwise. This doesn't affect +// evaluation but may be useful when recreating a JSONata +// expression from its AST. +func (n NameNode) Escaped() bool { + return n.escaped +} + +// A PathNode represents a JSON object path. It consists of one +// or more 'steps' or Nodes (most commonly NameNode objects). +type PathNode struct { + Steps []Node + KeepArrays bool +} + +func (n *PathNode) optimize() (Node, error) { + return n, nil +} + +func (n PathNode) String() string { + s := joinNodes(n.Steps, ".") + if n.KeepArrays { + s += "[]" + } + return s +} + +// A NegationNode represents a numeric negation operation. +type NegationNode struct { + RHS Node +} + +func parseNegation(p *parser, t token) (Node, error) { + return &NegationNode{ + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *NegationNode) optimize() (Node, error) { + + var err error + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + // If the operand is a number literal, negate it now + // instead of waiting for evaluation. + if number, ok := n.RHS.(*NumberNode); ok { + return &NumberNode{ + Value: -number.Value, + }, nil + } + + return n, nil +} + +func (n NegationNode) String() string { + return fmt.Sprintf("-%s", n.RHS) +} + +// A RangeNode represents the range operator. +type RangeNode struct { + LHS Node + RHS Node +} + +func (n *RangeNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n RangeNode) String() string { + return fmt.Sprintf("%s..%s", n.LHS, n.RHS) +} + +// An ArrayNode represents an array of items. +type ArrayNode struct { + Items []Node +} + +func parseArray(p *parser, t token) (Node, error) { + + var items []Node + + for hasItems := p.token.Type != typeBracketClose; hasItems; { // disallow trailing commas + + item := p.parseExpression(0) + + if p.token.Type == typeRange { + + p.consume(typeRange, true) + + item = &RangeNode{ + LHS: item, + RHS: p.parseExpression(0), + } + } + + items = append(items, item) + + if p.token.Type != typeComma { + break + } + p.consume(typeComma, true) + } + + p.consume(typeBracketClose, false) + + return &ArrayNode{ + Items: items, + }, nil +} + +func (n *ArrayNode) optimize() (Node, error) { + + var err error + + for i := range n.Items { + n.Items[i], err = n.Items[i].optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n ArrayNode) String() string { + return fmt.Sprintf("[%s]", joinNodes(n.Items, ", ")) +} + +// An ObjectNode represents an object, an unordered list of +// key-value pairs. +type ObjectNode struct { + Pairs [][2]Node +} + +func parseObject(p *parser, t token) (Node, error) { + + var pairs [][2]Node + + for hasItems := p.token.Type != typeBraceClose; hasItems; { // disallow trailing commas + + key := p.parseExpression(0) + p.consume(typeColon, true) + value := p.parseExpression(0) + + pairs = append(pairs, [2]Node{key, value}) + + if p.token.Type != typeComma { + break + } + p.consume(typeComma, true) + } + + p.consume(typeBraceClose, false) + + return &ObjectNode{ + Pairs: pairs, + }, nil +} + +func (n *ObjectNode) optimize() (Node, error) { + + var err error + + for i := range n.Pairs { + for j := 0; j < 2; j++ { + n.Pairs[i][j], err = n.Pairs[i][j].optimize() + if err != nil { + return nil, err + } + } + } + + return n, nil +} + +func (n ObjectNode) String() string { + + values := make([]string, len(n.Pairs)) + + for i, pair := range n.Pairs { + values[i] = fmt.Sprintf("%s: %s", pair[0], pair[1]) + } + + return fmt.Sprintf("{%s}", strings.Join(values, ", ")) +} + +// A BlockNode represents a block expression. +type BlockNode struct { + Exprs []Node +} + +func parseBlock(p *parser, t token) (Node, error) { + + var exprs []Node + + for p.token.Type != typeParenClose { // allow trailing semicolons + + exprs = append(exprs, p.parseExpression(0)) + + if p.token.Type != typeSemicolon { + break + } + p.consume(typeSemicolon, true) + } + + p.consume(typeParenClose, false) + + return &BlockNode{ + Exprs: exprs, + }, nil +} + +func (n *BlockNode) optimize() (Node, error) { + + var err error + + for i := range n.Exprs { + n.Exprs[i], err = n.Exprs[i].optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n BlockNode) String() string { + return fmt.Sprintf("(%s)", joinNodes(n.Exprs, "; ")) +} + +// A WildcardNode represents the wildcard operator. +type WildcardNode struct{} + +func parseWildcard(p *parser, t token) (Node, error) { + return &WildcardNode{}, nil +} + +func (n *WildcardNode) optimize() (Node, error) { + return n, nil +} + +func (WildcardNode) String() string { + return "*" +} + +// A DescendentNode represents the descendent operator. +type DescendentNode struct{} + +func parseDescendent(p *parser, t token) (Node, error) { + return &DescendentNode{}, nil +} + +func (n *DescendentNode) optimize() (Node, error) { + return n, nil +} + +func (DescendentNode) String() string { + return "**" +} + +// An ObjectTransformationNode represents the object transformation +// operator. +type ObjectTransformationNode struct { + Pattern Node + Updates Node + Deletes Node +} + +func parseObjectTransformation(p *parser, t token) (Node, error) { + + var deletes Node + + pattern := p.parseExpression(0) + p.consume(typePipe, true) + updates := p.parseExpression(0) + if p.token.Type == typeComma { + p.consume(typeComma, true) + deletes = p.parseExpression(0) + } + p.consume(typePipe, true) + + return &ObjectTransformationNode{ + Pattern: pattern, + Updates: updates, + Deletes: deletes, + }, nil +} + +func (n *ObjectTransformationNode) optimize() (Node, error) { + + var err error + + n.Pattern, err = n.Pattern.optimize() + if err != nil { + return nil, err + } + + n.Updates, err = n.Updates.optimize() + if err != nil { + return nil, err + } + + if n.Deletes != nil { + n.Deletes, err = n.Deletes.optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n ObjectTransformationNode) String() string { + + s := fmt.Sprintf("|%s|%s", n.Pattern, n.Updates) + if n.Deletes != nil { + s += fmt.Sprintf(", %s", n.Deletes) + } + s += "|" + return s +} + +// A ParamType represents the type of a parameter in a lambda +// function signature. +type ParamType uint + +// Supported parameter types. +const ( + ParamTypeNumber ParamType = 1 << iota + ParamTypeString + ParamTypeBool + ParamTypeNull + ParamTypeArray + ParamTypeObject + ParamTypeFunc + ParamTypeJSON + ParamTypeAny +) + +func parseParamType(r rune) (ParamType, bool) { + + var typ ParamType + + switch r { + case 'n': + typ = ParamTypeNumber + case 's': + typ = ParamTypeString + case 'b': + typ = ParamTypeBool + case 'l': + typ = ParamTypeNull + case 'a': + typ = ParamTypeArray + case 'o': + typ = ParamTypeObject + case 'f': + typ = ParamTypeFunc + case 'j': + typ = ParamTypeJSON + case 'x': + typ = ParamTypeAny + default: + return 0, false + } + + return typ, true +} + +func (typ ParamType) String() string { + + var s string + + if typ&ParamTypeNumber != 0 { + s += "n" + } + if typ&ParamTypeString != 0 { + s += "s" + } + if typ&ParamTypeBool != 0 { + s += "b" + } + if typ&ParamTypeNull != 0 { + s += "l" + } + if typ&ParamTypeArray != 0 { + s += "a" + } + if typ&ParamTypeObject != 0 { + s += "o" + } + if typ&ParamTypeFunc != 0 { + s += "f" + } + if typ&ParamTypeJSON != 0 { + s += "j" + } + if typ&ParamTypeAny != 0 { + s += "x" + } + + if len(s) > 1 { + s = "(" + s + ")" + } + + return s +} + +// A ParamOpt represents the options on a parameter in a lambda +// function signature. +type ParamOpt uint8 + +const ( + _ ParamOpt = iota + + // ParamOptional denotes an optional parameter. + ParamOptional + + // ParamVariadic denotes a variadic parameter. + ParamVariadic + + // ParamContextable denotes a parameter that can be + // replaced by the evaluation context if no value is + // provided by the caller. + ParamContextable +) + +func parseParamOpt(r rune) (ParamOpt, bool) { + + var opt ParamOpt + + switch r { + case '?': + opt = ParamOptional + case '+': + opt = ParamVariadic + case '-': + opt = ParamContextable + default: + return 0, false + } + + return opt, true +} + +func (opt ParamOpt) String() string { + switch opt { + case ParamOptional: + return "?" + case ParamVariadic: + return "+" + case ParamContextable: + return "-" + default: + return "" + } +} + +// A Param represents a parameter in a lambda function signature. +type Param struct { + Type ParamType + Option ParamOpt + SubParams []Param +} + +func (p Param) String() string { + + s := p.Type.String() + + if p.SubParams != nil { + s += "<" + for _, sub := range p.SubParams { + s += sub.String() + } + s += ">" + } + + s += p.Option.String() + return s +} + +func parseParams(s string) ([]Param, error) { + + params := []Param{} + + for len(s) > 0 { + + r, w := utf8.DecodeRuneInString(s) + + if r == ':' { + break + } + + if typ, ok := parseParamType(r); ok { + params = append(params, Param{ + Type: typ, + }) + s = s[w:] + continue + } + + if r == '(' { + part := getBracketedString(s, '(', ')') + var types ParamType + for _, c := range part { + typ, ok := parseParamType(c) + if !ok { + // TODO: Add position to this error. + return nil, &Error{ + Type: ErrInvalidUnionType, + Hint: string(c), + } + } + types |= typ + } + params = append(params, Param{ + Type: types, + }) + s = s[len(part)+2:] + continue + } + + if opt, ok := parseParamOpt(r); ok { + if len(params) == 0 { + // TODO: Add position to this error. + return nil, &Error{ + Type: ErrUnmatchedOption, + Hint: string(r), + } + } + params[len(params)-1].Option = opt + s = s[w:] + continue + } + + if r == '<' { + if len(params) == 0 { + // TODO: Add position to this error. + return nil, &Error{ + Type: ErrUnmatchedSubtype, + } + } + n := len(params) - 1 + if params[n].Type != ParamTypeArray && params[n].Type != ParamTypeFunc { + // TODO: Add position to this error. + return nil, &Error{ + Type: ErrInvalidSubtype, + Hint: params[n].Type.String(), + } + } + part := getBracketedString(s, '<', '>') + sub, err := parseParams(part) + if err != nil { + return nil, err + } + params[n].SubParams = sub + s = s[len(part)+2:] + continue + } + + // TODO: Add position to this error. + return nil, &Error{ + Type: ErrInvalidParamType, + Hint: string(r), + } + } + + return params, nil +} + +func getBracketedString(s string, open, close rune) string { + + var depth int + + for pos, c := range s { + + if pos == 0 && c != open { + break + } + + if c == open { + depth++ + continue + } + + if c == close { + depth-- + if depth == 0 { + return s[utf8.RuneLen(open):pos] + } + } + } + + return "" +} + +// A LambdaNode represents a user-defined JSONata function. +type LambdaNode struct { + Body Node + ParamNames []string + shorthand bool +} + +func (n *LambdaNode) optimize() (Node, error) { + + var err error + + n.Body, err = n.Body.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n LambdaNode) String() string { + + name := "function" + if n.shorthand { + name = "λ" + } + + params := make([]string, len(n.ParamNames)) + for i, s := range n.ParamNames { + params[i] = "$" + s + } + + return fmt.Sprintf("%s(%s){%s}", name, strings.Join(params, ", "), n.Body) +} + +// Shorthand returns true if the lambda function was defined +// with the shorthand symbol "λ", and false otherwise. This +// doesn't affect evaluation but may be useful when recreating +// a JSONata expression from its AST. +func (n LambdaNode) Shorthand() bool { + return n.shorthand +} + +// A TypedLambdaNode represents a user-defined JSONata function +// with a type signature. +type TypedLambdaNode struct { + *LambdaNode + In []Param + Out []Param +} + +func (n *TypedLambdaNode) optimize() (Node, error) { + + node, err := n.LambdaNode.optimize() + if err != nil { + return nil, err + } + n.LambdaNode = node.(*LambdaNode) + + return n, nil +} + +func (n TypedLambdaNode) String() string { + + name := "function" + if n.shorthand { + name = "λ" + } + + params := make([]string, len(n.ParamNames)) + for i, s := range n.ParamNames { + params[i] = "$" + s + } + + inputs := make([]string, len(n.In)) + for i, p := range n.In { + inputs[i] = p.String() + } + + return fmt.Sprintf("%s(%s)<%s>{%s}", name, strings.Join(params, ", "), strings.Join(inputs, ""), n.Body) +} + +// A PartialNode represents a partially applied function. +type PartialNode struct { + Func Node + Args []Node +} + +func (n *PartialNode) optimize() (Node, error) { + + var err error + + n.Func, err = n.Func.optimize() + if err != nil { + return nil, err + } + + for i := range n.Args { + n.Args[i], err = n.Args[i].optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n PartialNode) String() string { + return fmt.Sprintf("%s(%s)", n.Func, joinNodes(n.Args, ", ")) +} + +// A PlaceholderNode represents a placeholder argument +// in a partially applied function. +type PlaceholderNode struct{} + +func (n *PlaceholderNode) optimize() (Node, error) { + return n, nil +} + +func (PlaceholderNode) String() string { + return "?" +} + +// A FunctionCallNode represents a call to a function. +type FunctionCallNode struct { + Func Node + Args []Node +} + +const typePlaceholder = typeCondition + +func parseFunctionCall(p *parser, t token, lhs Node) (Node, error) { + + if isLambda, shorthand := isLambdaName(lhs); isLambda { + return parseLambdaDefinition(p, shorthand) + } + + var args []Node + var isPartial bool + + for hasArgs := p.token.Type != typeParenClose; hasArgs; { // disallow trailing commas + + var arg Node + + if p.token.Type == typePlaceholder { + isPartial = true + arg = &PlaceholderNode{} + p.consume(typePlaceholder, true) + } else { + arg = p.parseExpression(0) + } + + args = append(args, arg) + + if p.token.Type != typeComma { + break + } + p.consume(typeComma, true) + } + + p.consume(typeParenClose, false) + + if isPartial { + return &PartialNode{ + Func: lhs, + Args: args, + }, nil + } + + return &FunctionCallNode{ + Func: lhs, + Args: args, + }, nil +} + +func (n *FunctionCallNode) optimize() (Node, error) { + + var err error + + n.Func, err = n.Func.optimize() + if err != nil { + return nil, err + } + + for i := range n.Args { + n.Args[i], err = n.Args[i].optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n FunctionCallNode) String() string { + return fmt.Sprintf("%s(%s)", n.Func, joinNodes(n.Args, ", ")) +} + +func isLambdaName(n Node) (bool, bool) { + switch n := n.(type) { + case *NameNode: + return n.Value == "function" || n.Value == "λ", n.Value == "λ" + default: + return false, false + } +} + +func parseLambdaDefinition(p *parser, shorthand bool) (Node, error) { + + var params []Param + + paramNames, err := extractParamNames(p) + if err != nil { + return nil, err + } + + sig, isTyped := extractSignature(p) + if isTyped { + params, err = parseParams(sig) + if err != nil { + return nil, err + } + if len(params) != len(paramNames) { + return nil, newError(ErrParamCount, p.token) + } + } + + p.consume(typeBraceOpen, true) + body := p.parseExpression(0) + p.consume(typeBraceClose, true) + + lambda := &LambdaNode{ + Body: body, + ParamNames: paramNames, + shorthand: shorthand, + } + + if !isTyped { + return lambda, nil + } + + return &TypedLambdaNode{ + LambdaNode: lambda, + In: params, + }, nil +} + +func extractParamNames(p *parser) ([]string, error) { + + var names []string + usedNames := map[string]bool{} + + currToken := p.token + for hasArgs := p.token.Type != typeParenClose; hasArgs; { // disallow trailing commas + + arg := p.parseExpression(0) + + v, ok := arg.(*VariableNode) + if !ok { + return nil, newError(ErrIllegalParam, currToken) + } + + if usedNames[v.Name] { + return nil, newError(ErrDuplicateParam, currToken) + } + + usedNames[v.Name] = true + names = append(names, v.Name) + + if p.token.Type != typeComma { + break + } + p.consume(typeComma, true) + + currToken = p.token + } + + p.consume(typeParenClose, false) + + return names, nil +} + +func extractSignature(p *parser) (string, bool) { + + const ( + typeSigStart = typeLess + typeSigEnd = typeGreater + ) + + if p.token.Type != typeSigStart { + return "", false + } + + sig := "" + depth := 1 + +Loop: + for p.token.Type != typeBraceOpen && p.token.Type != typeEOF { + + p.advance(true) + + switch p.token.Type { + case typeSigEnd: + depth-- + if depth == 0 { + break Loop + } + case typeSigStart: + depth++ + } + + sig += p.token.Value + } + + p.consume(typeSigEnd, true) + return sig, true +} + +// A PredicateNode represents a predicate expression. +type PredicateNode struct { + Expr Node + Filters []Node +} + +func (n *PredicateNode) optimize() (Node, error) { + return n, nil +} + +func (n PredicateNode) String() string { + return fmt.Sprintf("%s[%s]", n.Expr, joinNodes(n.Filters, ", ")) +} + +// A GroupNode represents a group expression. +type GroupNode struct { + Expr Node + *ObjectNode +} + +func parseGroup(p *parser, t token, lhs Node) (Node, error) { + + obj, err := parseObject(p, t) + if err != nil { + return nil, err + } + + return &GroupNode{ + Expr: lhs, + ObjectNode: obj.(*ObjectNode), + }, nil +} + +func (n *GroupNode) optimize() (Node, error) { + + var err error + + n.Expr, err = n.Expr.optimize() + if err != nil { + return nil, err + } + + if _, isGroup := n.Expr.(*GroupNode); isGroup { + // TODO: Add position info. + return nil, &Error{ + Type: ErrGroupGroup, + } + } + + obj, err := n.ObjectNode.optimize() + if err != nil { + return nil, err + } + n.ObjectNode = obj.(*ObjectNode) + + return n, nil +} + +func (n GroupNode) String() string { + return fmt.Sprintf("%s%s", n.Expr, n.ObjectNode) +} + +// A ConditionalNode represents an if-then-else expression. +type ConditionalNode struct { + If Node + Then Node + Else Node +} + +func parseConditional(p *parser, t token, lhs Node) (Node, error) { + + var els Node + rhs := p.parseExpression(0) + + if p.token.Type == typeColon { + p.consume(typeColon, true) + els = p.parseExpression(0) + } + + return &ConditionalNode{ + If: lhs, + Then: rhs, + Else: els, + }, nil +} + +func (n *ConditionalNode) optimize() (Node, error) { + + var err error + + n.If, err = n.If.optimize() + if err != nil { + return nil, err + } + + n.Then, err = n.Then.optimize() + if err != nil { + return nil, err + } + + if n.Else != nil { + n.Else, err = n.Else.optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n ConditionalNode) String() string { + + s := fmt.Sprintf("%s ? %s", n.If, n.Then) + if n.Else != nil { + s += fmt.Sprintf(" : %s", n.Else) + } + + return s +} + +// An AssignmentNode represents a variable assignment. +type AssignmentNode struct { + Name string + Value Node +} + +func parseAssignment(p *parser, t token, lhs Node) (Node, error) { + + v, ok := lhs.(*VariableNode) + if !ok { + return nil, newErrorHint(ErrIllegalAssignment, t, lhs.String()) + } + + return &AssignmentNode{ + Name: v.Name, + Value: p.parseExpression(p.bp(t.Type) - 1), // right-associative + }, nil +} + +func (n *AssignmentNode) optimize() (Node, error) { + + var err error + + n.Value, err = n.Value.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n AssignmentNode) String() string { + return fmt.Sprintf("$%s := %s", n.Name, n.Value) +} + +// A NumericOperator is a mathematical operation between two +// numeric values. +type NumericOperator uint8 + +// Numeric operations supported by JSONata. +const ( + _ NumericOperator = iota + NumericAdd + NumericSubtract + NumericMultiply + NumericDivide + NumericModulo +) + +func (op NumericOperator) String() string { + switch op { + case NumericAdd: + return "+" + case NumericSubtract: + return "-" + case NumericMultiply: + return "*" + case NumericDivide: + return "/" + case NumericModulo: + return "%" + default: + return "" + } +} + +// A NumericOperatorNode represents a numeric operation. +type NumericOperatorNode struct { + Type NumericOperator + LHS Node + RHS Node +} + +func parseNumericOperator(p *parser, t token, lhs Node) (Node, error) { + + var op NumericOperator + + switch t.Type { + case typePlus: + op = NumericAdd + case typeMinus: + op = NumericSubtract + case typeMult: + op = NumericMultiply + case typeDiv: + op = NumericDivide + case typeMod: + op = NumericModulo + default: // should be unreachable + panicf("parseNumericOperator: unexpected operator %q", t.Value) + } + + return &NumericOperatorNode{ + Type: op, + LHS: lhs, + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *NumericOperatorNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n NumericOperatorNode) String() string { + return fmt.Sprintf("%s %s %s", n.LHS, n.Type, n.RHS) +} + +// A ComparisonOperator is an operation that compares two values. +type ComparisonOperator uint8 + +// Comparison operations supported by JSONata. +const ( + _ ComparisonOperator = iota + ComparisonEqual + ComparisonNotEqual + ComparisonLess + ComparisonLessEqual + ComparisonGreater + ComparisonGreaterEqual + ComparisonIn +) + +func (op ComparisonOperator) String() string { + switch op { + case ComparisonEqual: + return "=" + case ComparisonNotEqual: + return "!=" + case ComparisonLess: + return "<" + case ComparisonLessEqual: + return "<=" + case ComparisonGreater: + return ">" + case ComparisonGreaterEqual: + return ">=" + case ComparisonIn: + return "in" + default: + return "" + } +} + +// A ComparisonOperatorNode represents a comparison operation. +type ComparisonOperatorNode struct { + Type ComparisonOperator + LHS Node + RHS Node +} + +func parseComparisonOperator(p *parser, t token, lhs Node) (Node, error) { + + var op ComparisonOperator + + switch t.Type { + case typeEqual: + op = ComparisonEqual + case typeNotEqual: + op = ComparisonNotEqual + case typeLess: + op = ComparisonLess + case typeLessEqual: + op = ComparisonLessEqual + case typeGreater: + op = ComparisonGreater + case typeGreaterEqual: + op = ComparisonGreaterEqual + case typeIn: + op = ComparisonIn + default: // should be unreachable + panicf("parseComparisonOperator: unexpected operator %q", t.Value) + } + + return &ComparisonOperatorNode{ + Type: op, + LHS: lhs, + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *ComparisonOperatorNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n ComparisonOperatorNode) String() string { + return fmt.Sprintf("%s %s %s", n.LHS, n.Type, n.RHS) +} + +// A BooleanOperator is a logical AND or OR operation between +// two values. +type BooleanOperator uint8 + +// Boolean operations supported by JSONata. +const ( + _ BooleanOperator = iota + BooleanAnd + BooleanOr +) + +func (op BooleanOperator) String() string { + switch op { + case BooleanAnd: + return "and" + case BooleanOr: + return "or" + default: + return "" + } +} + +// A BooleanOperatorNode represents a boolean operation. +type BooleanOperatorNode struct { + Type BooleanOperator + LHS Node + RHS Node +} + +func parseBooleanOperator(p *parser, t token, lhs Node) (Node, error) { + + var op BooleanOperator + + switch t.Type { + case typeAnd: + op = BooleanAnd + case typeOr: + op = BooleanOr + default: // should be unreachable + panicf("parseBooleanOperator: unexpected operator %q", t.Value) + } + + return &BooleanOperatorNode{ + Type: op, + LHS: lhs, + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *BooleanOperatorNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n BooleanOperatorNode) String() string { + return fmt.Sprintf("%s %s %s", n.LHS, n.Type, n.RHS) +} + +// A StringConcatenationNode represents a string concatenation +// operation. +type StringConcatenationNode struct { + LHS Node + RHS Node +} + +func parseStringConcatenation(p *parser, t token, lhs Node) (Node, error) { + return &StringConcatenationNode{ + LHS: lhs, + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *StringConcatenationNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n StringConcatenationNode) String() string { + return fmt.Sprintf("%s & %s", n.LHS, n.RHS) +} + +// SortDir describes the sort order of a sort operation. +type SortDir uint8 + +// Sort orders supported by JSONata. +const ( + _ SortDir = iota + SortDefault + SortAscending + SortDescending +) + +// A SortTerm defines a JSONata sort term. +type SortTerm struct { + Dir SortDir + Expr Node +} + +// A SortNode represents a sort clause on a JSONata path step. +type SortNode struct { + Expr Node + Terms []SortTerm +} + +func parseSort(p *parser, t token, lhs Node) (Node, error) { + + var terms []SortTerm + + p.consume(typeParenOpen, true) + + for { + dir := SortDefault + + switch typ := p.token.Type; typ { + case typeLess: + dir = SortAscending + p.consume(typ, true) + case typeGreater: + dir = SortDescending + p.consume(typ, true) + } + + terms = append(terms, SortTerm{ + Dir: dir, + Expr: p.parseExpression(0), + }) + + if p.token.Type != typeComma { + break + } + p.consume(typeComma, true) + } + + p.consume(typeParenClose, true) + + return &SortNode{ + Expr: lhs, + Terms: terms, + }, nil +} + +func (n *SortNode) optimize() (Node, error) { + + var err error + + n.Expr, err = n.Expr.optimize() + if err != nil { + return nil, err + } + + for i := range n.Terms { + n.Terms[i].Expr, err = n.Terms[i].Expr.optimize() + if err != nil { + return nil, err + } + } + + return n, nil +} + +func (n SortNode) String() string { + + terms := make([]string, len(n.Terms)) + + for i, t := range n.Terms { + + var sym string + + switch t.Dir { + case SortAscending: + sym = "<" + case SortDescending: + sym = ">" + } + + terms[i] = sym + t.Expr.String() + } + + return fmt.Sprintf("%s^(%s)", n.Expr, strings.Join(terms, ", ")) +} + +// A FunctionApplicationNode represents a function application +// operation. +type FunctionApplicationNode struct { + LHS Node + RHS Node +} + +func parseFunctionApplication(p *parser, t token, lhs Node) (Node, error) { + return &FunctionApplicationNode{ + LHS: lhs, + RHS: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *FunctionApplicationNode) optimize() (Node, error) { + + var err error + + n.LHS, err = n.LHS.optimize() + if err != nil { + return nil, err + } + + n.RHS, err = n.RHS.optimize() + if err != nil { + return nil, err + } + + return n, nil +} + +func (n FunctionApplicationNode) String() string { + return fmt.Sprintf("%s ~> %s", n.LHS, n.RHS) +} + +// A dotNode is an interim structure used to process JSONata path +// expressions. It is deliberately unexported and creates a PathNode +// during its optimize phase. +type dotNode struct { + lhs Node + rhs Node +} + +func parseDot(p *parser, t token, lhs Node) (Node, error) { + return &dotNode{ + lhs: lhs, + rhs: p.parseExpression(p.bp(t.Type)), + }, nil +} + +func (n *dotNode) optimize() (Node, error) { + + path := &PathNode{} + + lhs, err := n.lhs.optimize() + if err != nil { + return nil, err + } + + switch lhs := lhs.(type) { + case *NumberNode, *StringNode, *BooleanNode, *NullNode: + // TODO: Add position info. + return nil, &Error{ + Type: ErrPathLiteral, + Hint: lhs.String(), + } + case *PathNode: + path.Steps = lhs.Steps + if lhs.KeepArrays { + path.KeepArrays = true + } + default: + path.Steps = []Node{lhs} + } + + rhs, err := n.rhs.optimize() + if err != nil { + return nil, err + } + + switch rhs := rhs.(type) { + case *NumberNode, *StringNode, *BooleanNode, *NullNode: + // TODO: Add position info. + return nil, &Error{ + Type: ErrPathLiteral, + Hint: rhs.String(), + } + case *PathNode: + path.Steps = append(path.Steps, rhs.Steps...) + if rhs.KeepArrays { + path.KeepArrays = true + } + default: + path.Steps = append(path.Steps, rhs) + } + + return path, nil +} + +func (n dotNode) String() string { + return fmt.Sprintf("%s.%s", n.lhs, n.rhs) +} + +// A singletonArrayNode is an interim data structure used when +// processing path expressions. It is deliberately unexported +// and gets converted into a PathNode during optimization. +type singletonArrayNode struct { + lhs Node +} + +func (n *singletonArrayNode) optimize() (Node, error) { + + lhs, err := n.lhs.optimize() + if err != nil { + return nil, err + } + + switch lhs := lhs.(type) { + case *PathNode: + lhs.KeepArrays = true + return lhs, nil + default: + return &PathNode{ + Steps: []Node{lhs}, + KeepArrays: true, + }, nil + } +} + +func (n singletonArrayNode) String() string { + return fmt.Sprintf("%s[]", n.lhs) +} + +// A predicateNode is an interim data structure used when processing +// predicate expressions. It is deliberately unexported and gets +// converted into a PredicateNode during optimization. +type predicateNode struct { + lhs Node // the context for this predicate + rhs Node // the predicate expression +} + +func parsePredicate(p *parser, t token, lhs Node) (Node, error) { + + if p.token.Type == typeBracketClose { + p.consume(typeBracketClose, false) + + // Empty brackets in a path mean that we should not + // flatten singleton arrays into single values. + return &singletonArrayNode{ + lhs: lhs, + }, nil + } + + rhs := p.parseExpression(0) + p.consume(typeBracketClose, false) + + return &predicateNode{ + lhs: lhs, + rhs: rhs, + }, nil +} + +func (n *predicateNode) optimize() (Node, error) { + + lhs, err := n.lhs.optimize() + if err != nil { + return nil, err + } + + rhs, err := n.rhs.optimize() + if err != nil { + return nil, err + } + + switch lhs := lhs.(type) { + case *GroupNode: + return nil, &Error{ + // TODO: Add position info. + Type: ErrGroupPredicate, + } + case *PathNode: + i := len(lhs.Steps) - 1 + switch last := lhs.Steps[i].(type) { + case *PredicateNode: + last.Filters = append(last.Filters, rhs) + default: + step := &PredicateNode{ + Expr: last, + Filters: []Node{rhs}, + } + lhs.Steps = append(lhs.Steps[:i], step) + } + return lhs, nil + default: + return &PredicateNode{ + Expr: lhs, + Filters: []Node{rhs}, + }, nil + } +} + +func (n *predicateNode) String() string { + return fmt.Sprintf("%s[%s]", n.lhs, n.rhs) +} + +// Helpers + +func joinNodes(nodes []Node, sep string) string { + + values := make([]string, len(nodes)) + + for i, n := range nodes { + values[i] = n.String() + } + + return strings.Join(values, sep) +} + +var jsonEscapes = map[rune]string{ + '"': "\"", + '\\': "\\", + '/': "/", + 'b': "\b", + 'f': "\f", + 'n': "\n", + 'r': "\r", + 't': "\t", +} + +// unescape replaces JSON escape sequences in a string with their +// unescaped equivalents. Valid escape sequences are: +// +// \X, where X is a character from jsonEscapes +// \uXXXX, where XXXX is a 4-digit hexadecimal Unicode code point. +// +// unescape returns the unescaped string and true if successful, +// otherwise it returns the invalid escape sequence and false. +func unescape(src string) (string, bool) { + + pos := strings.IndexRune(src, '\\') + if pos < 0 { + return src, true + } + + prefix := src[:pos] + pos++ + + esc, w := utf8.DecodeRuneInString(src[pos:]) + pos += w + + repl := jsonEscapes[esc] + + switch { + case repl != "": + case esc == 'u': + hex, w := decodeRunes(src[pos:], 4) + pos += w + + r := parseRune(hex) + + switch { + case utf8.ValidRune(r): + case utf16.IsSurrogate(r): + hex2, w := decodeRunes(src[pos:], 6) + pos += w + + if strings.HasPrefix(hex2, "\\u") { + r = utf16.DecodeRune(r, parseRune(hex2[2:])) + if r != utf8.RuneError { + break + } + } + fallthrough + default: + return "u" + hex, false + } + repl = string(r) + default: + return string(esc), false + } + + rest, ok := unescape(src[pos:]) + if !ok { + return rest, ok + } + + return prefix + repl + rest, true +} + +// decodeRunes reads n runes from the string s and returns them +// as a string along with the number of bytes read. The returned +// string will always be n runes long, padded with the unicode +// replacement character if the source string contains fewer +// than n runes. +func decodeRunes(s string, n int) (string, int) { + + pos := 0 + runes := make([]rune, n) + + for i := range runes { + r, w := utf8.DecodeRuneInString(s[pos:]) + runes[i] = r + pos += w + } + + return string(runes), pos +} + +// parseRune converts a string of hexadecimal digits into the +// equivalent rune. It returns an invalid rune if the input is +// not valid hex. +func parseRune(hex string) rune { + + n, err := strconv.ParseInt(hex, 16, 32) + if err != nil { + return -1 + } + + return rune(n) +} diff --git a/v1.5.4/jsonata-server/.gitignore b/v1.5.4/jsonata-server/.gitignore new file mode 100644 index 0000000..a1ffdbd --- /dev/null +++ b/v1.5.4/jsonata-server/.gitignore @@ -0,0 +1,3 @@ +# Executables +jsonata-server +*.exe \ No newline at end of file diff --git a/v1.5.4/jsonata-server/README.md b/v1.5.4/jsonata-server/README.md new file mode 100644 index 0000000..4c67f00 --- /dev/null +++ b/v1.5.4/jsonata-server/README.md @@ -0,0 +1,14 @@ +# JSONata Server + +A locally hosted version of [JSONata Exerciser](http://try.jsonata.org/) +for testing [jsonata-go](https://github.com/blues/jsonata). + +## Install + + go install github.com/blues/jsonata-go/jsonata-server + +## Usage + + $ jsonata-server [-port=] + +Then go to http://localhost:8080/ (or your preferred port number). diff --git a/v1.5.4/jsonata-server/bench.go b/v1.5.4/jsonata-server/bench.go new file mode 100644 index 0000000..0caadab --- /dev/null +++ b/v1.5.4/jsonata-server/bench.go @@ -0,0 +1,108 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package main + +import ( + "log" + "net/http" + + "encoding/json" + + jsonata "github.com/blues/jsonata-go/v1.5.4" +) + +var ( + benchData = []byte(` +{ + "req":"note.add", + "device":"sim32-1232353453453452346", + "app":"test1", + "file":"geiger.q", + "note":"abc123", + "by":"1", + "when":1512335179, + "where":"87JFH688+2GP", + "payload":"SGVsbG8sIHdvcmxkLg==", + "body": + { + "loc_olc":"87JFH688+2GP", + "env_temp":9.407184, + "env_humid":77.071495, + "env_press":1016.25323, + "bat_voltage":3.866328, + "bat_current":0.078125, + "bat_charge":64.42578, + "lnd_7318u":27.6, + "lnd_7318c":23.1, + "lnd_7128ec":9.3, + "pms_pm01_0":0, + "pms_pm02_5":0, + "pms_pm10_0":1, + "pms_c00_30":11076, + "pms_c00_50":3242, + "pms_c01_00":246, + "pms_c02_50":44, + "pms_c05_00":10, + "pms_c10_00":10, + "pms_csecs":118, + "opc_pm01_0":1.9840136, + "opc_pm02_5":3.9194343, + "opc_pm10_0":9.284608, + "opc_c00_38":139, + "opc_c00_54":154, + "opc_c01_00":121, + "opc_c02_10":30, + "opc_c05_00":3, + "opc_c10_00":0, + "opc_csecs":120 + } +}`) + + benchExpression = ` +( + $values := { + "device_uid": device, + "when_captured": $formatTime(when), + "loc_lat": $latitudeFromOLC(body.loc_olc), + "loc_lon": $longitudeFromOLC(body.loc_olc) + }; + + req = "note.add" and when ? $merge([body, $values]) : $error("unexpected req/when") +)` +) + +// Decode the JSON. +var data interface{} + +func init() { + if err := json.Unmarshal(benchData, &data); err != nil { + panic(err) + } +} + +func benchmark(w http.ResponseWriter, r *http.Request) { + + // Compile the JSONata expression. + expr, err := jsonata.Compile(benchExpression) + if err != nil { + bencherr(w, err) + } + + // Evaluate the JSONata expression. + _, err = expr.Eval(data) + if err != nil { + bencherr(w, err) + } + + if _, err := w.Write([]byte("success")); err != nil { + log.Fatal(err) + } +} + +func bencherr(w http.ResponseWriter, err error) { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + +} diff --git a/v1.5.4/jsonata-server/exts.go b/v1.5.4/jsonata-server/exts.go new file mode 100644 index 0000000..97c6eab --- /dev/null +++ b/v1.5.4/jsonata-server/exts.go @@ -0,0 +1,41 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package main + +import ( + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +// Default format for dates: e.g. 2006-01-02 15:04 MST +const defaultDateFormat = "[Y]-[M01]-[D01] [H01]:[m] [ZN]" + +// formatTime converts a unix time in seconds to a string with the +// given layout. If a time zone is provided, formatTime returns a +// timestamp with that time zone. Otherwise, it returns UTC time. +func formatTime(secs int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { + + if picture.String == "" { + picture = jtypes.NewOptionalString(defaultDateFormat) + } + + return jlib.FromMillis(secs*1000, picture, tz) +} + +// parseTime converts a timestamp string with the given layout to +// a unix time in seconds. +func parseTime(value string, picture jtypes.OptionalString, tz jtypes.OptionalString) (int64, error) { + + if picture.String == "" { + picture = jtypes.NewOptionalString(defaultDateFormat) + } + + ms, err := jlib.ToMillis(value, picture, tz) + if err != nil { + return 0, err + } + + return ms / 1000, nil +} diff --git a/v1.5.4/jsonata-server/main.go b/v1.5.4/jsonata-server/main.go new file mode 100644 index 0000000..def3a99 --- /dev/null +++ b/v1.5.4/jsonata-server/main.go @@ -0,0 +1,132 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "log" + "net/http" + _ "net/http/pprof" + "strings" + + jsonata "github.com/blues/jsonata-go/v1.5.4" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +func init() { + + argUndefined0 := jtypes.ArgUndefined(0) + + exts := map[string]jsonata.Extension{ + "formatTime": { + Func: formatTime, + UndefinedHandler: argUndefined0, + }, + "parseTime": { + Func: parseTime, + UndefinedHandler: argUndefined0, + }, + } + + if err := jsonata.RegisterExts(exts); err != nil { + panic(err) + } +} + +func main() { + + port := flag.Uint("port", 8080, "The port `number` to serve on") + flag.Parse() + + http.HandleFunc("/eval", evaluate) + http.HandleFunc("/bench", benchmark) + http.Handle("/", http.FileServer(http.Dir("site"))) + + log.Printf("Starting JSONata Server on port %d:\n", *port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil)) +} + +func evaluate(w http.ResponseWriter, r *http.Request) { + + input := strings.TrimSpace(r.FormValue("json")) + if input == "" { + http.Error(w, "Input is empty", http.StatusBadRequest) + return + } + + expression := strings.TrimSpace(r.FormValue("expr")) + if expression == "" { + http.Error(w, "Expression is empty", http.StatusBadRequest) + return + } + + b, status, err := eval(input, expression) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), status) + return + } + + if _, err := w.Write(b); err != nil { + log.Fatal(err) + } +} + +func eval(input, expression string) (b []byte, status int, err error) { + + defer func() { + if r := recover(); r != nil { + b = nil + status = http.StatusInternalServerError + err = fmt.Errorf("PANIC: %v", r) + return + } + }() + + // Decode the JSON. + var data interface{} + if err := json.Unmarshal([]byte(input), &data); err != nil { + return nil, http.StatusBadRequest, fmt.Errorf("input error: %s", err) + } + + // Compile the JSONata expression. + expr, err := jsonata.Compile(expression) + if err != nil { + return nil, http.StatusBadRequest, fmt.Errorf("compile error: %s", err) + } + + // Evaluate the JSONata expression. + result, err := expr.Eval(data) + if err != nil { + if err == jsonata.ErrUndefined { + // Don't treat not finding any results as an error. + return []byte("No results found"), http.StatusOK, nil + } + return nil, http.StatusInternalServerError, fmt.Errorf("eval error: %s", err) + } + + // Return the JSONified results. + b, err = jsonify(result) + if err != nil { + return nil, http.StatusInternalServerError, fmt.Errorf("encode error: %s", err) + } + + return b, http.StatusOK, nil +} + +func jsonify(v interface{}) ([]byte, error) { + + b := bytes.Buffer{} + e := json.NewEncoder(&b) + e.SetIndent("", " ") + if err := e.Encode(v); err != nil { + return nil, err + } + + return b.Bytes(), nil +} diff --git a/v1.5.4/jsonata-server/site/assets/css/codemirror.min.css b/v1.5.4/jsonata-server/site/assets/css/codemirror.min.css new file mode 100644 index 0000000..595b598 --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/css/codemirror.min.css @@ -0,0 +1,2 @@ +.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0} +/*# sourceMappingURL=codemirror.min.css.map */ \ No newline at end of file diff --git a/v1.5.4/jsonata-server/site/assets/css/normalize.min.css b/v1.5.4/jsonata-server/site/assets/css/normalize.min.css new file mode 100644 index 0000000..31a4a66 --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/css/normalize.min.css @@ -0,0 +1 @@ +/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*# sourceMappingURL=normalize.min.css.map */ \ No newline at end of file diff --git a/v1.5.4/jsonata-server/site/assets/css/styles.css b/v1.5.4/jsonata-server/site/assets/css/styles.css new file mode 100644 index 0000000..6af358b --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/css/styles.css @@ -0,0 +1,90 @@ +body { + font-size: 15px; + font-family: Helvetica, Arial, sans-serif; + color: #333; + background: #eee; +} + +header { + margin: 0 2em; + height: 5.5em; + line-height: 5.5em; +} + +header h1, header ul { + margin: 0; +} + +header h1 { + float: left; + font-size: 1.65em; +} + +header ul { + float: right; +} + +header li { + display: inline-block; + padding: 0 .75em; +} + +header li a { + display: block; +} + +a:link, a:visited { + color: #3b4f5e; + text-decoration: none; + text-transform: uppercase; +} + +a:active, a:hover { + color:#333; +} + +.main { + position: absolute; + top: 5.5em; + left: 0; + right: 0; + bottom: 0; +} + +/* SplitJS styles */ + +.split, .gutter.gutter-horizontal { + height: 100%; + float: left; +} + +.gutter { + background-color: inherit; + background-repeat: no-repeat; + background-position: 50%; +} + +.gutter.gutter-vertical { + cursor: ns-resize; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII="); +} + +.gutter.gutter-horizontal { + cursor: ew-resize; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=="); +} + +/* CodeMirror styles */ + +.CodeMirror { + font-family: 'Roboto Mono', monospace; + background: #fefefe; + border-radius: 2px; + border: 1px solid #ccc; + width: calc(100% - 2px); + height: calc(100% - 2px); +} + +textarea[readonly] + .CodeMirror { + background: #f6f6f6; +} diff --git a/v1.5.4/jsonata-server/site/assets/js/codemirror.min.js b/v1.5.4/jsonata-server/site/assets/js/codemirror.min.js new file mode 100644 index 0000000..641d051 --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/js/codemirror.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.CodeMirror=t()}(this,function(){"use strict";function e(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}function t(e){for(var t=e.childNodes.length;t>0;--t)e.removeChild(e.firstChild);return e}function r(e,r){return t(e).appendChild(r)}function n(e,t,r,n){var i=document.createElement(e);if(r&&(i.className=r),n&&(i.style.cssText=n),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var o=0;o=t)return l+(t-o);l+=s-o,l+=r-l%r,o=s+1}}function f(e,t){for(var r=0;r=t)return n+Math.min(l,t-i);if(i+=o-n,i+=r-i%r,n=o+1,i>=t)return n}}function p(e){for(;wo.length<=e;)wo.push(g(wo)+" ");return wo[e]}function g(e){return e[e.length-1]}function v(e,t){for(var r=[],n=0;n"€"&&(e.toUpperCase()!=e.toLowerCase()||xo.test(e))}function w(e,t){return t?!!(t.source.indexOf("\\w")>-1&&b(e))||t.test(e):b(e)}function x(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}function C(e){return e.charCodeAt(0)>=768&&Co.test(e)}function S(e,t,r){for(;(r<0?t>0:tr?-1:1;;){if(t==r)return t;var i=(t+r)/2,o=n<0?Math.ceil(i):Math.floor(i);if(o==t)return e(o)?t:r;e(o)?r=o:t=o+n}}function k(e,t){if((t-=e.first)<0||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var r=e;!r.lines;)for(var n=0;;++n){var i=r.children[n],o=i.chunkSize();if(t=e.first&&tr?H(r,k(e,r).text.length):function(e,t){var r=e.ch;return null==r||r>t?H(e.line,t):r<0?H(e.line,0):e}(t,k(e,t.line).text.length)}function G(e,t){for(var r=[],n=0;n=t:o.to>t);(n||(n=[])).push(new U(l,o.from,s?null:o.to))}}return n}(r,i,l),a=function(e,t,r){var n;if(e)for(var i=0;i=t:o.to>t)||o.from==t&&"bookmark"==l.type&&(!r||o.marker.insertLeft)){var s=null==o.from||(l.inclusiveLeft?o.from<=t:o.from0&&s)for(var w=0;w=0&&h<=0||c<=0&&h>=0)&&(c<=0&&(a.marker.inclusiveRight&&i.inclusiveLeft?F(u.to,r)>=0:F(u.to,r)>0)||c>=0&&(a.marker.inclusiveRight&&i.inclusiveLeft?F(u.from,n)<=0:F(u.from,n)<0)))return!0}}}function re(e){for(var t;t=J(e);)e=t.find(-1,!0).line;return e}function ne(e,t){var r=k(e,t),n=re(r);return r==n?t:O(n)}function ie(e,t){if(t>e.lastLine())return t;var r,n=k(e,t);if(!oe(e,n))return t;for(;r=ee(n);)n=r.find(1,!0).line;return O(n)+1}function oe(e,t){var r=Lo&&t.markedSpans;if(r)for(var n=void 0,i=0;it.maxLineLength&&(t.maxLineLength=r,t.maxLine=e)})}function ce(e,t,r){var n;ko=null;for(var i=0;it)return i;o.to==t&&(o.from!=o.to&&"before"==r?n=i:ko=i),o.from==t&&(o.from!=o.to&&"before"!=r?n=i:ko=i)}return null!=n?n:ko}function he(e,t){var r=e.order;return null==r&&(r=e.order=To(e.text,t)),r}function fe(e,t){return e._handlers&&e._handlers[t]||Mo}function de(e,t,r){if(e.removeEventListener)e.removeEventListener(t,r,!1);else if(e.detachEvent)e.detachEvent("on"+t,r);else{var n=e._handlers,i=n&&n[t];if(i){var o=f(i,r);o>-1&&(n[t]=i.slice(0,o).concat(i.slice(o+1)))}}}function pe(e,t){var r=fe(e,t);if(r.length)for(var n=Array.prototype.slice.call(arguments,2),i=0;i0}function ye(e){e.prototype.on=function(e,t){No(this,e,t)},e.prototype.off=function(e,t){de(this,e,t)}}function be(e){e.preventDefault?e.preventDefault():e.returnValue=!1}function we(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0}function xe(e){return null!=e.defaultPrevented?e.defaultPrevented:0==e.returnValue}function Ce(e){be(e),we(e)}function Se(e){return e.target||e.srcElement}function Le(e){var t=e.which;return null==t&&(1&e.button?t=1:2&e.button?t=3:4&e.button&&(t=2)),ro&&e.ctrlKey&&1==t&&(t=3),t}function ke(e){if(null==fo){var t=n("span","​");r(e,n("span",[t,document.createTextNode("x")])),0!=e.firstChild.offsetHeight&&(fo=t.offsetWidth<=1&&t.offsetHeight>2&&!(Ki&&ji<8))}var i=fo?n("span","​"):n("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return i.setAttribute("cm-text",""),i}function Te(e){if(null!=po)return po;var n=r(e,document.createTextNode("AخA")),i=lo(n,0,1).getBoundingClientRect(),o=lo(n,1,2).getBoundingClientRect();return t(e),!(!i||i.left==i.right)&&(po=o.right-i.right<3)}function Me(e){if("string"==typeof e&&Po.hasOwnProperty(e))e=Po[e];else if(e&&"string"==typeof e.name&&Po.hasOwnProperty(e.name)){var t=Po[e.name];"string"==typeof t&&(t={name:t}),(e=y(t,e)).name=t.name}else{if("string"==typeof e&&/^[\w\-]+\/[\w\-]+\+xml$/.test(e))return Me("application/xml");if("string"==typeof e&&/^[\w\-]+\/[\w\-]+\+json$/.test(e))return Me("application/json")}return"string"==typeof e?{name:e}:e||{name:"null"}}function Ne(e,t){t=Me(t);var r=Fo[t.name];if(!r)return Ne(e,"text/plain");var n=r(e,t);if(Eo.hasOwnProperty(t.name)){var i=Eo[t.name];for(var o in i)i.hasOwnProperty(o)&&(n.hasOwnProperty(o)&&(n["_"+o]=n[o]),n[o]=i[o])}if(n.name=t.name,t.helperType&&(n.helperType=t.helperType),t.modeProps)for(var l in t.modeProps)n[l]=t.modeProps[l];return n}function Oe(e,t){c(t,Eo.hasOwnProperty(e)?Eo[e]:Eo[e]={})}function Ae(e,t){if(!0===t)return t;if(e.copyState)return e.copyState(t);var r={};for(var n in t){var i=t[n];i instanceof Array&&(i=i.concat([])),r[n]=i}return r}function We(e,t){for(var r;e.innerMode&&(r=e.innerMode(t))&&r.mode!=e;)t=r.state,e=r.mode;return r||{mode:e,state:t}}function De(e,t,r){return!e.startState||e.startState(t,r)}function He(e,t,r,n){var i=[e.state.modeGen],o={};Ge(e,t.text,e.doc.mode,r,function(e,t){return i.push(e,t)},o,n);for(var l=r.state,s=function(n){r.baseTokens=i;var s=e.state.overlays[n],a=1,u=0;r.state=!0,Ge(e,t.text,s.mode,r,function(e,t){for(var r=a;ue&&i.splice(a,1,e,i[a+1],n),a+=2,u=Math.min(e,n)}if(t)if(s.opaque)i.splice(r,a-r,e,"overlay "+t),a=r+2;else for(;re.options.maxHighlightLength&&Ae(e.doc.mode,n.state),o=He(e,t,n);i&&(n.state=i),t.stateAfter=n.save(!i),t.styles=o.styles,o.classes?t.styleClasses=o.classes:t.styleClasses&&(t.styleClasses=null),r===e.doc.highlightFrontier&&(e.doc.modeFrontier=Math.max(e.doc.modeFrontier,++e.doc.highlightFrontier))}return t.styles}function Pe(e,t,r){var n=e.doc,i=e.display;if(!n.mode.startState)return new Ro(n,!0,t);var o=function(e,t,r){for(var n,i,o=e.doc,l=r?-1:t-(e.doc.mode.innerMode?1e3:100),s=t;s>l;--s){if(s<=o.first)return o.first;var a=k(o,s-1),u=a.stateAfter;if(u&&(!r||s+(u instanceof Io?u.lookAhead:0)<=o.modeFrontier))return s;var c=h(a.text,null,e.options.tabSize);(null==i||n>c)&&(i=s-1,n=c)}return i}(e,t,r),l=o>n.first&&k(n,o-1).stateAfter,s=l?Ro.fromSaved(n,l,o):new Ro(n,De(n.mode),o);return n.iter(o,t,function(r){Ee(e,r.text,s);var n=s.line;r.stateAfter=n==t-1||n%5==0||n>=i.viewFrom&&nt.start)return o}throw new Error("Mode "+e.name+" failed to advance stream.")}function Re(e,t,r,n){var i,o,l=e.doc,s=l.mode,a=k(l,(t=B(l,t)).line),u=Pe(e,t.line,r),c=new zo(a.text,e.options.tabSize,u);for(n&&(o=[]);(n||c.pose.options.maxHighlightLength?(s=!1,l&&Ee(e,t,n,h.pos),h.pos=t.length,a=null):a=Be(Ie(r,h,n.state,f),o),f){var d=f[0].name;d&&(a="m-"+(a?d+" "+a:d))}if(!s||c!=a){for(;uu&&h.from<=u);f++);if(h.to>=c)return e(r,n,i,o,l,s,a);e(r,n.slice(0,h.to-u),i,o,null,s,a),o=null,n=n.slice(h.to-u),u=h.to}}}(n.addToken,s)),n.map=[];!function(e,t,r){var n=e.markedSpans,i=e.text,o=0;if(!n){for(var l=1;lg||S.collapsed&&C.to==g&&C.from==g)?(null!=C.to&&C.to!=g&&y>C.to&&(y=C.to,c=""),S.className&&(u+=" "+S.className),S.css&&(a=(a?a+";":"")+S.css),S.startStyle&&C.from==g&&(h+=" "+S.startStyle),S.endStyle&&C.to==y&&(w||(w=[])).push(S.endStyle,C.to),S.title&&!f&&(f=S.title),S.collapsed&&(!d||Z(d.marker,S)<0)&&(d=C)):C.from>g&&y>C.from&&(y=C.from)}if(w)for(var L=0;L=p)break;for(var T=Math.min(p,y);;){if(m){var M=g+m.length;if(!d){var N=M>T?m.slice(0,T-g):m;t.addToken(t,N,s?s+u:u,h,g+N.length==y?c:"",f,a)}if(M>=T){m=m.slice(T-g),g=T;break}g=M,h=""}m=i.slice(o,o=r[v++]),s=Ve(r[v++],t.cm.options)}}}(l,n,Fe(e,l,t!=e.display.externalMeasured&&O(l))),l.styleClasses&&(l.styleClasses.bgClass&&(n.bgClass=a(l.styleClasses.bgClass,n.bgClass||"")),l.styleClasses.textClass&&(n.textClass=a(l.styleClasses.textClass,n.textClass||""))),0==n.map.length&&n.map.push(0,0,n.content.appendChild(ke(e.display.measure))),0==o?(t.measure.map=n.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(n.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(Xi){var u=n.content.lastChild;(/\bcm-tab\b/.test(u.className)||u.querySelector&&u.querySelector(".cm-tab"))&&(n.content.className="cm-tab-wrap-hack")}return pe(e,"renderLine",e,t.line,n.pre),n.pre.className&&(n.textClass=a(n.pre.className,n.textClass||"")),n}function je(e){var t=n("span","•","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Xe(e,t,r,i,o,l,s){if(t){var a,u=e.splitSpaces?function(e,t){if(e.length>1&&!/ /.test(e))return e;for(var r=t,n="",i=0;ir)return{map:e.measure.maps[i],cache:e.measure.caches[i],before:!0}}function gt(e,t,r,n){return yt(e,mt(e,t),r,n)}function vt(e,t){if(t>=e.display.viewFrom&&t=r.lineN&&t2&&o.push((a.bottom+u.top)/2-r.top)}}o.push(r.bottom-r.top)}}(e,t.view,t.rect),t.hasHeights=!0),(s=function(e,t,i,o){var l,s=bt(t.map,i,o),a=s.node,u=s.start,c=s.end,h=s.collapse;if(3==a.nodeType){for(var f=0;f<4;f++){for(;u&&C(t.line.text.charAt(s.coverStart+u));)--u;for(;s.coverStart+c1}(e))return t;var i=screen.logicalXDPI/screen.deviceXDPI,o=screen.logicalYDPI/screen.deviceYDPI;return{left:t.left*i,right:t.right*i,top:t.top*o,bottom:t.bottom*o}}(e.display.measure,l))}else{u>0&&(h=o="right");var d;l=e.options.lineWrapping&&(d=a.getClientRects()).length>1?d["right"==o?d.length-1:0]:a.getBoundingClientRect()}if(Ki&&ji<9&&!u&&(!l||!l.left&&!l.right)){var p=a.parentNode.getClientRects()[0];l=p?{left:p.left,right:p.left+It(e.display),top:p.top,bottom:p.bottom}:Yo}for(var g=l.top-t.rect.top,v=l.bottom-t.rect.top,m=(g+v)/2,y=t.view.measure.heights,b=0;bt)&&(i=(o=a-s)-1,t>=a&&(l="right")),null!=i){if(n=e[u+2],s==a&&r==(n.insertLeft?"left":"right")&&(l=r),"left"==r&&0==i)for(;u&&e[u-2]==e[u-3]&&e[u-1].insertLeft;)n=e[2+(u-=3)],l="left";if("right"==r&&i==a-s)for(;u=0&&(r=e[i]).left==r.right;i--);return r}function xt(e){if(e.measure&&(e.measure.cache={},e.measure.heights=null,e.rest))for(var t=0;t=n.text.length?(u=n.text.length,c="before"):u<=0&&(u=0,c="after"),!a)return l("before"==c?u-1:u,"before"==c);var h=ce(a,u,c),f=ko,d=s(u,h,"before"==c);return null!=f&&(d.other=s(u,f,"before"!=c)),d}function Wt(e,t){var r=0;t=B(e.doc,t),e.options.lineWrapping||(r=It(e.display)*t.ch);var n=k(e.doc,t.line),i=se(n)+at(e.display);return{left:r,right:r,top:i,bottom:i+n.height}}function Dt(e,t,r,n,i){var o=H(e,t,r);return o.xRel=i,n&&(o.outside=!0),o}function Ht(e,t,r){var n=e.doc;if((r+=e.display.viewOffset)<0)return Dt(n.first,0,null,!0,-1);var i=A(n,r),o=n.first+n.size-1;if(i>o)return Dt(n.first+n.size-1,k(n,o).text.length,null,!0,1);t<0&&(t=0);for(var l=k(n,i);;){var s=function(e,t,r,n,i){i-=se(t);var o=mt(e,t),l=Tt(t),s=0,a=t.text.length,u=!0,c=he(t,e.doc.direction);if(c){var h=(e.options.lineWrapping?function(e,t,r,n,i,o,l){var s=Ft(e,t,n,l),a=s.begin,u=s.end;/\s/.test(t.text.charAt(u-1))&&u--;for(var c=null,h=null,f=0;f=u||d.to<=a)){var p=1!=d.level,g=yt(e,n,p?Math.min(u,d.to)-1:Math.max(a,d.from)).right,v=gv)&&(c=d,h=v)}}c||(c=i[i.length-1]);c.fromu&&(c={from:c.from,to:u,level:c.level});return c}:function(e,t,r,n,i,o,l){var s=L(function(s){var a=i[s],u=1!=a.level;return Et(At(e,H(r,u?a.to:a.from,u?"before":"after"),"line",t,n),o,l,!0)},0,i.length-1),a=i[s];if(s>0){var u=1!=a.level,c=At(e,H(r,u?a.from:a.to,u?"after":"before"),"line",t,n);Et(c,o,l,!0)&&c.top>l&&(a=i[s-1])}return a})(e,t,r,o,c,n,i);u=1!=h.level,s=u?h.from:h.to-1,a=u?h.to:h.from-1}var f,d,p=null,g=null,v=L(function(t){var r=yt(e,o,t);return r.top+=l,r.bottom+=l,!!Et(r,n,i,!1)&&(r.top<=i&&r.left<=n&&(p=t,g=r),!0)},s,a),m=!1;if(g){var y=n-g.left=w.bottom}return v=S(t.text,v,1),Dt(r,v,d,m,n-f)}(e,l,i,t,r),a=ee(l),u=a&&a.find(0,!0);if(!a||!(s.ch>u.from.ch||s.ch==u.from.ch&&s.xRel>0))return s;i=O(l=u.to.line)}}function Ft(e,t,r,n){n-=Tt(t);var i=t.text.length,o=L(function(t){return yt(e,r,t-1).bottom<=n},i,0);return i=L(function(t){return yt(e,r,t).top>n},o,i),{begin:o,end:i}}function Pt(e,t,r,n){r||(r=mt(e,t));return Ft(e,t,r,Mt(e,t,yt(e,r,n),"line").top)}function Et(e,t,r,n){return!(e.bottom<=r)&&(e.top>r||(n?e.left:e.right)>t)}function zt(e){if(null!=e.cachedTextHeight)return e.cachedTextHeight;if(null==Uo){Uo=n("pre");for(var i=0;i<49;++i)Uo.appendChild(document.createTextNode("x")),Uo.appendChild(n("br"));Uo.appendChild(document.createTextNode("x"))}r(e.measure,Uo);var o=Uo.offsetHeight/50;return o>3&&(e.cachedTextHeight=o),t(e.measure),o||1}function It(e){if(null!=e.cachedCharWidth)return e.cachedCharWidth;var t=n("span","xxxxxxxxxx"),i=n("pre",[t]);r(e.measure,i);var o=t.getBoundingClientRect(),l=(o.right-o.left)/10;return l>2&&(e.cachedCharWidth=l),l||10}function Rt(e){for(var t=e.display,r={},n={},i=t.gutters.clientLeft,o=t.gutters.firstChild,l=0;o;o=o.nextSibling,++l)r[e.options.gutters[l]]=o.offsetLeft+o.clientLeft+i,n[e.options.gutters[l]]=o.clientWidth;return{fixedPos:Bt(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:r,gutterWidth:n,wrapperWidth:t.wrapper.clientWidth}}function Bt(e){return e.scroller.getBoundingClientRect().left-e.sizer.getBoundingClientRect().left}function Gt(e){var t=zt(e.display),r=e.options.lineWrapping,n=r&&Math.max(5,e.display.scroller.clientWidth/It(e.display)-3);return function(i){if(oe(e.doc,i))return 0;var o=0;if(i.widgets)for(var l=0;l=e.display.viewTo)return null;if((t-=e.display.viewFrom)<0)return null;for(var r=e.display.view,n=0;n=e.display.viewTo||a.to().linet||t==r&&l.to==t)&&(n(Math.max(l.from,t),Math.min(l.to,r),1==l.level?"rtl":"ltr",o),i=!0)}i||n(t,r,"ltr")}(g,r||0,null==n?p:n,function(e,t,s,d){var v="ltr"==s,m=o(e,v?"left":"right"),y=o(t-1,v?"right":"left"),b=null==r&&0==e,w=null==n&&t==p,x=0==d,C=!g||d==g.length-1;if(y.top-m.top<=3){var S=(f?w:b)&&C,L=(f?b:w)&&x?c:(v?m:y).left,k=S?h:(v?y:m).right;i(L,m.top,k-L,m.bottom)}else{var T,M,N,O;v?(T=f&&b&&x?c:m.left,M=f?h:l(e,s,"before"),N=f?c:l(t,s,"after"),O=f&&w&&C?h:y.right):(T=f?l(e,s,"before"):c,M=!f&&b&&x?h:m.right,N=!f&&w&&C?c:y.left,O=f?l(t,s,"after"):h),i(T,m.top,M-T,m.bottom),m.bottom0?t.blinker=setInterval(function(){return t.cursorDiv.style.visibility=(r=!r)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function $t(e){e.state.focused||(e.display.input.focus(),Qt(e))}function Zt(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,Jt(e))},100)}function Qt(e,t){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1),"nocursor"!=e.options.readOnly&&(e.state.focused||(pe(e,"focus",e,t),e.state.focused=!0,s(e.display.wrapper,"CodeMirror-focused"),e.curOp||e.display.selForContextMenu==e.doc.sel||(e.display.input.reset(),Xi&&setTimeout(function(){return e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),qt(e))}function Jt(e,t){e.state.delayingBlurEvent||(e.state.focused&&(pe(e,"blur",e,t),e.state.focused=!1,uo(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function er(e){for(var t=e.display,r=t.lineDiv.offsetTop,n=0;n.005||a<-.005)&&(N(i.line,o),tr(i.line),i.rest))for(var u=0;u=l&&(o=A(t,se(k(t,a))-e.wrapper.clientHeight),l=a)}return{from:o,to:Math.max(l,o+1)}}function nr(e){var t=e.display,r=t.view;if(t.alignWidgets||t.gutters.firstChild&&e.options.fixedGutter){for(var n=Bt(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,o=n+"px",l=0;lo&&(t.bottom=t.top+o);var s=e.doc.height+ut(r),a=t.tops-n;if(t.topi+o){var c=Math.min(t.top,(u?s:t.bottom)-o);c!=i&&(l.scrollTop=c)}var h=e.curOp&&null!=e.curOp.scrollLeft?e.curOp.scrollLeft:r.scroller.scrollLeft,f=ft(e)-(e.options.fixedGutter?r.gutters.offsetWidth:0),d=t.right-t.left>f;return d&&(t.right=t.left+f),t.left<10?l.scrollLeft=0:t.leftf+h-3&&(l.scrollLeft=t.right+(d?0:10)-f),l}function lr(e,t){null!=t&&(ur(e),e.curOp.scrollTop=(null==e.curOp.scrollTop?e.doc.scrollTop:e.curOp.scrollTop)+t)}function sr(e){ur(e);var t=e.getCursor();e.curOp.scrollToPos={from:t,to:t,margin:e.options.cursorScrollMargin}}function ar(e,t,r){null==t&&null==r||ur(e),null!=t&&(e.curOp.scrollLeft=t),null!=r&&(e.curOp.scrollTop=r)}function ur(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;cr(e,Wt(e,t.from),Wt(e,t.to),t.margin)}}function cr(e,t,r,n){var i=or(e,{left:Math.min(t.left,r.left),top:Math.min(t.top,r.top)-n,right:Math.max(t.right,r.right),bottom:Math.max(t.bottom,r.bottom)+n});ar(e,i.scrollLeft,i.scrollTop)}function hr(e,t){Math.abs(e.doc.scrollTop-t)<2||(Bi||Hr(e,{top:t}),fr(e,t,!0),Bi&&Hr(e),Or(e,100))}function fr(e,t,r){t=Math.min(e.display.scroller.scrollHeight-e.display.scroller.clientHeight,t),(e.display.scroller.scrollTop!=t||r)&&(e.doc.scrollTop=t,e.display.scrollbars.setScrollTop(t),e.display.scroller.scrollTop!=t&&(e.display.scroller.scrollTop=t))}function dr(e,t,r,n){t=Math.min(t,e.display.scroller.scrollWidth-e.display.scroller.clientWidth),(r?t==e.doc.scrollLeft:Math.abs(e.doc.scrollLeft-t)<2)&&!n||(e.doc.scrollLeft=t,nr(e),e.display.scroller.scrollLeft!=t&&(e.display.scroller.scrollLeft=t),e.display.scrollbars.setScrollLeft(t))}function pr(e){var t=e.display,r=t.gutters.offsetWidth,n=Math.round(e.doc.height+ut(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?r:0,docHeight:n,scrollHeight:n+ht(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:r}}function gr(e,t){t||(t=pr(e));var r=e.display.barWidth,n=e.display.barHeight;vr(e,t);for(var i=0;i<4&&r!=e.display.barWidth||n!=e.display.barHeight;i++)r!=e.display.barWidth&&e.options.lineWrapping&&er(e),vr(e,pr(e)),r=e.display.barWidth,n=e.display.barHeight}function vr(e,t){var r=e.display,n=r.scrollbars.update(t);r.sizer.style.paddingRight=(r.barWidth=n.right)+"px",r.sizer.style.paddingBottom=(r.barHeight=n.bottom)+"px",r.heightForcer.style.borderBottom=n.bottom+"px solid transparent",n.right&&n.bottom?(r.scrollbarFiller.style.display="block",r.scrollbarFiller.style.height=n.bottom+"px",r.scrollbarFiller.style.width=n.right+"px"):r.scrollbarFiller.style.display="",n.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(r.gutterFiller.style.display="block",r.gutterFiller.style.height=n.bottom+"px",r.gutterFiller.style.width=t.gutterWidth+"px"):r.gutterFiller.style.display=""}function mr(e){e.display.scrollbars&&(e.display.scrollbars.clear(),e.display.scrollbars.addClass&&uo(e.display.wrapper,e.display.scrollbars.addClass)),e.display.scrollbars=new $o[e.options.scrollbarStyle](function(t){e.display.wrapper.insertBefore(t,e.display.scrollbarFiller),No(t,"mousedown",function(){e.state.focused&&setTimeout(function(){return e.display.input.focus()},0)}),t.setAttribute("cm-not-content","true")},function(t,r){"horizontal"==r?dr(e,t):hr(e,t)},e),e.display.scrollbars.addClass&&s(e.display.wrapper,e.display.scrollbars.addClass)}function yr(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Zo},function(e){jo?jo.ops.push(e):e.ownsGroup=jo={ops:[e],delayedCallbacks:[]}}(e.curOp)}function br(e){!function(e,t){var r=e.ownsGroup;if(r)try{!function(e){var t=e.delayedCallbacks,r=0;do{for(;r=r.viewTo)||r.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new Qo(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}(t[r]);for(var i=0;i1&&(l=!0)),null!=u.scrollLeft&&(dr(e,u.scrollLeft),Math.abs(e.doc.scrollLeft-h)>1&&(l=!0)),!l)break}return i}(t,B(i,e.scrollToPos.from),B(i,e.scrollToPos.to),e.scrollToPos.margin);!function(e,t){if(!ge(e,"scrollCursorIntoView")){var r=e.display,i=r.sizer.getBoundingClientRect(),o=null;if(t.top+i.top<0?o=!0:t.bottom+i.top>(window.innerHeight||document.documentElement.clientHeight)&&(o=!1),null!=o&&!Qi){var l=n("div","​",null,"position: absolute;\n top: "+(t.top-r.viewOffset-at(e.display))+"px;\n height: "+(t.bottom-t.top+ht(e)+r.barHeight)+"px;\n left: "+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;");e.display.lineSpace.appendChild(l),l.scrollIntoView(o),e.display.lineSpace.removeChild(l)}}}(t,o)}var l=e.maybeHiddenMarkers,s=e.maybeUnhiddenMarkers;if(l)for(var a=0;at)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)Lo&&ne(e.doc,t)i.viewFrom?Tr(e):(i.viewFrom+=n,i.viewTo+=n);else if(t<=i.viewFrom&&r>=i.viewTo)Tr(e);else if(t<=i.viewFrom){var o=Mr(e,r,r+n,1);o?(i.view=i.view.slice(o.index),i.viewFrom=o.lineN,i.viewTo+=n):Tr(e)}else if(r>=i.viewTo){var l=Mr(e,t,t,-1);l?(i.view=i.view.slice(0,l.index),i.viewTo=l.lineN):Tr(e)}else{var s=Mr(e,t,t,-1),a=Mr(e,r,r+n,1);s&&a?(i.view=i.view.slice(0,s.index).concat(qe(e,s.lineN,a.lineN)).concat(i.view.slice(a.index)),i.viewTo+=n):Tr(e)}var u=i.externalMeasured;u&&(r=i.lineN&&t=n.viewTo)){var o=n.view[Kt(e,t)];if(null!=o.node){var l=o.changes||(o.changes=[]);-1==f(l,r)&&l.push(r)}}}function Tr(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function Mr(e,t,r,n){var i,o=Kt(e,t),l=e.display.view;if(!Lo||r==e.doc.first+e.doc.size)return{index:o,lineN:r};for(var s=e.display.viewFrom,a=0;a0){if(o==l.length-1)return null;i=s+l[o].size-t,o++}else i=s-t;t+=i,r+=i}for(;ne(e.doc,r)!=r;){if(o==(n<0?0:l.length-1))return null;r+=n*l[o-(n<0?1:0)].size,o+=n}return{index:o,lineN:r}}function Nr(e){for(var t=e.display.view,r=0,n=0;n=e.display.viewTo)){var r=+new Date+e.options.workTime,n=Pe(e,t.highlightFrontier),i=[];t.iter(n.line,Math.min(t.first+t.size,e.display.viewTo+500),function(o){if(n.line>=e.display.viewFrom){var l=o.styles,s=o.text.length>e.options.maxHighlightLength?Ae(t.mode,n.state):null,a=He(e,o,n,!0);s&&(n.state=s),o.styles=a.styles;var u=o.styleClasses,c=a.classes;c?o.styleClasses=c:u&&(o.styleClasses=null);for(var h=!l||l.length!=o.styles.length||u!=c&&(!u||!c||u.bgClass!=c.bgClass||u.textClass!=c.textClass),f=0;!h&&fr)return Or(e,e.options.workDelay),!0}),t.highlightFrontier=n.line,t.modeFrontier=Math.max(t.modeFrontier,n.line),i.length&&wr(e,function(){for(var t=0;t=n.viewFrom&&r.visible.to<=n.viewTo&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&0==Nr(e))return!1;ir(e)&&(Tr(e),r.dims=Rt(e));var s=i.first+i.size,a=Math.max(r.visible.from-e.options.viewportMargin,i.first),u=Math.min(s,r.visible.to+e.options.viewportMargin);n.viewFromu&&n.viewTo-u<20&&(u=Math.min(s,n.viewTo)),Lo&&(a=ne(e.doc,a),u=ie(e.doc,u));var c=a!=n.viewFrom||u!=n.viewTo||n.lastWrapHeight!=r.wrapperHeight||n.lastWrapWidth!=r.wrapperWidth;!function(e,t,r){var n=e.display;0==n.view.length||t>=n.viewTo||r<=n.viewFrom?(n.view=qe(e,t,r),n.viewFrom=t):(n.viewFrom>t?n.view=qe(e,t,n.viewFrom).concat(n.view):n.viewFromr&&(n.view=n.view.slice(0,Kt(e,r)))),n.viewTo=r}(e,a,u),n.viewOffset=se(k(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var h=Nr(e);if(!c&&0==h&&!r.force&&n.renderedView==n.view&&(null==n.updateLineNumbers||n.updateLineNumbers>=n.viewTo))return!1;var d=function(e){if(e.hasFocus())return null;var t=l();if(!t||!o(e.display.lineDiv,t))return null;var r={activeElt:t};if(window.getSelection){var n=window.getSelection();n.anchorNode&&n.extend&&o(e.display.lineDiv,n.anchorNode)&&(r.anchorNode=n.anchorNode,r.anchorOffset=n.anchorOffset,r.focusNode=n.focusNode,r.focusOffset=n.focusOffset)}return r}(e);return h>4&&(n.lineDiv.style.display="none"),function(e,r,n){function i(t){var r=t.nextSibling;return Xi&&ro&&e.display.currentWheelTarget==t?t.style.display="none":t.parentNode.removeChild(t),r}var o=e.display,l=e.options.lineNumbers,s=o.lineDiv,a=s.firstChild;for(var u=o.view,c=o.viewFrom,h=0;h-1&&(p=!1),Qe(e,d,c,n)),p&&(t(d.lineNumber),d.lineNumber.appendChild(document.createTextNode(D(e.options,c)))),a=d.node.nextSibling}else{var g=nt(e,d,c,n);s.insertBefore(g,a)}c+=d.size}for(;a;)a=i(a)}(e,n.updateLineNumbers,r.dims),h>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,function(e){if(e&&e.activeElt&&e.activeElt!=l()&&(e.activeElt.focus(),e.anchorNode&&o(document.body,e.anchorNode)&&o(document.body,e.focusNode))){var t=window.getSelection(),r=document.createRange();r.setEnd(e.anchorNode,e.anchorOffset),r.collapse(!1),t.removeAllRanges(),t.addRange(r),t.extend(e.focusNode,e.focusOffset)}}(d),t(n.cursorDiv),t(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,c&&(n.lastWrapHeight=r.wrapperHeight,n.lastWrapWidth=r.wrapperWidth,Or(e,400)),n.updateLineNumbers=null,!0}function Dr(e,t){for(var r=t.viewport,n=!0;(n&&e.options.lineWrapping&&t.oldDisplayWidth!=ft(e)||(r&&null!=r.top&&(r={top:Math.min(e.doc.height+ut(e.display)-dt(e),r.top)}),t.visible=rr(e.display,e.doc,r),!(t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)))&&Wr(e,t);n=!1){er(e);var i=pr(e);jt(e),gr(e,i),Pr(e,i),t.force=!1}t.signal(e,"update",e),e.display.viewFrom==e.display.reportedViewFrom&&e.display.viewTo==e.display.reportedViewTo||(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function Hr(e,t){var r=new Qo(e,t);if(Wr(e,r)){er(e),Dr(e,r);var n=pr(e);jt(e),gr(e,n),Pr(e,n),r.finish()}}function Fr(e){var t=e.display.gutters.offsetWidth;e.display.sizer.style.marginLeft=t+"px"}function Pr(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+ht(e)+"px"}function Er(e){var r=e.display.gutters,i=e.options.gutters;t(r);for(var o=0;o-1&&!e.lineNumbers&&(e.gutters=e.gutters.slice(0),e.gutters.splice(t,1))}function Ir(e){var t=e.wheelDeltaX,r=e.wheelDeltaY;return null==t&&e.detail&&e.axis==e.HORIZONTAL_AXIS&&(t=e.detail),null==r&&e.detail&&e.axis==e.VERTICAL_AXIS?r=e.detail:null==r&&(r=e.wheelDelta),{x:t,y:r}}function Rr(e){var t=Ir(e);return t.x*=el,t.y*=el,t}function Br(e,t){var r=Ir(t),n=r.x,i=r.y,o=e.display,l=o.scroller,s=l.scrollWidth>l.clientWidth,a=l.scrollHeight>l.clientHeight;if(n&&s||i&&a){if(i&&ro&&Xi)e:for(var u=t.target,c=o.view;u!=l;u=u.parentNode)for(var h=0;h=0){var l=I(o.from(),i.from()),s=z(o.to(),i.to()),a=o.empty()?i.from()==i.head:o.from()==o.head;n<=t&&--t,e.splice(--n,2,new rl(a?s:l,a?l:s))}}return new tl(e,t)}function Ur(e,t){return new tl([new rl(e,t||e)],0)}function Vr(e){return e.text?H(e.from.line+e.text.length-1,g(e.text).length+(1==e.text.length?e.from.ch:0)):e.to}function Kr(e,t){if(F(e,t.from)<0)return e;if(F(e,t.to)<=0)return Vr(t);var r=e.line+t.text.length-(t.to.line-t.from.line)-1,n=e.ch;return e.line==t.to.line&&(n+=Vr(t).ch-t.to.ch),H(r,n)}function jr(e,t){for(var r=[],n=0;n1&&e.remove(s.line+1,p-1),e.insert(s.line+1,y)}$e(e,"change",e,t)}function Zr(e,t,r){function n(e,i,o){if(e.linked)for(var l=0;ls-e.cm.options.historyEventDelay||"*"==t.origin.charAt(0)))&&(o=function(e,t){return t?(rn(e.done),g(e.done)):e.done.length&&!g(e.done).ranges?g(e.done):e.done.length>1&&!e.done[e.done.length-2].ranges?(e.done.pop(),g(e.done)):void 0}(i,i.lastOp==n)))l=g(o.changes),0==F(t.from,t.to)&&0==F(t.from,l.to)?l.to=Vr(t):o.changes.push(tn(e,t));else{var a=g(i.done);for(a&&a.ranges||ln(e.sel,i.done),o={changes:[tn(e,t)],generation:i.generation},i.done.push(o);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(r),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=s,i.lastOp=i.lastSelOp=n,i.lastOrigin=i.lastSelOrigin=t.origin,l||pe(e,"historyAdded")}function on(e,t,r,n){var i=e.history,o=n&&n.origin;r==i.lastSelOp||o&&i.lastSelOrigin==o&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==o||function(e,t,r,n){var i=t.charAt(0);return"*"==i||"+"==i&&r.ranges.length==n.ranges.length&&r.somethingSelected()==n.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}(e,o,g(i.done),t))?i.done[i.done.length-1]=t:ln(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=o,i.lastSelOp=r,n&&!1!==n.clearRedo&&rn(i.undone)}function ln(e,t){var r=g(t);r&&r.ranges&&r.equals(e)||t.push(e)}function sn(e,t,r,n){var i=t["spans_"+e.id],o=0;e.iter(Math.max(e.first,r),Math.min(e.first+e.size,n),function(r){r.markedSpans&&((i||(i=t["spans_"+e.id]={}))[o]=r.markedSpans),++o})}function an(e){if(!e)return null;for(var t,r=0;r-1&&(g(s)[h]=u[h],delete u[h])}}}return n}function hn(e,t,r,n){if(n){var i=e.anchor;if(r){var o=F(t,i)<0;o!=F(r,i)<0?(i=t,t=r):o!=F(t,r)<0&&(t=r)}return new rl(i,t)}return new rl(r||t,t)}function fn(e,t,r,n,i){null==i&&(i=e.cm&&(e.cm.display.shift||e.extend)),mn(e,new tl([hn(e.sel.primary(),t,r,i)],0),n)}function dn(e,t,r){for(var n=[],i=e.cm&&(e.cm.display.shift||e.extend),o=0;o=t.ch:s.to>t.ch))){if(i&&(pe(a,"beforeCursorEnter"),a.explicitlyCleared)){if(o.markedSpans){--l;continue}break}if(!a.atomic)continue;if(r){var u=a.find(n<0?1:-1),c=void 0;if((n<0?a.inclusiveRight:a.inclusiveLeft)&&(u=Ln(e,u,-n,u&&u.line==t.line?o:null)),u&&u.line==t.line&&(c=F(u,r))&&(n<0?c<0:c>0))return Cn(e,u,t,n,i)}var h=a.find(n<0?-1:1);return(n<0?a.inclusiveLeft:a.inclusiveRight)&&(h=Ln(e,h,n,h.line==t.line?o:null)),h?Cn(e,h,t,n,i):null}}return t}function Sn(e,t,r,n,i){var o=n||1,l=Cn(e,t,r,o,i)||!i&&Cn(e,t,r,o,!0)||Cn(e,t,r,-o,i)||!i&&Cn(e,t,r,-o,!0);return l||(e.cantEdit=!0,H(e.first,0))}function Ln(e,t,r,n){return r<0&&0==t.ch?t.line>e.first?B(e,H(t.line-1)):null:r>0&&t.ch==(n||k(e,t.line)).text.length?t.line0)){var c=[a,1],h=F(u.from,s.from),d=F(u.to,s.to);(h<0||!l.inclusiveLeft&&!h)&&c.push({from:u.from,to:s.from}),(d>0||!l.inclusiveRight&&!d)&&c.push({from:s.to,to:u.to}),i.splice.apply(i,c),a+=c.length-3}}return i}(e,t.from,t.to);if(n)for(var i=n.length-1;i>=0;--i)Nn(e,{from:n[i].from,to:n[i].to,text:i?[""]:t.text,origin:t.origin});else Nn(e,t)}}function Nn(e,t){if(1!=t.text.length||""!=t.text[0]||0!=F(t.from,t.to)){var r=jr(e,t);nn(e,t,r,e.cm?e.cm.curOp.id:NaN),Wn(e,t,r,j(e,t));var n=[];Zr(e,function(e,r){r||-1!=f(n,e.history)||(Pn(e.history,t),n.push(e.history)),Wn(e,t,null,j(e,t))})}}function On(e,t,r){if(!e.cm||!e.cm.state.suppressEdits||r){for(var n,i=e.history,o=e.sel,l="undo"==t?i.done:i.undone,s="undo"==t?i.undone:i.done,a=0;a=0;--d){var p=h(d);if(p)return p.v}}}}function An(e,t){if(0!=t&&(e.first+=t,e.sel=new tl(v(e.sel.ranges,function(e){return new rl(H(e.anchor.line+t,e.anchor.ch),H(e.head.line+t,e.head.ch))}),e.sel.primIndex),e.cm)){Lr(e.cm,e.first,e.first-t,t);for(var r=e.cm.display,n=r.viewFrom;ne.lastLine())){if(t.from.lineo&&(t={from:t.from,to:H(o,k(e,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=T(e,t.from,t.to),r||(r=jr(e,t)),e.cm?function(e,t,r){var n=e.doc,i=e.display,o=t.from,l=t.to,s=!1,a=o.line;e.options.lineWrapping||(a=O(re(k(n,o.line))),n.iter(a,l.line+1,function(e){if(e==i.maxLine)return s=!0,!0}));n.sel.contains(t.from,t.to)>-1&&ve(e);$r(n,t,r,Gt(e)),e.options.lineWrapping||(n.iter(a,o.line+t.text.length,function(e){var t=ae(e);t>i.maxLineLength&&(i.maxLine=e,i.maxLineLength=t,i.maxLineChanged=!0,s=!1)}),s&&(e.curOp.updateMaxLine=!0));(function(e,t){if(e.modeFrontier=Math.min(e.modeFrontier,t),!(e.highlightFrontierr;n--){var i=k(e,n).stateAfter;if(i&&(!(i instanceof Io)||n+i.lookAhead0||0==s&&!1!==l.clearWhenEmpty)return l;if(l.replacedWith&&(l.collapsed=!0,l.widgetNode=i("span",[l.replacedWith],"CodeMirror-widget"),n.handleMouseEvents||l.widgetNode.setAttribute("cm-ignore-events","true"),n.insertLeft&&(l.widgetNode.insertLeft=!0)),l.collapsed){if(te(e,t.line,t,r,l)||t.line!=r.line&&te(e,r.line,t,r,l))throw new Error("Inserting collapsed marker partially overlapping an existing one");Lo=!0}l.addToHistory&&nn(e,{from:t,to:r,origin:"markText"},e.sel,NaN);var a,u=t.line,h=e.cm;if(e.iter(u,r.line+1,function(e){h&&l.collapsed&&!h.options.lineWrapping&&re(e)==h.display.maxLine&&(a=!0),l.collapsed&&u!=t.line&&N(e,0),function(e,t){e.markedSpans=e.markedSpans?e.markedSpans.concat([t]):[t],t.marker.attachLine(e)}(e,new U(l,u==t.line?t.ch:null,u==r.line?r.ch:null)),++u}),l.collapsed&&e.iter(t.line,r.line+1,function(t){oe(e,t)&&N(t,0)}),l.clearOnEnter&&No(l,"beforeCursorEnter",function(){return l.clear()}),l.readOnly&&(So=!0,(e.history.done.length||e.history.undone.length)&&e.clearHistory()),l.collapsed&&(l.id=++il,l.atomic=!0),h){if(a&&(h.curOp.updateMaxLine=!0),l.collapsed)Lr(h,t.line,r.line+1);else if(l.className||l.title||l.startStyle||l.endStyle||l.css)for(var f=t.line;f<=r.line;f++)kr(h,f,"text");l.atomic&&wn(h.doc),$e(h,"markerAdded",h,l)}return l}function Gn(e){return e.findMarks(H(e.first,0),e.clipPos(H(e.lastLine())),function(e){return e.parent})}function Un(e){for(var t=function(t){var r=e[t],n=[r.primary.doc];Zr(r.primary.doc,function(e){return n.push(e)});for(var i=0;i-1)return t.state.draggingText(e),void setTimeout(function(){return t.display.input.focus()},20);try{var u=e.dataTransfer.getData("Text");if(u){var c;if(t.state.draggingText&&!t.state.draggingText.copy&&(c=t.listSelections()),yn(t.doc,Ur(r,r)),c)for(var h=0;h=0;t--)Dn(e.doc,"",n[t].from,n[t].to,"+delete");sr(e)})}function ri(e,t,r){var n=S(e.text,t+r,r);return n<0||n>e.text.length?null:n}function ni(e,t,r){var n=ri(e,t.ch,r);return null==n?null:new H(t.line,n,r<0?"after":"before")}function ii(e,t,r,n,i){if(e){var o=he(r,t.doc.direction);if(o){var l,s=i<0?g(o):o[0],a=i<0==(1==s.level)?"after":"before";if(s.level>0||"rtl"==t.doc.direction){var u=mt(t,r);l=i<0?r.text.length-1:0;var c=yt(t,u,l).top;l=L(function(e){return yt(t,u,e).top==c},i<0==(1==s.level)?s.from:s.to-1,l),"before"==a&&(l=ri(r,l,1))}else l=i<0?s.to:s.from;return new H(n,l,a)}}return new H(n,i<0?r.text.length:0,i<0?"before":"after")}function oi(e,t){var r=k(e.doc,t),n=re(r);return n!=r&&(t=O(n)),ii(!0,e,n,t,1)}function li(e,t){var r=k(e.doc,t),n=function(e){for(var t;t=ee(e);)e=t.find(1,!0).line;return e}(r);return n!=r&&(t=O(n)),ii(!0,e,r,t,-1)}function si(e,t){var r=oi(e,t.line),n=k(e.doc,r.line),i=he(n,e.doc.direction);if(!i||0==i[0].level){var o=Math.max(0,n.text.search(/\S/)),l=t.line==r.line&&t.ch<=o&&t.ch;return H(r.line,l?0:o,r.sticky)}return r}function ai(e,t,r){if("string"==typeof t&&!(t=vl[t]))return!1;e.display.input.ensurePolled();var n=e.display.shift,i=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),r&&(e.display.shift=!1),i=t(e)!=vo}finally{e.display.shift=n,e.state.suppressEdits=!1}return i}function ui(e,t,r,n){var i=e.state.keySeq;if(i){if(Zn(t))return"handled";if(/\'$/.test(t)?e.state.keySeq=null:ml.set(50,function(){e.state.keySeq==i&&(e.state.keySeq=null,e.display.input.reset())}),ci(e,i+" "+t,r,n))return!0}return ci(e,t,r,n)}function ci(e,t,r,n){var i=function(e,t,r){for(var n=0;n-1&&(F((i=s.ranges[i]).from(),t)<0||t.xRel>0)&&(F(i.to(),t)>0||t.xRel<0)?function(e,t,r,n){var i=e.display,o=!1,l=xr(e,function(t){Xi&&(i.scroller.draggable=!1),e.state.draggingText=!1,de(document,"mouseup",l),de(document,"mousemove",s),de(i.scroller,"dragstart",a),de(i.scroller,"drop",l),o||(be(t),n.addNew||fn(e.doc,r,null,null,n.extend),Xi||Ki&&9==ji?setTimeout(function(){document.body.focus(),i.input.focus()},20):i.input.focus())}),s=function(e){o=o||Math.abs(t.clientX-e.clientX)+Math.abs(t.clientY-e.clientY)>=10},a=function(){return o=!0};Xi&&(i.scroller.draggable=!0);e.state.draggingText=l,l.copy=!n.moveOnDrag,i.scroller.dragDrop&&i.scroller.dragDrop();No(document,"mouseup",l),No(document,"mousemove",s),No(i.scroller,"dragstart",a),No(i.scroller,"drop",l),Zt(e),setTimeout(function(){return i.input.focus()},20)}(e,n,t,o):function(e,t,r,n){function i(t){if(0!=F(m,t))if(m=t,"rectangle"==n.unit){for(var i=[],o=e.options.tabSize,l=h(k(u,r.line).text,r.ch,o),s=h(k(u,t.line).text,t.ch,o),a=Math.min(l,s),g=Math.max(l,s),v=Math.min(r.line,t.line),y=Math.min(e.lastLine(),Math.max(r.line,t.line));v<=y;v++){var b=k(u,v).text,w=d(b,a,o);a==g?i.push(new rl(H(v,w),H(v,w))):b.length>w&&i.push(new rl(H(v,w),H(v,d(b,g,o))))}i.length||i.push(new rl(r,r)),mn(u,Gr(p.ranges.slice(0,f).concat(i),f),{origin:"*mouse",scroll:!1}),e.scrollIntoView(t)}else{var x,C=c,S=vi(e,t,n.unit),L=C.anchor;F(S.anchor,L)>0?(x=S.head,L=I(C.from(),S.anchor)):(x=S.anchor,L=z(C.to(),S.head));var T=p.ranges.slice(0);T[f]=function(e,t){var r=t.anchor,n=t.head,i=k(e.doc,r.line);if(0==F(r,n)&&r.sticky==n.sticky)return t;var o=he(i);if(!o)return t;var l=ce(o,r.ch,r.sticky),s=o[l];if(s.from!=r.ch&&s.to!=r.ch)return t;var a=l+(s.from==r.ch==(1!=s.level)?0:1);if(0==a||a==o.length)return t;var u;if(n.line!=r.line)u=(n.line-r.line)*("ltr"==e.doc.direction?1:-1)>0;else{var c=ce(o,n.ch,n.sticky),h=c-l||(n.ch-r.ch)*(1==s.level?-1:1);u=c==a-1||c==a?h<0:h>0}var f=o[a+(u?-1:0)],d=u==(1==f.level),p=d?f.from:f.to,g=d?"after":"before";return r.ch==p&&r.sticky==g?t:new rl(new H(r.line,p,g),n)}(e,new rl(B(u,L),x)),mn(u,Gr(T,f),yo)}}function o(t){var r=++b,s=Vt(e,t,!0,"rectangle"==n.unit);if(s)if(0!=F(s,m)){e.curOp.focus=l(),i(s);var c=rr(a,u);(s.line>=c.to||s.liney.bottom?20:0;h&&setTimeout(xr(e,function(){b==r&&(a.scroller.scrollTop+=h,o(t))}),50)}}function s(t){e.state.selectingText=!1,b=1/0,be(t),a.input.focus(),de(document,"mousemove",w),de(document,"mouseup",x),u.history.lastSelOrigin=null}var a=e.display,u=e.doc;be(t);var c,f,p=u.sel,g=p.ranges;n.addNew&&!n.extend?(f=u.sel.contains(r),c=f>-1?g[f]:new rl(r,r)):(c=u.sel.primary(),f=u.sel.primIndex);if("rectangle"==n.unit)n.addNew||(c=new rl(r,r)),r=Vt(e,t,!0,!0),f=-1;else{var v=vi(e,r,n.unit);c=n.extend?hn(c,v.anchor,v.head,n.extend):v}n.addNew?-1==f?(f=g.length,mn(u,Gr(g.concat([c]),f),{scroll:!1,origin:"*mouse"})):g.length>1&&g[f].empty()&&"char"==n.unit&&!n.extend?(mn(u,Gr(g.slice(0,f).concat(g.slice(f+1)),0),{scroll:!1,origin:"*mouse"}),p=u.sel):pn(u,f,c,yo):(f=0,mn(u,new tl([c],0),yo),p=u.sel);var m=r;var y=a.wrapper.getBoundingClientRect(),b=0;var w=xr(e,function(e){Le(e)?o(e):s(e)}),x=xr(e,s);e.state.selectingText=x,No(document,"mousemove",w),No(document,"mouseup",x)}(e,n,t,o)}(t,n,o,e):Se(e)==r.scroller&&be(e):2==i?(n&&fn(t.doc,n),setTimeout(function(){return r.input.focus()},20)):3==i&&(ao?bi(t,e):Zt(t)))}}function vi(e,t,r){if("char"==r)return new rl(t,t);if("word"==r)return e.findWordAt(t);if("line"==r)return new rl(H(t.line,0),B(e.doc,H(t.line+1,0)));var n=r(e,t);return new rl(n.from,n.to)}function mi(e,t,r,n){var i,o;if(t.touches)i=t.touches[0].clientX,o=t.touches[0].clientY;else try{i=t.clientX,o=t.clientY}catch(t){return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;n&&be(t);var l=e.display,s=l.lineDiv.getBoundingClientRect();if(o>s.bottom||!me(e,r))return xe(t);o-=s.top-l.viewOffset;for(var a=0;a=i){return pe(e,r,e,A(e.doc,o),e.options.gutters[a],t),xe(t)}}}function yi(e,t){return mi(e,t,"gutterClick",!0)}function bi(e,t){st(e.display,t)||function(e,t){if(!me(e,"gutterContextMenu"))return!1;return mi(e,t,"gutterContextMenu",!1)}(e,t)||ge(e,t,"contextmenu")||e.display.input.onContextMenu(t)}function wi(e){e.display.wrapper.className=e.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+e.options.theme.replace(/(^|\s)\s*/g," cm-s-"),St(e)}function xi(e){Er(e),Lr(e),nr(e)}function Ci(e,t,r){if(!t!=!(r&&r!=Cl)){var n=e.display.dragFunctions,i=t?No:de;i(e.display.scroller,"dragstart",n.start),i(e.display.scroller,"dragenter",n.enter),i(e.display.scroller,"dragover",n.over),i(e.display.scroller,"dragleave",n.leave),i(e.display.scroller,"drop",n.drop)}}function Si(e){e.options.lineWrapping?(s(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(uo(e.display.wrapper,"CodeMirror-wrap"),ue(e)),Ut(e),Lr(e),St(e),setTimeout(function(){return gr(e)},100)}function Li(e,t){var o=this;if(!(this instanceof Li))return new Li(e,t);this.options=t=t?c(t):{},c(Sl,t,!1),zr(t);var l=t.value;"string"==typeof l&&(l=new al(l,t.mode,null,t.lineSeparator,t.direction)),this.doc=l;var s=new Li.inputStyles[t.inputStyle](this),a=this.display=new function(e,t,r){var o=this;this.input=r,o.scrollbarFiller=n("div",null,"CodeMirror-scrollbar-filler"),o.scrollbarFiller.setAttribute("cm-not-content","true"),o.gutterFiller=n("div",null,"CodeMirror-gutter-filler"),o.gutterFiller.setAttribute("cm-not-content","true"),o.lineDiv=i("div",null,"CodeMirror-code"),o.selectionDiv=n("div",null,null,"position: relative; z-index: 1"),o.cursorDiv=n("div",null,"CodeMirror-cursors"),o.measure=n("div",null,"CodeMirror-measure"),o.lineMeasure=n("div",null,"CodeMirror-measure"),o.lineSpace=i("div",[o.measure,o.lineMeasure,o.selectionDiv,o.cursorDiv,o.lineDiv],null,"position: relative; outline: none");var l=i("div",[o.lineSpace],"CodeMirror-lines");o.mover=n("div",[l],null,"position: relative"),o.sizer=n("div",[o.mover],"CodeMirror-sizer"),o.sizerWidth=null,o.heightForcer=n("div",null,null,"position: absolute; height: "+go+"px; width: 1px;"),o.gutters=n("div",null,"CodeMirror-gutters"),o.lineGutter=null,o.scroller=n("div",[o.sizer,o.heightForcer,o.gutters],"CodeMirror-scroll"),o.scroller.setAttribute("tabIndex","-1"),o.wrapper=n("div",[o.scrollbarFiller,o.gutterFiller,o.scroller],"CodeMirror"),Ki&&ji<8&&(o.gutters.style.zIndex=-1,o.scroller.style.paddingRight=0),Xi||Bi&&to||(o.scroller.draggable=!0),e&&(e.appendChild?e.appendChild(o.wrapper):e(o.wrapper)),o.viewFrom=o.viewTo=t.first,o.reportedViewFrom=o.reportedViewTo=t.first,o.view=[],o.renderedView=null,o.externalMeasured=null,o.viewOffset=0,o.lastWrapHeight=o.lastWrapWidth=0,o.updateLineNumbers=null,o.nativeBarWidth=o.barHeight=o.barWidth=0,o.scrollbarsClipped=!1,o.lineNumWidth=o.lineNumInnerWidth=o.lineNumChars=null,o.alignWidgets=!1,o.cachedCharWidth=o.cachedTextHeight=o.cachedPaddingH=null,o.maxLine=null,o.maxLineLength=0,o.maxLineChanged=!1,o.wheelDX=o.wheelDY=o.wheelStartX=o.wheelStartY=null,o.shift=!1,o.selForContextMenu=null,o.activeTouch=null,r.init(o)}(e,l,s);a.wrapper.CodeMirror=this,Er(this),wi(this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),mr(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,selectingText:!1,draggingText:!1,highlight:new ho,keySeq:null,specialChars:null},t.autofocus&&!to&&a.input.focus(),Ki&&ji<11&&setTimeout(function(){return o.display.input.reset(!0)},20),function(e){function t(){o.activeTouch&&(l=setTimeout(function(){return o.activeTouch=null},1e3),(s=o.activeTouch).end=+new Date)}function i(e,t){if(null==t.left)return!0;var r=t.left-e.left,n=t.top-e.top;return r*r+n*n>400}var o=e.display;No(o.scroller,"mousedown",xr(e,gi)),Ki&&ji<11?No(o.scroller,"dblclick",xr(e,function(t){if(!ge(e,t)){var r=Vt(e,t);if(r&&!yi(e,t)&&!st(e.display,t)){be(t);var n=e.findWordAt(r);fn(e.doc,n.anchor,n.head)}}})):No(o.scroller,"dblclick",function(t){return ge(e,t)||be(t)});ao||No(o.scroller,"contextmenu",function(t){return bi(e,t)});var l,s={end:0};No(o.scroller,"touchstart",function(t){if(!ge(e,t)&&!function(e){if(1!=e.touches.length)return!1;var t=e.touches[0];return t.radiusX<=1&&t.radiusY<=1}(t)&&!yi(e,t)){o.input.ensurePolled(),clearTimeout(l);var r=+new Date;o.activeTouch={start:r,moved:!1,prev:r-s.end<=300?s:null},1==t.touches.length&&(o.activeTouch.left=t.touches[0].pageX,o.activeTouch.top=t.touches[0].pageY)}}),No(o.scroller,"touchmove",function(){o.activeTouch&&(o.activeTouch.moved=!0)}),No(o.scroller,"touchend",function(r){var n=o.activeTouch;if(n&&!st(o,r)&&null!=n.left&&!n.moved&&new Date-n.start<300){var l,s=e.coordsChar(o.activeTouch,"page");l=!n.prev||i(n,n.prev)?new rl(s,s):!n.prev.prev||i(n,n.prev.prev)?e.findWordAt(s):new rl(H(s.line,0),B(e.doc,H(s.line+1,0))),e.setSelection(l.anchor,l.head),e.focus(),be(r)}t()}),No(o.scroller,"touchcancel",t),No(o.scroller,"scroll",function(){o.scroller.clientHeight&&(hr(e,o.scroller.scrollTop),dr(e,o.scroller.scrollLeft,!0),pe(e,"scroll",e))}),No(o.scroller,"mousewheel",function(t){return Br(e,t)}),No(o.scroller,"DOMMouseScroll",function(t){return Br(e,t)}),No(o.wrapper,"scroll",function(){return o.wrapper.scrollTop=o.wrapper.scrollLeft=0}),o.dragFunctions={enter:function(t){ge(e,t)||Ce(t)},over:function(t){ge(e,t)||(!function(e,t){var i=Vt(e,t);if(i){var o=document.createDocumentFragment();Yt(e,i,o),e.display.dragCursor||(e.display.dragCursor=n("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),e.display.lineSpace.insertBefore(e.display.dragCursor,e.display.cursorDiv)),r(e.display.dragCursor,o)}}(e,t),Ce(t))},start:function(t){return function(e,t){if(Ki&&(!e.state.draggingText||+new Date-ul<100))Ce(t);else if(!ge(e,t)&&!st(e.display,t)&&(t.dataTransfer.setData("Text",e.getSelection()),t.dataTransfer.effectAllowed="copyMove",t.dataTransfer.setDragImage&&!$i)){var r=n("img",null,null,"position: fixed; left: 0; top: 0;");r.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",qi&&(r.width=r.height=1,e.display.wrapper.appendChild(r),r._top=r.offsetTop),t.dataTransfer.setDragImage(r,0,0),qi&&r.parentNode.removeChild(r)}}(e,t)},drop:xr(e,Vn),leave:function(t){ge(e,t)||Kn(e)}};var a=o.input.getField();No(a,"keyup",function(t){return di.call(e,t)}),No(a,"keydown",xr(e,fi)),No(a,"keypress",xr(e,pi)),No(a,"focus",function(t){return Qt(e,t)}),No(a,"blur",function(t){return Jt(e,t)})}(this),Xn(),yr(this),this.curOp.forceUpdate=!0,Qr(this,l),t.autofocus&&!to||this.hasFocus()?setTimeout(u(Qt,this),20):Jt(this);for(var h in Ll)Ll.hasOwnProperty(h)&&Ll[h](o,t[h],Cl);ir(this),t.finishInit&&t.finishInit(this);for(var f=0;f150)){if(!n)return;r="prev"}}else u=0,r="not";"prev"==r?u=t>o.first?h(k(o,t-1).text,null,l):0:"add"==r?u=a+e.options.indentUnit:"subtract"==r?u=a-e.options.indentUnit:"number"==typeof r&&(u=a+r),u=Math.max(0,u);var f="",d=0;if(e.options.indentWithTabs)for(var g=Math.floor(u/l);g;--g)d+=l,f+="\t";if(d1)if(Tl&&Tl.text.join("\n")==t){if(n.ranges.length%Tl.text.length==0){a=[];for(var u=0;u=0;h--){var f=n.ranges[h],d=f.from(),p=f.to();f.empty()&&(r&&r>0?d=H(d.line,d.ch-r):e.state.overwrite&&!l?p=H(p.line,Math.min(k(o,p.line).text.length,p.ch+g(s).length)):Tl&&Tl.lineWise&&Tl.text.join("\n")==t&&(d=p=H(d.line,0))),c=e.curOp.updateInput;var m={from:d,to:p,text:a?a[h%a.length]:s,origin:i||(l?"paste":e.state.cutIncoming?"cut":"+input")};Mn(e.doc,m),$e(e,"inputRead",e,m)}t&&!l&&Oi(e,t),sr(e),e.curOp.updateInput=c,e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=!1}function Ni(e,t){var r=e.clipboardData&&e.clipboardData.getData("Text");if(r)return e.preventDefault(),t.isReadOnly()||t.options.disableInput||wr(t,function(){return Mi(t,r,0,null,"paste")}),!0}function Oi(e,t){if(e.options.electricChars&&e.options.smartIndent)for(var r=e.doc.sel,n=r.ranges.length-1;n>=0;n--){var i=r.ranges[n];if(!(i.head.ch>100||n&&r.ranges[n-1].head.line==i.head.line)){var o=e.getModeAt(i.head),l=!1;if(o.electricChars){for(var s=0;s-1){l=ki(e,i.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(k(e.doc,i.head.line).text.slice(0,i.head.ch))&&(l=ki(e,i.head.line,"smart"));l&&$e(e,"electricInput",e,i.head.line)}}}function Ai(e){for(var t=[],r=[],n=0;n=t.text.length?(r.ch=t.text.length,r.sticky="before"):r.ch<=0&&(r.ch=0,r.sticky="after");var o=ce(i,r.ch,r.sticky),l=i[o];if("ltr"==e.doc.direction&&l.level%2==0&&(n>0?l.to>r.ch:l.from=l.from&&f>=c.begin)){var d=h?"before":"after";return new H(r.line,f,d)}}var p=function(e,t,n){for(var o=function(e,t){return t?new H(r.line,a(e,1),"before"):new H(r.line,e,"after")};e>=0&&e0==(1!=l.level),u=s?n.begin:a(n.end,-1);if(l.from<=u&&u0?c.end:a(c.begin,-1);return null==v||n>0&&v==t.text.length||!(g=p(n>0?0:i.length-1,n,u(v)))?null:g}(e.cm,a,t,r):ni(a,t,r))){if(n||!function(){var n=t.line+r;return!(n=e.first+e.size)&&(t=new H(n,t.ch,t.sticky),a=k(e,n))}())return!1;t=ii(i,e.cm,a,t.line,r)}else t=o;return!0}var l=t,s=r,a=k(e,t.line);if("char"==n)o();else if("column"==n)o(!0);else if("word"==n||"group"==n)for(var u=null,c="group"==n,h=e.cm&&e.cm.getHelper(t,"wordChars"),f=!0;!(r<0)||o(!f);f=!1){var d=a.text.charAt(t.ch)||"\n",p=w(d,h)?"w":c&&"\n"==d?"n":!c||/\s/.test(d)?null:"p";if(!c||f||p||(p="s"),u&&u!=p){r<0&&(r=1,o(),t.sticky="after");break}if(p&&(u=p),r>0&&!o(!f))break}var g=Sn(e,t,l,s,!0);return P(l,g)&&(g.hitSide=!0),g}function Fi(e,t,r,n){var i,o=e.doc,l=t.left;if("page"==n){var s=Math.min(e.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),a=Math.max(s-.5*zt(e.display),3);i=(r>0?t.bottom:t.top)+r*a}else"line"==n&&(i=r>0?t.bottom+3:t.top-3);for(var u;(u=Ht(e,l,i)).outside;){if(r<0?i<=0:i>=o.height){u.hitSide=!0;break}i+=5*r}return u}function Pi(e,t){var r=vt(e,t.line);if(!r||r.hidden)return null;var n=k(e.doc,t.line),i=pt(r,n,t.line),o=he(n,e.doc.direction),l="left";if(o){l=ce(o,t.ch)%2?"right":"left"}var s=bt(i.map,t.ch,l);return s.offset="right"==s.collapse?s.end:s.start,s}function Ei(e,t){return t&&(e.bad=!0),e}function zi(e,t,r){var n;if(t==e.display.lineDiv){if(!(n=e.display.lineDiv.childNodes[r]))return Ei(e.clipPos(H(e.display.viewTo-1)),!0);t=null,r=0}else for(n=t;;n=n.parentNode){if(!n||n==e.display.lineDiv)return null;if(n.parentNode&&n.parentNode==e.display.lineDiv)break}for(var i=0;i=15&&(qi=!1,Xi=!0);var lo,so=ro&&(Yi||qi&&(null==oo||oo<12.11)),ao=Bi||Ki&&ji>=9,uo=function(t,r){var n=t.className,i=e(r).exec(n);if(i){var o=n.slice(i.index+i[0].length);t.className=n.slice(0,i.index)+(o?i[1]+o:"")}};lo=document.createRange?function(e,t,r,n){var i=document.createRange();return i.setEnd(n||e,r),i.setStart(e,t),i}:function(e,t,r){var n=document.body.createTextRange();try{n.moveToElementText(e.parentNode)}catch(e){return n}return n.collapse(!0),n.moveEnd("character",r),n.moveStart("character",t),n};var co=function(e){e.select()};Ji?co=function(e){e.selectionStart=0,e.selectionEnd=e.value.length}:Ki&&(co=function(e){try{e.select()}catch(e){}});var ho=function(){this.id=null};ho.prototype.set=function(e,t){clearTimeout(this.id),this.id=setTimeout(t,e)};var fo,po,go=30,vo={toString:function(){return"CodeMirror.Pass"}},mo={scroll:!1},yo={origin:"*mouse"},bo={origin:"+move"},wo=[""],xo=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Co=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/,So=!1,Lo=!1,ko=null,To=function(){function e(e){return e<=247?r.charAt(e):1424<=e&&e<=1524?"R":1536<=e&&e<=1785?n.charAt(e-1536):1774<=e&&e<=2220?"r":8192<=e&&e<=8203?"w":8204==e?"b":"L"}function t(e,t,r){this.level=e,this.from=t,this.to=r}var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",n="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111",i=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,o=/[stwN]/,l=/[LRr]/,s=/[Lb1n]/,a=/[1n]/;return function(r,n){var u="ltr"==n?"L":"R";if(0==r.length||"ltr"==n&&!i.test(r))return!1;for(var c=r.length,h=[],f=0;f=this.string.length},zo.prototype.sol=function(){return this.pos==this.lineStart},zo.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},zo.prototype.next=function(){if(this.post},zo.prototype.eatSpace=function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},zo.prototype.skipToEnd=function(){this.pos=this.string.length},zo.prototype.skipTo=function(e){var t=this.string.indexOf(e,this.pos);if(t>-1)return this.pos=t,!0},zo.prototype.backUp=function(e){this.pos-=e},zo.prototype.column=function(){return this.lastColumnPos0?null:(n&&!1!==t&&(this.pos+=n[0].length),n)}var i=function(e){return r?e.toLowerCase():e};if(i(this.string.substr(this.pos,e.length))==i(e))return!1!==t&&(this.pos+=e.length),!0},zo.prototype.current=function(){return this.string.slice(this.start,this.pos)},zo.prototype.hideFirstChars=function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}},zo.prototype.lookAhead=function(e){var t=this.lineOracle;return t&&t.lookAhead(e)},zo.prototype.baseToken=function(){var e=this.lineOracle;return e&&e.baseToken(this.pos)};var Io=function(e,t){this.state=e,this.lookAhead=t},Ro=function(e,t,r,n){this.state=t,this.doc=e,this.line=r,this.maxLookAhead=n||0,this.baseTokens=null,this.baseTokenPos=1};Ro.prototype.lookAhead=function(e){var t=this.doc.getLine(this.line+e);return null!=t&&e>this.maxLookAhead&&(this.maxLookAhead=e),t},Ro.prototype.baseToken=function(e){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=e;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-e}},Ro.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},Ro.fromSaved=function(e,t,r){return t instanceof Io?new Ro(e,Ae(e.mode,t.state),r,t.lookAhead):new Ro(e,Ae(e.mode,t),r)},Ro.prototype.save=function(e){var t=!1!==e?Ae(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new Io(t,this.maxLookAhead):t};var Bo=function(e,t,r){this.start=e.start,this.end=e.pos,this.string=e.current(),this.type=t||null,this.state=r},Go=function(e,t,r){this.text=e,_(this,t),this.height=r?r(this):1};Go.prototype.lineNo=function(){return O(this)},ye(Go);var Uo,Vo={},Ko={},jo=null,Xo=null,Yo={left:0,right:0,top:0,bottom:0},_o=function(e,t,r){this.cm=r;var i=this.vert=n("div",[n("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),o=this.horiz=n("div",[n("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");e(i),e(o),No(i,"scroll",function(){i.clientHeight&&t(i.scrollTop,"vertical")}),No(o,"scroll",function(){o.clientWidth&&t(o.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,Ki&&ji<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};_o.prototype.update=function(e){var t=e.scrollWidth>e.clientWidth+1,r=e.scrollHeight>e.clientHeight+1,n=e.nativeBarWidth;if(r){this.vert.style.display="block",this.vert.style.bottom=t?n+"px":"0";var i=e.viewHeight-(t?n:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=r?n+"px":"0",this.horiz.style.left=e.barLeft+"px";var o=e.viewWidth-e.barLeft-(r?n:0);this.horiz.firstChild.style.width=Math.max(0,e.scrollWidth-e.clientWidth+o)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(0==n&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:r?n:0,bottom:t?n:0}},_o.prototype.setScrollLeft=function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},_o.prototype.setScrollTop=function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},_o.prototype.zeroWidthHack=function(){var e=ro&&!Zi?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new ho,this.disableVert=new ho},_o.prototype.enableZeroWidthBar=function(e,t,r){function n(){var i=e.getBoundingClientRect();("vert"==r?document.elementFromPoint(i.right-1,(i.top+i.bottom)/2):document.elementFromPoint((i.right+i.left)/2,i.bottom-1))!=e?e.style.pointerEvents="none":t.set(1e3,n)}e.style.pointerEvents="auto",t.set(1e3,n)},_o.prototype.clear=function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)};var qo=function(){};qo.prototype.update=function(){return{bottom:0,right:0}},qo.prototype.setScrollLeft=function(){},qo.prototype.setScrollTop=function(){},qo.prototype.clear=function(){};var $o={native:_o,null:qo},Zo=0,Qo=function(e,t,r){var n=e.display;this.viewport=t,this.visible=rr(n,e.doc,t),this.editorIsHidden=!n.wrapper.offsetWidth,this.wrapperHeight=n.wrapper.clientHeight,this.wrapperWidth=n.wrapper.clientWidth,this.oldDisplayWidth=ft(e),this.force=r,this.dims=Rt(e),this.events=[]};Qo.prototype.signal=function(e,t){me(e,t)&&this.events.push(arguments)},Qo.prototype.finish=function(){for(var e=0;e=0&&F(e,n.to())<=0)return r}return-1};var rl=function(e,t){this.anchor=e,this.head=t};rl.prototype.from=function(){return I(this.anchor,this.head)},rl.prototype.to=function(){return z(this.anchor,this.head)},rl.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch},zn.prototype={chunkSize:function(){return this.lines.length},removeInner:function(e,t){for(var r=e,n=e+t;r1||!(this.children[0]instanceof zn))){var s=[];this.collapse(s),this.children=[new zn(s)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t50){for(var l=i.lines.length%25+25,s=l;s10);e.parent.maybeSpill()}},iterN:function(e,t,r){for(var n=0;ne.display.maxLineLength&&(e.display.maxLine=u,e.display.maxLineLength=c,e.display.maxLineChanged=!0)}null!=n&&e&&this.collapsed&&Lr(e,n,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&wn(e.doc)),e&&$e(e,"markerCleared",e,this,n,i),t&&br(e),this.parent&&this.parent.clear()}},ol.prototype.find=function(e,t){null==e&&"bookmark"==this.type&&(e=1);for(var r,n,i=0;i=0;a--)Mn(this,n[a]);s?vn(this,s):this.cm&&sr(this.cm)}),undo:Sr(function(){On(this,"undo")}),redo:Sr(function(){On(this,"redo")}),undoSelection:Sr(function(){On(this,"undo",!0)}),redoSelection:Sr(function(){On(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,r=0,n=0;n=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,r){e=B(this,e),t=B(this,t);var n=[],i=e.line;return this.iter(e.line,t.line+1,function(o){var l=o.markedSpans;if(l)for(var s=0;s=a.to||null==a.from&&i!=e.line||null!=a.from&&i==t.line&&a.from>=t.ch||r&&!r(a.marker)||n.push(a.marker.parent||a.marker)}++i}),n},getAllMarks:function(){var e=[];return this.iter(function(t){var r=t.markedSpans;if(r)for(var n=0;ne)return t=e,!0;e-=o,++r}),B(this,H(r,t))},indexFromPos:function(e){var t=(e=B(this,e)).ch;if(e.linet&&(t=e.from),null!=e.to&&e.to0)i=new H(i.line,i.ch+1),e.replaceRange(o.charAt(i.ch-1)+o.charAt(i.ch-2),H(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var l=k(e.doc,i.line-1).text;l&&(i=new H(i.line,1),e.replaceRange(o.charAt(0)+e.doc.lineSeparator()+l.charAt(l.length-1),H(i.line-1,l.length-1),i,"+transpose"))}r.push(new rl(i,i))}e.setSelections(r)})},newlineAndIndent:function(e){return wr(e,function(){for(var t=e.listSelections(),r=t.length-1;r>=0;r--)e.replaceRange(e.doc.lineSeparator(),t[r].anchor,t[r].head,"+input");t=e.listSelections();for(var n=0;ne&&0==F(t,this.pos)&&r==this.button};var wl,xl,Cl={toString:function(){return"CodeMirror.Init"}},Sl={},Ll={};Li.defaults=Sl,Li.optionHandlers=Ll;var kl=[];Li.defineInitHook=function(e){return kl.push(e)};var Tl=null,Ml=function(e){this.cm=e,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new ho,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};Ml.prototype.init=function(e){function t(e){if(!ge(i,e)){if(i.somethingSelected())Ti({lineWise:!1,text:i.getSelections()}),"cut"==e.type&&i.replaceSelection("",null,"cut");else{if(!i.options.lineWiseCopyCut)return;var t=Ai(i);Ti({lineWise:!0,text:t.text}),"cut"==e.type&&i.operation(function(){i.setSelections(t.ranges,0,mo),i.replaceSelection("",null,"cut")})}if(e.clipboardData){e.clipboardData.clearData();var r=Tl.text.join("\n");if(e.clipboardData.setData("Text",r),e.clipboardData.getData("Text")==r)return void e.preventDefault()}var l=Di(),s=l.firstChild;i.display.lineSpace.insertBefore(l,i.display.lineSpace.firstChild),s.value=Tl.text.join("\n");var a=document.activeElement;co(s),setTimeout(function(){i.display.lineSpace.removeChild(l),a.focus(),a==o&&n.showPrimarySelection()},50)}}var r=this,n=this,i=n.cm,o=n.div=e.lineDiv;Wi(o,i.options.spellcheck),No(o,"paste",function(e){ge(i,e)||Ni(e,i)||ji<=11&&setTimeout(xr(i,function(){return r.updateFromDOM()}),20)}),No(o,"compositionstart",function(e){r.composing={data:e.data,done:!1}}),No(o,"compositionupdate",function(e){r.composing||(r.composing={data:e.data,done:!1})}),No(o,"compositionend",function(e){r.composing&&(e.data!=r.composing.data&&r.readFromDOMSoon(),r.composing.done=!0)}),No(o,"touchstart",function(){return n.forceCompositionEnd()}),No(o,"input",function(){r.composing||r.readFromDOMSoon()}),No(o,"copy",t),No(o,"cut",t)},Ml.prototype.prepareSelection=function(){var e=Xt(this.cm,!1);return e.focus=this.cm.state.focused,e},Ml.prototype.showSelection=function(e,t){e&&this.cm.display.view.length&&((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},Ml.prototype.showPrimarySelection=function(){var e=window.getSelection(),t=this.cm,r=t.doc.sel.primary(),n=r.from(),i=r.to();if(t.display.viewTo==t.display.viewFrom||n.line>=t.display.viewTo||i.line=t.display.viewFrom&&Pi(t,n)||{node:s[0].measure.map[2],offset:0},u=i.linee.firstLine()&&(n=H(n.line-1,k(e.doc,n.line-1).length)),i.ch==k(e.doc,i.line).text.length&&i.linet.viewTo-1)return!1;var o,l,s;n.line==t.viewFrom||0==(o=Kt(e,n.line))?(l=O(t.view[0].line),s=t.view[0].node):(l=O(t.view[o].line),s=t.view[o-1].node.nextSibling);var a,u,c=Kt(e,i.line);if(c==t.view.length-1?(a=t.viewTo-1,u=t.lineDiv.lastChild):(a=O(t.view[c+1].line)-1,u=t.view[c+1].node.previousSibling),!s)return!1;for(var h=e.doc.splitLines(function(e,t,r,n,i){function o(){u&&(a+=c,u=!1)}function l(e){e&&(o(),a+=e)}function s(t){if(1==t.nodeType){var r=t.getAttribute("cm-text");if(null!=r)return void l(r||t.textContent.replace(/\u200b/g,""));var a,h=t.getAttribute("cm-marker");if(h){var f=e.findMarks(H(n,0),H(i+1,0),function(e){return function(t){return t.id==e}}(+h));return void(f.length&&(a=f[0].find(0))&&l(T(e.doc,a.from,a.to).join(c)))}if("false"==t.getAttribute("contenteditable"))return;var d=/^(pre|div|p)$/i.test(t.nodeName);d&&o();for(var p=0;p1&&f.length>1;)if(g(h)==g(f))h.pop(),f.pop(),a--;else{if(h[0]!=f[0])break;h.shift(),f.shift(),l++}for(var d=0,p=0,v=h[0],m=f[0],y=Math.min(v.length,m.length);dn.ch&&b.charCodeAt(b.length-p-1)==w.charCodeAt(w.length-p-1);)d--,p++;h[h.length-1]=b.slice(0,b.length-p).replace(/^\u200b+/,""),h[0]=h[0].slice(d).replace(/\u200b+$/,"");var C=H(l,d),S=H(a,f.length?g(f).length-p:0);return h.length>1||h[0]||F(C,S)?(Dn(e.doc,h,C,S,"+input"),!0):void 0},Ml.prototype.ensurePolled=function(){this.forceCompositionEnd()},Ml.prototype.reset=function(){this.forceCompositionEnd()},Ml.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Ml.prototype.readFromDOMSoon=function(){var e=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout(function(){if(e.readDOMTimeout=null,e.composing){if(!e.composing.done)return;e.composing=null}e.updateFromDOM()},80))},Ml.prototype.updateFromDOM=function(){var e=this;!this.cm.isReadOnly()&&this.pollContent()||wr(this.cm,function(){return Lr(e.cm)})},Ml.prototype.setUneditable=function(e){e.contentEditable="false"},Ml.prototype.onKeyPress=function(e){0!=e.charCode&&(e.preventDefault(),this.cm.isReadOnly()||xr(this.cm,Mi)(this.cm,String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),0))},Ml.prototype.readOnlyChanged=function(e){this.div.contentEditable=String("nocursor"!=e)},Ml.prototype.onContextMenu=function(){},Ml.prototype.resetPosition=function(){},Ml.prototype.needsContentAttribute=!0;var Nl=function(e){this.cm=e,this.prevInput="",this.pollingFast=!1,this.polling=new ho,this.hasSelection=!1,this.composing=null};Nl.prototype.init=function(e){function t(e){if(!ge(i,e)){if(i.somethingSelected())Ti({lineWise:!1,text:i.getSelections()});else{if(!i.options.lineWiseCopyCut)return;var t=Ai(i);Ti({lineWise:!0,text:t.text}),"cut"==e.type?i.setSelections(t.ranges,null,mo):(n.prevInput="",l.value=t.text.join("\n"),co(l))}"cut"==e.type&&(i.state.cutIncoming=!0)}}var r=this,n=this,i=this.cm,o=this.wrapper=Di(),l=this.textarea=o.firstChild;e.wrapper.insertBefore(o,e.wrapper.firstChild),Ji&&(l.style.width="0px"),No(l,"input",function(){Ki&&ji>=9&&r.hasSelection&&(r.hasSelection=null),n.poll()}),No(l,"paste",function(e){ge(i,e)||Ni(e,i)||(i.state.pasteIncoming=!0,n.fastPoll())}),No(l,"cut",t),No(l,"copy",t),No(e.scroller,"paste",function(t){st(e,t)||ge(i,t)||(i.state.pasteIncoming=!0,n.focus())}),No(e.lineSpace,"selectstart",function(t){st(e,t)||be(t)}),No(l,"compositionstart",function(){var e=i.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:e,range:i.markText(e,i.getCursor("to"),{className:"CodeMirror-composing"})}}),No(l,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},Nl.prototype.prepareSelection=function(){var e=this.cm,t=e.display,r=e.doc,n=Xt(e);if(e.options.moveInputWithCursor){var i=At(e,r.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),l=t.lineDiv.getBoundingClientRect();n.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+l.top-o.top)),n.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+l.left-o.left))}return n},Nl.prototype.showSelection=function(e){var t=this.cm.display;r(t.cursorDiv,e.cursors),r(t.selectionDiv,e.selection),null!=e.teTop&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},Nl.prototype.reset=function(e){if(!this.contextMenuPending&&!this.composing){var t=this.cm;if(t.somethingSelected()){this.prevInput="";var r=t.getSelection();this.textarea.value=r,t.state.focused&&co(this.textarea),Ki&&ji>=9&&(this.hasSelection=r)}else e||(this.prevInput=this.textarea.value="",Ki&&ji>=9&&(this.hasSelection=null))}},Nl.prototype.getField=function(){return this.textarea},Nl.prototype.supportsTouch=function(){return!1},Nl.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!to||l()!=this.textarea))try{this.textarea.focus()}catch(e){}},Nl.prototype.blur=function(){this.textarea.blur()},Nl.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},Nl.prototype.receivedFocus=function(){this.slowPoll()},Nl.prototype.slowPoll=function(){var e=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},Nl.prototype.fastPoll=function(){function e(){r.poll()||t?(r.pollingFast=!1,r.slowPoll()):(t=!0,r.polling.set(60,e))}var t=!1,r=this;r.pollingFast=!0,r.polling.set(20,e)},Nl.prototype.poll=function(){var e=this,t=this.cm,r=this.textarea,n=this.prevInput;if(this.contextMenuPending||!t.state.focused||Wo(r)&&!n&&!this.composing||t.isReadOnly()||t.options.disableInput||t.state.keySeq)return!1;var i=r.value;if(i==n&&!t.somethingSelected())return!1;if(Ki&&ji>=9&&this.hasSelection===i||ro&&/[\uf700-\uf7ff]/.test(i))return t.display.input.reset(),!1;if(t.doc.sel==t.display.selForContextMenu){var o=i.charCodeAt(0);if(8203!=o||n||(n="​"),8666==o)return this.reset(),this.cm.execCommand("undo")}for(var l=0,s=Math.min(n.length,i.length);l1e3||i.indexOf("\n")>-1?r.value=e.prevInput="":e.prevInput=i,e.composing&&(e.composing.range.clear(),e.composing.range=t.markText(e.composing.start,t.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},Nl.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},Nl.prototype.onKeyPress=function(){Ki&&ji>=9&&(this.hasSelection=null),this.fastPoll()},Nl.prototype.onContextMenu=function(e){function t(){if(null!=l.selectionStart){var e=i.somethingSelected(),t="​"+(e?l.value:"");l.value="⇚",l.value=t,n.prevInput=e?"":"​",l.selectionStart=1,l.selectionEnd=t.length,o.selForContextMenu=i.doc.sel}}function r(){if(n.contextMenuPending=!1,n.wrapper.style.cssText=c,l.style.cssText=u,Ki&&ji<9&&o.scrollbars.setScrollTop(o.scroller.scrollTop=a),null!=l.selectionStart){(!Ki||Ki&&ji<9)&&t();var e=0,r=function(){o.selForContextMenu==i.doc.sel&&0==l.selectionStart&&l.selectionEnd>0&&"​"==n.prevInput?xr(i,kn)(i):e++<10?o.detectingSelectAll=setTimeout(r,500):(o.selForContextMenu=null,o.input.reset())};o.detectingSelectAll=setTimeout(r,200)}}var n=this,i=n.cm,o=i.display,l=n.textarea,s=Vt(i,e),a=o.scroller.scrollTop;if(s&&!qi){i.options.resetSelectionOnContextMenu&&-1==i.doc.sel.contains(s)&&xr(i,mn)(i.doc,Ur(s),mo);var u=l.style.cssText,c=n.wrapper.style.cssText;n.wrapper.style.cssText="position: absolute";var h=n.wrapper.getBoundingClientRect();l.style.cssText="position: absolute; width: 30px; height: 30px;\n top: "+(e.clientY-h.top-5)+"px; left: "+(e.clientX-h.left-5)+"px;\n z-index: 1000; background: "+(Ki?"rgba(255, 255, 255, .05)":"transparent")+";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";var f;if(Xi&&(f=window.scrollY),o.input.focus(),Xi&&window.scrollTo(null,f),o.input.reset(),i.somethingSelected()||(l.value=n.prevInput=" "),n.contextMenuPending=!0,o.selForContextMenu=i.doc.sel,clearTimeout(o.detectingSelectAll),Ki&&ji>=9&&t(),ao){Ce(e);var d=function(){de(window,"mouseup",d),setTimeout(r,20)};No(window,"mouseup",d)}else setTimeout(r,50)}},Nl.prototype.readOnlyChanged=function(e){e||this.reset(),this.textarea.disabled="nocursor"==e},Nl.prototype.setUneditable=function(){},Nl.prototype.needsContentAttribute=!1,function(e){function t(t,n,i,o){e.defaults[t]=n,i&&(r[t]=o?function(e,t,r){r!=Cl&&i(e,t,r)}:i)}var r=e.optionHandlers;e.defineOption=t,e.Init=Cl,t("value","",function(e,t){return e.setValue(t)},!0),t("mode",null,function(e,t){e.doc.modeOption=t,Yr(e)},!0),t("indentUnit",2,Yr,!0),t("indentWithTabs",!1),t("smartIndent",!0),t("tabSize",4,function(e){_r(e),St(e),Lr(e)},!0),t("lineSeparator",null,function(e,t){if(e.doc.lineSep=t,t){var r=[],n=e.doc.first;e.doc.iter(function(e){for(var i=0;;){var o=e.text.indexOf(t,i);if(-1==o)break;i=o+t.length,r.push(H(n,o))}n++});for(var i=r.length-1;i>=0;i--)Dn(e.doc,t,r[i],H(r[i].line,r[i].ch+t.length))}}),t("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g,function(e,t,r){e.state.specialChars=new RegExp(t.source+(t.test("\t")?"":"|\t"),"g"),r!=Cl&&e.refresh()}),t("specialCharPlaceholder",je,function(e){return e.refresh()},!0),t("electricChars",!0),t("inputStyle",to?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),t("spellcheck",!1,function(e,t){return e.getInputField().spellcheck=t},!0),t("rtlMoveVisually",!io),t("wholeLineUpdateBefore",!0),t("theme","default",function(e){wi(e),xi(e)},!0),t("keyMap","default",function(e,t,r){var n=ei(t),i=r!=Cl&&ei(r);i&&i.detach&&i.detach(e,n),n.attach&&n.attach(e,i||null)}),t("extraKeys",null),t("configureMouse",null),t("lineWrapping",!1,Si,!0),t("gutters",[],function(e){zr(e.options),xi(e)},!0),t("fixedGutter",!0,function(e,t){e.display.gutters.style.left=t?Bt(e.display)+"px":"0",e.refresh()},!0),t("coverGutterNextToScrollbar",!1,function(e){return gr(e)},!0),t("scrollbarStyle","native",function(e){mr(e),gr(e),e.display.scrollbars.setScrollTop(e.doc.scrollTop),e.display.scrollbars.setScrollLeft(e.doc.scrollLeft)},!0),t("lineNumbers",!1,function(e){zr(e.options),xi(e)},!0),t("firstLineNumber",1,xi,!0),t("lineNumberFormatter",function(e){return e},xi,!0),t("showCursorWhenSelecting",!1,jt,!0),t("resetSelectionOnContextMenu",!0),t("lineWiseCopyCut",!0),t("pasteLinesPerSelection",!0),t("readOnly",!1,function(e,t){"nocursor"==t&&(Jt(e),e.display.input.blur()),e.display.input.readOnlyChanged(t)}),t("disableInput",!1,function(e,t){t||e.display.input.reset()},!0),t("dragDrop",!0,Ci),t("allowDropFileTypes",null),t("cursorBlinkRate",530),t("cursorScrollMargin",0),t("cursorHeight",1,jt,!0),t("singleCursorHeightPerLine",!0,jt,!0),t("workTime",100),t("workDelay",100),t("flattenSpans",!0,_r,!0),t("addModeClass",!1,_r,!0),t("pollInterval",100),t("undoDepth",200,function(e,t){return e.doc.history.undoDepth=t}),t("historyEventDelay",1250),t("viewportMargin",10,function(e){return e.refresh()},!0),t("maxHighlightLength",1e4,_r,!0),t("moveInputWithCursor",!0,function(e,t){t||e.display.input.resetPosition()}),t("tabindex",null,function(e,t){return e.display.input.getField().tabIndex=t||""}),t("autofocus",null),t("direction","ltr",function(e,t){return e.doc.setDirection(t)},!0)}(Li),function(e){var t=e.optionHandlers,r=e.helpers={};e.prototype={constructor:e,focus:function(){window.focus(),this.display.input.focus()},setOption:function(e,r){var n=this.options,i=n[e];n[e]==r&&"mode"!=e||(n[e]=r,t.hasOwnProperty(e)&&xr(this,t[e])(this,r,i),pe(this,"optionChange",this,e))},getOption:function(e){return this.options[e]},getDoc:function(){return this.doc},addKeyMap:function(e,t){this.state.keyMaps[t?"push":"unshift"](ei(e))},removeKeyMap:function(e){for(var t=this.state.keyMaps,r=0;rr&&(ki(this,i.head.line,e,!0),r=i.head.line,n==this.doc.sel.primIndex&&sr(this));else{var o=i.from(),l=i.to(),s=Math.max(r,o.line);r=Math.min(this.lastLine(),l.line-(l.ch?0:1))+1;for(var a=s;a0&&pn(this.doc,n,new rl(o,u[n].to()),mo)}}}),getTokenAt:function(e,t){return Re(this,e,t)},getLineTokens:function(e,t){return Re(this,H(e),t,!0)},getTokenTypeAt:function(e){e=B(this.doc,e);var t,r=Fe(this,k(this.doc,e.line)),n=0,i=(r.length-1)/2,o=e.ch;if(0==o)t=r[2];else for(;;){var l=n+i>>1;if((l?r[2*l-1]:0)>=o)i=l;else{if(!(r[2*l+1]o&&(e=o,i=!0),n=k(this.doc,e)}else n=e;return Mt(this,n,{top:0,left:0},t||"page",r||i).top+(i?this.doc.height-se(n):0)},defaultTextHeight:function(){return zt(this.display)},defaultCharWidth:function(){return It(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(e,t,r,n,i){var o=this.display,l=(e=At(this,B(this.doc,e))).bottom,s=e.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),o.sizer.appendChild(t),"over"==n)l=e.top;else if("above"==n||"near"==n){var a=Math.max(o.wrapper.clientHeight,this.doc.height),u=Math.max(o.sizer.clientWidth,o.lineSpace.clientWidth);("above"==n||e.bottom+t.offsetHeight>a)&&e.top>t.offsetHeight?l=e.top-t.offsetHeight:e.bottom+t.offsetHeight<=a&&(l=e.bottom),s+t.offsetWidth>u&&(s=u-t.offsetWidth)}t.style.top=l+"px",t.style.left=t.style.right="","right"==i?(s=o.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?s=0:"middle"==i&&(s=(o.sizer.clientWidth-t.offsetWidth)/2),t.style.left=s+"px"),r&&function(e,t){var r=or(e,t);null!=r.scrollTop&&hr(e,r.scrollTop),null!=r.scrollLeft&&dr(e,r.scrollLeft)}(this,{left:s,top:l,right:s+t.offsetWidth,bottom:l+t.offsetHeight})},triggerOnKeyDown:Cr(fi),triggerOnKeyPress:Cr(pi),triggerOnKeyUp:di,triggerOnMouseDown:Cr(gi),execCommand:function(e){if(vl.hasOwnProperty(e))return vl[e].call(null,this)},triggerElectric:Cr(function(e){Oi(this,e)}),findPosH:function(e,t,r,n){var i=1;t<0&&(i=-1,t=-t);for(var o=B(this.doc,e),l=0;l0&&l(t.charAt(r-1));)--r;for(;n.5)&&Ut(this),pe(this,"refresh",this)}),swapDoc:Cr(function(e){var t=this.doc;return t.cm=null,Qr(this,e),St(this),this.display.input.reset(),ar(this,e.scrollLeft,e.scrollTop),this.curOp.forceScroll=!0,$e(this,"swapDoc",this,t),t}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},ye(e),e.registerHelper=function(t,n,i){r.hasOwnProperty(t)||(r[t]=e[t]={_global:[]}),r[t][n]=i},e.registerGlobalHelper=function(t,n,i,o){e.registerHelper(t,n,o),r[t]._global.push({pred:i,val:o})}}(Li);var Ol="iter insert remove copy getEditor constructor".split(" ");for(var Al in al.prototype)al.prototype.hasOwnProperty(Al)&&f(Ol,Al)<0&&(Li.prototype[Al]=function(e){return function(){return e.apply(this.doc,arguments)}}(al.prototype[Al]));return ye(al),Li.inputStyles={textarea:Nl,contenteditable:Ml},Li.defineMode=function(e){Li.defaults.mode||"null"==e||(Li.defaults.mode=e),function(e,t){arguments.length>2&&(t.dependencies=Array.prototype.slice.call(arguments,2)),Fo[e]=t}.apply(this,arguments)},Li.defineMIME=function(e,t){Po[e]=t},Li.defineMode("null",function(){return{token:function(e){return e.skipToEnd()}}}),Li.defineMIME("text/plain","null"),Li.defineExtension=function(e,t){Li.prototype[e]=t},Li.defineDocExtension=function(e,t){al.prototype[e]=t},Li.fromTextArea=function(e,t){function r(){e.value=a.getValue()}if(t=t?c(t):{},t.value=e.value,!t.tabindex&&e.tabIndex&&(t.tabindex=e.tabIndex),!t.placeholder&&e.placeholder&&(t.placeholder=e.placeholder),null==t.autofocus){var n=l();t.autofocus=n==e||null!=e.getAttribute("autofocus")&&n==document.body}var i;if(e.form&&(No(e.form,"submit",r),!t.leaveSubmitMethodAlone)){var o=e.form;i=o.submit;try{var s=o.submit=function(){r(),o.submit=i,o.submit(),o.submit=s}}catch(e){}}t.finishInit=function(t){t.save=r,t.getTextArea=function(){return e},t.toTextArea=function(){t.toTextArea=isNaN,r(),e.parentNode.removeChild(t.getWrapperElement()),e.style.display="",e.form&&(de(e.form,"submit",r),"function"==typeof e.form.submit&&(e.form.submit=i))}},e.style.display="none";var a=Li(function(t){return e.parentNode.insertBefore(t,e.nextSibling)},t);return a},function(e){e.off=de,e.on=No,e.wheelEventPixels=Rr,e.Doc=al,e.splitLines=Ao,e.countColumn=h,e.findColumn=d,e.isWordChar=b,e.Pass=vo,e.signal=pe,e.Line=Go,e.changeEnd=Vr,e.scrollbarModel=$o,e.Pos=H,e.cmpPos=F,e.modes=Fo,e.mimeModes=Po,e.resolveMode=Me,e.getMode=Ne,e.modeExtensions=Eo,e.extendMode=Oe,e.copyState=Ae,e.startState=De,e.innerMode=We,e.commands=vl,e.keyMap=gl,e.keyName=Jn,e.isModifierKey=Zn,e.lookupKey=$n,e.normalizeKeyMap=qn,e.StringStream=zo,e.SharedTextMarker=ll,e.TextMarker=ol,e.LineWidget=nl,e.e_preventDefault=be,e.e_stopPropagation=we,e.e_stop=Ce,e.addClass=s,e.contains=o,e.rmClass=uo,e.keyNames=hl}(Li),Li.version="5.32.0",Li}); \ No newline at end of file diff --git a/v1.5.4/jsonata-server/site/assets/js/javascript.min.js b/v1.5.4/jsonata-server/site/assets/js/javascript.min.js new file mode 100644 index 0000000..c045877 --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/js/javascript.min.js @@ -0,0 +1 @@ +!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";e.defineMode("javascript",function(t,r){function n(e,t,r){return Ve=e,Ae=r,t}function a(e,t){var r=e.next();if('"'==r||"'"==r)return t.tokenize=function(e){return function(t,r){var i,o=!1;if(Ie&&"@"==t.peek()&&t.match(Pe))return r.tokenize=a,n("jsonld-keyword","meta");for(;null!=(i=t.next())&&(i!=e||o);)o=!o&&"\\"==i;return o||(r.tokenize=a),n("string","string")}}(r),t.tokenize(e,t);if("."==r&&e.match(/^\d+(?:[eE][+\-]?\d+)?/))return n("number","number");if("."==r&&e.match(".."))return n("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(r))return n(r);if("="==r&&e.eat(">"))return n("=>","operator");if("0"==r&&e.eat(/x/i))return e.eatWhile(/[\da-f]/i),n("number","number");if("0"==r&&e.eat(/o/i))return e.eatWhile(/[0-7]/i),n("number","number");if("0"==r&&e.eat(/b/i))return e.eatWhile(/[01]/i),n("number","number");if(/\d/.test(r))return e.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),n("number","number");if("/"==r)return e.eat("*")?(t.tokenize=i,i(e,t)):e.eat("/")?(e.skipToEnd(),n("comment","comment")):Me(e,t,1)?(function(e){for(var t,r=!1,n=!1;null!=(t=e.next());){if(!r){if("/"==t&&!n)return;"["==t?n=!0:n&&"]"==t&&(n=!1)}r=!r&&"\\"==t}}(e),e.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),n("regexp","string-2")):(e.eat("="),n("operator","operator",e.current()));if("`"==r)return t.tokenize=o,o(e,t);if("#"==r)return e.skipToEnd(),n("error","error");if(Oe.test(r))return">"==r&&t.lexical&&">"==t.lexical.type||(e.eat("=")?"!"!=r&&"="!=r||e.eat("="):/[<>*+\-]/.test(r)&&(e.eat(r),">"==r&&e.eat(r))),n("operator","operator",e.current());if(Ce.test(r)){e.eatWhile(Ce);var c=e.current();if("."!=t.lastType){if(qe.propertyIsEnumerable(c)){var u=qe[c];return n(u.type,u.style,c)}if("async"==c&&e.match(/^(\s|\/\*.*?\*\/)*[\(\w]/,!1))return n("async","keyword",c)}return n("variable","variable",c)}}function i(e,t){for(var r,i=!1;r=e.next();){if("/"==r&&i){t.tokenize=a;break}i="*"==r}return n("comment","comment")}function o(e,t){for(var r,i=!1;null!=(r=e.next());){if(!i&&("`"==r||"$"==r&&e.eat("{"))){t.tokenize=a;break}i=!i&&"\\"==r}return n("quasi","string-2",e.current())}function c(e,t){t.fatArrowAt&&(t.fatArrowAt=null);var r=e.string.indexOf("=>",e.start);if(!(r<0)){if($e){var n=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(e.string.slice(e.start,r));n&&(r=n.index)}for(var a=0,i=!1,o=r-1;o>=0;--o){var c=e.string.charAt(o),u=Se.indexOf(c);if(u>=0&&u<3){if(!a){++o;break}if(0==--a){"("==c&&(i=!0);break}}else if(u>=3&&u<6)++a;else if(Ce.test(c))i=!0;else{if(/["'\/]/.test(c))return;if(i&&!a){++o;break}}}i&&!a&&(t.fatArrowAt=o)}}function u(e,t,r,n,a,i){this.indented=e,this.column=t,this.type=r,this.prev=a,this.info=i,null!=n&&(this.align=n)}function s(e,t){for(var r=e.localVars;r;r=r.next)if(r.name==t)return!0;for(var n=e.context;n;n=n.prev)for(r=n.vars;r;r=r.next)if(r.name==t)return!0}function f(){for(var e=arguments.length-1;e>=0;e--)Ne.cc.push(arguments[e])}function l(){return f.apply(null,arguments),!0}function d(e){function t(t){for(var r=t;r;r=r.next)if(r.name==e)return!0;return!1}var n=Ne.state;if(Ne.marked="def",n.context){if(t(n.localVars))return;n.localVars={name:e,next:n.localVars}}else{if(t(n.globalVars))return;r.globalVars&&(n.globalVars={name:e,next:n.globalVars})}}function p(e){return"public"==e||"private"==e||"protected"==e||"abstract"==e||"readonly"==e}function m(){Ne.state.context={prev:Ne.state.context,vars:Ne.state.localVars},Ne.state.localVars=Ue}function k(){Ne.state.localVars=Ne.state.context.vars,Ne.state.context=Ne.state.context.prev}function v(e,t){var r=function(){var r=Ne.state,n=r.indented;if("stat"==r.lexical.type)n=r.lexical.indented;else for(var a=r.lexical;a&&")"==a.type&&a.align;a=a.prev)n=a.indented;r.lexical=new u(n,Ne.stream.column(),e,null,r.lexical,t)};return r.lex=!0,r}function y(){var e=Ne.state;e.lexical.prev&&(")"==e.lexical.type&&(e.indented=e.lexical.indented),e.lexical=e.lexical.prev)}function b(e){function t(r){return r==e?l():";"==e?f():l(t)}return t}function w(e,t){return"var"==e?l(v("vardef",t.length),Z,b(";"),y):"keyword a"==e?l(v("form"),g,w,y):"keyword b"==e?l(v("form"),w,y):"keyword d"==e?Ne.stream.match(/^\s*$/,!1)?l():l(v("stat"),M,b(";"),y):"debugger"==e?l(b(";")):"{"==e?l(v("}"),B,y):";"==e?l():"if"==e?("else"==Ne.state.lexical.info&&Ne.state.cc[Ne.state.cc.length-1]==y&&Ne.state.cc.pop()(),l(v("form"),g,w,y,ne)):"function"==e?l(se):"for"==e?l(v("form"),ae,w,y):"class"==e||$e&&"interface"==t?(Ne.marked="keyword",l(v("form"),de,y)):"variable"==e?$e&&"type"==t?(Ne.marked="keyword",l(G,b("operator"),G,b(";"))):$e&&"declare"==t?(Ne.marked="keyword",l(w)):$e&&("module"==t||"enum"==t)&&Ne.stream.match(/^\s*\w/,!1)?(Ne.marked="keyword",l(v("form"),_,b("{"),v("}"),B,y,y)):$e&&"namespace"==t?(Ne.marked="keyword",l(v("form"),x,B,y)):l(v("stat"),q):"switch"==e?l(v("form"),g,b("{"),v("}","switch"),B,y,y):"case"==e?l(x,b(":")):"default"==e?l(b(":")):"catch"==e?l(v("form"),m,b("("),fe,b(")"),w,y,k):"export"==e?l(v("stat"),ve,y):"import"==e?l(v("stat"),be,y):"async"==e?l(w):"@"==t?l(x,w):f(v("stat"),x,b(";"),y)}function x(e,t){return j(e,t,!1)}function h(e,t){return j(e,t,!0)}function g(e){return"("!=e?f():l(v(")"),x,b(")"),y)}function j(e,t,r){if(Ne.state.fatArrowAt==Ne.stream.start){var n=r?T:I;if("("==e)return l(m,v(")"),N(fe,")"),y,b("=>"),n,k);if("variable"==e)return f(m,_,b("=>"),n,k)}var a=r?A:V;return We.hasOwnProperty(e)?l(a):"function"==e?l(se,a):"class"==e||$e&&"interface"==t?(Ne.marked="keyword",l(v("form"),le,y)):"keyword c"==e||"async"==e?l(r?h:x):"("==e?l(v(")"),M,b(")"),y,a):"operator"==e||"spread"==e?l(r?h:x):"["==e?l(v("]"),je,y,a):"{"==e?U(P,"}",null,a):"quasi"==e?f(E,a):"new"==e?l(function(e){return function(t){return"."==t?l(e?C:$):"variable"==t&&$e?l(R,e?A:V):f(e?h:x)}}(r)):l()}function M(e){return e.match(/[;\}\)\],]/)?f():f(x)}function V(e,t){return","==e?l(x):A(e,t,!1)}function A(e,t,r){var n=0==r?V:A,a=0==r?x:h;return"=>"==e?l(m,r?T:I,k):"operator"==e?/\+\+|--/.test(t)||$e&&"!"==t?l(n):$e&&"<"==t&&Ne.stream.match(/^([^>]|<.*?>)*>\s*\(/,!1)?l(v(">"),N(G,">"),y,n):"?"==t?l(x,b(":"),a):l(a):"quasi"==e?f(E,n):";"!=e?"("==e?U(h,")","call",n):"."==e?l(O,n):"["==e?l(v("]"),M,b("]"),y,n):$e&&"as"==t?(Ne.marked="keyword",l(G,n)):"regexp"==e?(Ne.state.lastType=Ne.marked="operator",Ne.stream.backUp(Ne.stream.pos-Ne.stream.start-1),l(a)):void 0:void 0}function E(e,t){return"quasi"!=e?f():"${"!=t.slice(t.length-2)?l(E):l(x,z)}function z(e){if("}"==e)return Ne.marked="string-2",Ne.state.tokenize=o,l(E)}function I(e){return c(Ne.stream,Ne.state),f("{"==e?w:x)}function T(e){return c(Ne.stream,Ne.state),f("{"==e?w:h)}function $(e,t){if("target"==t)return Ne.marked="keyword",l(V)}function C(e,t){if("target"==t)return Ne.marked="keyword",l(A)}function q(e){return":"==e?l(y,w):f(V,b(";"),y)}function O(e){if("variable"==e)return Ne.marked="property",l()}function P(e,t){if("async"==e)return Ne.marked="property",l(P);if("variable"==e||"keyword"==Ne.style){if(Ne.marked="property","get"==t||"set"==t)return l(S);var r;return $e&&Ne.state.fatArrowAt==Ne.stream.start&&(r=Ne.stream.match(/^\s*:\s*/,!1))&&(Ne.state.fatArrowAt=Ne.stream.pos+r[0].length),l(W)}return"number"==e||"string"==e?(Ne.marked=Ie?"property":Ne.style+" property",l(W)):"jsonld-keyword"==e?l(W):$e&&p(t)?(Ne.marked="keyword",l(P)):"["==e?l(x,H,b("]"),W):"spread"==e?l(h,W):"*"==t?(Ne.marked="keyword",l(P)):":"==e?f(W):void 0}function S(e){return"variable"!=e?f(W):(Ne.marked="property",l(se))}function W(e){return":"==e?l(h):"("==e?f(se):void 0}function N(e,t,r){function n(a,i){if(r?r.indexOf(a)>-1:","==a){var o=Ne.state.lexical;return"call"==o.info&&(o.pos=(o.pos||0)+1),l(function(r,n){return r==t||n==t?f():f(e)},n)}return a==t||i==t?l():l(b(t))}return function(r,a){return r==t||a==t?l():f(e,n)}}function U(e,t,r){for(var n=3;n"==e)return l(G)}function K(e,t){return"variable"==e||"keyword"==Ne.style?(Ne.marked="property",l(K)):"?"==t?l(K):":"==e?l(G):"["==e?l(x,H,b("]"),K):void 0}function L(e){return"variable"==e?l(L):":"==e?l(G):void 0}function Q(e,t){return"<"==t?l(v(">"),N(G,">"),y,Q):"|"==t||"."==e?l(G):"["==e?l(b("]"),Q):"extends"==t||"implements"==t?(Ne.marked="keyword",l(G)):void 0}function R(e,t){if("<"==t)return l(v(">"),N(G,">"),y,Q)}function X(){return f(G,Y)}function Y(e,t){if("="==t)return l(G)}function Z(){return f(_,H,te,re)}function _(e,t){return $e&&p(t)?(Ne.marked="keyword",l(_)):"variable"==e?(d(t),l()):"spread"==e?l(_):"["==e?U(_,"]"):"{"==e?U(ee,"}"):void 0}function ee(e,t){return"variable"!=e||Ne.stream.match(/^\s*:/,!1)?("variable"==e&&(Ne.marked="property"),"spread"==e?l(_):"}"==e?f():l(b(":"),_,te)):(d(t),l(te))}function te(e,t){if("="==t)return l(h)}function re(e){if(","==e)return l(Z)}function ne(e,t){if("keyword b"==e&&"else"==t)return l(v("form","else"),w,y)}function ae(e){if("("==e)return l(v(")"),ie,b(")"),y)}function ie(e){return"var"==e?l(Z,b(";"),ce):";"==e?l(ce):"variable"==e?l(oe):f(x,b(";"),ce)}function oe(e,t){return"in"==t||"of"==t?(Ne.marked="keyword",l(x)):l(V,ce)}function ce(e,t){return";"==e?l(ue):"in"==t||"of"==t?(Ne.marked="keyword",l(x)):f(x,b(";"),ue)}function ue(e){")"!=e&&l(x)}function se(e,t){return"*"==t?(Ne.marked="keyword",l(se)):"variable"==e?(d(t),l(se)):"("==e?l(m,v(")"),N(fe,")"),y,D,w,k):$e&&"<"==t?l(v(">"),N(X,">"),y,se):void 0}function fe(e,t){return"@"==t&&l(x,fe),"spread"==e?l(fe):$e&&p(t)?(Ne.marked="keyword",l(fe)):f(_,H,te)}function le(e,t){return"variable"==e?de(e,t):pe(e,t)}function de(e,t){if("variable"==e)return d(t),l(pe)}function pe(e,t){return"<"==t?l(v(">"),N(X,">"),y,pe):"extends"==t||"implements"==t||$e&&","==e?l($e?G:x,pe):"{"==e?l(v("}"),me,y):void 0}function me(e,t){return"async"==e||"variable"==e&&("static"==t||"get"==t||"set"==t||$e&&p(t))&&Ne.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(Ne.marked="keyword",l(me)):"variable"==e||"keyword"==Ne.style?(Ne.marked="property",l($e?ke:se,me)):"["==e?l(x,H,b("]"),$e?ke:se,me):"*"==t?(Ne.marked="keyword",l(me)):";"==e?l(me):"}"==e?l():"@"==t?l(x,me):void 0}function ke(e,t){return"?"==t?l(ke):":"==e?l(G,te):"="==t?l(h):f(se)}function ve(e,t){return"*"==t?(Ne.marked="keyword",l(ge,b(";"))):"default"==t?(Ne.marked="keyword",l(x,b(";"))):"{"==e?l(N(ye,"}"),ge,b(";")):f(w)}function ye(e,t){return"as"==t?(Ne.marked="keyword",l(b("variable"))):"variable"==e?f(h,ye):void 0}function be(e){return"string"==e?l():f(we,xe,ge)}function we(e,t){return"{"==e?U(we,"}"):("variable"==e&&d(t),"*"==t&&(Ne.marked="keyword"),l(he))}function xe(e){if(","==e)return l(we,xe)}function he(e,t){if("as"==t)return Ne.marked="keyword",l(we)}function ge(e,t){if("from"==t)return Ne.marked="keyword",l(x)}function je(e){return"]"==e?l():f(N(h,"]"))}function Me(e,t,r){return t.tokenize==a&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(t.lastType)||"quasi"==t.lastType&&/\{\s*$/.test(e.string.slice(0,e.pos-(r||0)))}var Ve,Ae,Ee=t.indentUnit,ze=r.statementIndent,Ie=r.jsonld,Te=r.json||Ie,$e=r.typescript,Ce=r.wordCharacters||/[\w$\xa1-\uffff]/,qe=function(){function e(e){return{type:e,style:"keyword"}}var t=e("keyword a"),r=e("keyword b"),n=e("keyword c"),a=e("keyword d"),i=e("operator"),o={type:"atom",style:"atom"};return{if:e("if"),while:t,with:t,else:r,do:r,try:r,finally:r,return:a,break:a,continue:a,new:e("new"),delete:n,void:n,throw:n,debugger:e("debugger"),var:e("var"),const:e("var"),let:e("var"),function:e("function"),catch:e("catch"),for:e("for"),switch:e("switch"),case:e("case"),default:e("default"),in:i,typeof:i,instanceof:i,true:o,false:o,null:o,undefined:o,NaN:o,Infinity:o,this:e("this"),class:e("class"),super:e("atom"),yield:n,export:e("export"),import:e("import"),extends:n,await:n}}(),Oe=/[+\-*&%=<>!?|~^@]/,Pe=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,Se="([{}])",We={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,"jsonld-keyword":!0},Ne={state:null,column:null,marked:null,cc:null},Ue={name:"this",next:{name:"arguments"}};return y.lex=!0,{startState:function(e){var t={tokenize:a,lastType:"sof",cc:[],lexical:new u((e||0)-Ee,0,"block",!1),localVars:r.localVars,context:r.localVars&&{vars:r.localVars},indented:e||0};return r.globalVars&&"object"==typeof r.globalVars&&(t.globalVars=r.globalVars),t},token:function(e,t){if(e.sol()&&(t.lexical.hasOwnProperty("align")||(t.lexical.align=!1),t.indented=e.indentation(),c(e,t)),t.tokenize!=i&&e.eatSpace())return null;var r=t.tokenize(e,t);return"comment"==Ve?r:(t.lastType="operator"!=Ve||"++"!=Ae&&"--"!=Ae?Ve:"incdec",function(e,t,r,n,a){var i=e.cc;for(Ne.state=e,Ne.stream=a,Ne.marked=null,Ne.cc=i,Ne.style=t,e.lexical.hasOwnProperty("align")||(e.lexical.align=!0);;)if((i.length?i.pop():Te?x:w)(r,n)){for(;i.length&&i[i.length-1].lex;)i.pop()();return Ne.marked?Ne.marked:"variable"==r&&s(e,n)?"variable-2":t}}(t,r,Ve,Ae,e))},indent:function(t,n){if(t.tokenize==i)return e.Pass;if(t.tokenize!=a)return 0;var o,c=n&&n.charAt(0),u=t.lexical;if(!/^\s*else\b/.test(n))for(var s=t.cc.length-1;s>=0;--s){var f=t.cc[s];if(f==y)u=u.prev;else if(f!=ne)break}for(;("stat"==u.type||"form"==u.type)&&("}"==c||(o=t.cc[t.cc.length-1])&&(o==V||o==A)&&!/^[,\.=+\-*:?[\(]/.test(n));)u=u.prev;ze&&")"==u.type&&"stat"==u.prev.type&&(u=u.prev);var l=u.type,d=c==l;return"vardef"==l?u.indented+("operator"==t.lastType||","==t.lastType?u.info+1:0):"form"==l&&"{"==c?u.indented:"form"==l?u.indented+Ee:"stat"==l?u.indented+(function(e,t){return"operator"==e.lastType||","==e.lastType||Oe.test(t.charAt(0))||/[,.]/.test(t.charAt(0))}(t,n)?ze||Ee:0):"switch"!=u.info||d||0==r.doubleIndentSwitch?u.align?u.column+(d?0:1):u.indented+(d?0:Ee):u.indented+(/^(?:case|default)\b/.test(n)?Ee:2*Ee)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:Te?null:"/*",blockCommentEnd:Te?null:"*/",blockCommentContinue:Te?null:" * ",lineComment:Te?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:Te?"json":"javascript",jsonldMode:Ie,jsonMode:Te,expressionAllowed:Me,skipExpression:function(e){var t=e.cc[e.cc.length-1];t!=x&&t!=h||e.cc.pop()}}}),e.registerHelper("wordChars","javascript",/[\w$]/),e.defineMIME("text/javascript","javascript"),e.defineMIME("text/ecmascript","javascript"),e.defineMIME("application/javascript","javascript"),e.defineMIME("application/x-javascript","javascript"),e.defineMIME("application/ecmascript","javascript"),e.defineMIME("application/json",{name:"javascript",json:!0}),e.defineMIME("application/x-json",{name:"javascript",json:!0}),e.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),e.defineMIME("text/typescript",{name:"javascript",typescript:!0}),e.defineMIME("application/typescript",{name:"javascript",typescript:!0})}); \ No newline at end of file diff --git a/v1.5.4/jsonata-server/site/assets/js/jsonata-codemirror.js b/v1.5.4/jsonata-server/site/assets/js/jsonata-codemirror.js new file mode 100644 index 0000000..14a7ccd --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/js/jsonata-codemirror.js @@ -0,0 +1,299 @@ + +// CodeMirror syntax highlighting rules for JSONata. + +CodeMirror.defineMode("jsonata", function(config, parserConfig) { + var templateMode = parserConfig.template; + var jsonata = parserConfig.jsonata; + + const operators = { + '.': 75, + '[': 80, + ']': 0, + '{': 70, + '}': 0, + '(': 80, + ')': 0, + ',': 0, + '@': 75, + '#': 70, + ';': 80, + ':': 80, + '?': 20, + '+': 50, + '-': 50, + '*': 60, + '/': 60, + '%': 60, + '|': 20, + '=': 40, + '<': 40, + '>': 40, + '`': 80, + '**': 60, + '..': 20, + ':=': 30, + '!=': 40, + '<=': 40, + '>=': 40, + 'and': 30, + 'or': 25, + '||' : 50, + '!': 0 // not an operator, but needed as a stop character for name tokens + }; + + const escapes = { // JSON string escape sequences - see json.org + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }; + + var tokenizer = function(path) { + var position = 0; + var length = path.length; + + var create = function(type, value) { + var obj = { type: type, value: value, position: position}; + return obj; + }; + + var next = function() { + if(position >= length) return null; + var currentChar = path.charAt(position); + // skip whitespace + while(position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = path.charAt(position); + } + // handle double-char operators + if(currentChar === '.' && path.charAt(position+1) === '.') { + // double-dot .. range operator + position += 2; + return create('operator', '..'); + } + if(currentChar === '|' && path.charAt(position+1) === '|') { + // double-pipe || string concatenator + position += 2; + return create('operator', '||'); + } + if(currentChar === ':' && path.charAt(position+1) === '=') { + // := assignment + position += 2; + return create('operator', ':='); + } + if(currentChar === '!' && path.charAt(position+1) === '=') { + // != + position += 2; + return create('operator', '!='); + } + if(currentChar === '>' && path.charAt(position+1) === '=') { + // >= + position += 2; + return create('operator', '>='); + } + if(currentChar === '<' && path.charAt(position+1) === '=') { + // <= + position += 2; + return create('operator', '<='); + } + if(currentChar === '*' && path.charAt(position+1) === '*') { + // ** descendant wildcard + position += 2; + return create('operator', '**'); + } + // test for operators + if(operators.hasOwnProperty(currentChar)) { + position++; + return create('operator', currentChar); + } + // test for string literals + if(currentChar === '"' || currentChar === "'") { + var quoteType = currentChar; + // double quoted string literal - find end of string + position++; + var qstr = ""; + while(position < length) { + currentChar = path.charAt(position); + if(currentChar === '\\') { // escape sequence + position++; + currentChar = path.charAt(position); + if(escapes.hasOwnProperty(currentChar)) { + qstr += escapes[currentChar]; + } else if(currentChar === 'u') { + // \u should be followed by 4 hex digits + var octets = path.substr(position+1, 4); + if(/^[0-9a-fA-F]+$/.test(octets)) { + var codepoint = parseInt(octets, 16); + qstr += String.fromCharCode(codepoint); + position += 4; + } else { + throw new Error('The escape sequence \\u must be followed by 4 hex digits at column ' + position); + } + } else { + // illegal escape sequence + throw new Error('unsupported escape sequence: \\' + currentChar + ' at column ' + position); + } + } else if(currentChar === quoteType) { + position++; + return create('string', qstr); + } else { + qstr += currentChar; + } + position++; + } + throw new Error('no terminating quote found in string literal starting at column ' + position); + } + // test for numbers + var numregex = /^-?(0|([1-9][0-9]*))(\.[0-9]+)?([Ee][-+]?[0-9]+)?/; + var match = numregex.exec(path.substring(position)); + if(match !== null) { + var num = parseFloat(match[0]); + if(!isNaN(num) && isFinite(num)) { + position += match[0].length; + return create('number', num); + } else { + throw new Error('Number out of range: ' + match[0]); + } + } + // test for names + var i = position; + var ch; + var name; + while(true) { + ch = path.charAt(i); + if(i == length || ' \t\n\r\v'.indexOf(ch) > -1 || operators.hasOwnProperty(ch)) { + if(path.charAt(position) === '$') { + // variable reference + name = path.substring(position + 1, i); + position = i; + return create('variable', name); + } else { + name = path.substring(position, i); + position = i; + switch(name) { + case 'and': + case 'or': + return create('operator', name); + case 'true': + return create('value', true); + case 'false': + return create('value', false); + case 'null': + return create('value', null); + default: + if(position == length && name === '') { + // whitespace at end of input + return null; + } + return create('name', name); + } + } + } else { + i++; + } + } + }; + + return next; + }; + + var templatizer = function(text) { + var position = 0; + var length = text.length; + + var create = function(type, value) { + var obj = { type: type, value: value, position: position}; + return obj; + }; + + var next = function() { + if(position >= length) return null; + var currentChar = text.charAt(position); + // skip whitespace + while(position < length && ' \t\n\r\v'.indexOf(currentChar) > -1) { + position++; + currentChar = text.charAt(position); + } + + if(currentChar === '{' && text.charAt(position+1) === '{') { + // found {{ + position += 2; + // parse what follows using the jsonata parser + var rest = text.substring(position); + try { + jsonata.parser(rest); + // if we get here, we parsed to the end of the buffer with no closing handlebars + position += rest.length; + return create('variable'); + } catch (err) { + if (err.token === '(end)') { + position = length; + return create('variable'); + } + if (rest.charAt(err.position - 1) != "}" || rest.charAt(err.position) != "}") { + // no closing handlbars + position += err.position; + return create('variable'); + } + position += err.position + 1; + return create('variable'); + } + } else { + // search forward for next {{ + position = text.indexOf("{{", position); + if(position != -1) { + return create('operator'); + } + position = length; + return create('operator'); + } + + }; + + return next; + }; + + var TOKEN_NAMES = { + 'operator': 'operator', + 'variable': 'string-2', + 'string': 'string', + 'number': 'number', + 'value': 'keyword', + 'name': 'attribute' + }; + + var currentIndent = 0; + + return { + token: function(stream) { + var lexer; + if(templateMode) { + lexer = templatizer(stream.string.substr(stream.pos)); + } else { + lexer = tokenizer(stream.string.substr(stream.pos)); + } + var token; + try { + token = lexer(); + } catch(err) { + token = null; + } + if(token === null) { + stream.skipToEnd(); + return null; + } + var length = token.position; + while(length > 0) { + stream.next(); + length--; + } + + var style = TOKEN_NAMES[token.type]; + return style; + } + }; +}); diff --git a/v1.5.4/jsonata-server/site/assets/js/split.min.js b/v1.5.4/jsonata-server/site/assets/js/split.min.js new file mode 100644 index 0000000..2954e15 --- /dev/null +++ b/v1.5.4/jsonata-server/site/assets/js/split.min.js @@ -0,0 +1,2 @@ +/*! Split.js - v1.3.5 */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var e=window,t=e.document,n="addEventListener",i="removeEventListener",r="getBoundingClientRect",s=function(){return!1},o=e.attachEvent&&!e[n],a=["","-webkit-","-moz-","-o-"].filter(function(e){var n=t.createElement("div");return n.style.cssText="width:"+e+"calc(9px)",!!n.style.length}).shift()+"calc",l=function(e){return"string"==typeof e||e instanceof String?t.querySelector(e):e};return function(u,c){function z(e,t,n){var i=A(y,t,n);Object.keys(i).forEach(function(t){return e.style[t]=i[t]})}function h(e,t){var n=B(y,t);Object.keys(n).forEach(function(t){return e.style[t]=n[t]})}function f(e){var t=E[this.a],n=E[this.b],i=t.size+n.size;t.size=e/this.size*i,n.size=i-e/this.size*i,z(t.element,t.size,this.aGutterSize),z(n.element,n.size,this.bGutterSize)}function m(e){var t;this.dragging&&((t="touches"in e?e.touches[0][b]-this.start:e[b]-this.start)<=E[this.a].minSize+M+this.aGutterSize?t=E[this.a].minSize+this.aGutterSize:t>=this.size-(E[this.b].minSize+M+this.bGutterSize)&&(t=this.size-(E[this.b].minSize+this.bGutterSize)),f.call(this,t),c.onDrag&&c.onDrag())}function g(){var e=E[this.a].element,t=E[this.b].element;this.size=e[r]()[y]+t[r]()[y]+this.aGutterSize+this.bGutterSize,this.start=e[r]()[G]}function d(){var t=this,n=E[t.a].element,r=E[t.b].element;t.dragging&&c.onDragEnd&&c.onDragEnd(),t.dragging=!1,e[i]("mouseup",t.stop),e[i]("touchend",t.stop),e[i]("touchcancel",t.stop),t.parent[i]("mousemove",t.move),t.parent[i]("touchmove",t.move),delete t.stop,delete t.move,n[i]("selectstart",s),n[i]("dragstart",s),r[i]("selectstart",s),r[i]("dragstart",s),n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.style.userSelect="",r.style.webkitUserSelect="",r.style.MozUserSelect="",r.style.pointerEvents="",t.gutter.style.cursor="",t.parent.style.cursor=""}function S(t){var i=this,r=E[i.a].element,o=E[i.b].element;!i.dragging&&c.onDragStart&&c.onDragStart(),t.preventDefault(),i.dragging=!0,i.move=m.bind(i),i.stop=d.bind(i),e[n]("mouseup",i.stop),e[n]("touchend",i.stop),e[n]("touchcancel",i.stop),i.parent[n]("mousemove",i.move),i.parent[n]("touchmove",i.move),r[n]("selectstart",s),r[n]("dragstart",s),o[n]("selectstart",s),o[n]("dragstart",s),r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",o.style.userSelect="none",o.style.webkitUserSelect="none",o.style.MozUserSelect="none",o.style.pointerEvents="none",i.gutter.style.cursor=j,i.parent.style.cursor=j,g.call(i)}function v(e){e.forEach(function(t,n){if(n>0){var i=F[n-1],r=E[i.a],s=E[i.b];r.size=e[n-1],s.size=t,z(r.element,r.size,i.aGutterSize),z(s.element,s.size,i.bGutterSize)}})}function p(){F.forEach(function(e){e.parent.removeChild(e.gutter),E[e.a].element.style[y]="",E[e.b].element.style[y]=""})}void 0===c&&(c={});var y,b,G,E,w=l(u[0]).parentNode,D=e.getComputedStyle(w).flexDirection,U=c.sizes||u.map(function(){return 100/u.length}),k=void 0!==c.minSize?c.minSize:100,x=Array.isArray(k)?k:u.map(function(){return k}),L=void 0!==c.gutterSize?c.gutterSize:10,M=void 0!==c.snapOffset?c.snapOffset:30,O=c.direction||"horizontal",j=c.cursor||("horizontal"===O?"ew-resize":"ns-resize"),C=c.gutter||function(e,n){var i=t.createElement("div");return i.className="gutter gutter-"+n,i},A=c.elementStyle||function(e,t,n){var i={};return"string"==typeof t||t instanceof String?i[e]=t:i[e]=o?t+"%":a+"("+t+"% - "+n+"px)",i},B=c.gutterStyle||function(e,t){return n={},n[e]=t+"px",n;var n};"horizontal"===O?(y="width","clientWidth",b="clientX",G="left","paddingLeft"):"vertical"===O&&(y="height","clientHeight",b="clientY",G="top","paddingTop");var F=[];return E=u.map(function(e,t){var i,s={element:l(e),size:U[t],minSize:x[t]};if(t>0&&(i={a:t-1,b:t,dragging:!1,isFirst:1===t,isLast:t===u.length-1,direction:O,parent:w},i.aGutterSize=L,i.bGutterSize=L,i.isFirst&&(i.aGutterSize=L/2),i.isLast&&(i.bGutterSize=L/2),"row-reverse"===D||"column-reverse"===D)){var a=i.a;i.a=i.b,i.b=a}if(!o&&t>0){var c=C(t,O);h(c,L),c[n]("mousedown",S.bind(i)),c[n]("touchstart",S.bind(i)),w.insertBefore(c,s.element),i.gutter=c}0===t||t===u.length-1?z(s.element,s.size,L/2):z(s.element,s.size,L);var f=s.element[r]()[y];return f0&&F.push(i),s}),o?{setSizes:v,destroy:p}:{setSizes:v,getSizes:function(){return E.map(function(e){return e.size})},collapse:function(e){if(e===F.length){var t=F[e-1];g.call(t),o||f.call(t,t.size-t.bGutterSize)}else{var n=F[e];g.call(n),o||f.call(n,n.aGutterSize)}},destroy:p}}}); \ No newline at end of file diff --git a/v1.5.4/jsonata-server/site/favicon.ico b/v1.5.4/jsonata-server/site/favicon.ico new file mode 100644 index 0000000..6aa72e2 Binary files /dev/null and b/v1.5.4/jsonata-server/site/favicon.ico differ diff --git a/v1.5.4/jsonata-server/site/index.html b/v1.5.4/jsonata-server/site/index.html new file mode 100644 index 0000000..ae9847f --- /dev/null +++ b/v1.5.4/jsonata-server/site/index.html @@ -0,0 +1,312 @@ + + + + +JSONata Server + + + + + + + + + + + + +
+

JSONata Server

+ +
+
+
+ +
+ +
+ + + diff --git a/v1.5.4/jsonata-test/.gitignore b/v1.5.4/jsonata-test/.gitignore new file mode 100644 index 0000000..f217b61 --- /dev/null +++ b/v1.5.4/jsonata-test/.gitignore @@ -0,0 +1,2 @@ +# Executables +jsonata-test diff --git a/v1.5.4/jsonata-test/README.md b/v1.5.4/jsonata-test/README.md new file mode 100644 index 0000000..6572577 --- /dev/null +++ b/v1.5.4/jsonata-test/README.md @@ -0,0 +1,46 @@ +# JSONata Test + +A CLI tool for running jsonata-go against the [JSONata test suite](https://github.com/jsonata-js/jsonata/tree/master/test/test-suite). + +## Install + + go install github.com/blues/jsonata-test + +## Usage + +1. Clone the [jsonata-js](https://github.com/jsonata-js/jsonata) repository to your local machine. + + git clone https://github.com/jsonata-js/jsonata + +2. To access a particular version of the test suite, check out the relevant branch, e.g. + + git checkout v1.8.4 + +3. Run the test tool, specifying the location of the JSONata `test-suite` directory in the command line, e.g. + + jsonata-test ~/projects/jsonata/test/test-suite + +## Known issues + +This library was originally developed against jsonata-js 1.5 and has thus far implemented a subset of features from newer version of that library. You can see potential differences by looking at the [jsonata-js changelog](https://github.com/jsonata-js/jsonata/blob/master/CHANGELOG.md). + +While most tests pass from jsonata-js 1.5 do pass, currently there are **1598 tests** in the JSONata 1.8.4 test suite. Running against the 1.8.4 test suite results in **307 failing tests**. The failures are mostly related to functionality in newer versions of JSONata that this library does not yet implement. The outstanding issues are summarised below, split into the categories "Won't fix", "To be fixed" and "To be investigated". + +### Won't Fix + +#### Regex matches on zero-length strings + +jsonata-js throws an error if a regular expression matches a zero length string. It does this because repeatedly calling JavaScript's [Regexp.exec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec) method can cause an infinite loop if it matches a zero length string. Go's regex handling doesn't have this problem so there's no real need to take the precaution. + +### To be investigated + +#### Null handling + +jsonata-go uses `*interface{}(nil)` to represent the JSON value null. This is a nil value but it's distinguishable from `interface{}(nil)` (non-pointer) which indicates that a value does not exist. That's useful inside JSONata but Go's json package does not make that distinction. Some tests fail because jsonata-go returns a differently-typed nil. I don't *think* this will cause any practical problems (because the returned value still equals nil) but I'd like to look into it more. + +#### Rounding when converting numbers to strings + +JSONata's `$string()` function converts objects to their JSON representation. In jsonata-js, any numbers in the object are rounded to 15 decimal places so that floating point errors are discarded. The rounding takes place in a callback function passed to JavaScript's JSON encoding function. I haven't found a way to replicate this in Go. It would be a nice feature to have though. + +### To be fixed +Some functions like `$formatInteger()` and `$parseInteger()` from newer versions of the jsonata-js library are not yet implemented. diff --git a/v1.5.4/jsonata-test/main.go b/v1.5.4/jsonata-test/main.go new file mode 100644 index 0000000..2aa9fd1 --- /dev/null +++ b/v1.5.4/jsonata-test/main.go @@ -0,0 +1,358 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + + jsonata "github.com/blues/jsonata-go/v1.5.4" + types "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type testCase struct { + Expr string + ExprFile string `json:"expr-file"` + Category string + Data interface{} + Dataset string + Description string + TimeLimit int + Depth int + Bindings map[string]interface{} + Result interface{} + Undefined bool + Error string `json:"code"` + Token string + Unordered bool +} + +func main() { + var group string + var verbose bool + + flag.BoolVar(&verbose, "verbose", false, "verbose output") + flag.StringVar(&group, "group", "", "restrict to one or more test groups") + flag.Parse() + + if flag.NArg() != 1 { + fmt.Fprintln(os.Stderr, "Syntax: jsonata-test [options] ") + os.Exit(1) + } + + root := flag.Arg(0) + testdir := filepath.Join(root, "groups") + datadir := filepath.Join(root, "datasets") + + err := run(testdir, datadir, group) + if err != nil { + fmt.Fprintf(os.Stderr, "Error while running: %s\n", err) + os.Exit(2) + } + + fmt.Fprintln(os.Stdout, "OK") +} + +// run runs all test cases +func run(testdir string, datadir string, filter string) error { + var numPassed, numFailed int + err := filepath.Walk(testdir, func(path string, info os.FileInfo, walkFnErr error) error { + var dirName string + + if info.IsDir() { + if path == testdir { + return nil + } + dirName = filepath.Base(path) + if filter != "" && !strings.Contains(dirName, filter) { + return filepath.SkipDir + } + return nil + } + + // Ignore files with names ending with .jsonata, these + // are not test cases + if filepath.Ext(path) == ".jsonata" { + return nil + } + + testCases, err := loadTestCases(path) + if err != nil { + return fmt.Errorf("walk %s: %s", path, err) + } + + for _, testCase := range testCases { + failed, err := runTest(testCase, datadir, path) + + if err != nil { + return err + } + if failed { + numFailed++ + } else { + numPassed++ + } + } + + return nil + }) + + if err != nil { + return fmt.Errorf("walk %s: ", err) + } + + fmt.Fprintln(os.Stdout) + fmt.Fprintln(os.Stdout, numPassed, "passed", numFailed, "failed") + return nil +} + +// runTest runs a single test case +func runTest(tc testCase, dataDir string, path string) (bool, error) { + // Some tests assume JavaScript-style object traversal, + // these are marked as unordered and can be skipped + // See https://github.com/jsonata-js/jsonata/issues/179 + if tc.Unordered { + return false, nil + } + + if tc.TimeLimit != 0 { + return false, nil + } + + // If this test has an associated dataset, load it + data := tc.Data + if tc.Dataset != "" { + var dest interface{} + err := readJSONFile(filepath.Join(dataDir, tc.Dataset+".json"), &dest) + if err != nil { + return false, err + } + data = dest + } + + var failed bool + expr, unQuoted := replaceQuotesInPaths(tc.Expr) + got, _ := eval(expr, tc.Bindings, data) + + if !equalResults(got, tc.Result) { + failed = true + printTestCase(os.Stderr, tc, strings.TrimSuffix(filepath.Base(path), ".json")) + fmt.Fprintf(os.Stderr, "Test file: %s \n", path) + + if tc.Category != "" { + fmt.Fprintf(os.Stderr, "Category: %s \n", tc.Category) + } + if tc.Description != "" { + fmt.Fprintf(os.Stderr, "Description: %s \n", tc.Description) + } + + fmt.Fprintf(os.Stderr, "Expression: %s\n", expr) + if unQuoted { + fmt.Fprintf(os.Stderr, "Unquoted: %t\n", unQuoted) + } + fmt.Fprintf(os.Stderr, "Expected Result: %v [%T]\n", tc.Result, tc.Result) + fmt.Fprintf(os.Stderr, "Actual Result: %v [%T]\n", got, got) + } + + // TODO this block is commented out to make staticcheck happy, + // but we should check that the error is the same as the js one + // var exp error + // if tc.Undefined { + // exp = jsonata.ErrUndefined + // } else { + // exp = convertError(tc.Error) + // } + + // if !reflect.DeepEqual(err, exp) { + // TODO: Compare actual/expected errors + // } + + return failed, nil +} + +// loadTestExprFile loads a jsonata expression from a file and returns the +// expression +// For example, one test looks like this +// +// { +// "expr-file": "case000.jsonata", +// "dataset": null, +// "bindings": {}, +// "result": 2 +// } +// +// We want to load the expression from case000.jsonata so we can use it +// as an expression in the test case +func loadTestExprFile(testPath string, exprFileName string) (string, error) { + splitPath := strings.Split(testPath, "/") + splitPath[len(splitPath)-1] = exprFileName + exprFilePath := strings.Join(splitPath, "/") + + content, err := os.ReadFile(exprFilePath) + if err != nil { + return "", err + } + + return string(content), nil +} + +// loadTestCases loads all of the json data for tests and converts them to test cases +func loadTestCases(path string) ([]testCase, error) { + // Test cases are contained in json files. They consist of either + // one test case in the file or an array of test cases. + // Since we don't know which it will be until we load the file, + // first try to demarshall it a single case, and if there is an + // error, try again demarshalling it into an array of test cases + var tc testCase + err := readJSONFile(path, &tc) + if err != nil { + var tcs []testCase + err := readJSONFile(path, &tcs) + if err != nil { + return nil, err + + } + + // If any of the tests specify an expression file, load it from + // disk and add it to the test case + for _, testCase := range tcs { + if testCase.ExprFile != "" { + expr, err := loadTestExprFile(path, testCase.ExprFile) + if err != nil { + return nil, err + } + testCase.Expr = expr + } + } + return tcs, nil + } + + // If we have gotten here then there was only one test specified in the + // tests file. + + // If the test specifies an expression file, load it from + // disk and add it to the test case + if tc.ExprFile != "" { + expr, err := loadTestExprFile(path, tc.ExprFile) + if err != nil { + return nil, err + } + tc.Expr = expr + } + + return []testCase{tc}, nil +} + +func printTestCase(w io.Writer, tc testCase, name string) { + fmt.Fprintln(w) + fmt.Fprintf(w, "Failed Test Case: %s\n", name) + switch { + case tc.Data != nil: + fmt.Fprintf(w, "Data: %v\n", tc.Data) + case tc.Dataset != "": + fmt.Fprintf(w, "Dataset: %s\n", tc.Dataset) + default: + fmt.Fprintln(w, "Data: N/A") + } + if tc.Error != "" { + fmt.Fprintf(w, "Expected error code: %v\n", tc.Error) + } + if len(tc.Bindings) > 0 { + fmt.Fprintf(w, "Bindings: %v\n", tc.Bindings) + } +} + +func eval(expression string, bindings map[string]interface{}, data interface{}) (interface{}, error) { + expr, err := jsonata.Compile(expression) + if err != nil { + return nil, err + } + + err = expr.RegisterVars(bindings) + if err != nil { + return nil, err + } + + return expr.Eval(data) +} + +func equalResults(x, y interface{}) bool { + if reflect.DeepEqual(x, y) { + return true + } + + vx := types.Resolve(reflect.ValueOf(x)) + vy := types.Resolve(reflect.ValueOf(y)) + + if types.IsArray(vx) && types.IsArray(vy) { + if vx.Len() != vy.Len() { + return false + } + for i := 0; i < vx.Len(); i++ { + if !equalResults(vx.Index(i).Interface(), vy.Index(i).Interface()) { + return false + } + } + return true + } + + ix, okx := types.AsNumber(vx) + iy, oky := types.AsNumber(vy) + if okx && oky && ix == iy { + return true + } + + sx, okx := types.AsString(vx) + sy, oky := types.AsString(vy) + if okx && oky && sx == sy { + return true + } + + bx, okx := types.AsBool(vx) + by, oky := types.AsBool(vy) + if okx && oky && bx == by { + return true + } + + return false +} + +func readJSONFile(path string, dest interface{}) error { + b, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("ReadFile %s: %s", path, err) + } + + err = json.Unmarshal(b, dest) + if err != nil { + return fmt.Errorf("unmarshal %s: %s", path, err) + } + + return nil +} + +var ( + reQuotedPath = regexp.MustCompile(`([A-Za-z\$\\*\` + "`" + `])\.[\"']([ \.0-9A-Za-z]+?)[\"']`) + reQuotedPathStart = regexp.MustCompile(`^[\"']([ \.0-9A-Za-z]+?)[\"']\.([A-Za-z\$\*\"\'])`) +) + +func replaceQuotesInPaths(s string) (string, bool) { + var changed bool + + if reQuotedPathStart.MatchString(s) { + s = reQuotedPathStart.ReplaceAllString(s, "`$1`.$2") + changed = true + } + + for reQuotedPath.MatchString(s) { + s = reQuotedPath.ReplaceAllString(s, "$1.`$2`") + changed = true + } + + return s, changed +} diff --git a/v1.5.4/jsonata-test/main_test.go b/v1.5.4/jsonata-test/main_test.go new file mode 100644 index 0000000..0eaed40 --- /dev/null +++ b/v1.5.4/jsonata-test/main_test.go @@ -0,0 +1,92 @@ +package main + +import "testing" + +func TestReplaceQuotesInPaths(t *testing.T) { + + inputs := []string{ + `[Address, Other."Alternative.Address"].City`, + `Account.( $AccName := function() { $."Account Name" }; Order[OrderID = "order104"].Product{ "Account": $AccName(), "SKU-" & $string(ProductID): $."Product Name" } )`, + `Account.Order.Product."Product Name".$uppercase().$substringBefore(" ")`, + `"foo".**.fud`, + `foo.**."fud"`, + `"foo".**."fud"`, + `Account.Order.Product[$."Product Name" ~> /hat/i].ProductID`, + `$sort(Account.Order.Product."Product Name")`, + `Account.Order.Product ~> $map(λ($prod, $index) { $index+1 & ": " & $prod."Product Name" })`, + `Account.Order.Product ~> $map(λ($prod, $index, $arr) { $index+1 & "/" & $count($arr) & ": " & $prod."Product Name" })`, + `Account.Order{OrderID: Product."Product Name"}`, + `Account.Order.{OrderID: Product."Product Name"}`, + `Account.Order.Product{$."Product Name": Price, $."Product Name": Price}`, + `Account.Order{ OrderID: { "TotalPrice":$sum(Product.(Price * Quantity)), "Items": Product."Product Name" }}`, + `{ "Order": Account.Order.{ "ID": OrderID, "Product": Product.{ "Name": $."Product Name", "SKU": ProductID, "Details": { "Weight": Description.Weight, "Dimensions": Description.(Width & " x " & Height & " x " & Depth) } }, "Total Price": $sum(Product.(Price * Quantity)) }}`, + `Account.Order.Product[$contains($."Product Name", /hat/)].ProductID`, + `Account.Order.Product[$contains($."Product Name", /hat/i)].ProductID`, + `Account.Order.Product.$replace($."Product Name", /hat/i, function($match) { "foo" })`, + `Account.Order.Product.$replace($."Product Name", /(h)(at)/i, function($match) { $uppercase($match.match) })`, + `$.'7a'`, + `$.'7'`, + `$lowercase($."NI.Number")`, + `$lowercase("COMPENSATION IS : " & Employment."Executive.Compensation")`, + `Account[$$.Account."Account Name" = "Firefly"].*[OrderID="order104"].Product.Price`, + } + + outputs := []string{ + "[Address, Other.`Alternative.Address`].City", + "Account.( $AccName := function() { $.`Account Name` }; Order[OrderID = \"order104\"].Product{ \"Account\": $AccName(), \"SKU-\" & $string(ProductID): $.`Product Name` } )", + "Account.Order.Product.`Product Name`.$uppercase().$substringBefore(\" \")", + "`foo`.**.fud", + "foo.**.`fud`", + "`foo`.**.`fud`", + "Account.Order.Product[$.`Product Name` ~> /hat/i].ProductID", + "$sort(Account.Order.Product.`Product Name`)", + "Account.Order.Product ~> $map(λ($prod, $index) { $index+1 & \": \" & $prod.`Product Name` })", + "Account.Order.Product ~> $map(λ($prod, $index, $arr) { $index+1 & \"/\" & $count($arr) & \": \" & $prod.`Product Name` })", + "Account.Order{OrderID: Product.`Product Name`}", + "Account.Order.{OrderID: Product.`Product Name`}", + "Account.Order.Product{$.`Product Name`: Price, $.`Product Name`: Price}", + "Account.Order{ OrderID: { \"TotalPrice\":$sum(Product.(Price * Quantity)), \"Items\": Product.`Product Name` }}", + "{ \"Order\": Account.Order.{ \"ID\": OrderID, \"Product\": Product.{ \"Name\": $.`Product Name`, \"SKU\": ProductID, \"Details\": { \"Weight\": Description.Weight, \"Dimensions\": Description.(Width & \" x \" & Height & \" x \" & Depth) } }, \"Total Price\": $sum(Product.(Price * Quantity)) }}", + "Account.Order.Product[$contains($.`Product Name`, /hat/)].ProductID", + "Account.Order.Product[$contains($.`Product Name`, /hat/i)].ProductID", + "Account.Order.Product.$replace($.`Product Name`, /hat/i, function($match) { \"foo\" })", + "Account.Order.Product.$replace($.`Product Name`, /(h)(at)/i, function($match) { $uppercase($match.match) })", + "$.`7a`", + "$.`7`", + "$lowercase($.`NI.Number`)", + "$lowercase(\"COMPENSATION IS : \" & Employment.`Executive.Compensation`)", + "Account[$$.Account.`Account Name` = \"Firefly\"].*[OrderID=\"order104\"].Product.Price", + } + + for i := range inputs { + + got, ok := replaceQuotesInPaths(inputs[i]) + if got != outputs[i] { + t.Errorf("\n Input: %s\nExp. Output: %s\nAct. Output: %s", inputs[i], outputs[i], got) + } + if !ok { + t.Errorf("%s: Expected true, got %t", inputs[i], ok) + } + } +} + +func TestReplaceQuotesInPathsNoOp(t *testing.T) { + + inputs := []string{ + `42 ~> "hello"`, + `"john@example.com" ~> $substringAfter("@") ~> $substringBefore(".")`, + `$ ~> |Account.Order.Product|{"Total":Price*Quantity},["Description", "SKU"]|`, + `$ ~> |(Account.Order.Product)[0]|{"Description":"blah"}|`, + } + + for i := range inputs { + + got, ok := replaceQuotesInPaths(inputs[i]) + if got != inputs[i] { + t.Errorf("\n Input: %s\nExp. Output: %s\nAct. Output: %s", inputs[i], inputs[i], got) + } + if ok { + t.Errorf("%s: Expected false, got %t", inputs[i], ok) + } + } +} diff --git a/v1.5.4/jsonata.go b/v1.5.4/jsonata.go new file mode 100644 index 0000000..731e360 --- /dev/null +++ b/v1.5.4/jsonata.go @@ -0,0 +1,386 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "encoding/json" + "fmt" + "reflect" + "sync" + "time" + "unicode" + + "github.com/blues/jsonata-go/v1.5.4/jlib" + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +var ( + globalRegistryMutex sync.RWMutex + globalRegistry map[string]reflect.Value +) + +// An Extension describes custom functionality added to a +// JSONata expression. +type Extension struct { + + // Func is a Go function that implements the custom + // functionality and returns either one or two values. + // The second return value, if provided, must be an + // error. + Func interface{} + + // UndefinedHandler is a function that determines how + // this extension handles undefined arguments. If + // UndefinedHandler is non-nil, it is called before + // Func with the same arguments. If the handler returns + // true, Func is not called and undefined is returned + // instead. + UndefinedHandler jtypes.ArgHandler + + // EvalContextHandler is a function that determines how + // this extension handles missing arguments. If + // EvalContextHandler is non-nil, it is called before + // Func with the same arguments. If the handler returns + // true, the evaluation context is inserted as the first + // argument when Func is called. + EvalContextHandler jtypes.ArgHandler +} + +// RegisterExts registers custom functions for use in JSONata +// expressions. It is designed to be called once on program +// startup (e.g. from an init function). +// +// Custom functions registered at the package level will be +// available to all Expr objects. To register custom functions +// with specific Expr objects, use the RegisterExts method. +func RegisterExts(exts map[string]Extension) error { + + values, err := processExts(exts) + if err != nil { + return err + } + + updateGlobalRegistry(values) + return nil +} + +// RegisterVars registers custom variables for use in JSONata +// expressions. It is designed to be called once on program +// startup (e.g. from an init function). +// +// Custom variables registered at the package level will be +// available to all Expr objects. To register custom variables +// with specific Expr objects, use the RegisterVars method. +func RegisterVars(vars map[string]interface{}) error { + + values, err := processVars(vars) + if err != nil { + return err + } + + updateGlobalRegistry(values) + return nil +} + +// An Expr represents a JSONata expression. +type Expr struct { + node jparse.Node + registry map[string]reflect.Value +} + +// Compile parses a JSONata expression and returns an Expr +// that can be evaluated against JSON data. If the input is +// not a valid JSONata expression, Compile returns an error +// of type jparse.Error. +func Compile(expr string) (*Expr, error) { + + node, err := jparse.Parse(expr) + if err != nil { + return nil, err + } + + e := &Expr{ + node: node, + } + + globalRegistryMutex.RLock() + e.updateRegistry(globalRegistry) + globalRegistryMutex.RUnlock() + + return e, nil +} + +// MustCompile is like Compile except it panics if given an +// invalid expression. +func MustCompile(expr string) *Expr { + + e, err := Compile(expr) + if err != nil { + panicf("could not compile %s: %s", expr, err) + } + + return e +} + +// Eval executes a JSONata expression against the given data +// source. The input is typically the result of unmarshaling +// a JSON string. The output is an object suitable for +// marshaling into a JSON string. Use EvalBytes to skip the +// unmarshal/marshal steps and work solely with JSON strings. +// +// Eval can be called multiple times, with different input +// data if required. +func (e *Expr) Eval(data interface{}) (interface{}, error) { + input, ok := data.(reflect.Value) + if !ok { + input = reflect.ValueOf(data) + } + + result, err := eval(e.node, input, e.newEnv(input)) + if err != nil { + return nil, err + } + + if !result.IsValid() { + return nil, ErrUndefined + } + + if !result.CanInterface() { + return nil, fmt.Errorf("Eval returned a non-interface value") + } + + if result.Kind() == reflect.Ptr && result.IsNil() { + return nil, nil + } + + return result.Interface(), nil +} + +// EvalBytes is like Eval but it accepts and returns byte slices +// instead of objects. +func (e *Expr) EvalBytes(data []byte) ([]byte, error) { + + var v interface{} + + err := json.Unmarshal(data, &v) + if err != nil { + return nil, err + } + + v, err = e.Eval(v) + if err != nil { + return nil, err + } + + return json.Marshal(v) +} + +// RegisterExts registers custom functions for use during +// evaluation. Custom functions registered with this method +// are only available to this Expr object. To make custom +// functions available to all Expr objects, use the package +// level RegisterExts function. +func (e *Expr) RegisterExts(exts map[string]Extension) error { + + values, err := processExts(exts) + if err != nil { + return err + } + + e.updateRegistry(values) + return nil +} + +// RegisterVars registers custom variables for use during +// evaluation. Custom variables registered with this method +// are only available to this Expr object. To make custom +// variables available to all Expr objects, use the package +// level RegisterVars function. +func (e *Expr) RegisterVars(vars map[string]interface{}) error { + + values, err := processVars(vars) + if err != nil { + return err + } + + e.updateRegistry(values) + return nil +} + +// String returns a string representation of an Expr. +func (e *Expr) String() string { + if e.node == nil { + return "" + } + return e.node.String() +} + +func (e *Expr) updateRegistry(values map[string]reflect.Value) { + + for name, v := range values { + if e.registry == nil { + e.registry = make(map[string]reflect.Value, len(values)) + } + e.registry[name] = v + } +} + +func (e *Expr) newEnv(input reflect.Value) *environment { + + tc := timeCallables(time.Now()) + + env := newEnvironment(baseEnv, len(tc)+len(e.registry)+1) + + env.bind("$", input) + env.bindAll(tc) + env.bindAll(e.registry) + + return env +} + +var ( + milisT = mustGoCallable("millis", Extension{ + Func: func(millis int64) int64 { + return millis + }, + }) + + nowT = mustGoCallable("now", Extension{ + Func: func(millis int64, picture jtypes.OptionalString, tz jtypes.OptionalString) (string, error) { + return jlib.FromMillis(millis, picture, tz) + }, + }) +) + +func timeCallables(t time.Time) map[string]reflect.Value { + + ms := t.UnixNano() / int64(time.Millisecond) + + millis := &partialCallable{ + callableName: callableName{ + name: "millis", + }, + fn: milisT, + args: []jparse.Node{ + &jparse.NumberNode{ + Value: float64(ms), + }, + }, + } + + now := &partialCallable{ + callableName: callableName{ + name: "now", + }, + fn: nowT, + args: []jparse.Node{ + &jparse.NumberNode{ + Value: float64(ms), + }, + &jparse.PlaceholderNode{}, + &jparse.PlaceholderNode{}, + }, + } + + return map[string]reflect.Value{ + "millis": reflect.ValueOf(millis), + "now": reflect.ValueOf(now), + } +} + +func processExts(exts map[string]Extension) (map[string]reflect.Value, error) { + + var m map[string]reflect.Value + + for name, ext := range exts { + + if !validName(name) { + return nil, fmt.Errorf("%s is not a valid name", name) + } + + callable, err := newGoCallable(name, ext) + if err != nil { + return nil, fmt.Errorf("%s is not a valid function: %s", name, err) + } + + if m == nil { + m = make(map[string]reflect.Value, len(exts)) + } + m[name] = reflect.ValueOf(callable) + } + + return m, nil +} + +func processVars(vars map[string]interface{}) (map[string]reflect.Value, error) { + + var m map[string]reflect.Value + + for name, value := range vars { + + if !validName(name) { + return nil, fmt.Errorf("%s is not a valid name", name) + } + + if !validVar(value) { + return nil, fmt.Errorf("%s is not a valid variable", name) + } + + if m == nil { + m = make(map[string]reflect.Value, len(vars)) + } + m[name] = reflect.ValueOf(value) + } + + return m, nil +} + +func updateGlobalRegistry(values map[string]reflect.Value) { + + globalRegistryMutex.Lock() + + for name, v := range values { + if globalRegistry == nil { + globalRegistry = make(map[string]reflect.Value, len(values)) + } + globalRegistry[name] = v + } + + globalRegistryMutex.Unlock() +} + +func validName(s string) bool { + + if len(s) == 0 { + return false + } + + for _, r := range s { + if !isLetter(r) && !isDigit(r) && r != '_' { + return false + } + } + + return true +} + +func validVar(v interface{}) bool { + // TODO: Variable validation. + return true +} + +func isLetter(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || unicode.IsLetter(r) +} + +func isDigit(r rune) bool { + return (r >= '0' && r <= '9') || unicode.IsDigit(r) +} + +// Cross-package exports so that everything we need can also be obtained at the root package level +var ArgUndefined = jtypes.ArgUndefined + +type ArgHandler = jtypes.ArgHandler diff --git a/v1.5.4/jsonata_test.go b/v1.5.4/jsonata_test.go new file mode 100644 index 0000000..6530b9b --- /dev/null +++ b/v1.5.4/jsonata_test.go @@ -0,0 +1,8107 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package jsonata + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + "time" + "unicode/utf8" + + "github.com/blues/jsonata-go/v1.5.4/jparse" + "github.com/blues/jsonata-go/v1.5.4/jtypes" +) + +type testCase struct { + + // Expression is either a single JSONata expression (type: string) + // or a slice of JSONata expressions (type: []string) that produce + // the same results. + Expression interface{} + + // Vars is a map of variables to use when evaluating the given + // expression(s). + Vars map[string]interface{} + + // Exts is a map of custom extensions to use when evaluating the + // given expression(s). + Exts map[string]Extension + + // Output is the expected output for the given expression(s). + Output interface{} + + // Error is the expected error for the given expression(s). + Error error + + // Skip indicates whether or not this test case should be included + // in the test run. Set to true to exclude a test case. Run "go test" + // with the verbose flag to see which test cases were skipped. + Skip bool +} + +var testdata struct { + account interface{} + address interface{} + library interface{} + foobar interface{} +} + +func TestMain(m *testing.M) { + + // Decode and cache frequently used JSON. + testdata.account = readJSON("account.json") + testdata.address = readJSON("address.json") + testdata.library = readJSON("library.json") + testdata.foobar = readJSON("foobar.json") + + os.Exit(m.Run()) +} + +func TestLiterals(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `"Hello"`, + `'Hello'`, + }, + Output: "Hello", + }, + { + Expression: `"Wayne's World"`, + Output: "Wayne's World", + }, + { + Expression: "42", + Output: float64(42), + }, + { + Expression: "-42", + Output: float64(-42), + }, + { + Expression: "3.14159", + Output: 3.14159, + }, + { + Expression: "6.022e23", + Output: 6.022e23, + }, + { + Expression: "1.602E-19", + Output: 1.602e-19, + }, + { + Expression: "1.602E+19", + Output: 1.602e+19, + }, + { + Expression: "10e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Token: "10e1000", + Position: 0, + }, + }, + { + Expression: "-10e1000", + Error: &jparse.Error{ + Type: jparse.ErrNumberRange, + Token: "10e1000", + Position: 1, + }, + }, + { + Expression: "1e", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Token: "1e", + Position: 0, + }, + }, + { + Expression: "-1e", + Error: &jparse.Error{ + Type: jparse.ErrInvalidNumber, + Token: "1e", + Position: 1, + }, + }, + { + Expression: "-false", + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: "false", + Value: "-", + }, + }, + }) +} + +func TestStringLiterals(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `"hello\tworld"`, + "'hello\\tworld'", + "\"hello\\tworld\"", + }, + Output: "hello\tworld", + }, + { + Expression: []string{ + `"hello\nworld"`, + "'hello\\nworld'", + "\"hello\\nworld\"", + }, + Output: "hello\nworld", + }, + { + Expression: []string{ + `"hello \"world\""`, + "\"hello \\\"world\\\"\"", + }, + Output: `hello "world"`, + }, + { + Expression: []string{ + `"C:\\Test\\test.txt"`, + }, + Output: "C:\\Test\\test.txt", + }, + { + Expression: []string{ + `"\u03BB-calculus rocks"`, + }, + Output: `λ-calculus rocks`, + }, + { + Expression: []string{ + `"\uD834\uDD1E"`, + }, + Output: "𝄞", // U+1D11E treble clef + }, + { + Expression: []string{ + `"\y"`, + "'\\y'", + "\"\\y\"", + }, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscape, + Position: 1, + Token: "\\y", + Hint: "y", + }, + }, + { + Expression: []string{ + `"\u"`, + "'\\u'", + "\"\\u\"", + }, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\u", + Hint: "u" + strings.Repeat(string(utf8.RuneError), 4), + }, + }, + { + Expression: []string{ + `"\u123t"`, + "'\\u123t'", + "\"\\u123t\"", + }, + Error: &jparse.Error{ + Type: jparse.ErrIllegalEscapeHex, + Position: 1, + Token: "\\u123t", + Hint: "u123t", + }, + }, + }) +} + +func TestPaths(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.bar", + Output: float64(42), + }, + { + Expression: "foo.blah", + Output: []interface{}{ + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "world", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + }, + }, + { + Expression: "foo.blah.baz", + Output: []interface{}{ + map[string]interface{}{ + "fud": "hello", + }, + map[string]interface{}{ + "fud": "world", + }, + }, + }, + { + Expression: "foo.blah.baz.fud", + Output: []interface{}{ + "hello", + "world", + }, + }, + { + Expression: "foo.blah.bazz", + Output: "gotcha", + }, + }) +} + +func TestPaths2(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: "Other.Misc", + Output: nil, + Skip: true, // returns ErrUndefined + }, + }) +} + +func TestPaths3(t *testing.T) { + + data := []interface{}{ + []interface{}{ + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: "bazz", + Output: "gotcha", + }, + }) +} + +func TestPaths4(t *testing.T) { + + data := []interface{}{ + 42, + []interface{}{ + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + }, + "here", + map[string]interface{}{ + "fud": "hello", + }, + "hello", + map[string]interface{}{ + "fud": "world", + }, + "world", + "gotcha", + } + + runTestCases(t, data, []*testCase{ + { + Expression: "fud", + Output: []interface{}{ + "hello", + "world", + }, + }, + }) +} + +func TestSingletonArrays(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: `Phone[type="mobile"].number`, + Output: "077 7700 1234", + }, + { + Expression: []string{ + `Phone[type="mobile"][].number`, + `Phone[][type="mobile"].number`, + }, + Output: []interface{}{ + "077 7700 1234", + }, + }, + { + Expression: `Phone[type="office"][].number`, + Output: []interface{}{ + "01962 001234", + "01962 001235", + }, + }, + { + Expression: `Phone{type: number}`, + Output: map[string]interface{}{ + "home": "0203 544 1234", + "office": []interface{}{ + "01962 001234", + "01962 001235", + }, + "mobile": "077 7700 1234", + }, + }, + { + Expression: `Phone{type: number[]}`, + Output: map[string]interface{}{ + "home": []interface{}{ + "0203 544 1234", + }, + "office": []interface{}{ + "01962 001234", + "01962 001235", + }, + "mobile": []interface{}{ + "077 7700 1234", + }, + }, + }, + }) +} + +func TestArraySelectors(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.blah[0]", + Output: map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + }, + { + Expression: []string{ + "foo.blah[0].baz.fud", + "foo.blah[0][0].baz.fud", + "foo.blah[0][0][0].baz.fud", + }, + Output: "hello", + }, + { + Expression: []string{ + "foo.blah[1].baz.fud", + "(foo.blah)[1].baz.fud", + }, + Output: "world", + }, + { + Expression: "foo.blah[-1].bazz", + Output: "gotcha", + }, + { + Expression: []string{ + "foo.blah.baz.fud[0]", + "foo.blah.baz.fud[-1]", + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + { + Expression: []string{ + "(foo.blah.baz.fud)[0]", + "(foo.blah.baz.fud)[-2]", + "(foo.blah.baz.fud)[2-4]", + "(foo.blah.baz.fud)[-(4-2)]", + }, + Output: "hello", + }, + { + Expression: []string{ + "(foo.blah.baz.fud)[1]", + "(foo.blah.baz.fud)[2 *0.5]", + "(foo.blah.baz.fud)[0.25 * 4]", + "(foo.blah.baz.fud)[-1]", + "(foo.blah.baz.fud)[$$.foo.bar / 30]", + }, + Output: "world", + }, + { + Expression: "foo.blah[0].baz", + Output: map[string]interface{}{ + "fud": "hello", + }, + }, + { + Expression: "foo.blah.baz[0]", + Output: []interface{}{ + map[string]interface{}{ + "fud": "hello", + }, + map[string]interface{}{ + "fud": "world", + }, + }, + }, + { + Expression: []string{ + "(foo.blah.baz)[0]", + "(foo.blah.baz)[0.5]", + "(foo.blah.baz)[0.99]", + "(foo.blah.baz)[-1.5]", + }, + Output: map[string]interface{}{ + "fud": "hello", + }, + }, + { + Expression: []string{ + "(foo.blah.baz)[1]", + "(foo.blah.baz)[-1]", + "(foo.blah.baz)[-0.01]", + "(foo.blah.baz)[-0.5]", + }, + Output: map[string]interface{}{ + "fud": "world", + }, + }, + }) +} + +func TestArraySelectors2(t *testing.T) { + + data := []interface{}{ + []interface{}{ + 1, + 2, + }, + []interface{}{ + 3, + 4, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: []string{ + "$[0]", + "$[-2]", + }, + Output: []interface{}{ + 1, + 2, + }, + }, + { + Expression: []string{ + "$[1]", + "$[-1]", + }, + Output: []interface{}{ + 3, + 4, + }, + }, + { + Expression: []string{ + "$[1][0]", + "$[1.1][0.9]", + }, + Output: 3, + }, + }) +} + +func TestArraySelectors3(t *testing.T) { + + runTestCases(t, readJSON("nest3.json"), []*testCase{ + { + Expression: "nest0.nest1[0]", + Output: []interface{}{ + float64(1), + float64(3), + float64(5), + float64(6), + }, + }, + }) +} + +func TestArraySelectors4(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "[1..10][[1..3,8,-1]]", + Output: []interface{}{ + float64(2), + float64(3), + float64(4), + float64(9), + float64(10), + }, + }, + { + Expression: "[1..10][[1..3,8,5]]", + Output: []interface{}{ + float64(2), + float64(3), + float64(4), + float64(6), + float64(9), + }, + }, + { + Expression: "[1..10][[1..3,8,false]]", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + float64(10), + }, + }, + }) + +} + +func TestQuotedSelectors(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.`blah`", + Output: []interface{}{ + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "world", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + }, + }, + { + Expression: []string{ + "`foo`.blah.baz.fud", + "foo.`blah`.baz.fud", + "foo.blah.`baz`.fud", + "foo.blah.baz.`fud`", + "`foo`.`blah`.`baz`.`fud`", + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + { + Expression: "foo.`blah.baz`", + Output: "here", + }, + }) +} + +func TestNumericOperators(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: []string{ + "foo.bar + bar", + "bar + foo.bar", + }, + Output: float64(140), + }, + { + Expression: []string{ + "foo.bar * bar", + "bar * foo.bar", + }, + Output: float64(4116), + }, + { + Expression: "foo.bar - bar", + Output: float64(-56), + }, + { + Expression: "bar - foo.bar", + Output: float64(56), + }, + { + Expression: "foo.bar / bar", + Output: 0.42857142857142855, + }, + { + Expression: "bar / foo.bar", + Output: 2.3333333333333335, + }, + { + Expression: "foo.bar % bar", + Output: float64(42), + }, + { + Expression: "bar % foo.bar", + Output: float64(14), + }, + { + Expression: []string{ + "bar + foo.bar * bar", + "foo.bar * bar + bar", + }, + Output: float64(4214), + }, + + // If either operand returns no results, all arithmetic + // operators return no results. + + { + Expression: []string{ + "nothing + 3", + "nothing - 3", + "0.5 * nothing", + "0.5 / nothing", + "nothing % nothing", + }, + Error: ErrUndefined, + }, + + // If either operand is a non-number type, return an error. + + { + Expression: "'5' + 5", + Error: &EvalError{ + Type: ErrNonNumberLHS, + Token: `"5"`, + Value: "+", + }, + }, + { + Expression: "5 - '5'", + Error: &EvalError{ + Type: ErrNonNumberRHS, + Token: `"5"`, + Value: "-", + }, + }, + { + Expression: "'5' * '5'", + Error: &EvalError{ + Type: ErrNonNumberLHS, // LHS is evaluated first + Token: `"5"`, + Value: "*", + }, + }, + + // If the result is out of range, return an error. + + { + Expression: "10e300 * 10e100", + Error: &EvalError{ + Type: ErrNumberInf, + Value: "*", + }, + }, + { + Expression: "-10e300 * 10e100", + Error: &EvalError{ + Type: ErrNumberInf, + Value: "*", + }, + }, + { + Expression: "1 / (10e300 * 10e100)", + Error: &EvalError{ + Type: ErrNumberInf, + Value: "*", + }, + }, + + // If the result is NaN, return an error. + + { + Expression: "0/0", + Error: &EvalError{ + Type: ErrNumberNaN, + Value: "/", + }, + }, + }) +} + +func TestComparisonOperators(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "3 > -3", + "3 >= 3", + "3 <= 3", + "3 = 3", + "'3' = '3'", + "1 / 4 = 0.25", + `"hello" = 'hello'`, + "'hello' != 'world'", + "'hello' < 'world'", + "'hello' <= 'world'", + "true = true", + "false = false", + "true != false", + }, + Output: true, + }, + { + Expression: []string{ + "3 > 3", + "3 < 3", + "'3' = 3", + "'hello' > 'world'", + "'hello' >= 'world'", + "true = 'true'", + "false = 0", + }, + Output: false, + }, + { + Expression: "null = null", + Output: true, + }, + + // Less/greater than operators require number or string + // operands. + + { + Expression: "null <= 'world'", + Error: &EvalError{ + Type: ErrNonComparableLHS, + Token: "null", + Value: "<=", + }, + }, + { + Expression: "3 >= true", + Error: &EvalError{ + Type: ErrNonComparableRHS, + Token: "true", + Value: ">=", + }, + }, + + // Less/greater than operators require operands of the + // same type. + + { + Expression: "'32' < 42", + Error: &EvalError{ + Type: ErrTypeMismatch, + Value: "<", + }, + }, + }) +} + +func TestComparisonOperators2(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: []string{ + "bar = 98", + "foo.bar = 42", + "foo.bar < bar", + "foo.bar <= bar", + "foo.bar != bar", + "bar > foo.bar", + "bar = foo.bar + 56", + }, + Output: true, + }, + { + Expression: []string{ + "foo.bar > bar", + "foo.bar >= bar", + "foo.bar = bar", + "bar < foo.bar", + "bar <= foo.bar", + "bar != foo.bar + 56", + }, + Output: false, + }, + { + Expression: []string{ + `foo.blah.baz[fud = "hello"]`, + `foo.blah.baz[fud != "world"]`, + }, + Output: map[string]interface{}{ + "fud": "hello", + }, + }, + + // If either operand evaluates to no results, all + // comparison operators return false. + + { + Expression: []string{ + "bar = nothing", + "nothing != bar", + "bar > nothing", + "nothing >= bar", + "nothing < bar", + "bar <= nothing", + }, + Output: false, + }, + }) +} + +func TestComparisonOperators3(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order.Product[Price > 30].Price", + Output: []interface{}{ + 34.45, + 34.45, + 107.99, + }, + }, + { + Expression: "Account.Order.Product.Price[$<=35]", + Output: []interface{}{ + 34.45, + 21.67, + 34.45, + }, + }, + }) +} + +func TestIncludeOperator(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "1 in [1,2]", + `"world" in ["hello", "world"]`, + `"hello" in "hello"`, + }, + Output: true, + }, + { + Expression: []string{ + "3 in [1,2]", + `"hello" in [1,2]`, + `in in ["hello", "world"]`, + `"world" in in`, + }, + Output: false, + }, + }) +} + +func TestIncludeOperator2(t *testing.T) { + + runTestCases(t, testdata.library, []*testCase{ + { + Expression: `library.books["Aho" in authors].title`, + Output: []interface{}{ + "The AWK Programming Language", + "Compilers: Principles, Techniques, and Tools", + }, + }, + }) +} + +func TestIncludeOperator3(t *testing.T) { + + data := []interface{}{ + map[string]interface{}{ + "content": map[string]interface{}{ + "integration": map[string]interface{}{ + "name": "fakeIntegrationName", + }, + }, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: `content.integration.$lowercase(name)`, + Output: "fakeintegrationname", + }, + }) +} + +func TestParens(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "(4 + 2) / 2", + Output: float64(3), + }, + { + Expression: []string{ + "foo.blah.baz.fud", + "(foo).blah.baz.fud", + "foo.(blah).baz.fud", + "foo.blah.(baz).fud", + "foo.blah.baz.(fud)", + + "(foo.blah).baz.fud", + "foo.(blah.baz).fud", + "foo.blah.(baz.fud)", + + "(foo.blah.baz).fud", + "foo.(blah.baz.fud)", + + "(foo.blah.baz.fud)", + + "(foo).blah.baz.(fud)", + "(foo).(blah).baz.(fud)", + "(foo).(blah).(baz).(fud)", + + "(foo.(blah).baz.fud)", + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + }) +} + +func TestStringConcat(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "'hello' & ' ' & 'world'", + Output: "hello world", + }, + + // Non-string operands are converted to strings. + + { + Expression: "'Hawaii' & ' ' & 5 & '-' & 0", + Output: "Hawaii 5-0", + }, + { + Expression: "10.0 & 'hello'", + Output: "10hello", + }, + { + Expression: "3 + 1 & 2.5", + Output: "42.5", + }, + { + Expression: "true & ' or ' & false", + Output: "true or false", + }, + { + Expression: "null & ' and void'", + Output: "null and void", + }, + { + Expression: "[1,2]&[3,4]", + Output: "[1,2][3,4]", + }, + { + Expression: "[1,2]&3", + Output: "[1,2]3", + }, + { + Expression: "1&3", + Output: "13", + }, + { + Expression: "1&[3]", + Output: "1[3]", + }, + + // Operands that return no results become blank strings. + + { + Expression: "'Hello' & nothing", + Output: "Hello", + }, + { + Expression: "nothing & 'World'", + Output: "World", + }, + { + Expression: "nothing & nothing", + Output: "", + }, + }) +} + +func TestStringConcat2(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: []string{ + "foo.blah[0].baz.fud & foo.blah[1].baz.fud", + "foo.(blah[0].baz.fud & blah[1].baz.fud)", + }, + Output: "helloworld", + }, + { + Expression: "foo.(blah[0].baz.fud & none)", + Output: "hello", + }, + { + Expression: "foo.(none.here & blah[1].baz.fud)", + Output: "world", + }, + }) +} + +func TestStringConcat3(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "'Prices: ' & Account.Order.Product.Price", + Output: "Prices: [34.45,21.67,34.45,107.99]", + }, + }) +} + +func TestArrayFlattening(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order.[Product.Price]", + Output: []interface{}{ + []interface{}{ + 34.45, + 21.67, + }, + []interface{}{ + 34.45, + 107.99, + }, + }, + }, + }) +} + +func TestArrayFlattening2(t *testing.T) { + + runTestCases(t, readJSON("nest2.json"), []*testCase{ + { + Expression: []string{ + "nest0", + "$.nest0", + }, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + }, + }, + { + Expression: []string{ + "$[0]", + "$[-2]", + }, + Output: map[string]interface{}{ + "nest0": []interface{}{ + float64(1), + float64(2), + }, + }, + }, + { + Expression: []string{ + "$[1]", + "$[-1]", + }, + Output: map[string]interface{}{ + "nest0": []interface{}{ + float64(3), + float64(4), + }, + }, + }, + { + Expression: "$[0].nest0", + Output: []interface{}{ + float64(1), + float64(2), + }, + }, + { + Expression: "$[1].nest0", + Output: []interface{}{ + float64(3), + float64(4), + }, + }, + { + Expression: "$[0].nest0[0]", + Output: float64(1), + }, + { + Expression: "$[1].nest0[1]", + Output: float64(4), + }, + }) +} + +func TestArrayFlattening3(t *testing.T) { + + runTestCases(t, readJSON("nest1.json"), []*testCase{ + { + Expression: "nest0.nest1.nest2.nest3", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + }, + }, + { + Expression: "nest0.nest1.nest2.[nest3]", + Output: []interface{}{ + []interface{}{ + float64(1), + }, + []interface{}{ + float64(2), + }, + []interface{}{ + float64(3), + }, + []interface{}{ + float64(4), + }, + []interface{}{ + float64(5), + }, + []interface{}{ + float64(6), + }, + []interface{}{ + float64(7), + }, + []interface{}{ + float64(8), + }, + }, + }, + { + Expression: "nest0.nest1.[nest2.nest3]", + Output: []interface{}{ + []interface{}{ + float64(1), + float64(2), + }, + []interface{}{ + float64(3), + float64(4), + }, + []interface{}{ + float64(5), + float64(6), + }, + []interface{}{ + float64(7), + float64(8), + }, + }, + }, + { + Expression: "nest0.[nest1.nest2.nest3]", + Output: []interface{}{ + []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + }, + []interface{}{ + float64(5), + float64(6), + float64(7), + float64(8), + }, + }, + }, + { + Expression: "nest0.[nest1.[nest2.nest3]]", + Output: []interface{}{ + []interface{}{ + []interface{}{ + float64(1), + float64(2), + }, + []interface{}{ + float64(3), + float64(4), + }, + }, + []interface{}{ + []interface{}{ + float64(5), + float64(6), + }, + []interface{}{ + float64(7), + float64(8), + }, + }, + }, + }, + { + Expression: "nest0.[nest1.nest2.[nest3]]", + Output: []interface{}{ + []interface{}{ + []interface{}{ + float64(1), + }, + []interface{}{ + float64(2), + }, + []interface{}{ + float64(3), + }, + []interface{}{ + float64(4), + }, + }, + []interface{}{ + []interface{}{ + float64(5), + }, + []interface{}{ + float64(6), + }, + []interface{}{ + float64(7), + }, + []interface{}{ + float64(8), + }, + }, + }, + }, + { + Expression: "nest0.nest1.[nest2.[nest3]]", + Output: []interface{}{ + []interface{}{ + []interface{}{ + float64(1), + }, + []interface{}{ + float64(2), + }, + }, + []interface{}{ + []interface{}{ + float64(3), + }, + []interface{}{ + float64(4), + }, + }, + []interface{}{ + []interface{}{ + float64(5), + }, + []interface{}{ + float64(6), + }, + }, + []interface{}{ + []interface{}{ + float64(7), + }, + []interface{}{ + float64(8), + }, + }, + }, + }, + { + Expression: "nest0.[nest1.[nest2.[nest3]]]", + Output: []interface{}{ + []interface{}{ + []interface{}{ + []interface{}{ + float64(1), + }, + []interface{}{ + float64(2), + }, + }, + []interface{}{ + []interface{}{ + float64(3), + }, + []interface{}{ + float64(4), + }, + }, + }, + []interface{}{ + []interface{}{ + []interface{}{ + float64(5), + }, + []interface{}{ + float64(6), + }, + }, + []interface{}{ + []interface{}{ + float64(7), + }, + []interface{}{ + float64(8), + }, + }, + }, + }, + }, + }) +} + +func TestOperatorPrecedence(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "4 + 2 / 2", + Output: float64(5), + }, + { + Expression: "(4 + 2) / 2", + Output: float64(3), + }, + { + Expression: "1 / 2 * 2", + Output: 1.0, + }, + { + Expression: "1 / (2 * 2)", + Output: 0.25, + }, + }) +} + +func TestPredicates(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "nothing[x=6][y=3].number", + Error: ErrUndefined, + }, + }) +} + +func TestPredicates2(t *testing.T) { + + data := map[string]interface{}{ + "clues": []interface{}{ + map[string]interface{}{ + "x": 6, + "y": 3, + "number": 7, + }, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: "clues[x=6][y=3].number", + Output: 7, + }, + }) +} + +func TestPredicates3(t *testing.T) { + + data := []interface{}{ + map[string]interface{}{ + "x": 6, + "y": 2, + "number": 7, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: "$[x=6][y=2].number", + Output: 7, + }, + { + Expression: "$[x=6][y=3].number", + Error: ErrUndefined, + }, + }) +} + +func TestPredicates4(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `Account.Order.Product[Description.Colour = "Purple"][0].Price`, + `Account.Order.Product[$lowercase(Description.Colour) = "purple"][0].Price`, + }, + Output: []interface{}{ + 34.45, + 34.45, + }, + }, + }) +} + +func TestNotFound(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: []string{ + "fdf", + "fdf.ett", + "fdf.ett[10]", + "fdf.ett[vc > 10]", + "fdf.ett + 27", + "$fdsd", + }, + Error: ErrUndefined, + }, + }) +} + +func TestSortOperator(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `Account.Order.Product.Price^($)`, + `Account.Order.Product.Price^(<$)`, + }, + Output: []interface{}{ + 21.67, + 34.45, + 34.45, + 107.99, + }, + }, + { + Expression: `Account.Order.Product.Price^(>$)`, + Output: []interface{}{ + 107.99, + 34.45, + 34.45, + 21.67, + }, + }, + { + Expression: `Account.Order.Product^(Price).Description.Colour`, + Output: []interface{}{ + "Orange", + "Purple", + "Purple", + "Black", + }, + }, + { + Expression: `Account.Order.Product^(Price).SKU`, + Output: []interface{}{ + "0406634348", + "0406654608", + "040657863", + "0406654603", + }, + }, + { + Expression: `Account.Order.Product^(Price * Quantity).Description.Colour`, + Output: []interface{}{ + "Orange", + "Purple", + "Black", + "Purple", + }, + }, + { + Expression: `Account.Order.Product^(Quantity, Description.Colour).Description.Colour`, + Output: []interface{}{ + "Black", + "Orange", + "Purple", + "Purple", + }, + }, + { + Expression: `Account.Order.Product^(Quantity, >Description.Colour).Description.Colour`, + Output: []interface{}{ + "Orange", + "Black", + "Purple", + "Purple", + }, + }, + }) +} + +func TestSortOperator2(t *testing.T) { + + runTestCases(t, readJSON("account2.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Output: []interface{}{ + "0406634348", + "040657863", + "0406654603", + "0406654608", + }, + }, + }) +} + +func TestSortOperator3(t *testing.T) { + + runTestCases(t, readJSON("account3.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Output: []interface{}{ + "0406654608", + "040657863", + "0406654603", + "0406634348", + }, + }, + }) +} + +func TestSortOperator4(t *testing.T) { + + runTestCases(t, readJSON("account4.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Output: []interface{}{ + "040657863", + "0406654603", + "0406654608", + "0406634348", + }, + }, + }) +} + +func TestSortOperator5(t *testing.T) { + + runTestCases(t, readJSON("account5.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Error: &EvalError{ + Type: ErrSortMismatch, + Token: "Price", + }, + }, + }) +} + +func TestSortOperator6(t *testing.T) { + + runTestCases(t, readJSON("account6.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Error: &EvalError{ + Type: ErrNonSortable, + Token: "Price", + }, + }, + }) +} + +func TestSortOperator7(t *testing.T) { + + runTestCases(t, readJSON("account7.json"), []*testCase{ + { + Expression: `Account.Order.Product^(Price).SKU`, + Error: fmt.Errorf("The expressions within an order-by clause must evaluate to numeric or string values"), // TODO: use a proper error + Skip: true, // returns ErrUndefined + }, + }) +} + +func TestWildcards(t *testing.T) { + + runTestCasesFunc(t, equalArraysUnordered, testdata.foobar, []*testCase{ + { + Expression: "foo.*", + Output: []interface{}{ + float64(42), + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "world", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + "here", + }, + }, + { + Expression: "foo.*[0]", + Output: float64(42), + Skip: true, // We can't predict the order of the items in "foo.*" + }, + }) +} + +func TestWildcards2(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.*.baz", + Output: []interface{}{ + map[string]interface{}{ + "fud": "hello", + }, + map[string]interface{}{ + "fud": "world", + }, + }, + }, + { + Expression: "foo.*.bazz", + Output: "gotcha", + }, + { + Expression: []string{ + "foo.*.baz.*", + "*.*.baz.fud", + "*.*.*.fud", + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + }) +} + +func TestWildcards3(t *testing.T) { + + runTestCasesFunc(t, equalArraysUnordered, testdata.address, []*testCase{ + { + Expression: `*[type="home"]`, + Output: []interface{}{ + map[string]interface{}{ + "type": "home", + "number": "0203 544 1234", + }, + map[string]interface{}{ + "type": "home", + "address": []interface{}{ + "freddy@my-social.com", + "frederic.smith@very-serious.com", + }, + }, + }, + }, + }) +} + +func TestWildcards4(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account[$$.Account.`Account Name` = 'Firefly'].*[OrderID='order104'].Product.Price", + Output: []interface{}{ + 34.45, + 107.99, + }, + }, + }) +} + +func TestDescendents(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.**.blah", + Output: []interface{}{ + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "hello", + }, + }, + map[string]interface{}{ + "baz": map[string]interface{}{ + "fud": "world", + }, + }, + map[string]interface{}{ + "bazz": "gotcha", + }, + }, + }, + { + Expression: "foo.**.baz", + Output: []interface{}{ + map[string]interface{}{ + "fud": "hello", + }, + map[string]interface{}{ + "fud": "world", + }, + }, + }, + { + Expression: []string{ + "foo.**.fud", + "`foo`.**.fud", + "foo.**.`fud`", + "`foo`.**.`fud`", + "foo.*.**.fud", + "foo.**.*.fud", + "foo.**.fud[0]", + }, + Output: []interface{}{ + "hello", + "world", + }, + }, + { + Expression: []string{ + "(foo.**.fud)[0]", + "(**.fud)[0]", + }, + Output: "hello", + }, + }) +} + +func TestDescendents2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order.**.Colour", + Output: []interface{}{ + "Purple", + "Orange", + "Purple", + "Black", + }, + }, + { + Expression: []string{ + "**.Price", + "**.Price[0]", + }, + Output: []interface{}{ + 34.45, + 21.67, + 34.45, + 107.99, + }, + }, + { + Expression: "(**.Price)[0]", + Output: 34.45, + }, + { + Expression: "**[2]", + Output: "Firefly", + Skip: true, // We can't guarantee the order of object sub-items! + }, + { + Expression: []string{ + "Account.Order.blah", + "Account.Order.blah.**", + }, + Error: ErrUndefined, + }, + }) +} + +func TestDescendents3(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "**", + Error: ErrUndefined, + }, + }) +} + +func TestBlockExpressions(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "()", + Error: ErrUndefined, + }, + { + Expression: []string{ + "(1; 2; 3)", + "(1; 2; 3;)", + }, + Output: float64(3), + }, + { + Expression: "($a:=1; $b:=2; $c:=($a:=4; $a+$b); $a+$c)", + Output: float64(7), + }, + }) +} + +func TestBlockExpressions2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order.Product.($var1 := Price ; $var2:=Quantity; $var1 * $var2)", + Output: []interface{}{ + 68.9, + 21.67, + 137.8, + 107.99, + }, + }, + { + Expression: []string{ + `( + $func := function($arg) {$arg.Account.Order[0].OrderID}; + $func($) + )`, + `( + $func := function($arg) {$arg.Account.Order[0]}; + $func($).OrderID + )`, + }, + Output: "order103", + }, + }) +} + +func TestArrayConstructor(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "[]", + Output: []interface{}{}, + }, + { + Expression: "[1]", + Output: []interface{}{ + float64(1), + }, + }, + { + Expression: "[1, 2]", + Output: []interface{}{ + float64(1), + float64(2), + }, + }, + { + Expression: "[1, 2,3]", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + { + Expression: "[1, 2, [3, 4]]", + Output: []interface{}{ + float64(1), + float64(2), + []interface{}{ + float64(3), + float64(4), + }, + }, + }, + { + Expression: `[1, "two", ["three", 4]]`, + Output: []interface{}{ + float64(1), + "two", + []interface{}{ + "three", + float64(4), + }, + }, + }, + { + Expression: `[1, $two, ["three", $four]]`, + Vars: map[string]interface{}{ + "two": float64(2), + "four": "four", + }, + Output: []interface{}{ + float64(1), + float64(2), + []interface{}{ + "three", + "four", + }, + }, + }, + { + Expression: "[1, 2, 3][0]", + Output: float64(1), + }, + { + Expression: "[1, 2, [3, 4]][-1]", + Output: []interface{}{ + float64(3), + float64(4), + }, + }, + { + Expression: "[1, 2, [3, 4]][-1][-1]", + Output: float64(4), + }, + { + Expression: "[1..5][-1]", + Output: float64(5), + }, + { + Expression: "[1, 2, 3].$", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + }) +} + +func TestArrayConstructor2(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "foo.blah.baz.[fud]", + Output: []interface{}{ + []interface{}{ + "hello", + }, + []interface{}{ + "world", + }, + }, + }, + { + Expression: "foo.blah.baz.[fud, fud]", + Output: []interface{}{ + []interface{}{ + "hello", + "hello", + }, + []interface{}{ + "world", + "world", + }, + }, + }, + { + Expression: "foo.blah.baz.[[fud, fud]]", + Output: []interface{}{ + []interface{}{ + []interface{}{ + "hello", + "hello", + }, + }, + []interface{}{ + []interface{}{ + "world", + "world", + }, + }, + }, + }, + { + Expression: `["foo.bar", foo.bar, ["foo.baz", foo.blah.baz]]`, + Output: []interface{}{ + "foo.bar", + float64(42), + []interface{}{ + "foo.baz", + map[string]interface{}{ + "fud": "hello", + }, + map[string]interface{}{ + "fud": "world", + }, + }, + }, + }, + }) +} + +func TestArrayConstructor3(t *testing.T) { + + data := readJSON("foobar2.json") + + runTestCases(t, data, []*testCase{ + { + Expression: "foo.blah.[baz].fud", + Output: "hello", + }, + { + Expression: "foo.blah.[baz, buz].fud", + Output: []interface{}{ + "hello", + "world", + }, + }, + }) +} + +func TestArrayConstructor4(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: "[Address, Other.`Alternative.Address`].City", + Output: []interface{}{ + "Winchester", + "London", + }, + }, + }) +} + +func TestArrayConstructor5(t *testing.T) { + + data := []interface{}{} + + runTestCases(t, data, []*testCase{ + { + Expression: "[1, 2, 3].$", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + }) +} + +func TestArrayConstructor6(t *testing.T) { + + data := []interface{}{4, 5, 6} + + runTestCases(t, data, []*testCase{ + { + Expression: "[1, 2, 3].$", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + }) +} + +func TestObjectConstructor(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "{}", + Output: map[string]interface{}{}, + }, + { + Expression: `{"key":"value"}`, + Output: map[string]interface{}{ + "key": "value", + }, + }, + { + Expression: `{"one": 1, "two": 2}`, + Output: map[string]interface{}{ + "one": float64(1), + "two": float64(2), + }, + }, + { + Expression: `{"one": 1, "two": 2}.two`, + Output: float64(2), + }, + { + Expression: `{"one": 1, "two": {"three": 3, "four": "4"}}`, + Output: map[string]interface{}{ + "one": float64(1), + "two": map[string]interface{}{ + "three": float64(3), + "four": "4", + }, + }, + }, + { + Expression: `{"one": 1, "two": [3, "four"]}`, + Output: map[string]interface{}{ + "one": float64(1), + "two": []interface{}{ + float64(3), + "four", + }, + }, + }, + { + Expression: `{"test": ()}`, + Output: map[string]interface{}{}, + }, + { + Expression: `blah.{}`, + Error: ErrUndefined, + }, + }) +} + +func TestObjectConstructor2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order{OrderID: Product.`Product Name`}", + Output: map[string]interface{}{ + "order103": []interface{}{ + "Bowler Hat", + "Trilby hat", + }, + "order104": []interface{}{ + "Bowler Hat", + "Cloak", + }, + }, + }, + { + Expression: "Account.Order.{OrderID: Product.`Product Name`}", + Output: []interface{}{ + map[string]interface{}{ + "order103": []interface{}{ + "Bowler Hat", + "Trilby hat", + }, + }, + map[string]interface{}{ + "order104": []interface{}{ + "Bowler Hat", + "Cloak", + }, + }, + }, + }, + { + Expression: "Account.Order.Product{$string(ProductID): Price}", + Output: map[string]interface{}{ + "345664": 107.99, + "858236": 21.67, + "858383": []interface{}{ + 34.45, + 34.45, + }, + }, + }, + { + Expression: "Account.Order.Product{$string(ProductID): (Price)[0]}", + Output: map[string]interface{}{ + "345664": 107.99, + "858236": 21.67, + "858383": 34.45, + }, + }, + { + Expression: "Account.Order.Product.{$string(ProductID): Price}", + Output: []interface{}{ + map[string]interface{}{ + "858383": 34.45, + }, + map[string]interface{}{ + "858236": 21.67, + }, + map[string]interface{}{ + "858383": 34.45, + }, + map[string]interface{}{ + "345664": 107.99, + }, + }, + }, + { + Expression: "Account.Order.Product{ProductID: `Product Name`}", + Error: &EvalError{ + Type: ErrIllegalKey, + Token: "ProductID", + }, + }, + { + Expression: "Account.Order.Product.{ProductID: `Product Name`}", + Error: &EvalError{ + Type: ErrIllegalKey, + Token: "ProductID", + }, + }, + { + Expression: "Account.Order{OrderID: $sum(Product.(Price*Quantity))}", + Output: map[string]interface{}{ + "order103": 90.57000000000001, + "order104": 245.79000000000002, + }, + }, + { + Expression: "Account.Order.{OrderID: $sum(Product.(Price*Quantity))}", + Output: []interface{}{ + map[string]interface{}{ + "order103": 90.57000000000001, + }, map[string]interface{}{ + "order104": 245.79000000000002, + }, + }, + }, + { + Expression: "Account.Order.Product{`Product Name`: Price, `Product Name`: Price}", + Error: &EvalError{ + Type: ErrDuplicateKey, + Token: "`Product Name`", + Value: "Bowler Hat", + }, + }, + { + Expression: ` + Account.Order{ + OrderID: { + "TotalPrice": $sum(Product.(Price * Quantity)), + "Items": Product.` + "`Product Name`" + ` + } + }`, + Output: map[string]interface{}{ + "order103": map[string]interface{}{ + "TotalPrice": 90.57000000000001, + "Items": []interface{}{ + "Bowler Hat", + "Trilby hat", + }, + }, + "order104": map[string]interface{}{ + "TotalPrice": 245.79000000000002, + "Items": []interface{}{ + "Bowler Hat", + "Cloak", + }, + }, + }, + }, + { + Expression: ` + { + "Order": Account.Order.{ + "ID": OrderID, + "Product": Product.{ + "Name": ` + "`Product Name`" + `, + "SKU": ProductID, + "Details": { + "Weight": Description.Weight, + "Dimensions": Description.(Width & " x " & Height & " x " & Depth) + } + }, + "Total Price": $sum(Product.(Price * Quantity)) + } + }`, + Output: map[string]interface{}{ + "Order": []interface{}{ + map[string]interface{}{ + "ID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Name": "Bowler Hat", + "SKU": float64(858383), + "Details": map[string]interface{}{ + "Weight": 0.75, + "Dimensions": "300 x 200 x 210", + }, + }, + map[string]interface{}{ + "Name": "Trilby hat", + "SKU": float64(858236), + "Details": map[string]interface{}{ + "Weight": 0.6, + "Dimensions": "300 x 200 x 210", + }, + }, + }, + "Total Price": 90.57000000000001, + }, + map[string]interface{}{ + "ID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Name": "Bowler Hat", + "SKU": float64(858383), + "Details": map[string]interface{}{ + "Weight": 0.75, + "Dimensions": "300 x 200 x 210", + }, + }, + map[string]interface{}{ + "Name": "Cloak", + "SKU": float64(345664), + "Details": map[string]interface{}{ + "Weight": float64(2), + "Dimensions": "30 x 20 x 210", + }, + }, + }, + "Total Price": 245.79000000000002, + }, + }, + }, + }, + }) +} + +func TestObjectConstructor3(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: `Phone{type: $join(number, ", "), "phone":number}`, + Output: map[string]interface{}{ + "home": "0203 544 1234", + "phone": []interface{}{ + "0203 544 1234", + "01962 001234", + "01962 001235", + "077 7700 1234", + }, + "office": "01962 001234, 01962 001235", + "mobile": "077 7700 1234", + }, + }, + }) +} + +func TestRangeOperator(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "[0..9]", + Output: []interface{}{ + float64(0), + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + }, + }, + { + Expression: "[0..9][$ % 2 = 0]", + Output: []interface{}{ + float64(0), + float64(2), + float64(4), + float64(6), + float64(8), + }, + }, + + { + Expression: "[0, 4..9, 20, 22]", + Output: []interface{}{ + float64(0), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + float64(20), + float64(22), + }, + }, + { + Expression: "[5..5]", + Output: []interface{}{ + float64(5), + }, + }, + { + Expression: "[5..2]", + Output: []interface{}{}, + }, + { + Expression: "[5..2, 2..5]", + Output: []interface{}{ + float64(2), + float64(3), + float64(4), + float64(5), + }, + }, + { + Expression: "[-2..2]", + Output: []interface{}{ + float64(-2), + float64(-1), + float64(0), + float64(1), + float64(2), + }, + }, + { + Expression: "[-2..2].($*$)", + Output: []interface{}{ + float64(4), + float64(1), + float64(0), + float64(1), + float64(4), + }, + }, + { + Expression: "[2*4..3*4-1]", + Output: []interface{}{ + float64(8), + float64(9), + float64(10), + float64(11), + }, + }, + { + Expression: "[-2..notfound]", + Output: []interface{}{}, + }, + { + Expression: "[notfound..5, 3, -2..notfound]", + Output: []interface{}{ + float64(3), + }, + }, + { + Expression: []string{ + "['1'..5]", + "['1'..'5']", // LHS is evaluated first + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: `"1"`, + Value: "..", + }, + }, + { + Expression: []string{ + "[1.1..5]", + "[1.1..'5']", // LHS is evaluated first + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "1.1", + Value: "..", + }, + }, + { + Expression: []string{ + "[true..5]", + "[true..'5']", // LHS is evaluated first + }, + Error: &EvalError{ + Type: ErrNonIntegerLHS, + Token: "true", + Value: "..", + }, + }, + + { + Expression: "[1..'5']", + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: `"5"`, + Value: "..", + }, + }, + { + Expression: "[1..5.5]", + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "5.5", + Value: "..", + }, + }, + { + Expression: "[1..false]", + Error: &EvalError{ + Type: ErrNonIntegerRHS, + Token: "false", + Value: "..", + }, + }, + }) +} + +func TestConditionals(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "true ? true", + "true ? true : false", + "1 > 0 ? true : false", + }, + Output: true, + }, + { + Expression: []string{ + "false ? true : false", + "'hello' = 'world' ? true : false", + }, + Output: false, + }, + { + Expression: "false ? true", + Error: ErrUndefined, + }, + }) +} + +func TestConditionals2(t *testing.T) { + + runTestCases(t, "Bus", []*testCase{ + { + Expression: []string{ + `["Red"[$$="Bus"], "White"[$$="Police Car"]][0]`, + `$lookup({"Bus": "Red", "Police Car": "White"}, $$)`, + }, + Output: "Red", + }, + }) +} + +func TestConditionals3(t *testing.T) { + + runTestCases(t, "Police Car", []*testCase{ + { + Expression: []string{ + `["Red"[$$="Bus"], "White"[$$="Police Car"]][0]`, + `$lookup({"Bus": "Red", "Police Car": "White"}, $$)`, + }, + Output: "White", + }, + }) +} + +func TestConditionals4(t *testing.T) { + + runTestCases(t, "Tuk tuk", []*testCase{ + { + Expression: []string{ + `["Red"[$$="Bus"], "White"[$$="Police Car"]][0]`, + //`$lookup({"Bus": "Red", "Police Car": "White"}, $$)`, // returns nil + }, + Error: ErrUndefined, + }, + }) +} + +func TestConditionals5(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `Account.Order.Product.(Price < 30 ? "Cheap")`, + Output: "Cheap", + }, + { + Expression: `Account.Order.Product.(Price < 30 ? "Cheap" : "Expensive")`, + Output: []interface{}{ + "Expensive", + "Cheap", + "Expensive", + "Expensive", + }, + }, + { + Expression: `Account.Order.Product.(Price < 30 ? "Cheap" : Price < 100 ? "Expensive" : "Rip off")`, + Output: []interface{}{ + "Expensive", + "Cheap", + "Expensive", + "Rip off", + }, + }, + }) +} + +func TestBooleanExpressions(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "true", + "true or true", + "true and true", + "true or false", + "false or true", + "true or nothing", + "$not(false)", + }, + Output: true, + }, + { + Expression: []string{ + "false", + "false or false", + "false and false", + "false and true", + "true and false", + "nothing and false", + "$not(true)", + }, + Output: false, + }, + }) +} + +func TestBooleanExpressions2(t *testing.T) { + + data := map[string]interface{}{ + "and": 1, + "or": 2, + } + + runTestCases(t, data, []*testCase{ + { + Expression: []string{ + "and=1 and or=2", + "and>1 or or<=2", + "and and and", + }, + Output: true, + }, + { + Expression: []string{ + "and>1 or or!=2", + }, + Output: false, + }, + }) +} + +func TestBooleanExpressions3(t *testing.T) { + + data := []interface{}{ + map[string]interface{}{ + "content": map[string]interface{}{ + "origin": map[string]interface{}{ + "name": "fakeIntegrationName", + }, + }, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: `$[].content.origin.$lowercase(name)`, + Output: []interface{}{ + "fakeintegrationname", + }, + }, + }) +} + +func TestNullExpressions(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "null", + Output: nil, + }, + { + Expression: "[null]", + Output: []interface{}{ + nil, + }, + Skip: true, // uses the wrong kind of nil (*interface{}(nil) instead of plain nil)? + }, + { + Expression: "[null, null]", + Output: []interface{}{ + nil, + nil, + }, + Skip: true, // uses the wrong kind of nil (*interface{}(nil) instead of plain nil)? + }, + { + Expression: "$not(null)", + Output: true, + }, + { + Expression: "null = null", + Output: true, + }, + { + Expression: "null != null", + Output: false, + }, + { + Expression: `{"true": true, "false":false, "null": null}`, + Output: map[string]interface{}{ + "true": true, + "false": false, + "null": nil, + }, + Skip: true, // uses the wrong kind of nil (*interface{}(nil) instead of plain nil)? + }, + }) +} + +func TestVariables(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$price.foo.bar", + Vars: map[string]interface{}{ + "price": map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": 45, + }, + }, + }, + Output: 45, + }, + { + Expression: "$var[1]", + Vars: map[string]interface{}{ + "var": []interface{}{ + 1, + 2, + 3, + }, + }, + Output: 2, + }, + { + Expression: "[1,2,3].$v", + Vars: map[string]interface{}{ + "v": []interface{}{ + nil, + }, + }, + Output: nil, + Skip: true, // jsonata-js passes in [undefined]. Not sure how to do that in Go. + }, + { + Expression: []string{ + "$a := 5", + "$a := $b := 5", + "($a := $b := 5; $a)", + "($a := $b := 5; $b)", + }, + Output: float64(5), + }, + { + Expression: "($a := 5; $a := $a + 2; $a)", + Output: float64(7), + }, + { + Expression: "5 := 5", + Error: &jparse.Error{ + Type: jparse.ErrIllegalAssignment, + Token: ":=", + Hint: "5", + Position: 2, + }, + }, + }) +} + +func TestVariables2(t *testing.T) { + + runTestCases(t, testdata.foobar, []*testCase{ + { + Expression: "$.foo.bar", + Vars: map[string]interface{}{ + "price": map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": 45, + }, + }, + }, + Output: float64(42), + }, + }) +} + +func TestVariableScope(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `( $foo := "defined"; ( $foo := nothing ); $foo )`, + Output: "defined", + }, + { + Expression: `( $foo := "defined"; ( $foo := nothing; $foo ) )`, + Error: ErrUndefined, + }, + }) +} + +func TestLambdas(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `function($x){$x*$x}(5)`, + Output: float64(25), + }, + { + Expression: `function($x){$x>5 ? "foo"}(6)`, + Output: "foo", + }, + { + Expression: `function($x){$x>5 ? "foo"}(3)`, + Error: ErrUndefined, + }, + { + Expression: `($factorial:= function($x){$x <= 1 ? 1 : $x * $factorial($x-1)}; $factorial(4))`, + Output: float64(24), + }, + { + Expression: `($fibonacci := function($x){$x <= 1 ? $x : $fibonacci($x-1) + $fibonacci($x-2)}; [1,2,3,4,5,6,7,8,9].$fibonacci($))`, + Output: []interface{}{ + float64(1), + float64(1), + float64(2), + float64(3), + float64(5), + float64(8), + float64(13), + float64(21), + float64(34), + }, + }, + { + Expression: ` + ( + $even := function($n) { $n = 0 ? true : $odd($n-1) }; + $odd := function($n) { $n = 0 ? false : $even($n-1) }; + $even(82) + )`, + Output: true, + }, + { + Expression: ` + ( + $even := function($n) { $n = 0 ? true : $odd($n-1) }; + $odd := function($n) { $n = 0 ? false : $even($n-1) }; + $even(65) + )`, + Output: false, + }, + { + Expression: ` + ( + $even := function($n) { $n = 0 ? true : $odd($n-1) }; + $odd := function($n) { $n = 0 ? false : $even($n-1) }; + $odd(65) + )`, + Output: true, + }, + { + Expression: ` + ( + $gcd := λ($a, $b){$b = 0 ? $a : $gcd($b, $a%$b) }; + [$gcd(8,12), $gcd(9,12)] + )`, + Output: []interface{}{ + float64(4), + float64(3), + }, + }, + { + Expression: ` + ( + $range := function($start, $end, $step) { ( + $step:=($step?$step:1); + $start+$step > $end ? $start : $append($start, $range($start+$step, $end, $step)) + )}; + $range(0,15) + )`, + Output: []interface{}{ + float64(0), + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + float64(10), + float64(11), + float64(12), + float64(13), + float64(14), + float64(15), + }, + }, + { + Expression: ` + ( + $range := function($start, $end, $step) { ( + $step:=($step?$step:1); + $start+$step > $end ? $start : $append($start, $range($start+$step, $end, $step)) + )}; + $range(0,15,2) + )`, + Output: []interface{}{ + float64(0), + float64(2), + float64(4), + float64(6), + float64(8), + float64(10), + float64(12), + float64(14), + }, + }, + }) +} + +func TestLambdas2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `($nth_price := function($n) { (Account.Order.Product.Price)[$n] }; $nth_price(1) )`, + Output: 21.67, + }, + }) +} + +func TestPartials(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: ` + ( + $add := function($x, $y){$x+$y}; + $add2 := $add(?, 2); + $add2(3) + )`, + Output: float64(5), + }, + { + Expression: ` + ( + $add := function($x, $y){$x+$y}; + $add2 := $add(2, ?); + $add2(4) + )`, + Output: float64(6), + }, + { + Expression: ` + ( + $last_letter := $substring(?, -1); + $last_letter("Hello World") + )`, + Output: "d", + }, + { + Expression: ` + ( + $firstn := $substring(?, 0, ?); + $first5 := $firstn(?, 5); + $first5("Hello World") + )`, + Output: "Hello", + }, + { + Expression: ` + ( + $firstn := $substr(?, 0, ?); + $first5 := $firstn(?, 5); + $first5("Hello World") + )`, + Exts: map[string]Extension{ + "substr": { + Func: func(s string, start, length int) string { + if length <= 0 || start >= len(s) { + return "" + } + + if start < 0 { + start += len(s) + } + + if start > 0 { + s = s[start:] + } + + if length < len(s) { + s = s[:length] + } + + return s + }, + }, + }, + Output: "Hello", + }, + { + Expression: `substring(?, 0, ?)`, + Error: &EvalError{ + Type: ErrNonCallablePartial, + Token: "substring", + }, + }, + { + Expression: `nothing(?)`, + Error: &EvalError{ + Type: ErrNonCallablePartial, + Token: "nothing", + }, + }, + }) +} + +func TestFuncBoolean(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$boolean("Hello World")`, + `$boolean(true)`, + `$boolean(10)`, + `$boolean(10.5)`, + `$boolean(-0.5)`, + `$boolean([1])`, + `$boolean([true])`, + `$boolean([1,2,3])`, + `$boolean([[[true]]])`, + `$boolean({"hello":"world"})`, + }, + Output: true, + }, + { + Expression: []string{ + `$boolean("")`, + `$boolean(false)`, + `$boolean(0)`, + `$boolean(0.0)`, + `$boolean(-0)`, + `$boolean(null)`, + `$boolean([])`, + `$boolean([null])`, + `$boolean([false])`, + `$boolean([0])`, + `$boolean([0,0])`, + `$boolean([[]])`, + `$boolean([[null]])`, + `$boolean({})`, + `$boolean($boolean)`, + `$boolean(function(){true})`, + }, + Output: false, + }, + { + Expression: `$boolean(2,3)`, + Error: &ArgCountError{ + Func: "boolean", + Expected: 1, + Received: 2, + }, + }, + }) +} + +func TestFuncBoolean2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `$boolean(Account)`, + `$boolean(Account.Order.Product.Price)`, + }, + Output: true, + }, + { + Expression: []string{ + `$boolean(Account.blah)`, + }, + Error: ErrUndefined, + }, + }) +} + +func TestFuncExists(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$boolean("Hello World")`, + `$exists("")`, + `$exists(true)`, + `$exists(false)`, + `$exists(0)`, + `$exists(-0.5)`, + `$exists(null)`, + `$exists([])`, + `$exists([0])`, + `$exists([1,2,3])`, + `$exists([[]])`, + `$exists([[null]])`, + `$exists([[[true]]])`, + `$exists({})`, + `$exists({"hello":"world"})`, + `$exists($exists)`, + `$exists(function(){true})`, + }, + Output: true, + }, + { + Expression: []string{ + `$exists(nothing)`, + `$exists($ha)`, + }, + Output: false, + }, + { + Expression: "$exists()", + Error: &ArgCountError{ + Func: "exists", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$exists(2,3)", + Error: &ArgCountError{ + Func: "exists", + Expected: 1, + Received: 2, + }, + }, + }) +} + +func TestFuncExists2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `$exists(Account)`, + `$exists(Account.Order.Product.Price)`, + }, + Output: true, + }, + { + Expression: []string{ + `$exists(blah)`, + `$exists(Account.blah)`, + `$exists(Account.Order[2])`, + `$exists(Account.Order[0].blah)`, + }, + Output: false, + }, + }) +} + +func TestFuncCount(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$count([])", + "$count([nothing])", + "$count([nothing,nada,now't])", + }, + Output: 0, + }, + { + Expression: []string{ + "$count(0)", + "$count(false)", + "$count(null)", + `$count("")`, + }, + Output: 1, + }, + { + Expression: []string{ + "$count([1,2,3])", + "$count([1,2,3,nothing,nada,nichts])", + "$count([1..3])", + `$count(["1","2","3"])`, + `$count(["1","2",3])`, + `$count([0.5,true,{"one":1}])`, + }, + Output: 3, + }, + { + Expression: "$count()", + Error: &ArgCountError{ + Func: "count", + Expected: 1, + Received: 0, + }, + }, + { + Expression: []string{ + "$count([],[])", + "$count([1,2,3],[])", + }, + Error: &ArgCountError{ + Func: "count", + Expected: 1, + Received: 2, + }, + }, + { + Expression: []string{ + "$count(1,2,3)", + "$count([],[],[])", + "$count([1,2],[],[])", + }, + Error: &ArgCountError{ + Func: "count", + Expected: 1, + Received: 3, + }, + }, + { + Expression: "$count(nothing)", + Output: 0, + }, + { + Expression: "$count([1,2,3,4]) / 2", + Output: float64(2), + }, + }) +} + +func TestFuncCount2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$count(Account.Order.Product.(Price * Quantity))", + Output: 4, + }, + { + Expression: "Account.Order.$count(Product.(Price * Quantity))", + Output: []interface{}{ + 2, + 2, + }, + }, + { + Expression: `Account.Order.(OrderID & ": " & $count(Product.(Price*Quantity)))`, + Output: []interface{}{ + "order103: 2", + "order104: 2", + }, + }, + }) +} + +func TestFuncAppend(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$append([], [])", + "$append([nothing], [nothing])", + }, + Output: []interface{}{}, + }, + { + Expression: []string{ + "$append(1, 2)", + "$append([1], [2])", + }, + Output: []interface{}{ + float64(1), + float64(2), + }, + }, + { + Expression: "$append([1,2], [3,4])", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + }, + }, + { + Expression: []string{ + "$append(1, [3,4])", + "$append([1,3], 4)", + "$append([1,3,4], [])", + "$append([1,3,4], nothing)", + "$append([1,3,4], [nothing])", + "$append([1,3,4], [nothing,nada])", + }, + Output: []interface{}{ + float64(1), + float64(3), + float64(4), + }, + }, + { + Expression: []string{ + "$append(1, nothing)", + "$append(nothing, 1)", + }, + Output: float64(1), + }, + { + Expression: []string{ + "$append([1], nothing)", + "$append([1,nothing], nothing)", + "$append(nothing, [1])", + "$append(nothing, [1,nothing])", + }, + Output: []interface{}{ + float64(1), + }, + }, + { + Expression: []string{ + "$append([2,3,4], nothing)", + "$append([2,3], [4,nothing])", + "$append([2], [3,4,nothing])", + "$append(2, [3,4,nothing])", + }, + Output: []interface{}{ + float64(2), + float64(3), + float64(4), + }, + }, + { + Expression: "$append(nothing, nothing)", + Error: ErrUndefined, + }, + { + Expression: "$append()", + Error: &ArgCountError{ + Func: "append", + Expected: 2, + Received: 0, + }, + Skip: true, // returns ErrUndefined + }, + { + Expression: "$append([])", + Error: &ArgCountError{ + Func: "append", + Expected: 2, + Received: 1, + }, + }, + { + Expression: "$append([],[],[])", + Error: &ArgCountError{ + Func: "append", + Expected: 2, + Received: 3, + }, + }, + }) +} + +func TestFuncReverse(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$reverse([])", + "$reverse([nothing])", + }, + Output: []interface{}{}, + }, + { + Expression: []string{ + "$reverse(1)", + "$reverse([1])", + }, + Output: []interface{}{ + float64(1), + }, + }, + { + Expression: []string{ + "$reverse([1..5])", + "$reverse([1,2,3,4,nothing,5,nada])", + }, + Output: []interface{}{ + float64(5), + float64(4), + float64(3), + float64(2), + float64(1), + }, + }, + { + Expression: `$reverse(["hello","world"])`, + Output: []interface{}{ + "world", + "hello", + }, + }, + { + Expression: `$reverse([true,0.5,"hello",{"one":1},[1,2,3]])`, + Output: []interface{}{ + []interface{}{ + float64(1), + float64(2), + float64(3), + }, + map[string]interface{}{ + "one": float64(1), + }, + "hello", + 0.5, + true, + }, + }, + { + Expression: "$reverse(nothing)", + Error: ErrUndefined, + }, + { + Expression: "$reverse()", + Error: &ArgCountError{ + Func: "reverse", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$reverse([],[])", + Error: &ArgCountError{ + Func: "reverse", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$reverse(1,2,3)", + Error: &ArgCountError{ + Func: "reverse", + Expected: 1, + Received: 3, + }, + }, + }) +} + +func TestFuncReverse2(t *testing.T) { + + data := []interface{}{1, 2, 3} + + runTestCases(t, data, []*testCase{ + { + Expression: "[$, $reverse($), $]", + Output: []interface{}{ + []interface{}{ + 1, + 2, + 3, + }, + []interface{}{ + 3, + 2, + 1, + }, + []interface{}{ + 1, + 2, + 3, + }, + }, + Skip: true, // sub-arrays are wrapped in extra brackets + }, + }) +} + +func TestFuncSort(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$sort(nothing)", + Error: ErrUndefined, + }, + { + Expression: []string{ + "$sort(1)", + "$sort([1])", + }, + Output: []interface{}{ + float64(1), + }, + }, + { + Expression: "$sort([1,3,2])", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + { + Expression: "$sort([1,3,22,11])", + Output: []interface{}{ + float64(1), + float64(3), + float64(11), + float64(22), + }, + }, + }) +} + +func TestFuncSort2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$sort(Account.Order.Product.Price)", + Output: []interface{}{ + 21.67, + 34.45, + 34.45, + 107.99, + }, + }, + { + Expression: "$sort(Account.Order.Product.`Product Name`)", + Output: []interface{}{ + "Bowler Hat", + "Bowler Hat", + "Cloak", + "Trilby hat", + }, + }, + { + Expression: `$sort(Account.Order.Product, function($a, $b) { $a.(Price * Quantity) > $b.(Price * Quantity) }).(Price & " x " & Quantity)`, + Output: []interface{}{ + "21.67 x 1", + "34.45 x 2", + "107.99 x 1", + "34.45 x 4", + }, + }, + { + Expression: `$sort(Account.Order.Product, function($a, $b) { $a.Price > $b.Price }).SKU`, + Output: []interface{}{ + "0406634348", + "0406654608", + "040657863", + "0406654603", + }, + }, + { + Expression: ` + (Account.Order.Product + ~> $sort(λ($a,$b){$a.Quantity < $b.Quantity}) + ~> $sort(λ($a,$b){$a.Price > $b.Price}) + ).SKU`, + Output: []interface{}{ + "0406634348", + "040657863", + "0406654608", + "0406654603", + }, + }, + { + Expression: "$sort(Account.Order.Product)", + Error: fmt.Errorf("argument 1 of function sort must be an array of strings or numbers"), // TODO: Use a proper error + }, + }) +} + +func TestFuncSort3(t *testing.T) { + + data := []interface{}{1, 3, 2} + + runTestCases(t, data, []*testCase{ + { + Expression: "[[$], [$sort($)], [$]]", + Output: []interface{}{ + []float64{ + 1, + 3, + 2, + }, + []float64{ + 1, + 2, + 3, + }, + []float64{ + 1, + 3, + 2, + }, + }, + Skip: true, // inner arrays are wrapped in extra brackets + }, + }) +} + +func TestFuncShuffle(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$shuffle([])", + "$shuffle([nothing])", + }, + Output: []interface{}{}, + }, + { + Expression: []string{ + "$shuffle(1)", + "$shuffle([1])", + }, + Output: []interface{}{ + float64(1), + }, + }, + { + Expression: "$count($shuffle([1..10]))", + Output: 10, + }, + { + Expression: "$sort($shuffle([1..10]))", + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + float64(10), + }, + }, + { + Expression: "$shuffle(nothing)", + Error: ErrUndefined, + }, + { + Expression: "$shuffle()", + Error: &ArgCountError{ + Func: "shuffle", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$shuffle([],[])", + Error: &ArgCountError{ + Func: "shuffle", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$shuffle(1,2,3)", + Error: &ArgCountError{ + Func: "shuffle", + Expected: 1, + Received: 3, + }, + }, + }) +} + +func TestFuncShuffle2(t *testing.T) { + + // TODO: These tests don't actually verify that the values + // have been shuffled. + runTestCasesFunc(t, equalArraysUnordered, nil, []*testCase{ + { + Expression: []string{ + "$shuffle([1..10])", + "$shuffle($reverse([1..10]))", + }, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + float64(6), + float64(7), + float64(8), + float64(9), + float64(10), + }, + }, + { + Expression: `$shuffle([true,-0.5,"hello",[1,2,3],{"one":1}])`, + Output: []interface{}{ + true, + -0.5, + "hello", + []interface{}{ + float64(1), + float64(2), + float64(3), + }, + map[string]interface{}{ + "one": float64(1), + }, + }, + }, + }) +} + +func TestFuncZip(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$zip(1,2,3)`, + `$zip(1,2,[3])`, + `$zip([1],[2],[3])`, + `$zip([1],[2,3],[3,4,5])`, + }, + Output: []interface{}{ + []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + }, + { + Expression: `$zip([1,2,3])`, + Output: []interface{}{ + []interface{}{ + float64(1), + }, + []interface{}{ + float64(2), + }, + []interface{}{ + float64(3), + }, + }, + }, + { + Expression: `$zip([1,2,3],["one","two","three"])`, + Output: []interface{}{ + []interface{}{ + float64(1), + "one", + }, + []interface{}{ + float64(2), + "two", + }, + []interface{}{ + float64(3), + "three", + }, + }, + }, + { + Expression: `$zip([1,2,3],["one","two","three"],[true,false,true])`, + Output: []interface{}{ + []interface{}{ + float64(1), + "one", + true, + }, + []interface{}{ + float64(2), + "two", + false, + }, + []interface{}{ + float64(3), + "three", + true, + }, + }, + }, + { + Expression: `$zip([1,2,3],["one","two"],[true,false,true])`, + Output: []interface{}{ + []interface{}{ + float64(1), + "one", + true, + }, + []interface{}{ + float64(2), + "two", + false, + }, + }, + }, + { + Expression: []string{ + `$zip(nothing)`, + `$zip(nothing,nada,now't)`, + `$zip(1,2,3,nothing)`, + `$zip([1,2,3],nothing)`, + `$zip([1,2,3],[nothing])`, + `$zip([1,2,3],[nothing,nada,now't])`, + }, + Output: []interface{}{}, + }, + { + Expression: `$zip()`, + Error: fmt.Errorf("cannot call zip with no arguments"), + }, + }) +} + +func TestFuncSum(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$sum(0)", + "$sum([])", + "$sum([0])", + "$sum([nothing])", + "$sum([0,nothing])", + }, + Output: float64(0), + }, + { + Expression: "$sum(1)", + Output: float64(1), + }, + { + Expression: []string{ + "$sum(15)", + "$sum([1..5])", + "$sum([1..5])", + "$sum([1..4,5])", + "$sum([1,2,3,4,5])", + "$sum([1,2,3,4,nothing,nada,5])", + }, + Output: float64(15), + }, + { + Expression: []string{ + `$sum("")`, + "$sum(true)", + `$sum({"one":1})`, + }, + Error: errors.New("cannot call sum on a non-array type"), // TODO: Don't rely on error strings + }, + { + Expression: []string{ + `$sum([1,2,"3"])`, + "$sum([1,2,true])", + }, + Error: errors.New("cannot call sum on an array with non-number types"), // TODO: Don't rely on error strings + }, + { + Expression: "$sum()", + Error: &ArgCountError{ + Func: "sum", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$sum(1,2)", + Error: &ArgCountError{ + Func: "sum", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$sum(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncSum2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$sum(Account.Order.Product.(Price * Quantity))", + Output: 336.36, + }, + { + Expression: "Account.Order.$sum(Product.(Price * Quantity))", + Output: []interface{}{ + 90.57000000000001, + 245.79000000000002, + }, + }, + { + Expression: `Account.Order.(OrderID & ": " & $sum(Product.(Price*Quantity)))`, + Output: []interface{}{ + // TODO: Why does jsonata-js only display to 2dp? + "order103: 90.57000000000001", + "order104: 245.79000000000002", + }, + }, + { + Expression: "$sum(Account.Order)", + Error: fmt.Errorf("cannot call sum on an array with non-number types"), // TODO: relying on error strings is bad + }, + }) +} + +func TestFuncMax(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$max(1)", + "$max([1,0])", + "$max([1,0,-1.5])", + }, + Output: float64(1), + }, + { + Expression: []string{ + "$max(-1)", + "$max([-1,-5])", + "$max([-1,-5,nothing])", + }, + Output: float64(-1), + }, + { + Expression: []string{ + "$max(3)", + "$max([1,2,3])", + "$max([1..3])", + "$max([1..3,nothing])", + }, + Output: float64(3), + }, + { + Expression: []string{ + `$max("")`, + `$max(true)`, + `$max({"one":1})`, + }, + Error: fmt.Errorf("cannot call max on a non-array type"), // TODO: Don't rely on the error string + }, + { + Expression: []string{ + `$max(["1","2","3"])`, + `$max(["1","2",3])`, + }, + Error: fmt.Errorf("cannot call max on an array with non-number types"), // TODO: Don't rely on the error string + }, + { + Expression: "$max()", + Error: &ArgCountError{ + Func: "max", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$max([],[])", + Error: &ArgCountError{ + Func: "max", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$max(1,2,3)", + Error: &ArgCountError{ + Func: "max", + Expected: 1, + Received: 3, + }, + }, + { + Expression: []string{ + "$max(nothing)", + "$max([])", + "$max([nothing])", + "$max([nothing,nada,nichts])", + }, + Error: ErrUndefined, + }, + }) +} + +func TestFuncMax2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$max(Account.Order.Product.(Price * Quantity))", + Output: 137.8, + }, + { + Expression: "$max(Account.Order.Product.(Price * Quantity))", + Output: 137.8, + }, + { + Expression: "Account.Order.$max(Product.(Price * Quantity))", + Output: []interface{}{ + 68.9, + 137.8, + }, + }, + }) +} + +func TestFuncMin(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$min(1)", + "$min([1,2,3])", + "$min([1..3])", + "$min([1..3,nothing])", + }, + Output: float64(1), + }, + { + Expression: []string{ + "$min(-5)", + "$min([-1,-2,-3,-4,-5])", + "$min([-5..0])", + "$min([-5,nothing])", + }, + Output: float64(-5), + }, + { + Expression: []string{ + `$min("")`, + `$min(true)`, + `$min({"one":1})`, + }, + Error: fmt.Errorf("cannot call min on a non-array type"), // TODO: Don't rely on error strings + }, + { + Expression: []string{ + `$min(["1","2","3"])`, + `$min(["1","2",3])`, + }, + Error: fmt.Errorf("cannot call min on an array with non-number types"), // TODO: Don't rely on error strings + }, + { + Expression: "$min()", + Error: &ArgCountError{ + Func: "min", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$min([],[])", + Error: &ArgCountError{ + Func: "min", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$min(1,2,3)", + Error: &ArgCountError{ + Func: "min", + Expected: 1, + Received: 3, + }, + }, + { + Expression: []string{ + "$min([])", + "$min(nothing)", + "$min([nothing,nada,now't])", + }, + Error: ErrUndefined, + }, + }) +} + +func TestFuncMin2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$min(Account.Order.Product.(Price * Quantity))", + Output: 21.67, + }, + { + Expression: "Account.Order.$min(Product.(Price * Quantity))", + Output: []interface{}{ + 21.67, + 107.99, + }, + }, + { + Expression: `Account.Order.(OrderID & ": " & $min(Product.(Price*Quantity)))`, + Output: []interface{}{ + "order103: 21.67", + "order104: 107.99", + }, + }, + }) +} + +func TestFuncAverage(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$average(1)", + Output: float64(1), + }, + { + Expression: []string{ + "$average(2)", + "$average([1,2,3])", + "$average([1..3])", + "$average([1..3,nothing])", + }, + Output: float64(2), + }, + { + Expression: []string{ + `$average("")`, + `$average(true)`, + `$average({"one":1})`, + }, + Error: fmt.Errorf("cannot call average on a non-array type"), // TODO: Don't rely on the error string + }, + { + Expression: []string{ + `$average(["1","2","3"])`, + `$average(["1","2",3])`, + }, + Error: fmt.Errorf("cannot call average on an array with non-number types"), // TODO: Don't rely on the error string + }, + { + Expression: "$average()", + Error: &ArgCountError{ + Func: "average", + Expected: 1, + Received: 0, + }, + }, + { + Expression: "$average([],[])", + Error: &ArgCountError{ + Func: "average", + Expected: 1, + Received: 2, + }, + }, + { + Expression: "$average(1,2,3)", + Error: &ArgCountError{ + Func: "average", + Expected: 1, + Received: 3, + }, + }, + { + Expression: []string{ + "$average([])", + "$average(nothing)", + "$average([nothing,nada,now't])"}, + Error: ErrUndefined, + }, + }) +} + +func TestFuncAverage2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "$average(Account.Order.Product.(Price * Quantity))", + Output: 84.09, + }, + { + Expression: "Account.Order.$average(Product.(Price * Quantity))", + Output: []interface{}{ + 45.285000000000004, + 122.89500000000001, + }, + }, + { + Expression: `Account.Order.(OrderID & ": " & $average(Product.(Price*Quantity)))`, + Output: []interface{}{ + // TODO: Why does jsonata-js only display to 3dp? + "order103: 45.285000000000004", + "order104: 122.89500000000001", + }, + }, + }) +} + +func TestFuncSpread(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$spread("Hello World")`, + Output: "Hello World", + }, + { + Expression: `$spread([1,2,3])`, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + }, + }, + { + Expression: `$string($spread(function($x){$x*$x}))`, + Output: "", + }, + { + Expression: "$spread(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncSpread2(t *testing.T) { + + data := []map[string]interface{}{ + { + "one": 1, + }, + { + "two": 2, + }, + { + "three": 3, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: `$spread($[0])`, + Output: []interface{}{ + map[string]interface{}{ + "one": 1, + }, + }, + }, + { + Expression: `$spread($)`, + Output: []interface{}{ + map[string]interface{}{ + "one": 1, + }, + map[string]interface{}{ + "two": 2, + }, + map[string]interface{}{ + "three": 3, + }, + }, + }, + }) +} + +func TestFuncSpread3(t *testing.T) { + + runTestCasesFunc(t, equalArraysUnordered, testdata.account, []*testCase{ + { + Expression: "$spread((Account.Order.Product.Description))", + Output: []interface{}{ + map[string]interface{}{ + "Colour": "Purple", + }, + map[string]interface{}{ + "Width": float64(300), + }, + map[string]interface{}{ + "Height": float64(200), + }, + map[string]interface{}{ + "Depth": float64(210), + }, + map[string]interface{}{ + "Weight": 0.75, + }, + map[string]interface{}{ + "Colour": "Orange", + }, + map[string]interface{}{ + "Width": float64(300), + }, + map[string]interface{}{ + "Height": float64(200), + }, + map[string]interface{}{ + "Depth": float64(210), + }, + map[string]interface{}{ + "Weight": 0.6, + }, + map[string]interface{}{ + "Colour": "Purple", + }, + map[string]interface{}{ + "Width": float64(300), + }, + map[string]interface{}{ + "Height": float64(200), + }, + map[string]interface{}{ + "Depth": float64(210), + }, + map[string]interface{}{ + "Weight": 0.75, + }, + map[string]interface{}{ + "Colour": "Black", + }, + map[string]interface{}{ + "Width": float64(30), + }, + map[string]interface{}{ + "Height": float64(20), + }, + map[string]interface{}{ + "Depth": float64(210), + }, + map[string]interface{}{ + "Weight": float64(2), + }, + }, + }, + }) +} + +/* +func TestFuncSpread4(t *testing.T) { + + t.SkipNow() + + data := []struct { + Int int + Bool bool + String string + Interface interface{} + unexported bool + }{ + { + Int: 1, + Bool: true, + String: "string", + }, + { + Int: 0, + Bool: false, + String: "", + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: `$spread($[0])`, + Output: []interface{}{ + map[string]interface{}{ + "Int": 1, + }, + map[string]interface{}{ + "Bool": true, + }, + map[string]interface{}{ + "String": "string", + }, + map[string]interface{}{ + "Interface": nil, + }, + }, + }, + { + Expression: `$spread($[1])`, + Output: []interface{}{ + map[string]interface{}{ + "Int": 0, + }, + map[string]interface{}{ + "Bool": false, + }, + map[string]interface{}{ + "String": "", + }, + map[string]interface{}{ + "Interface": nil, + }, + }, + }, + { + Expression: `$spread($)`, + Output: []interface{}{ + map[string]interface{}{ + "Int": 1, + }, + map[string]interface{}{ + "Bool": true, + }, + map[string]interface{}{ + "String": "string", + }, + map[string]interface{}{ + "Interface": nil, + }, + map[string]interface{}{ + "Int": 0, + }, + map[string]interface{}{ + "Bool": false, + }, + map[string]interface{}{ + "String": "", + }, + map[string]interface{}{ + "Interface": nil, + }, + }, + }, + }) +} +*/ + +func TestFuncMerge(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$merge({"a":1})`, + Output: map[string]interface{}{ + "a": float64(1), + }, + }, + { + Expression: `$merge([{"a":1}, {"b":2}])`, + Output: map[string]interface{}{ + "a": float64(1), + "b": float64(2), + }, + }, + { + Expression: `$merge([{"a": 1}, {"b": 2, "c": 3}])`, + Output: map[string]interface{}{ + "a": float64(1), + "b": float64(2), + "c": float64(3), + }, + }, + { + Expression: `$merge([{"a": 1}, {"b": 2, "a": 3}])`, + Output: map[string]interface{}{ + "a": float64(3), + "b": float64(2), + }, + }, + { + Expression: []string{ + `$merge([])`, + `$merge({})`, + }, + Output: map[string]interface{}{}, + }, + { + Expression: `$merge(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncEach(t *testing.T) { + + runTestCasesFunc(t, equalArraysUnordered, testdata.address, []*testCase{ + { + Expression: `$each(Address, λ($v, $k) {$k & ": " & $v})`, + Output: []interface{}{ + "Street: Hursley Park", + "City: Winchester", + "Postcode: SO21 2JN", + }, + }, + }) +} + +func TestFuncMap(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$map([1,2,3], $string)`, + Output: []interface{}{ + "1", + "2", + "3", + }, + }, + { + Expression: `$map([1,4,9,16], $squareroot)`, + Exts: map[string]Extension{ + "squareroot": { + Func: func(x float64) float64 { + return math.Sqrt(x) + }, + }, + }, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + }, + }, + { + Expression: ` + ( + $data := { + "one": [1,2,3,4,5], + "two": [5,4,3,2,1] + }; + $add := function($x){$x*$x}; + $map($data.one, $add) + )`, + Output: []interface{}{ + float64(1), + float64(4), + float64(9), + float64(16), + float64(25), + }, + }, + { + Expression: "$map($string)", + Error: &ArgCountError{ + Func: "map", + Expected: 2, + Received: 1, + }, + }, + }) +} + +func TestFuncMap2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `Account.Order.Product ~> $map(λ($prod, $index) { $index+1 & ": " & $prod.` + "`Product Name`" + ` })`, + Output: []interface{}{ + "1: Bowler Hat", + "2: Trilby hat", + "3: Bowler Hat", + "4: Cloak", + }, + }, + { + Expression: `Account.Order.Product ~> $map(λ($prod, $index, $arr) { $index+1 & "/" & $count($arr) & ": " & $prod.` + "`Product Name`" + ` })`, + Output: []interface{}{ + "1/4: Bowler Hat", + "2/4: Trilby hat", + "3/4: Bowler Hat", + "4/4: Cloak", + }, + }, + { + Expression: `$map(Phone, function($v, $i) {$v.type="office" ? $i: null})`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncMap3(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: []string{ + `$map(Phone, function($v, $i) {$i[$v.type="office"]})`, + `$map(Phone, function($v, $i) {$v.type="office" ? $i})`, + }, + Output: []interface{}{ + 1, + 2, + }, + }, + { + Expression: `$map(Phone, function($v, $i) {$v.type="office" ? $i: null})`, + Output: []interface{}{ + nil, + 1, + 2, + nil, + }, + Skip: true, // works with non-null else value. Returning the wrong kind of nils? + }, + }) +} + +func TestFuncMapZip(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `( + $data := { + "one": [1,2,3,4,5], + "two": [5,4,3,2,1] + }; + $map($zip($data.one, $data.two), $sum) + )`, + `( + $data := { + "one": [1,2,3,4,5], + "two": [5,4,3,2,1] + }; + $data.$zip(one, two) ~> $map($sum) + )`, + }, + Output: []interface{}{ + float64(6), + float64(6), + float64(6), + float64(6), + float64(6), + }, + }, + { + Expression: []string{ + `( + $data := { + "one": [1], + "two": [5] + }; + $data[].$zip(one, two) ~> $map($sum) + )`, + `( + $data := { + "one": 1, + "two": 5 + }; + $data[].$zip(one, two) ~> $map($sum) + )`, + }, + Output: []interface{}{ + float64(6), + }, + }, + }) +} + +func TestFuncFilter(t *testing.T) { + + runTestCases(t, testdata.library, []*testCase{ + { + Expression: `$filter([1..10], function($v) {$v % 2})`, + Output: []interface{}{ + float64(1), + float64(3), + float64(5), + float64(7), + float64(9), + }, + }, + }) +} + +func TestFuncFilter2(t *testing.T) { + + runTestCases(t, testdata.library, []*testCase{ + { + Expression: `(library.books~>$filter(λ($v, $i, $a) {$v.price = $max($a.price)})).isbn`, + Output: "9780262510875", + }, + { + Expression: `(nothing~>$filter(λ($v, $i, $a) {$v.price = $max($a.price)})).isbn`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncReduce(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: ` + ( + $seq := [1,2,3,4,5]; + $reduce($seq, function($x, $y){$x+$y}) + )`, + Output: float64(15), + }, + { + Expression: ` + ( + $concat := function($s){function($a, $b){$string($a) & $s & $string($b)}}; + $comma_join := $concat(' ... '); + $reduce([1,2,3,4,5], $comma_join) + )`, + Output: "1 ... 2 ... 3 ... 4 ... 5", + }, + { + Expression: ` + ( + $seq := [1,2,3,4,5]; + $reduce($seq, function($x, $y){$x+$y}, 2) + )`, + Output: float64(17), + }, + { + Expression: ` + ( + $seq := 1; + $reduce($seq, function($x, $y){$x+$y}) + )`, + Output: float64(1), + }, + { + Expression: ` + ( + $product := function($a, $b) { $a * $b }; + $power := function($x, $n) { $n = 0 ? 1 : $reduce([1..$n].($x), $product) }; + [0..5].$power(2, $) + )`, + Output: []interface{}{ + float64(1), + float64(2), + float64(4), + float64(8), + float64(16), + float64(32), + }, + }, + { + Expression: ` + ( + $seq := 1; + $reduce($seq, function($x){$x}) + )`, + Error: fmt.Errorf("second argument of function \"reduce\" must be a function that takes two arguments"), + }, + }) +} + +func TestFuncReduce2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `$reduce(Account.Order.Product.Quantity, $append)`, + Output: []interface{}{ + float64(2), + float64(1), + float64(4), + float64(1), + }, + }, + }) +} + +func TestFuncReduce3(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: `$reduce(Account.Order.Product.Quantity, $append)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncSift(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: ` + ( + $data := { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10 + }; + $sift($data, function($v){$v % 2}) + )`, + Output: map[string]interface{}{ + "one": float64(1), + "three": float64(3), + "five": float64(5), + "seven": float64(7), + "nine": float64(9), + }, + }, + { + Expression: ` + ( + $data := { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10 + }; + $sift($data, function($v,$k){$contains($k,"o")}) + )`, + Output: map[string]interface{}{ + "one": float64(1), + "two": float64(2), + "four": float64(4), + }, + }, + { + Expression: ` + ( + $data := { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10 + }; + $sift($data, function($v,$k){$length($k) >= $v}) + )`, + Output: map[string]interface{}{ + "one": float64(1), + "two": float64(2), + "three": float64(3), + "four": float64(4), + }, + }, + }) +} + +func TestFuncSift2(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: `$sift(λ($v){$v.**.Postcode})`, + Output: map[string]interface{}{ + "Address": map[string]interface{}{ + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN", + }, + "Other": map[string]interface{}{ + "Over 18 ?": true, + "Misc": nil, + "Alternative.Address": map[string]interface{}{ + "Street": "Brick Lane", + "City": "London", + "Postcode": "E1 6RF", + }, + }, + }, + }, + { + Expression: `**[*].$sift(λ($v){$v.Postcode})`, + Output: []interface{}{ + map[string]interface{}{ + "Address": map[string]interface{}{ + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN", + }, + }, + map[string]interface{}{ + "Alternative.Address": map[string]interface{}{ + "Street": "Brick Lane", + "City": "London", + "Postcode": "E1 6RF", + }, + }, + }, + }, + { + Expression: []string{ + `$sift(λ($v, $k){$match($k, /^A/)})`, + `$sift(λ($v, $k){$k ~> /^A/})`, + }, + Output: map[string]interface{}{ + "Age": float64(28), + "Address": map[string]interface{}{ + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN", + }, + }, + }, + }) +} + +func TestHigherOrderFunctions(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: ` + ( + $twice:=function($f){function($x){$f($f($x))}}; + $add3:=function($y){$y+3}; + $add6:=$twice($add3); + $add6(7) + )`, + Output: float64(13), + }, + { + Expression: `λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})}(λ($f) { λ($n) { $n < 2 ? 1 : $n * $f($n - 1) } })(6)`, + Output: float64(720), + }, + { + Expression: `λ($f) { λ($x) { $x($x) }( λ($g) { $f( (λ($a) {$g($g)($a)}))})}(λ($f) { λ($n) { $n <= 1 ? $n : $f($n-1) + $f($n-2) } })(6)`, + Output: float64(8), + }, + }) +} + +func TestClosures(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: ` + Account.( + $AccName := function() { $.` + "`Account Name`" + `}; + Order[OrderID = "order104"].Product{ + "Account": $AccName(), + "SKU-" & $string(ProductID): $.` + "`Product Name`" + ` + } + )`, + Output: map[string]interface{}{ + "Account": "Firefly", + "SKU-858383": "Bowler Hat", + "SKU-345664": "Cloak", + }, + }, + }) +} + +func TestFuncString(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$string(5)`, + Output: "5", + }, + { + Expression: `$string(22/7)`, + Output: "3.142857142857143", // TODO: jsonata-js returns "3.142857142857" + }, + { + Expression: `$string(1e100)`, + Output: "1e+100", + }, + { + Expression: `$string(1e-100)`, + Output: "1e-100", + }, + { + Expression: `$string(1e-6)`, + Output: "0.000001", + }, + { + Expression: `$string(1e-7)`, + Output: "1e-7", + }, + { + Expression: `$string(1e+20)`, + Output: "100000000000000000000", + }, + { + Expression: `$string(1e+21)`, + Output: "1e+21", + }, + { + Expression: `$string(true)`, + Output: "true", + }, + { + Expression: `$string(false)`, + Output: "false", + }, + { + Expression: `$string(null)`, + Output: "null", + }, + { + Expression: `$string(blah)`, + Error: ErrUndefined, + }, + { + Expression: []string{ + `$string($string)`, + `$string(/hat/)`, + `$string(function(){true})`, + `$string(function(){1})`, + }, + Output: "", + }, + { + Expression: `$string({"string": "hello"})`, + Output: `{"string":"hello"}`, + }, + { + Expression: `$string(["string", 5])`, + Output: `["string",5]`, + }, + { + Expression: ` + $string({ + "string": "hello", + "number": 78.8 / 2, + "null":null, + "boolean": false, + "function": $sum, + "lambda": function(){true}, + "object": { + "str": "another", + "lambda2": function($n){$n} + }, + "array": [] + })`, + // TODO: Can we get this to print in field order like jsonata-js? + Output: `{"array":[],"boolean":false,"function":"","lambda":"","null":null,"number":39.4,"object":{"lambda2":"","str":"another"},"string":"hello"}`, + //Output: `{"string":"hello","number":39.4,"null":null,"boolean":false,"function":"","lambda":"","object":{"str":"another","lambda2":""},"array":[]}`, + }, + { + Expression: `$string(1/0)`, + Error: &EvalError{ + Type: ErrNumberInf, + Value: "/", + }, + }, + { + Expression: `$string({"inf": 1/0})`, + Error: &EvalError{ + Type: ErrNumberInf, + Value: "/", + }, + }, + { + Expression: `$string(2,3)`, + Error: &ArgCountError{ + Func: "string", + Expected: 1, + Received: 2, + }, + }, + }) +} + +func TestFuncString2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `Account.Order.$string($sum(Product.(Price* Quantity)))`, + // TODO: jsonata-js rounds to "90.57" and "245.79" + Output: []interface{}{ + "90.57000000000001", + "245.79000000000002", + }, + }, + }) +} + +func TestFuncSubstring(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$substring("hello world", 0, 5)`, + Output: "hello", + }, + { + Expression: []string{ + `$substring("hello world", -5, 5)`, + `$substring("hello world", 6)`, + }, + Output: "world", + }, + { + Expression: `$substring("hello world", -100, 4)`, + Output: "hell", + }, + { + Expression: []string{ + `$substring("hello world", 100)`, + `$substring("hello world", 100, 5)`, + `$substring("hello world", 0, 0)`, + `$substring("hello world", 0, -100)`, + `$substring("超明體繁", 2, 0)`, + }, + Output: "", + }, + { + Expression: []string{ + `$substring("超明體繁", 2)`, + `$substring("超明體繁", -2)`, + `$substring("超明體繁", -2, 2)`, + }, + Output: "體繁", + }, + { + Expression: `$substring(nothing, 6)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncSubstringBefore(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$substringBefore("Hello World", " ")`, + Output: "Hello", + }, + { + Expression: `$substringBefore("Hello World", "l")`, + Output: "He", + }, + { + Expression: `$substringBefore("Hello World", "f")`, + Output: "Hello World", + }, + { + Expression: `$substringBefore("Hello World", "He")`, + Output: "", + }, + { + Expression: `$substringBefore("Hello World", "")`, + Output: "", + }, + { + Expression: `$substringBefore("超明體繁", "體")`, + Output: "超明", + }, + { + Expression: `$substringBefore(nothing, "He")`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncSubstringAfter(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$substringAfter("Hello World", " ")`, + Output: "World", + }, + { + Expression: `$substringAfter("Hello World", "l")`, + Output: "lo World", + }, + { + Expression: `$substringAfter("Hello World", "f")`, + Output: "Hello World", + }, + { + Expression: `$substringAfter("Hello World", "ld")`, + Output: "", + }, + { + Expression: `$substringAfter("Hello World", "")`, + Output: "Hello World", + }, + { + Expression: `$substringAfter("超明體繁", "明")`, + Output: "體繁", + }, + { + Expression: `$substringAfter(nothing, "ld")`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncLowercase(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$lowercase("Hello World")`, + Output: "hello world", + }, + { + Expression: `$lowercase("Étude in Black")`, + Output: "étude in black", + }, + { + Expression: `$lowercase(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncUppercase(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$uppercase("Hello World")`, + Output: "HELLO WORLD", + }, + { + Expression: `$uppercase("étude in black")`, + Output: "ÉTUDE IN BLACK", + }, + { + Expression: `$uppercase(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncLength(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$length("")`, + Output: 0, + }, + { + Expression: `$length("hello")`, + Output: 5, + }, + { + Expression: `$length(nothing)`, + Error: ErrUndefined, + }, + { + Expression: `$length("\u03BB-calculus")`, + Output: 10, + }, + { + Expression: `$length("\uD834\uDD1E")`, + Output: 1, + }, + { + Expression: `$length("𝄞")`, + Output: 1, + }, + { + Expression: `$length("超明體繁")`, + Output: 4, + }, + { + Expression: []string{ + `$length("\t")`, + `$length("\n")`, + }, + Output: 1, + }, + { + Expression: []string{ + `$length(1234)`, + `$length(true)`, + `$length(false)`, + `$length(null)`, + `$length(1.0)`, + `$length(["hello"])`, + }, + Error: &ArgTypeError{ + Func: "length", + Which: 1, + }, + }, + { + Expression: `$length("hello", "world")`, + Error: &ArgCountError{ + Func: "length", + Expected: 1, + Received: 2, + }, + }, + }) +} + +func TestFuncTrim(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$trim("Hello World")`, + `$trim(" Hello \n \t World \t ")`, + }, + Output: "Hello World", + }, + { + Expression: "$trim()", + Error: &ArgCountError{ + Func: "trim", + Expected: 1, + Received: 0, + }, + Skip: true, // returns ErrUndefined (is it using context?) + }, + }) +} + +func TestFuncPad(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$pad("foo", 5)`, + `$pad("foo", 5, "")`, + `$pad("foo", 5, " ")`, + }, + Output: "foo ", + }, + { + Expression: `$pad("foo", -5)`, + Output: " foo", + }, + { + Expression: `$pad("foo", -5, ".")`, + Output: "..foo", + }, + { + Expression: `$pad("foo", 5, "超")`, + Output: "foo超超", + }, + { + Expression: []string{ + `$pad("foo", 1)`, + `$pad("foo", -1)`, + }, + Output: "foo", + }, + { + Expression: `$pad("foo", 8, "-+")`, + Output: "foo-+-+-", + }, + { + Expression: `$pad("超明體繁", 5)`, + Output: "超明體繁 ", + }, + { + Expression: `$pad("", 6, "超明體繁")`, + Output: "超明體繁超明", + }, + { + Expression: `$pad(nothing, -1)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncContains(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$contains("Hello World", "lo")`, + `$contains("Hello World", "World")`, + }, + Output: true, + }, + { + Expression: []string{ + `$contains("Hello World", "Word")`, + `$contains("Hello World", "world")`, + }, + Output: false, + }, + { + Expression: `$contains("超明體繁", "明體")`, + Output: true, + }, + { + Expression: `$contains("超明體繁", "體明")`, + Output: false, + }, + { + Expression: `$contains(nothing, "World")`, + Error: ErrUndefined, + }, + { + Expression: `$contains(23, 3)`, + Error: &ArgTypeError{ + Func: "contains", + Which: 1, + }, + }, + { + Expression: `$contains("23", 3)`, + Error: &ArgTypeError{ + Func: "contains", + Which: 2, + }, + }, + }) +} + +func TestFuncSplit(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$split("Hello World", " ")`, + Output: []string{ + "Hello", + "World", + }, + }, + { + Expression: `$split("Hello World", " ")`, + Output: []string{ + "Hello", + "", + "World", + }, + }, + { + Expression: `$split("Hello", " ")`, + Output: []string{ + "Hello", + }, + }, + { + Expression: `$split("Hello", "")`, + Output: []string{ + "H", + "e", + "l", + "l", + "o", + }, + }, + { + Expression: `$split("超明體繁", "")`, + Output: []string{ + "超", + "明", + "體", + "繁", + }, + }, + { + Expression: `$sum($split("12345", "").$number($))`, + Output: float64(15), + }, + { + Expression: []string{ + `$split("a, b, c, d", ", ")`, + `$split("a, b, c, d", ", ", 10)`, + //`$split("a, b, c, d", ",").$trim()`, // returns ErrUndefined + }, + Output: []string{ + "a", + "b", + "c", + "d", + }, + }, + { + Expression: []string{ + `$split("a, b, c, d", ", ", 2)`, + `$split("a, b, c, d", ", ", 2.5)`, + }, + Output: []string{ + "a", + "b", + }, + }, + { + Expression: `$split("a, b, c, d", ", ", 0)`, + Output: []string{}, + }, + { + Expression: `$split(nothing, " ")`, + Error: ErrUndefined, + }, + { + Expression: `$split("a, b, c, d", ", ", -3)`, + Error: fmt.Errorf("third argument of the split function must evaluate to a positive number"), // TODO: Use a proper error for this + }, + { + Expression: []string{ + `$split("a, b, c, d", ", ", null)`, + `$split("a, b, c, d", ", ", "2")`, + `$split("a, b, c, d", ", ", true)`, + }, + Error: &ArgTypeError{ + Func: "split", + Which: 3, + }, + }, + { + Expression: `$split(12345, 3)`, + Error: &ArgTypeError{ + Func: "split", + Which: 1, + }, + }, + { + Expression: `$split(12345)`, + Error: &ArgCountError{ + Func: "split", + Expected: 3, + Received: 1, + }, + }, + }) +} + +func TestFuncJoin(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + `$join("hello", "")`, + `$join(["hello"], "")`, + }, + Output: "hello", + }, + { + Expression: `$join(["hello", "world"], "")`, + Output: "helloworld", + }, + { + Expression: `$join(["hello", "world"], ", ")`, + Output: "hello, world", + }, + { + Expression: `$join(["超","明","體","繁"])`, + Output: "超明體繁", + }, + { + Expression: `$join([], ", ")`, + Output: "", + }, + { + Expression: `$join(true, ", ")`, + Error: fmt.Errorf("function join takes an array of strings"), // TODO: Use a proper error + }, + { + Expression: `$join([1,2,3], ", ")`, + Error: fmt.Errorf("function join takes an array of strings"), // TODO: Use a proper error + }, + { + Expression: `$join("hello", 3)`, + Error: &ArgTypeError{ + Func: "join", + Which: 2, + }, + }, + { + Expression: `$join()`, + Error: &ArgCountError{ + Func: "join", + Expected: 2, + Received: 0, + }, + }, + }) +} + +func TestFuncJoin2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `$join(Account.Order.Product.Description.Colour, ", ")`, + Output: "Purple, Orange, Purple, Black", + }, + { + Expression: `$join(Account.Order.Product.Description.Colour, "")`, + Output: "PurpleOrangePurpleBlack", + }, + { + Expression: `$join(Account.blah.Product.Description.Colour, ", ")`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncReplace(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$replace("Hello World", "World", "Everyone")`, + Output: "Hello Everyone", + }, + { + Expression: `$replace("the cat sat on the mat", "at", "it")`, + Output: "the cit sit on the mit", + }, + { + Expression: `$replace("the cat sat on the mat", "at", "it", 0)`, + Output: "the cat sat on the mat", + }, + { + Expression: `$replace("the cat sat on the mat", "at", "it", 2)`, + Output: "the cit sit on the mat", + }, + { + Expression: `$replace(nothing, "at", "it", 2)`, + Error: ErrUndefined, + }, + { + Expression: `$replace("hello")`, + Error: &ArgCountError{ + Func: "replace", + Expected: 4, + Received: 1, + }, + }, + { + Expression: `$replace("hello", 1)`, + Error: &ArgCountError{ + Func: "replace", + Expected: 4, + Received: 2, + }, + }, + { + Expression: `$replace("hello", "l", "1", null)`, + Error: &ArgTypeError{ + Func: "replace", + Which: 4, + }, + }, + { + Expression: `$replace(123, 2, 1)`, + Error: &ArgTypeError{ + Func: "replace", + Which: 1, + }, + }, + { + Expression: `$replace("hello", 2, 1)`, + Error: &ArgTypeError{ + Func: "replace", + Which: 2, + }, + }, + { + Expression: `$replace("hello", "l", "1", -2)`, + Error: fmt.Errorf("fourth argument of function replace must evaluate to a positive number"), // TODO: Use a proper error + }, + { + Expression: `$replace("hello", "", "bye")`, + Error: fmt.Errorf("second argument of function replace can't be an empty string"), // TODO: Use a proper error + }, + }) +} + +func TestFormatNumber(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$formatNumber(12345.6, "#,###.00")`, + Output: "12,345.60", + }, + { + Expression: `$formatNumber(12345678.9, "9,999.99")`, + Output: "12,345,678.90", + }, + { + Expression: `$formatNumber(123412345678.9, "9,9,99.99")`, + Output: "123412345,6,78.90", + }, + { + Expression: `$formatNumber(1234.56789, "9,999.999,999")`, + Output: "1,234.567,890", + }, + { + Expression: `$formatNumber(123.9, "9999")`, + Output: "0124", + }, + { + Expression: `$formatNumber(0.14, "01%")`, + Output: "14%", + }, + { + Expression: `$formatNumber(0.4857,"###.###‰")`, + Output: "485.7‰", + }, + { + Expression: `$formatNumber(0.14, "###pm", {"per-mille": "pm"})`, + Output: "140pm", + }, + { + Expression: `$formatNumber(-6, "000")`, + Output: "-006", + }, + { + Expression: `$formatNumber(1234.5678, "00.000e0")`, + Output: "12.346e2", + }, + { + Expression: `$formatNumber(1234.5678, "00.000e000")`, + Output: "12.346e002", + }, + { + Expression: `$formatNumber(1234.5678, "①①.①①①e①", {"zero-digit": "\u245f"})`, + Output: "①②.③④⑥e②", + }, + { + Expression: []string{ + `$formatNumber(1234.5678, "𝟎𝟎.𝟎𝟎𝟎e𝟎", {"zero-digit": "𝟎"})`, + `$formatNumber(1234.5678, "𝟎𝟎.𝟎𝟎𝟎e𝟎", {"zero-digit": "\ud835\udfce"})`, + }, + Output: "𝟏𝟐.𝟑𝟒𝟔e𝟐", + }, + { + Expression: `$formatNumber(0.234, "0.0e0")`, + Output: "2.3e-1", + }, + { + Expression: `$formatNumber(0.234, "#.00e0")`, + Output: "0.23e0", + }, + { + Expression: `$formatNumber(0.123, "#.e9")`, + Output: "0.1e0", + }, + { + Expression: `$formatNumber(0.234, ".00e0")`, + Output: ".23e0", + }, + { + Expression: `$formatNumber(2392.14*(-36.58), "000,000.000###;###,###.000###")`, + Output: "87,504.4812", + }, + { + Expression: `$formatNumber(2.14*86.58,"PREFIX##00.000###SUFFIX")`, + Output: "PREFIX185.2812SUFFIX", + }, + { + Expression: `$formatNumber(1E20,"#,######")`, + Output: "100,000000,000000,000000", + }, + + // TODO: Make proper errors for these. + + { + Expression: `$formatNumber(20,"#;#;#")`, + Error: fmt.Errorf("picture string must contain 1 or 2 subpictures"), + }, + { + Expression: `$formatNumber(20,"#.0.0")`, + Error: fmt.Errorf("a subpicture cannot contain more than one decimal separator"), + }, + { + Expression: `$formatNumber(20,"#0%%")`, + Error: fmt.Errorf("a subpicture cannot contain more than one percent character"), + }, + { + Expression: `$formatNumber(20,"#0‰‰")`, + Error: fmt.Errorf("a subpicture cannot contain more than one per-mille character"), + }, + { + Expression: `$formatNumber(20,"#0%‰")`, + Error: fmt.Errorf("a subpicture cannot contain both percent and per-mille characters"), + }, + { + Expression: `$formatNumber(20,".e0")`, + Error: fmt.Errorf("a mantissa part must contain at least one decimal or optional digit"), + }, + { + Expression: `$formatNumber(20,"0+.e0")`, + Error: fmt.Errorf("a subpicture cannot contain a passive character that is both preceded by and followed by an active character"), + }, + { + Expression: `$formatNumber(20,"0,.e0")`, + Error: fmt.Errorf("a group separator cannot be adjacent to a decimal separator"), + }, + { + Expression: `$formatNumber(20,"0,")`, + Error: fmt.Errorf("an integer part cannot end with a group separator"), + }, + { + Expression: `$formatNumber(20,"0,,0")`, + Error: fmt.Errorf("a subpicture cannot contain adjacent group separators"), + }, + { + Expression: `$formatNumber(20,"0#.e0")`, + Error: fmt.Errorf("an integer part cannot contain a decimal digit followed by an optional digit"), + }, + { + Expression: `$formatNumber(20,"#0.#0e0")`, + Error: fmt.Errorf("a fractional part cannot contain an optional digit followed by a decimal digit"), + }, + { + Expression: `$formatNumber(20,"#0.0e0%")`, + Error: fmt.Errorf("a subpicture cannot contain a percent/per-mille character and an exponent separator"), + }, + { + Expression: `$formatNumber(20,"#0.0e0,0")`, + Error: fmt.Errorf("an exponent part must consist solely of one or more decimal digits"), + }, + }) +} + +func TestFuncFormatBase(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$formatBase(100)", + Output: "100", + }, + { + Expression: "$formatBase(nothing)", + Error: ErrUndefined, + }, + { + Expression: []string{ + "$formatBase(100, 2)", + "$formatBase(99.5, 2.5)", + }, + Output: "1100100", + }, + { + Expression: "$formatBase(-100, 2)", + Output: "-1100100", + }, + { + Expression: "$formatBase(100, 1)", + Error: fmt.Errorf("the second argument to formatBase must be between 2 and 36"), + /*Error: &EvalError1{ + Errno: ErrInvalidBase, + Position: -3, + Value: "1", + },*/ + }, + { + Expression: "$formatBase(100, 37)", + Error: fmt.Errorf("the second argument to formatBase must be between 2 and 36"), + /*Error: &EvalError1{ + Errno: ErrInvalidBase, + Position: -3, + Value: "37", + },*/ + }, + }) +} + +func TestFuncBase64Encode(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$base64encode("hello:world")`, + Output: "aGVsbG86d29ybGQ=", + }, + { + Expression: `$base64encode(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncBase64Decode(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$base64decode("aGVsbG86d29ybGQ=")`, + Output: "hello:world", + }, + { + Expression: `$base64decode(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncNumber(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$number(0)", + `$number("0")`, + }, + Output: float64(0), + }, + { + Expression: []string{ + "$number(10)", + `$number("10")`, + }, + Output: float64(10), + }, + { + Expression: []string{ + "$number(-0.05)", + `$number("-0.05")`, + }, + Output: -0.05, + }, + { + Expression: `$number("1e2")`, + Output: float64(100), + }, + { + Expression: `$number("-1e2")`, + Output: float64(-100), + }, + { + Expression: `$number("1.0e-2")`, + Output: 0.01, + }, + { + Expression: `$number("1e0")`, + Output: float64(1), + }, + { + Expression: `$number("10e500")`, + Error: fmt.Errorf("unable to cast %q to a number", "10e500"), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "10e500", + },*/ + }, + { + Expression: `$number("Hello world")`, + Error: fmt.Errorf("unable to cast %q to a number", "Hello world"), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "Hello world", + },*/ + }, + { + Expression: `$number("1/2")`, + Error: fmt.Errorf("unable to cast %q to a number", "1/2"), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "1/2", + },*/ + }, + { + Expression: `$number("1234 hello")`, + Error: fmt.Errorf("unable to cast %q to a number", "1234 hello"), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "1234 hello", + },*/ + }, + { + Expression: `$number("")`, + Error: fmt.Errorf("unable to cast %q to a number", ""), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "", + },*/ + }, + { + Expression: `$number("[1]")`, + Error: fmt.Errorf("unable to cast %q to a number", "[1]"), + /*Error: &EvalError1{ + Errno: ErrCastNumber, + Position: -10, + Value: "[1]", + },*/ + }, + + { + Expression: `$number(true)`, + Output: 1., + }, + { + Expression: `$number(false)`, + Output: 0., + }, + { + Expression: `$number(null)`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number([])`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number([1,2])`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number(["hello"])`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number(["2"])`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number({})`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number({"hello":"world"})`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number($number)`, + Error: &ArgTypeError{ + Func: "number", + Which: 1, + }, + }, + { + Expression: `$number(1,2)`, + Error: &ArgCountError{ + Func: "number", + Expected: 1, + Received: 2, + }, + }, + { + Expression: `$number(nothing)`, + Error: ErrUndefined, + }, + }) +} + +func TestFuncAbs(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: []string{ + "$abs(3.7)", + "$abs(-3.7)", + }, + Output: 3.7, + }, + { + Expression: "$abs(0)", + Output: float64(0), + }, + { + Expression: "$abs(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncFloor(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$floor(3.7)", + Output: float64(3), + }, + { + Expression: "$floor(-3.7)", + Output: float64(-4), + }, + { + Expression: "$floor(0)", + Output: float64(0), + }, + { + Expression: "$floor(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncCeil(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$ceil(3.7)", + Output: float64(4), + }, + { + Expression: "$ceil(-3.7)", + Output: float64(-3), + }, + { + Expression: "$ceil(0)", + Output: float64(0), + }, + { + Expression: "$ceil(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncRound(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$round(4)", + Output: float64(4), + }, + { + Expression: "$round(2.3)", + Output: float64(2), + }, + { + Expression: "$round(2.7)", + Output: float64(3), + }, + { + Expression: "$round(2.5)", + Output: float64(2), + }, + { + Expression: "$round(3.5)", + Output: float64(4), + }, + { + Expression: []string{ + "$round(-0.5)", + "$round(-0.3)", + "$round(0.5)", + }, + Output: float64(0), + }, + { + Expression: []string{ + "$round(-7.5)", + "$round(-8.5)", + }, + Output: float64(-8), + }, + { + Expression: "$round(4.49, 1)", + Output: float64(4.5), + }, + { + Expression: "$round(4.525, 2)", + Output: float64(4.52), + }, + { + Expression: "$round(4.515, 2)", + Output: float64(4.52), + }, + { + Expression: "$round(12345, -2)", + Output: float64(12300), + }, + { + Expression: []string{ + "$round(12450, -2)", + "$round(12350, -2)", + }, + Output: float64(12400), + }, + { + Expression: "$round(6.022e-23, 24)", + Output: 6.0e-23, + }, + { + Expression: "$round(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncSqrt(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$sqrt(4)", + Output: float64(2), + }, + { + Expression: "$sqrt(2)", + Output: math.Sqrt2, + }, + { + Expression: "$sqrt(-2)", + Error: fmt.Errorf("the sqrt function cannot be applied to a negative number"), + }, + { + Expression: "$sqrt(nothing)", + Error: ErrUndefined, + }, + }) +} + +func TestFuncSqrt2(t *testing.T) { + + runTestCasesFunc(t, equalFloats(1e-13), nil, []*testCase{ + { + Expression: "$sqrt(10) * $sqrt(10)", + Output: float64(10), + }, + }) +} + +func TestFuncPower(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "$power(4,2)", + Output: float64(16), + }, + { + Expression: "$power(4,0.5)", + Output: float64(2), + }, + { + Expression: "$power(10,-2)", + Output: 0.01, + }, + { + Expression: "$power(-2,3)", + Output: float64(-8), + }, + { + Expression: "$power(nothing,3)", + Error: ErrUndefined, + }, + { + Expression: "$power(-2,1/3)", + Error: fmt.Errorf("the power function has resulted in a value that cannot be represented as a JSON number"), + }, + { + Expression: "$power(100,1000)", + Error: fmt.Errorf("the power function has resulted in a value that cannot be represented as a JSON number"), + }, + }) +} + +func TestFuncRandom(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "($x := $random(); $x >= 0 and $x < 1)", + Output: true, + }, + { + Expression: "$random() = $random()", + Output: false, + }, + }) +} + +func TestFuncKeys(t *testing.T) { + + runTestCasesFunc(t, equalArraysUnordered, testdata.account, []*testCase{ + { + Expression: "$keys(Account)", + Output: []string{ + "Account Name", + "Order", + }, + }, + { + Expression: "$keys(Account.Order.Product)", + Output: []string{ + "Product Name", + "ProductID", + "SKU", + "Description", + "Price", + "Quantity", + }, + }, + }) +} + +func TestFuncKeys2(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$keys({"foo":{}})`, + Output: "foo", + /*Output: []string{ + "foo", + },*/ + }, + { + Expression: []string{ + "$keys({})", + `$keys("foo")`, + `$keys(function(){1})`, + `$keys(["foo", "bar"])`, + }, + Error: ErrUndefined, + }, + }) +} + +func TestFuncLookup(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `$lookup(Account, "Account Name")`, + Output: "Firefly", + }, + { + Expression: `$lookup(Account.Order.Product, "Product Name")`, + Output: []interface{}{ + "Bowler Hat", + "Trilby hat", + "Bowler Hat", + "Cloak", + }, + }, + { + Expression: `$lookup(Account.Order.Product.ProductID, "Product Name")`, + Error: ErrUndefined, + Skip: true, // returns a type error instead of ErrUndefined + }, + }) +} + +func TestFuncLookup2(t *testing.T) { + + data := map[string]interface{}{ + "temp": 22.7, + "wind": 7, + "gust": nil, + "timestamp": 1508971317377, + } + + runTestCases(t, data, []*testCase{ + { + Expression: []string{ + `$lookup($, "gust")`, + `$lookup($$, "gust")`, + }, + Output: nil, + }, + }) +} + +func TestDefaultContext(t *testing.T) { + + runTestCases(t, "5", []*testCase{ + { + Expression: "$number()", + Output: float64(5), + }, + }) +} + +func TestDefaultContext2(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: "[1..5].$string()", + Output: []interface{}{ + "1", + "2", + "3", + "4", + "5", + }, + }, + { + Expression: `[1..5].("Item " & $string())`, + Output: []interface{}{ + "Item 1", + "Item 2", + "Item 3", + "Item 4", + "Item 5", + }, + }, + }) +} + +func TestDefaultContext3(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `Account.Order.Product.` + "`Product Name`" + `.$uppercase().$substringBefore(" ")`, + Output: []interface{}{ + "BOWLER", + "TRILBY", + "BOWLER", + "CLOAK", + }, + }, + }) +} + +func TestApplyOperator(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: ` + ( + $uppertrim := $trim ~> $uppercase; + $uppertrim(" Hello World ") + )`, + Output: "HELLO WORLD", + }, + { + Expression: `"john@example.com" ~> $substringAfter("@") ~> $substringBefore(".")`, + Output: "example", + }, + { + Expression: ` + ( + $domain := $substringAfter(?,"@") ~> $substringBefore(?,"."); + $domain("john@example.com") + )`, + Output: "example", + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + [1..5] ~> $map($square) + )`, + Output: []interface{}{ + float64(1), + float64(4), + float64(9), + float64(16), + float64(25), + }, + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + [1..5] ~> $map($square) ~> $sum() + )`, + Output: float64(55), + }, + { + Expression: ` + ( + $betweenBackets := $substringAfter(?, "(") ~> $substringBefore(?, ")"); + $betweenBackets("test(foo)bar") + )`, + Output: "foo", + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + $chain := λ($f, $g){λ($x){$g($f($x))}}; + $instructions := [$sum, $square]; + $sumsq := $instructions ~> $reduce($chain); + [1..5] ~> $sumsq() + )`, + Output: float64(225), + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + $chain := λ($f, $g){λ($x){ $x ~> $f ~> $g }}; + $instructions := [$sum, $square, $string]; + $sumsq := $instructions ~> $reduce($chain); + [1..5] ~> $sumsq() + )`, + Output: "225", + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + $instructions := $sum ~> $square; + [1..5] ~> $instructions() + )`, + Output: float64(225), + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + $sum_of_squares := $map(?, $square) ~> $sum; + [1..5] ~> $sum_of_squares() + )`, + Output: float64(55), + }, + { + Expression: ` + ( + $times := λ($x, $y) { $x * $y }; + $product := $reduce(?, $times); + $square := function($x){$x*$x}; + $product_of_squares := $map(?, $square) ~> $product; + [1..5] ~> $product_of_squares() + )`, + Output: float64(14400), + }, + { + Expression: ` + ( + $square := function($x){$x*$x}; + [1..5] ~> $map($square) ~> $reduce(λ($x, $y) { $x * $y }); + )`, + Output: float64(14400), + }, + { + Expression: `"" ~> $substringAfter("@") ~> $substringBefore(".")`, + Output: "", + }, + { + Expression: `foo ~> $substringAfter("@") ~> $substringBefore(".")`, + Error: ErrUndefined, + }, + }) +} + +func TestApplyOperator2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order[0].OrderID ~> $uppercase()", + Output: "ORDER103", + }, + { + Expression: "Account.Order[0].OrderID ~> $uppercase() ~> $lowercase()", + Output: "order103", + }, + { + Expression: "Account.Order.OrderID ~> $join()", + Output: "order103order104", + }, + { + Expression: `Account.Order.OrderID ~> $join(", ")`, + Output: "order103, order104", + }, + { + Expression: "Account.Order.Product.(Price * Quantity) ~> $sum()", + Output: 336.36, + }, + { + Expression: ` + ( + $prices := Account.Order.Product.Price; + $quantities := Account.Order.Product.Quantity; + $product := λ($arr) { $arr[0] * $arr[1] }; + $zip($prices, $quantities) ~> $map($product) ~> $sum() + )`, + Output: 336.36, + }, + { + Expression: `42 ~> "hello"`, + Error: &EvalError{ + Type: ErrNonCallableApply, + Token: `"hello"`, + Value: "~>", + }, + }, + }) +} + +func TestTransformOperator(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `$ ~> |Account.Order.Product|{"Total":Price*Quantity},["Description", "SKU"]|`, + Output: map[string]interface{}{ + "Account": map[string]interface{}{ + "Account Name": "Firefly", + "Order": []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "Price": 34.45, + "Quantity": float64(2), + "Total": 68.9, + }, + map[string]interface{}{ + "Product Name": "Trilby hat", + "ProductID": float64(858236), + "Price": 21.67, + "Quantity": float64(1), + "Total": 21.67, + }, + }, + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "Price": 34.45, + "Quantity": float64(4), + "Total": 137.8, + }, + map[string]interface{}{ + "ProductID": float64(345664), + "Product Name": "Cloak", + "Price": 107.99, + "Quantity": float64(1), + "Total": 107.99, + }, + }, + }, + }, + }, + }, + }, + { + Expression: `Account.Order ~> |Product|{"Total":Price*Quantity},["Description", "SKU"]|`, + Output: []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "Price": 34.45, + "Quantity": float64(2), + "Total": 68.9, + }, + map[string]interface{}{ + "Product Name": "Trilby hat", + "ProductID": float64(858236), + "Price": 21.67, + "Quantity": float64(1), + "Total": 21.67, + }, + }, + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "Price": 34.45, + "Quantity": float64(4), + "Total": 137.8, + }, + map[string]interface{}{ + "ProductID": float64(345664), + "Product Name": "Cloak", + "Price": 107.99, + "Quantity": float64(1), + "Total": 107.99, + }, + }, + }, + }, + }, + { + Expression: `$ ~> |Account.Order.Product|{"Total":Price*Quantity, "Price": Price * 1.2}|`, + Output: map[string]interface{}{ + "Account": map[string]interface{}{ + "Account Name": "Firefly", + "Order": []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "0406654608", + "Description": map[string]interface{}{ + "Colour": "Purple", + "Width": float64(300), + "Height": float64(200), + "Depth": float64(210), + "Weight": 0.75, + }, + "Price": 41.34, + "Quantity": float64(2), + "Total": 68.9, + }, + map[string]interface{}{ + "Product Name": "Trilby hat", + "ProductID": float64(858236), + "SKU": "0406634348", + "Description": map[string]interface{}{ + "Colour": "Orange", + "Width": float64(300), + "Height": float64(200), + "Depth": float64(210), + "Weight": 0.6, + }, + "Price": 26.004, + "Quantity": float64(1), + "Total": 21.67, + }, + }, + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "040657863", + "Description": map[string]interface{}{ + "Colour": "Purple", + "Width": float64(300), + "Height": float64(200), + "Depth": float64(210), + "Weight": 0.75, + }, + "Price": 41.34, + "Quantity": float64(4), + "Total": 137.8, + }, + map[string]interface{}{ + "ProductID": float64(345664), + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": map[string]interface{}{ + "Colour": "Black", + "Width": float64(30), + "Height": float64(20), + "Depth": float64(210), + "Weight": float64(2), + }, + "Price": 129.588, + "Quantity": float64(1), + "Total": 107.99, + }, + }, + }, + }, + }, + }, + }, + { + Expression: []string{ + `$ ~> |Account.Order.Product|{},"Description"|`, + `$ ~> |Account.Order.Product|nomatch,"Description"|`, + }, + Output: map[string]interface{}{ + "Account": map[string]interface{}{ + "Account Name": "Firefly", + "Order": []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "0406654608", + "Price": 34.45, + "Quantity": float64(2), + }, + map[string]interface{}{ + "Product Name": "Trilby hat", + "ProductID": float64(858236), + "SKU": "0406634348", + "Price": 21.67, + "Quantity": float64(1), + }, + }, + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "040657863", + "Price": 34.45, + "Quantity": float64(4), + }, + map[string]interface{}{ + "ProductID": float64(345664), + "SKU": "0406654603", + "Product Name": "Cloak", + "Price": 107.99, + "Quantity": float64(1), + }, + }, + }, + }, + }, + }, + }, + { + Expression: `$ ~> |(Account.Order.Product)[0]|{"Description":"blah"}|`, + Output: map[string]interface{}{ + "Account": map[string]interface{}{ + "Account Name": "Firefly", + "Order": []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "0406654608", + "Description": "blah", + "Price": 34.45, + "Quantity": float64(2), + }, + map[string]interface{}{ + "Product Name": "Trilby hat", + "ProductID": float64(858236), + "SKU": "0406634348", + "Description": map[string]interface{}{ + "Colour": "Orange", + "Width": float64(300), + "Height": float64(200), + "Depth": float64(210), + "Weight": 0.6, + }, + "Price": 21.67, + "Quantity": float64(1), + }, + }, + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": []interface{}{ + map[string]interface{}{ + "Product Name": "Bowler Hat", + "ProductID": float64(858383), + "SKU": "040657863", + "Description": map[string]interface{}{ + "Colour": "Purple", + "Width": float64(300), + "Height": float64(200), + "Depth": float64(210), + "Weight": 0.75, + }, + "Price": 34.45, + "Quantity": float64(4), + }, + map[string]interface{}{ + "ProductID": float64(345664), + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": map[string]interface{}{ + "Colour": "Black", + "Width": float64(30), + "Height": float64(20), + "Depth": float64(210), + "Weight": float64(2), + }, + "Price": 107.99, + "Quantity": float64(1), + }, + }, + }, + }, + }, + }, + }, + { + Expression: `Account ~> |Order|{"Product":"blah"},nomatch|`, + Output: map[string]interface{}{ + "Account Name": "Firefly", + "Order": []interface{}{ + map[string]interface{}{ + "OrderID": "order103", + "Product": "blah", + }, + map[string]interface{}{ + "OrderID": "order104", + "Product": "blah", + }, + }, + }, + }, + { + Expression: `$ ~> |foo.bar|{"Description":"blah"}|`, + Output: testdata.account, + }, + { + Expression: `foo ~> |foo.bar|{"Description":"blah"}|`, + Error: ErrUndefined, + }, + { + Expression: `Account ~> |Order|5|`, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: "5", + }, + }, + { + Expression: `Account ~> |Order|"blah"|`, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: `"blah"`, + }, + }, + { + Expression: `Account ~> |Order|[]|`, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: "[]", + }, + }, + { + Expression: `Account ~> |Order|null|`, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: "null", + }, + }, + { + Expression: `Account ~> |Order|false|`, + Error: &EvalError{ + Type: ErrIllegalUpdate, + Token: "false", + }, + }, + { + Expression: `Account ~> |Order|{},5|`, + Error: &EvalError{ + Type: ErrIllegalDelete, + Token: "5", + }, + }, + { + Expression: `Account ~> |Order|{},{}|`, + Error: &EvalError{ + Type: ErrIllegalDelete, + Token: "{}", + }, + }, + { + Expression: `Account ~> |Order|{},null|`, + Error: &EvalError{ + Type: ErrIllegalDelete, + Token: "null", + }, + }, + { + Expression: `Account ~> |Order|{},[1,2,3]|`, + Error: &EvalError{ + Type: ErrIllegalDelete, + Token: "[1, 2, 3]", + }, + }, + }) +} + +func TestRegex(t *testing.T) { + + runTestCasesFunc(t, equalRegexMatches, nil, []*testCase{ + { + Expression: `/ab/ ("ab")`, + Output: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{}, + }, + }, + { + Expression: `/ab/ ()`, + Error: ErrUndefined, + }, + { + Expression: `/ab+/ ("ababbabbcc")`, + Output: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{}, + }, + }, + { + Expression: `/a(b+)/ ("ababbabbcc")`, + Output: map[string]interface{}{ + "match": "ab", + "start": 0, + "end": 2, + "groups": []string{ + "b", + }, + }, + }, + { + Expression: `/a(b+)/ ("ababbabbcc").next()`, + Output: map[string]interface{}{ + "match": "abb", + "start": 2, + "end": 5, + "groups": []string{ + "bb", + }, + }, + }, + { + Expression: `/a(b+)/ ("ababbabbcc").next().next()`, + Output: map[string]interface{}{ + "match": "abb", + "start": 5, + "end": 8, + "groups": []string{ + "bb", + }, + }, + }, + { + Expression: `/a(b+)/ ("ababbabbcc").next().next().next()`, + Error: ErrUndefined, + }, + { + Expression: []string{ + `/a(b+)/i ("Ababbabbcc")`, + `/(?i)a(b+)/ ("Ababbabbcc")`, + }, + Output: map[string]interface{}{ + "match": "Ab", + "start": 0, + "end": 2, + "groups": []string{ + "b", + }, + }, + }, + { + Expression: `//`, + Error: &jparse.Error{ + Type: jparse.ErrEmptyRegex, + Position: 1, + }, + }, + { + Expression: `/`, + Error: &jparse.Error{ + Type: jparse.ErrUnterminatedRegex, + Position: 1, + Hint: "/", + }, + }, + }) +} + +func TestRegex2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `Account.Order.Product[$.` + "`Product Name`" + ` ~> /hat/i].ProductID`, + `Account.Order.Product[$.` + "`Product Name`" + ` ~> /(?i)hat/].ProductID`, + }, + Output: []interface{}{ + float64(858383), + float64(858236), + float64(858383), + }, + }, + }) +} + +func TestRegexMatch(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$match("ababbabbcc",/ab/)`, + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{}, + }, + { + "match": "ab", + "index": 2, + "groups": []string{}, + }, + { + "match": "ab", + "index": 5, + "groups": []string{}, + }, + }, + }, + { + Expression: `$match("ababbabbcc",/a(b+)/)`, + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{ + "b", + }, + }, + { + "match": "abb", + "index": 2, + "groups": []string{ + "bb", + }, + }, + { + "match": "abb", + "index": 5, + "groups": []string{ + "bb", + }, + }, + }, + }, + { + Expression: `$match("ababbabbcc",/a(b+)/, 1)`, + Output: []map[string]interface{}{ + { + "match": "ab", + "index": 0, + "groups": []string{ + "b", + }, + }, + }, + }, + { + Expression: []string{ + `$match("ababbabbcc",/a(b+)/, 0)`, + `$match("ababbabbcc",/a(xb+)/)`, + }, + Output: []map[string]interface{}{}, + }, + { + Expression: `$match(nothing,/a(xb+)/)`, + Error: ErrUndefined, + }, + { + Expression: `$match("a, b, c, d", /ab/, -3)`, + Error: fmt.Errorf("third argument of function match must evaluate to a positive number"), // TODO: use a proper error + }, + { + Expression: `$match(12345, 3)`, + Error: &ArgTypeError{ + Func: "match", + Which: 1, + }, + }, + { + Expression: []string{ + `$match("a, b, c, d", "ab")`, + `$match("a, b, c, d", true)`, + }, + Error: &ArgTypeError{ + Func: "match", + Which: 2, + }, + }, + { + Expression: []string{ + `$match("a, b, c, d", /ab/, null)`, + `$match("a, b, c, d", /ab/, "2")`, + }, + Error: &ArgTypeError{ + Func: "match", + Which: 3, + }, + }, + { + Expression: `$match(12345)`, + Error: &ArgCountError{ + Func: "match", + Expected: 3, + Received: 1, + }, + }, + }) +} + +func TestRegexReplace(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$replace("ababbxabbcc",/b+/, "yy")`, + Output: "ayyayyxayycc", + }, + { + Expression: `$replace("ababbxabbcc",/b+/, "yy", 2)`, + Output: "ayyayyxabbcc", + }, + { + Expression: `$replace("ababbxabbcc",/b+/, "yy", 0)`, + Output: "ababbxabbcc", + }, + { + Expression: `$replace("ababbxabbcc",/d+/, "yy")`, + Output: "ababbxabbcc", + }, + { + Expression: `$replace("John Smith", /(\w+)\s(\w+)/, "$2, $1")`, + Output: "Smith, John", + }, + { + Expression: `$replace("265USD", /([0-9]+)USD/, "$$$1")`, + Output: "$265", + }, + { + Expression: `$replace("265USD", /([0-9]+)USD/, "$w")`, + Output: "$w", + }, + { + Expression: `$replace("265USD", /([0-9]+)USD/, "$0 -> $$$1")`, + Output: "265USD -> $265", + }, + { + Expression: `$replace("265USD", /([0-9]+)USD/, "$0$1$2")`, + Output: "265USD265", + }, + { + Expression: `$replace("abcd", /(ab)|(a)/, "[1=$1][2=$2]")`, + Output: "[1=ab][2=]cd", + }, + { + Expression: `$replace("abracadabra", /bra/, "*")`, + Output: "a*cada*", + }, + { + Expression: `$replace("abracadabra", /a.*a/, "*")`, + Output: "*", + }, + { + Expression: `$replace("abracadabra", /a.*?a/, "*")`, + Output: "*c*bra", + }, + { + Expression: `$replace("abracadabra", /a/, "")`, + Output: "brcdbr", + }, + { + Expression: `$replace("abracadabra", /a(.)/, "a$1$1")`, + Output: "abbraccaddabbra", + }, + { + Expression: `$replace("abracadabra", /.*?/, "$1")`, + Skip: true, // jsonata-js throws error D1004 + }, + { + Expression: `$replace("AAAA", /A+/, "b")`, + Output: "b", + }, + { + Expression: `$replace("AAAA", /A+?/, "b")`, + Output: "bbbb", + }, + { + Expression: `$replace("darted", /^(.*?)d(.*)$/, "$1c$2")`, + Output: "carted", + }, + { + Expression: `$replace("abcdefghijklmno", /(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)(m)/, "$8$5$12$12$18$123")`, + Output: "hella8l3no", + }, + { + Expression: `$replace("abcdefghijklmno", /xyz/, "$8$5$12$12$18$123")`, + Output: "abcdefghijklmno", + }, + { + Expression: `$replace("abcdefghijklmno", /ijk/, "$8$5$12$12$18$123")`, + Output: "abcdefgh22823lmno", + }, + { + Expression: `$replace("abcdefghijklmno", /(ijk)/, "$8$5$12$12$18$123")`, + Output: "abcdefghijk2ijk2ijk8ijk23lmno", + }, + { + Expression: `$replace("abcdefghijklmno", /ijk/, "$x")`, + Output: "abcdefgh$xlmno", + }, + { + Expression: `$replace("abcdefghijklmno", /(ijk)/, "$x$")`, + Output: "abcdefgh$x$lmno", + }, + }) +} + +func TestRegexReplace2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: []string{ + `Account.Order.Product.$replace($.` + "`Product Name`" + `, /hat/i, function($match) { "foo" })`, + `Account.Order.Product.$replace($.` + "`Product Name`" + `, /(?i)hat/, function($match) { "foo" })`, + }, + Output: []interface{}{ + "Bowler foo", + "Trilby foo", + "Bowler foo", + "Cloak", + }, + }, + { + Expression: []string{ + `Account.Order.Product.$replace($.` + "`Product Name`" + `, /(h)(at)/i, function($match) { $uppercase($match.match) })`, + `Account.Order.Product.$replace($.` + "`Product Name`" + `, /(?i)(h)(at)/, function($match) { $uppercase($match.match) })`, + }, + Output: []interface{}{ + "Bowler HAT", + "Trilby HAT", + "Bowler HAT", + "Cloak", + }, + }, + { + Expression: `Account.Order.Product.$replace($.` + "`Product Name`" + `, /(?i)hat/, + function($match) { true })`, + Error: fmt.Errorf("third argument of function replace must be a function that returns a string"), + }, + { + Expression: `Account.Order.Product.$replace($.` + "`Product Name`" + `, /(?i)hat/, + function($match) { 42 })`, + Error: fmt.Errorf("third argument of function replace must be a function that returns a string"), + }, + }) +} + +func TestRegexReplace3(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$replace("temperature = 68F today", /(-?\d+(?:\.\d*)?)F\b/, + function($m) { ($number($m.groups[0]) - 32) * 5/9 & "C" })`, + Output: "temperature = 20C today", + }, + }) +} + +func TestRegexContains(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$contains("ababbxabbcc", /ab+/)`, + Output: true, + }, + { + Expression: `$contains("ababbxabbcc", /ax+/)`, + Output: false, + }, + }) +} + +func TestRegexContains2(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: "Account.Order.Product[$contains(`Product Name`, /hat/)].ProductID", + Output: float64(858236), + }, + { + Expression: []string{ + "Account.Order.Product[$contains(`Product Name`, /hat/i)].ProductID", + "Account.Order.Product[$contains(`Product Name`, /(?i)hat/)].ProductID", + }, + Output: []interface{}{ + float64(858383), + float64(858236), + float64(858383), + }, + }, + }) +} + +func TestRegexSplit(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$split("ababbxabbcc",/b+/)`, + Output: []string{ + "a", + "a", + "xa", + "cc", + }, + }, + { + Expression: `$split("ababbxabbcc",/b+/, 2)`, + Output: []string{ + "a", + "a", + }, + }, + { + Expression: `$split("ababbxabbcc",/d+/)`, + Output: []string{ + "ababbxabbcc", + }, + }, + }) +} + +var reNow = regexp.MustCompile(`^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d.\d\d\dZ$`) + +func TestFuncNow(t *testing.T) { + + expr, err := Compile("$now()") + if err != nil { + t.Fatalf("Compile failed: %s", err) + } + + var results [2]string + + for i := range results { + + output, err := expr.Eval(nil) + if err != nil { + t.Fatalf("Eval failed: %s", err) + } + + results[i] = output.(string) + // $now() returns a timestamp that includes milliseconds, so + // sleeping for 1ms should be enough to reliably produce a + // different result. + time.Sleep(1 * time.Millisecond) + } + + for _, s := range results { + if !reNow.MatchString(s) { + t.Errorf("Timestamp %q does not match expected regex %q", s, reNow) + } + } + + if results[0] == results[1] { + t.Errorf("calling $now() %d times returned identical timestamps: %q", len(results), results[0]) + } +} + +func TestFuncNow2(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `{"now": $now(), "delay": $sum([1..10000]), "later": $now()}.(now = later)`, + Output: true, + }, + { + Expression: `$now()`, + Exts: map[string]Extension{ + "now": { + Func: func() string { + return "time for tea" + }, + }, + }, + Output: "time for tea", + }, + }) +} + +func TestFuncMillis(t *testing.T) { + + expr, err := Compile("$millis()") + if err != nil { + t.Fatalf("Compile failed: %s", err) + } + + var results [2]int64 + + for i := range results { + + output, err := expr.Eval(nil) + if err != nil { + t.Fatalf("Eval failed: %s", err) + } + + results[i] = output.(int64) + // $millis() returns the unix time in milliseconds, so + // sleeping for 1ms should be enough to reliably produce + // a different result. + time.Sleep(1 * time.Millisecond) + } + + for _, ms := range results { + if ms <= 1502264152715 || ms >= 2000000000000 { + t.Errorf("Unix time %d does not fall between expected values 1502264152715 and 2000000000000", ms) + } + } + + if results[0] == results[1] { + t.Errorf("calling $millis() %d times returned identical unix times: %d", len(results), results[0]) + } +} + +func TestFuncMillis2(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `{"now": $millis(), "delay": $sum([1..10000]), "later": $millis()}.(now = later)`, + Output: true, + }, + }) +} + +func TestFuncToMillis(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$toMillis("1970-01-01T00:00:00.001Z")`, + Output: int64(1), + }, + { + Expression: `$toMillis("2017-10-30T16:25:32.935Z")`, + Output: int64(1509380732935), + }, + { + Expression: `$toMillis(foo)`, + Error: ErrUndefined, + }, + { + Expression: `$toMillis("foo")`, + Error: fmt.Errorf(`could not parse time "foo"`), + }, + }) +} + +func TestFuncFromMillis(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `$fromMillis(1)`, + Output: "1970-01-01T00:00:00.001Z", + }, + { + Expression: `$fromMillis(1509380732935)`, + Output: "2017-10-30T16:25:32.935Z", + }, + { + Expression: `$fromMillis(foo)`, + Error: ErrUndefined, + }, + }) +} + +func TestLambdaSignatures(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `λ($arg){$not($arg)}(true)`, + Output: false, + }, + { + Expression: `λ($arg){$not($arg)}(foo)`, + Output: true, + }, + { + Expression: `λ($arg){$not($arg)}(null)`, + Output: true, + }, + { + Expression: `function($x,$y){$x+$y}(2, 6)`, + Output: float64(8), + }, + { + Expression: `[1..5].function($x,$y){$x+$y}(2, 6)`, + Output: []interface{}{ + float64(8), + float64(8), + float64(8), + float64(8), + float64(8), + }, + }, + { + Expression: `[1..5].function($x,$y){$x+$y}(6)`, + Output: []interface{}{ + float64(7), + float64(8), + float64(9), + float64(10), + float64(11), + }, + }, + { + Expression: `λ($str){$uppercase($str)}("hello")`, + Output: "HELLO", + }, + { + Expression: `λ($str, $prefix){$prefix & $str}("World", "Hello ")`, + Output: "Hello World", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}("a")`, + Output: "a", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}("a", "-")`, + Output: "a", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}(["a"], "-")`, + Output: "a", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}(["a", "b"], "-")`, + Output: "a-b", + }, + { + Expression: `λ($arr, $sep){$join($arr, $sep)}(["a", "b"], "-")`, + Output: "a-b", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}([], "-")`, + Output: "", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}(foo, "-")`, + Error: ErrUndefined, + }, + { + Expression: `λ($obj){$obj}({"hello": "world"})`, + Output: map[string]interface{}{ + "hello": "world", + }, + }, + { + Expression: `λ($arr)>>{$arr}([[1]])`, + Output: []interface{}{ + []interface{}{ + float64(1), + }, + }, + }, + { + Expression: `λ($num)<(ns)-:n>{$number($num)}(5)`, + Output: float64(5), + }, + { + Expression: `λ($num)<(ns)-:n>{$number($num)}("5")`, + Output: float64(5), + }, + { + Expression: `[1..5].λ($num)<(ns)-:n>{$number($num)}()`, + Output: []interface{}{ + float64(1), + float64(2), + float64(3), + float64(4), + float64(5), + }, + }, + { + Expression: ` + ( + $twice := function($f){function($x){$f($f($x))}}; + $add2 := function($x){$x+2}; + $add4 := $twice($add2); + $add4(5) + )`, + Output: float64(9), + }, + { + Expression: ` + ( + $twice := function($f):f>{function($x){$f($f($x))}}; + $add2 := function($x){$x+2}; + $add4 := $twice($add2); + $add4(5) + )`, + Output: float64(9), + }, + { + Expression: `λ($arg)>{$arg}(5)`, + Error: &jparse.Error{ + // TODO: Get position info. + Type: jparse.ErrInvalidSubtype, + Hint: "n", + }, + }, + }) +} + +func TestLambdaSignatures2(t *testing.T) { + + runTestCases(t, testdata.address, []*testCase{ + { + Expression: `Age.function($x,$y){$x+$y}(6)`, + Output: float64(34), + }, + { + Expression: `FirstName.λ($str, $prefix){$prefix & $str}("Hello ")`, + Output: "Hello Fred", + }, + { + Expression: `λ($arr, $sep)s?:s>{$join($arr, $sep)}(["a"])`, + Output: "a", + }, + }) +} + +func TestLambdaSignatures3(t *testing.T) { + + runTestCases(t, testdata.account, []*testCase{ + { + Expression: `Account.Order.Product.Description.Colour.λ($str){$uppercase($str)}()`, + Output: []interface{}{ + "PURPLE", + "ORANGE", + "PURPLE", + "BLACK", + }, + }, + }) +} + +func TestLambdaSignatureViolations(t *testing.T) { + + runTestCases(t, nil, []*testCase{ + { + Expression: `λ($arg1, $arg2){[$arg1, $arg2]}(1,"2")`, + Error: &ArgTypeError{ + Func: "lambda", + Which: 2, + }, + }, + { + Expression: `λ($arg1, $arg2){[$arg1, $arg2]}(1,3,"2")`, + Error: &ArgCountError{ + Func: "lambda", + Expected: 2, + Received: 3, + }, + }, + { + Expression: `λ($arg1, $arg2){[$arg1, $arg2]}(1,3, 2,"g")`, + Error: &ArgTypeError{ + Func: "lambda", + Which: 4, + }, + }, + { + Expression: `λ($arr)>{$arr}(["3"]) `, + Error: &ArgTypeError{ + Func: "lambda", + Which: 1, + }, + }, + { + Expression: `λ($arr)>{$arr}([1, 2, "3"]) `, + Error: &ArgTypeError{ + Func: "lambda", + Which: 1, + }, + }, + { + Expression: `λ($arr)>{$arr}("f")`, + Error: &ArgTypeError{ + Func: "lambda", + Which: 1, + }, + }, + { + Expression: ` + ( + $fun := λ($arr)>{$arr}; + $fun("f") + )`, + Error: &ArgTypeError{ + Func: "fun", + Which: 1, + }, + }, + { + Expression: `λ($arr)<(sa)>>{$arr}([[1]])`, + Error: &jparse.Error{ + // TODO: Get position info. + Type: jparse.ErrInvalidUnionType, + Hint: "<", + }, + }, + }) +} + +func TestTransform(t *testing.T) { + + data := map[string]interface{}{ + "state": map[string]interface{}{ + "tempReadings": []float64{ + 27.2, + 28.9, + 28, + 28.2, + 28.4, + }, + "readingsCount": 5, + "sumTemperatures": 140.7, + "avgTemperature": 28.14, + "maxTemperature": 28.9, + "minTemperature": 27.2, + }, + "event": map[string]interface{}{ + "t": 28.4, + }, + } + + runTestCases(t, data, []*testCase{ + { + Expression: ` + ( + $tempReadings := $count(state.tempReadings) = 5 ? + [state.tempReadings[[1..4]], event.t] : + [state.tempReadings, event.t]; + + { + "tempReadings": $tempReadings, + "sumTemperatures": $sum($tempReadings), + "avgTemperature": $average($tempReadings) ~> $round(2), + "maxTemperature": $max($tempReadings), + "minTemperature": $min($tempReadings) + } + )`, + Output: map[string]interface{}{ + "tempReadings": []interface{}{ + 28.9, + float64(28), + 28.2, + 28.4, + 28.4, + }, + "sumTemperatures": 141.9, + "avgTemperature": 28.38, + "maxTemperature": 28.9, + "minTemperature": float64(28), + }, + }, + }) +} + +// Helper functions + +type compareFunc func(interface{}, interface{}) bool + +func runTestCases(t *testing.T, input interface{}, tests []*testCase) { + runTestCasesFunc(t, reflect.DeepEqual, input, tests) +} + +func runTestCasesFunc(t *testing.T, compare compareFunc, input interface{}, tests []*testCase) { + + for _, test := range tests { + if test.Skip { + t.Logf("Skipping: %q", test.Expression) + continue + } + runTestCase(t, compare, input, test) + } +} + +func runTestCase(t *testing.T, equal compareFunc, input interface{}, test *testCase) { + + var exps []string + + switch e := test.Expression.(type) { + case string: + exps = append(exps, e) + case []string: + exps = append(exps, e...) + default: + t.Fatalf("Bad expression: %T %v", e, e) + } + + var output interface{} + + for _, exp := range exps { + + expr, err := Compile(exp) + if err == nil { + must(t, "Vars", expr.RegisterVars(test.Vars)) + must(t, "Exts", expr.RegisterExts(test.Exts)) + output, err = expr.Eval(input) + } + + if !equal(output, test.Output) { + t.Errorf("\nExpression: %s\nExp. Value: %v [%T]\nAct. Value: %v [%T]", exp, test.Output, test.Output, output, output) + } + if !reflect.DeepEqual(err, test.Error) { + t.Errorf("\nExpression: %s\nExp. Error: %v [%T]\nAct. Error: %v [%T]", exp, test.Error, test.Error, err, err) + } + } +} + +func equalRegexMatches(v1 interface{}, v2 interface{}) bool { + + makeMap := func(in interface{}) map[string]interface{} { + m := in.(map[string]interface{}) + res := map[string]interface{}{} + for k, v := range m { + if k != "next" { + res[k] = v + } + } + return res + } + + switch { + case v1 == nil && v2 == nil: + return true + case v1 == nil, v2 == nil: + return false + default: + return reflect.DeepEqual(makeMap(v1), makeMap(v2)) + } +} + +func equalFloats(tolerance float64) func(interface{}, interface{}) bool { + return func(v1, v2 interface{}) bool { + + n1, ok := v1.(float64) + if !ok { + return false + } + + n2, ok := v2.(float64) + if !ok { + return false + } + + return math.Abs(n1-n2) <= tolerance + } +} + +func equalArraysUnordered(a1, a2 interface{}) bool { + + v1 := reflect.ValueOf(a1) + v2 := reflect.ValueOf(a2) + + if !v1.IsValid() || !v2.IsValid() { + return false + } + + if v1.Type() != v2.Type() { + return false + } + + if v1.Len() != v2.Len() { + return false + } + + matched := map[int]bool{} + + for i := 0; i < v1.Len(); i++ { + + found := false + i1 := jtypes.Resolve(v1.Index(i)).Interface() + + for j := 0; j < v2.Len(); j++ { + + if matched[j] { + continue + } + + i2 := jtypes.Resolve(v2.Index(j)).Interface() + + if !reflect.DeepEqual(i1, i2) { + continue + } + + found = true + matched[j] = true + break + } + + if !found { + return false + } + } + + return true +} + +func must(t *testing.T, prefix string, err error) { + if err != nil { + t.Fatalf("%s: %s", prefix, err) + } +} + +func readJSON(filename string) interface{} { + + data, err := os.ReadFile(filepath.Join("testdata", filename)) + if err != nil { + panicf("os.ReadFile error: %s", err) + } + + var dest interface{} + if err = json.Unmarshal(data, &dest); err != nil { + panicf("json.Unmarshal error: %s", err) + } + + return dest +} diff --git a/v1.5.4/jtypes/funcs.go b/v1.5.4/jtypes/funcs.go new file mode 100644 index 0000000..ebbc168 --- /dev/null +++ b/v1.5.4/jtypes/funcs.go @@ -0,0 +1,179 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package jtypes (golint) +package jtypes + +import ( + "reflect" +) + +// Resolve (golint) +func Resolve(v reflect.Value) reflect.Value { + for { + switch v.Kind() { + case reflect.Interface, reflect.Ptr: + if !v.IsNil() { + v = v.Elem() + break + } + fallthrough + default: + return v + } + } +} + +// IsBool (golint) +func IsBool(v reflect.Value) bool { + return v.Kind() == reflect.Bool || resolvedKind(v) == reflect.Bool +} + +// IsString (golint) +func IsString(v reflect.Value) bool { + return v.Kind() == reflect.String || resolvedKind(v) == reflect.String +} + +// IsNumber (golint) +func IsNumber(v reflect.Value) bool { + return isFloat(v) || isInt(v) || isUint(v) +} + +// IsCallable (golint) +func IsCallable(v reflect.Value) bool { + v = Resolve(v) + return v.IsValid() && + (v.Type().Implements(TypeCallable) || reflect.PointerTo(v.Type()).Implements(TypeCallable)) +} + +// IsArray (golint) +func IsArray(v reflect.Value) bool { + return isArrayKind(v.Kind()) || isArrayKind(resolvedKind(v)) +} + +func isArrayKind(k reflect.Kind) bool { + return k == reflect.Slice || k == reflect.Array +} + +// IsArrayOf (golint) +func IsArrayOf(v reflect.Value, hasType func(reflect.Value) bool) bool { + if !IsArray(v) { + return false + } + + v = Resolve(v) + for i := 0; i < v.Len(); i++ { + if !hasType(v.Index(i)) { + return false + } + } + + return true +} + +// IsMap (golint) +func IsMap(v reflect.Value) bool { + return resolvedKind(v) == reflect.Map +} + +// IsStruct (golint) +func IsStruct(v reflect.Value) bool { + return resolvedKind(v) == reflect.Struct +} + +// AsBool (golint) +func AsBool(v reflect.Value) (bool, bool) { + v = Resolve(v) + + switch { + case IsBool(v): + return v.Bool(), true + default: + return false, false + } +} + +// AsString (golint) +func AsString(v reflect.Value) (string, bool) { + v = Resolve(v) + + switch { + case IsString(v): + return v.String(), true + default: + return "", false + } +} + +// AsNumber (golint) +func AsNumber(v reflect.Value) (float64, bool) { + v = Resolve(v) + + switch { + case isFloat(v): + return v.Float(), true + case isInt(v), isUint(v): + return v.Convert(typeFloat64).Float(), true + default: + return 0, false + } +} + +// AsCallable (golint) +func AsCallable(v reflect.Value) (Callable, bool) { + v = Resolve(v) + + if v.IsValid() && v.Type().Implements(TypeCallable) && v.CanInterface() { + return v.Interface().(Callable), true + } + + if v.IsValid() && reflect.PointerTo(v.Type()).Implements(TypeCallable) && v.CanAddr() && v.Addr().CanInterface() { + return v.Addr().Interface().(Callable), true + } + + return nil, false +} + +func isInt(v reflect.Value) bool { + return isIntKind(v.Kind()) || isIntKind(resolvedKind(v)) +} + +func isIntKind(k reflect.Kind) bool { + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + +func isUint(v reflect.Value) bool { + return isUintKind(v.Kind()) || isUintKind(resolvedKind(v)) +} + +func isUintKind(k reflect.Kind) bool { + switch k { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + default: + return false + } +} + +func isFloat(v reflect.Value) bool { + return isFloatKind(v.Kind()) || isFloatKind(resolvedKind(v)) +} + +func isFloatKind(k reflect.Kind) bool { + switch k { + case reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + +func resolvedKind(v reflect.Value) reflect.Kind { + return Resolve(v).Kind() +} diff --git a/v1.5.4/jtypes/types.go b/v1.5.4/jtypes/types.go new file mode 100644 index 0000000..f065e88 --- /dev/null +++ b/v1.5.4/jtypes/types.go @@ -0,0 +1,253 @@ +// Copyright 2018 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +// Package jtypes provides types and utilities for third party +// extension functions. +package jtypes + +import ( + "errors" + "reflect" +) + +var undefined reflect.Value + +var ( + typeBool = reflect.TypeOf((*bool)(nil)).Elem() + typeInt = reflect.TypeOf((*int)(nil)).Elem() + typeFloat64 = reflect.TypeOf((*float64)(nil)).Elem() + typeString = reflect.TypeOf((*string)(nil)).Elem() + + // TypeOptional (golint) + TypeOptional = reflect.TypeOf((*Optional)(nil)).Elem() + // TypeCallable (golint) + TypeCallable = reflect.TypeOf((*Callable)(nil)).Elem() + // TypeConvertible (golint) + TypeConvertible = reflect.TypeOf((*Convertible)(nil)).Elem() + // TypeVariant (golint) + TypeVariant = reflect.TypeOf((*Variant)(nil)).Elem() + // TypeValue (golint) + TypeValue = reflect.TypeOf((*reflect.Value)(nil)).Elem() + // TypeInterface (golint) + TypeInterface = reflect.TypeOf((*interface{})(nil)).Elem() +) + +// ErrUndefined (golint) +var ErrUndefined = errors.New("undefined") + +// Variant (golint) +type Variant interface { + ValidTypes() []reflect.Type +} + +// Callable (golint) +type Callable interface { + Name() string + ParamCount() int + Call([]reflect.Value) (reflect.Value, error) +} + +// Convertible (golint) +type Convertible interface { + ConvertTo(reflect.Type) (reflect.Value, bool) +} + +// Optional (golint) +type Optional interface { + IsSet() bool + Set(reflect.Value) + Type() reflect.Type +} + +type isSet bool + +// IsSet (golint) +func (opt *isSet) IsSet() bool { + return bool(*opt) +} + +// OptionalBool (golint) +type OptionalBool struct { + isSet + Bool bool +} + +// NewOptionalBool (golint) +func NewOptionalBool(value bool) OptionalBool { + opt := OptionalBool{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalBool) Set(v reflect.Value) { + opt.isSet = true + opt.Bool = v.Bool() +} + +// Type (golint) +func (opt *OptionalBool) Type() reflect.Type { + return typeBool +} + +// OptionalInt (golint) +type OptionalInt struct { + isSet + Int int +} + +// NewOptionalInt (golint) +func NewOptionalInt(value int) OptionalInt { + opt := OptionalInt{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalInt) Set(v reflect.Value) { + opt.isSet = true + opt.Int = int(v.Int()) +} + +// Type (golint) +func (opt *OptionalInt) Type() reflect.Type { + return typeInt +} + +// OptionalFloat64 (golint) +type OptionalFloat64 struct { + isSet + Float64 float64 +} + +// NewOptionalFloat64 (golint) +func NewOptionalFloat64(value float64) OptionalFloat64 { + opt := OptionalFloat64{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalFloat64) Set(v reflect.Value) { + opt.isSet = true + opt.Float64 = v.Float() +} + +// Type (golint) +func (opt *OptionalFloat64) Type() reflect.Type { + return typeFloat64 +} + +// OptionalString (golint) +type OptionalString struct { + isSet + String string +} + +// NewOptionalString (golint) +func NewOptionalString(value string) OptionalString { + opt := OptionalString{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalString) Set(v reflect.Value) { + opt.isSet = true + opt.String = v.String() +} + +// Type (golint) +func (opt *OptionalString) Type() reflect.Type { + return typeString +} + +// OptionalInterface (golint) +type OptionalInterface struct { + isSet + Interface interface{} +} + +// NewOptionalInterface (golint) +func NewOptionalInterface(value interface{}) OptionalInterface { + opt := OptionalInterface{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalInterface) Set(v reflect.Value) { + opt.isSet = true + opt.Interface = v.Interface() +} + +// Type (golint) +func (opt *OptionalInterface) Type() reflect.Type { + return TypeInterface +} + +// OptionalValue (golint) +type OptionalValue struct { + isSet + Value reflect.Value +} + +// NewOptionalValue (golint) +func NewOptionalValue(value reflect.Value) OptionalValue { + opt := OptionalValue{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalValue) Set(v reflect.Value) { + opt.isSet = true + opt.Value = v.Interface().(reflect.Value) +} + +// Type (golint) +func (opt *OptionalValue) Type() reflect.Type { + return TypeValue +} + +// OptionalCallable (golint) +type OptionalCallable struct { + isSet + Callable Callable +} + +// NewOptionalCallable (golint) +func NewOptionalCallable(value Callable) OptionalCallable { + opt := OptionalCallable{} + opt.Set(reflect.ValueOf(value)) + return opt +} + +// Set (golint) +func (opt *OptionalCallable) Set(v reflect.Value) { + opt.isSet = true + opt.Callable = v.Interface().(Callable) +} + +// Type (golint) +func (opt *OptionalCallable) Type() reflect.Type { + return TypeCallable +} + +// ArgHandler (golint) +type ArgHandler func([]reflect.Value) bool + +// ArgCountEquals (golint) +func ArgCountEquals(n int) ArgHandler { + return func(argv []reflect.Value) bool { + return len(argv) == n + } +} + +// ArgUndefined (golint) +func ArgUndefined(i int) ArgHandler { + return func(argv []reflect.Value) bool { + return len(argv) > i && argv[i] == undefined + } +} diff --git a/v1.5.4/testdata/account.json b/v1.5.4/testdata/account.json new file mode 100644 index 0000000..e2ca519 --- /dev/null +++ b/v1.5.4/testdata/account.json @@ -0,0 +1,74 @@ +{ + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Price": 21.67, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account2.json b/v1.5.4/testdata/account2.json new file mode 100644 index 0000000..920527a --- /dev/null +++ b/v1.5.4/testdata/account2.json @@ -0,0 +1,74 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[0] has no Price field.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Price": 21.67, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account3.json b/v1.5.4/testdata/account3.json new file mode 100644 index 0000000..d116478 --- /dev/null +++ b/v1.5.4/testdata/account3.json @@ -0,0 +1,74 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[1] has no Price field.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account4.json b/v1.5.4/testdata/account4.json new file mode 100644 index 0000000..bba1dd7 --- /dev/null +++ b/v1.5.4/testdata/account4.json @@ -0,0 +1,73 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[0] and Account.Order[0].Product[1] have no Price field.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account5.json b/v1.5.4/testdata/account5.json new file mode 100644 index 0000000..450f491 --- /dev/null +++ b/v1.5.4/testdata/account5.json @@ -0,0 +1,75 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[0].Price is a string instead of a number.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": "foo", + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Price": 21.67, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account6.json b/v1.5.4/testdata/account6.json new file mode 100644 index 0000000..77f2b6a --- /dev/null +++ b/v1.5.4/testdata/account6.json @@ -0,0 +1,75 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[0].Price is a boolean instead of a number.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": true, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Price": 21.67, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/account7.json b/v1.5.4/testdata/account7.json new file mode 100644 index 0000000..5e110d8 --- /dev/null +++ b/v1.5.4/testdata/account7.json @@ -0,0 +1,75 @@ +{ + "Description": "Copy of account.json where Account.Order[0].Product[1].Price is null instead of a number.", + "Account": { + "Account Name": "Firefly", + "Order": [ + { + "OrderID": "order103", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "0406654608", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 2 + }, + { + "Product Name": "Trilby hat", + "ProductID": 858236, + "SKU": "0406634348", + "Description": { + "Colour": "Orange", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.6 + }, + "Price": null, + "Quantity": 1 + } + ] + }, + { + "OrderID": "order104", + "Product": [ + { + "Product Name": "Bowler Hat", + "ProductID": 858383, + "SKU": "040657863", + "Description": { + "Colour": "Purple", + "Width": 300, + "Height": 200, + "Depth": 210, + "Weight": 0.75 + }, + "Price": 34.45, + "Quantity": 4 + }, + { + "ProductID": 345664, + "SKU": "0406654603", + "Product Name": "Cloak", + "Description": { + "Colour": "Black", + "Width": 30, + "Height": 20, + "Depth": 210, + "Weight": 2.0 + }, + "Price": 107.99, + "Quantity": 1 + } + ] + } + ] + } +} + diff --git a/v1.5.4/testdata/address.json b/v1.5.4/testdata/address.json new file mode 100644 index 0000000..eb6d5a1 --- /dev/null +++ b/v1.5.4/testdata/address.json @@ -0,0 +1,47 @@ +{ + "FirstName": "Fred", + "Surname": "Smith", + "Age": 28, + "Address": { + "Street": "Hursley Park", + "City": "Winchester", + "Postcode": "SO21 2JN" + }, + "Phone": [ + { + "type": "home", + "number": "0203 544 1234" + }, + { + "type": "office", + "number": "01962 001234" + }, + { + "type": "office", + "number": "01962 001235" + }, + { + "type": "mobile", + "number": "077 7700 1234" + } + ], + "Email": [ + { + "type": "work", + "address": ["fred.smith@my-work.com", "fsmith@my-work.com"] + }, + { + "type": "home", + "address": ["freddy@my-social.com", "frederic.smith@very-serious.com"] + } + ], + "Other": { + "Over 18 ?": true, + "Misc": null, + "Alternative.Address": { + "Street": "Brick Lane", + "City": "London", + "Postcode": "E1 6RF" + } + } +} diff --git a/v1.5.4/testdata/foobar.json b/v1.5.4/testdata/foobar.json new file mode 100644 index 0000000..a9ee267 --- /dev/null +++ b/v1.5.4/testdata/foobar.json @@ -0,0 +1,8 @@ +{ + "foo": { + "bar": 42, + "blah": [{"baz": {"fud": "hello"}}, {"baz": {"fud": "world"}}, {"bazz": "gotcha"}], + "blah.baz": "here" + }, + "bar": 98 +} diff --git a/v1.5.4/testdata/foobar2.json b/v1.5.4/testdata/foobar2.json new file mode 100644 index 0000000..058369e --- /dev/null +++ b/v1.5.4/testdata/foobar2.json @@ -0,0 +1,7 @@ +{ + "foo": { + "bar": 42, + "blah": [{"baz": {"fud": "hello"}}, {"buz": {"fud": "world"}}, {"bazz": "gotcha"}], + "blah.baz": "here" + }, "bar": 98 +} diff --git a/v1.5.4/testdata/library.json b/v1.5.4/testdata/library.json new file mode 100644 index 0000000..f6b9411 --- /dev/null +++ b/v1.5.4/testdata/library.json @@ -0,0 +1,74 @@ +{ + "library": { + "books": [ + { + "title": "Structure and Interpretation of Computer Programs", + "authors": ["Abelson", "Sussman"], + "isbn": "9780262510875", + "price": 38.90, + "copies": 2 + }, + { + "title": "The C Programming Language", + "authors": ["Kernighan", "Richie"], + "isbn": "9780131103627", + "price": 33.59, + "copies": 3 + }, + { + "title": "The AWK Programming Language", + "authors": ["Aho", "Kernighan", "Weinberger"], + "isbn": "9780201079814", + "copies": 1 + }, + { + "title": "Compilers: Principles, Techniques, and Tools", + "authors": ["Aho", "Lam", "Sethi", "Ullman"], + "isbn": "9780201100884", + "price": 23.38, + "copies": 1 + } + ], + "loans": [ + { + "customer": "10001", + "isbn": "9780262510875", + "return": "2016-12-05" + }, + { + "customer": "10003", + "isbn": "9780201100884", + "return": "2016-10-22" + } + ], + "customers": [ + { + "id": "10001", + "name": "Joe Doe", + "address": { + "street": "2 Long Road", + "city": "Winchester", + "postcode": "SO22 5PU" + } + }, + { + "id": "10002", + "name": "Fred Bloggs", + "address": { + "street": "56 Letsby Avenue", + "city": "Winchester", + "postcode": "SO22 4WD" + } + }, + { + "id": "10003", + "name": "Jason Arthur", + "address": { + "street": "1 Preddy Gate", + "city": "Southampton", + "postcode": "SO14 0MG" + } + } + ] + } +} diff --git a/v1.5.4/testdata/nest1.json b/v1.5.4/testdata/nest1.json new file mode 100644 index 0000000..75e8839 --- /dev/null +++ b/v1.5.4/testdata/nest1.json @@ -0,0 +1,6 @@ +{ + "nest0": [ + {"nest1": [{"nest2": [{"nest3": [1]}, {"nest3": [2]}]}, {"nest2": [{"nest3": [3]}, {"nest3": [4]}]}]}, + {"nest1": [{"nest2": [{"nest3": [5]}, {"nest3": [6]}]}, {"nest2": [{"nest3": [7]}, {"nest3": [8]}]}]} + ] +} diff --git a/v1.5.4/testdata/nest2.json b/v1.5.4/testdata/nest2.json new file mode 100644 index 0000000..ecd0699 --- /dev/null +++ b/v1.5.4/testdata/nest2.json @@ -0,0 +1,4 @@ +[ + {"nest0": [1, 2]}, + {"nest0": [3, 4]} +] diff --git a/v1.5.4/testdata/nest3.json b/v1.5.4/testdata/nest3.json new file mode 100644 index 0000000..46d36e5 --- /dev/null +++ b/v1.5.4/testdata/nest3.json @@ -0,0 +1,4 @@ +[ + {"nest0": [{"nest1": [1, 2]}, {"nest1": [3, 4]}]}, + {"nest0": [{"nest1": [5]}, {"nest1": [6]}]} +]