snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
coverage.out
manual_test.go
*.out
*.err
.vscode

View File

@@ -0,0 +1,12 @@
language: go
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- go test -bench=. -benchmem -timeout 9m -coverprofile coverage.out
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken=$COVERALLS_TOKEN
- go test -bench=Random -benchtime 3m -timeout 9m -benchmem -coverprofile coverage.out
go: "1.15"

View File

@@ -0,0 +1,12 @@
Copyright (c) 2017, Paessler AG <support@paessler.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,163 @@
# Gval
[![Go Reference](https://pkg.go.dev/badge/github.com/PaesslerAG/gval.svg)](https://pkg.go.dev/github.com/PaesslerAG/gval)
[![Build Status](https://api.travis-ci.org/PaesslerAG/gval.svg?branch=master)](https://travis-ci.org/PaesslerAG/gval)
[![Coverage Status](https://coveralls.io/repos/github/PaesslerAG/gval/badge.svg?branch=master)](https://coveralls.io/github/PaesslerAG/gval?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/PaesslerAG/gval)](https://goreportcard.com/report/github.com/PaesslerAG/gval)
Gval (Go eVALuate) provides support for evaluating arbitrary expressions, in particular Go-like expressions.
![gopher](./prtg-batmin-gopher.png)
## Evaluate
Gval can evaluate expressions with parameters, arimethetic, logical, and string operations:
- basic expression: [10 > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Basic)
- parameterized expression: [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)
- nested parameterized expression: [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)
- arithmetic expression: [(requests_made * requests_succeeded / 100) >= 90](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Arithmetic)
- string expression: [http_response_body == "service is ok"](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-String)
- float64 expression: [(mem_used / total_mem) * 100](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Float64)
It can easily be extended with custom functions or operators:
- custom date comparator: [date(\`2014-01-02\`) > date(\`2014-01-01 23:59:59\`)](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-DateComparison)
- string length: [strlen("someReallyLongInputString") <= 16](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Strlen)
You can parse gval.Expressions once and re-use them multiple times. Parsing is the compute-intensive phase of the process, so if you intend to use the same expression with different parameters, just parse it once:
- [Parsing and Evaluation](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluable)
The normal Go-standard order of operators is respected. When writing an expression, be sure that you either order the operators correctly, or use parentheses to clarify which portions of an expression should be run first.
Strings, numbers, and booleans can be used like in Go:
- [(7 < "47" == true ? "hello world!\n\u263a") + \` more text\`](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Encoding)
## Parameter
Variables can be accessed via string literals. They can be used for values with string keys if the parameter is a `map[string]interface{}` or `map[interface{}]interface{}` and for fields or methods if the parameter is a struct.
- [foo > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Parameter)
### Bracket Selector
Map and array elements and Struct Field can be accessed via `[]`.
- [foo[0]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Array)
- [foo["b" + "a" + "r"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-ExampleEvaluate_ComplexAccessor)
### Dot Selector
A nested variable with a name containing only letters and underscores can be accessed via a dot selector.
- [foo.bar > 0](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedParameter)
### Custom Selector
Parameter names like `response-time` will be interpreted as `response` minus `time`. While gval doesn't support these parameter names directly, you can easily access them via a custom extension like [JSON Path](https://github.com/PaesslerAG/jsonpath):
- [$["response-time"]](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Jsonpath)
Jsonpath is also suitable for accessing array elements.
### Fields and Methods
If you have structs in your parameters, you can access their fields and methods in the usual way:
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-FlatAccessor)
It also works if the parameter is a struct directly
[Hello + World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-Accessor)
or if the fields are nested
[foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Evaluate-NestedAccessor)
This may be convenient but note that using accessors on strucs makes the expression about four times slower than just using a parameter (consult the benchmarks for more precise measurements on your system). If there are functions you want to use, it's faster (and probably cleaner) to define them as functions (see the Evaluate section). These approaches use no reflection, and are designed to be fast and clean.
## Default Language
The default language is in serveral sub languages like text, arithmetic or propositional logic defined. See [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval/#Gval) for details. All sub languages are merged into gval.Full which contains the following elements:
- Modifiers: `+` `-` `/` `*` `&` `|` `^` `**` `%` `>>` `<<`
- Comparators: `>` `>=` `<` `<=` `==` `!=` `=~` `!~`
- Logical ops: `||` `&&`
- Numeric constants, as 64-bit floating point (`12345.678`)
- String constants (double quotes: `"foobar"`)
- Date function 'Date(x)', using any permutation of RFC3339, ISO8601, ruby date, or unix date
- Boolean constants: `true` `false`
- Parentheses to control order of evaluation `(` `)`
- Json Arrays : `[1, 2, "foo"]`
- Json Objects : `{"a":1, "b":2, "c":"foo"}`
- Prefixes: `!` `-` `~`
- Ternary conditional: `?` `:`
- Null coalescence: `??`
## Customize
Gval is completly customizable. Every constant, function or operator can be defined separately and existing expression languages can be reused:
- [foo.Hello + foo.World()](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-Language)
For details see [Godoc](https://pkg.go.dev/github.com/PaesslerAG/gval).
### Implementing custom selector
In a case you want to provide custom logic for selectors you can implement `SelectGVal(ctx context.Context, k string) (interface{}, error)` on your struct.
Function receives next part of the path and can return any type of var that is again evaluated through standard gval procedures.
[Example Custom Selector](https://pkg.go.dev/github.com/PaesslerAG/gval/#example-custom-selector)
### External gval Languages
A list of external libraries for gval. Feel free to add your own library.
- [gvalstrings](https://github.com/generikvault/gvalstrings) parse single quoted strings in gval.
- [jsonpath](https://github.com/PaesslerAG/jsonpath) full support for jsonpath in gval.
## Performance
The library is built with the intention of being quick but has not been aggressively profiled and optimized. For most applications, though, it is completely fine.
If performance is an issue, make sure to create your expression language with all functions, constants and operators only once. Evaluating an expression like gval.Evaluate("expression, const1, func1, func2, ...) creates a new gval.Language everytime it is called and slows execution.
The library comes with a bunch of benchmarks to measure the performance of parsing and evaluating expressions. You can run them with `go test -bench=.`.
For a very rough idea of performance, here are the results from a benchmark run on a Dell Latitude E7470 Win 10 i5-6300U.
``` text
BenchmarkGval/const_evaluation-4 500000000 3.57 ns/op
BenchmarkGval/const_parsing-4 1000000 1144 ns/op
BenchmarkGval/single_parameter_evaluation-4 10000000 165 ns/op
BenchmarkGval/single_parameter_parsing-4 1000000 1648 ns/op
BenchmarkGval/parameter_evaluation-4 5000000 352 ns/op
BenchmarkGval/parameter_parsing-4 500000 2773 ns/op
BenchmarkGval/common_evaluation-4 3000000 434 ns/op
BenchmarkGval/common_parsing-4 300000 4419 ns/op
BenchmarkGval/complex_evaluation-4 100000000 11.6 ns/op
BenchmarkGval/complex_parsing-4 100000 17936 ns/op
BenchmarkGval/literal_evaluation-4 300000000 3.84 ns/op
BenchmarkGval/literal_parsing-4 500000 2559 ns/op
BenchmarkGval/modifier_evaluation-4 500000000 3.54 ns/op
BenchmarkGval/modifier_parsing-4 500000 3755 ns/op
BenchmarkGval/regex_evaluation-4 50000 21347 ns/op
BenchmarkGval/regex_parsing-4 200000 6480 ns/op
BenchmarkGval/constant_regex_evaluation-4 1000000 1000 ns/op
BenchmarkGval/constant_regex_parsing-4 200000 9417 ns/op
BenchmarkGval/accessors_evaluation-4 3000000 417 ns/op
BenchmarkGval/accessors_parsing-4 1000000 1778 ns/op
BenchmarkGval/accessors_method_evaluation-4 1000000 1931 ns/op
BenchmarkGval/accessors_method_parsing-4 1000000 1729 ns/op
BenchmarkGval/accessors_method_parameter_evaluation-4 1000000 2162 ns/op
BenchmarkGval/accessors_method_parameter_parsing-4 500000 2618 ns/op
BenchmarkGval/nested_accessors_evaluation-4 2000000 681 ns/op
BenchmarkGval/nested_accessors_parsing-4 1000000 2115 ns/op
BenchmarkRandom-4 500000 3631 ns/op
ok
```
## API Breaks
Gval is designed with easy expandability in mind and API breaks will be avoided if possible. If API breaks are unavoidable they wil be explicitly stated via an increased major version number.
-------------------------------------
Credits to Reene French for the gophers.

View File

@@ -0,0 +1,348 @@
package gval
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
// Selector allows for custom variable selection from structs
//
// Return value is again handled with variable() until end of the given path
type Selector interface {
SelectGVal(c context.Context, key string) (interface{}, error)
}
// Evaluable evaluates given parameter
type Evaluable func(c context.Context, parameter interface{}) (interface{}, error)
//EvalInt evaluates given parameter to an int
func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return int(f), nil
}
//EvalFloat64 evaluates given parameter to a float64
func (e Evaluable) EvalFloat64(c context.Context, parameter interface{}) (float64, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToFloat(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return f, nil
}
//EvalBool evaluates given parameter to a bool
func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error) {
v, err := e(c, parameter)
if err != nil {
return false, err
}
b, ok := convertToBool(v)
if !ok {
return false, fmt.Errorf("expected bool but got %v (%T)", v, v)
}
return b, nil
}
//EvalString evaluates given parameter to a string
func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error) {
o, err := e(c, parameter)
if err != nil {
return "", err
}
return fmt.Sprintf("%v", o), nil
}
//Const Evaluable represents given constant
func (*Parser) Const(value interface{}) Evaluable {
return constant(value)
}
//go:noinline
func constant(value interface{}) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
return value, nil
}
}
//Var Evaluable represents value at given path.
//It supports with default language VariableSelector:
// map[interface{}]interface{},
// map[string]interface{} and
// []interface{} and via reflect
// struct fields,
// struct methods,
// slices and
// map with int or string key.
func (p *Parser) Var(path ...Evaluable) Evaluable {
if p.selector == nil {
return variable(path)
}
return p.selector(path)
}
// Evaluables is a slice of Evaluable.
type Evaluables []Evaluable
// EvalStrings evaluates given parameter to a string slice
func (evs Evaluables) EvalStrings(c context.Context, parameter interface{}) ([]string, error) {
strs := make([]string, len(evs))
for i, p := range evs {
k, err := p.EvalString(c, parameter)
if err != nil {
return nil, err
}
strs[i] = k
}
return strs, nil
}
func variable(path Evaluables) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
keys, err := path.EvalStrings(c, v)
if err != nil {
return nil, err
}
for i, k := range keys {
switch o := v.(type) {
case Selector:
v, err = o.SelectGVal(c, k)
if err != nil {
return nil, fmt.Errorf("failed to select '%s' on %T: %w", k, o, err)
}
continue
case map[interface{}]interface{}:
v = o[k]
continue
case map[string]interface{}:
v = o[k]
continue
case []interface{}:
if i, err := strconv.Atoi(k); err == nil && i >= 0 && len(o) > i {
v = o[i]
continue
}
default:
var ok bool
v, ok = reflectSelect(k, o)
if !ok {
return nil, fmt.Errorf("unknown parameter %s", strings.Join(keys[:i+1], "."))
}
}
}
return v, nil
}
}
func reflectSelect(key string, value interface{}) (selection interface{}, ok bool) {
vv := reflect.ValueOf(value)
vvElem := resolvePotentialPointer(vv)
switch vvElem.Kind() {
case reflect.Map:
mapKey, ok := reflectConvertTo(vv.Type().Key().Kind(), key)
if !ok {
return nil, false
}
vvElem = vv.MapIndex(reflect.ValueOf(mapKey))
vvElem = resolvePotentialPointer(vvElem)
if vvElem.IsValid() {
return vvElem.Interface(), true
}
case reflect.Slice:
if i, err := strconv.Atoi(key); err == nil && i >= 0 && vv.Len() > i {
vvElem = resolvePotentialPointer(vv.Index(i))
return vvElem.Interface(), true
}
case reflect.Struct:
field := vvElem.FieldByName(key)
if field.IsValid() {
return field.Interface(), true
}
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
}
return nil, false
}
func resolvePotentialPointer(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
return value.Elem()
}
return value
}
func reflectConvertTo(k reflect.Kind, value string) (interface{}, bool) {
switch k {
case reflect.String:
return value, true
case reflect.Int:
if i, err := strconv.Atoi(value); err == nil {
return i, true
}
}
return nil, false
}
func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
a := make([]interface{}, len(args))
for i, arg := range args {
ai, err := arg(c, v)
if err != nil {
return nil, err
}
a[i] = ai
}
return fun(c, a...)
}
}
func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
f, err := fun(c, v)
if err != nil {
return nil, fmt.Errorf("could not call function: %v", err)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to execute function '%s': %s", fullname, r)
ret = nil
}
}()
ff := reflect.ValueOf(f)
if ff.Kind() != reflect.Func {
return nil, fmt.Errorf("could not call '%s' type %T", fullname, f)
}
a := make([]reflect.Value, len(args))
for i := range args {
arg, err := args[i](c, v)
if err != nil {
return nil, err
}
a[i] = reflect.ValueOf(arg)
}
rr := ff.Call(a)
r := make([]interface{}, len(rr))
for i, e := range rr {
r[i] = e.Interface()
}
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && ff.Type().Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
return err, nil
case 1:
return r[0], err
default:
return r, err
}
}
}
//IsConst returns if the Evaluable is a Parser.Const() value
func (e Evaluable) IsConst() bool {
pc := reflect.ValueOf(constant(nil)).Pointer()
pe := reflect.ValueOf(e).Pointer()
return pc == pe
}
func regEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return regex.MatchString(s), nil
}, nil
}
func notRegEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return !matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return !regex.MatchString(s), nil
}, nil
}

View File

@@ -0,0 +1,126 @@
package gval
import (
"context"
"fmt"
"reflect"
)
type function func(ctx context.Context, arguments ...interface{}) (interface{}, error)
func toFunc(f interface{}) function {
if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
return function(func(ctx context.Context, arguments ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
result, err := f(arguments...)
v = result
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
return v, err
}
})
}
if f, ok := f.(func(ctx context.Context, arguments ...interface{}) (interface{}, error)); ok {
return function(f)
}
fun := reflect.ValueOf(f)
t := fun.Type()
return func(ctx context.Context, args ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
in, err := createCallArguments(ctx, t, args)
if err != nil {
errCh <- err
return
}
out := fun.Call(in)
r := make([]interface{}, len(out))
for i, e := range out {
r[i] = e.Interface()
}
err = nil
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
v = nil
case 1:
v = r[0]
default:
v = r
}
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
return v, err
}
}
}
func createCallArguments(ctx context.Context, t reflect.Type, args []interface{}) ([]reflect.Value, error) {
variadic := t.IsVariadic()
numIn := t.NumIn()
// if first argument is a context, use the given execution context
if numIn > 0 {
thisFun := reflect.ValueOf(createCallArguments)
thisT := thisFun.Type()
if t.In(0) == thisT.In(0) {
args = append([]interface{}{ctx}, args...)
}
}
if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
return nil, fmt.Errorf("invalid number of parameters")
}
in := make([]reflect.Value, len(args))
var inType reflect.Type
for i, arg := range args {
if !variadic || i < numIn-1 {
inType = t.In(i)
} else if i == numIn-1 {
inType = t.In(numIn - 1).Elem()
}
argVal := reflect.ValueOf(arg)
if arg == nil {
argVal = reflect.ValueOf(reflect.Interface)
} else if !argVal.Type().AssignableTo(inType) {
return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
inType.String(), i, arg)
}
in[i] = argVal
}
return in, nil
}

View File

@@ -0,0 +1,327 @@
// Package gval provides a generic expression language.
// All functions, infix and prefix operators can be replaced by composing languages into a new one.
//
// The package contains concrete expression languages for common application in text, arithmetic, decimal arithmetic, propositional logic and so on.
// They can be used as basis for a custom expression language or to evaluate expressions directly.
package gval
import (
"context"
"fmt"
"math"
"reflect"
"text/scanner"
"time"
"github.com/shopspring/decimal"
)
//Evaluate given parameter with given expression in gval full language
func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error) {
return EvaluateWithContext(context.Background(), expression, parameter, opts...)
}
//Evaluate given parameter with given expression in gval full language using a context
func EvaluateWithContext(c context.Context, expression string, parameter interface{}, opts ...Language) (interface{}, error) {
l := full
if len(opts) > 0 {
l = NewLanguage(append([]Language{l}, opts...)...)
}
return l.EvaluateWithContext(c, expression, parameter)
}
// Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, and Json
// Operator in: a in b is true iff value a is an element of array b
// Operator ??: a ?? b returns a if a is not false or nil, otherwise n
// Operator ?: a ? b : c returns b if bool a is true, otherwise b
//
// Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date
func Full(extensions ...Language) Language {
if len(extensions) == 0 {
return full
}
return NewLanguage(append([]Language{full}, extensions...)...)
}
// Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// Arithmetic operators expect float64 operands.
// Called with unfitting input, they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Arithmetic() Language {
return arithmetic
}
// DecimalArithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// DecimalArithmetic operators expect decimal.Decimal operands (github.com/shopspring/decimal)
// and are used to calculate money/decimal rather than floating point calculations.
// Called with unfitting input, they try to convert the input to decimal.Decimal.
// They can parse strings and convert any type of int or float.
func DecimalArithmetic() Language {
return decimalArithmetic
}
// Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).
//
// Bitmask operators expect float64 operands.
// Called with unfitting input they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Bitmask() Language {
return bitmask
}
// Text contains base, lexical order on strings (<=,<,>,>=),
// regex match (=~) and regex not match (!~)
func Text() Language {
return text
}
// PropositionalLogic contains base, not(!), and (&&), or (||) and Base.
//
// Propositional operator expect bool operands.
// Called with unfitting input they try to convert the input to bool.
// Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true.
// 0 and the strings "FALSE" and "false" are interpreted as false.
func PropositionalLogic() Language {
return propositionalLogic
}
// JSON contains json objects ({string:expression,...})
// and json arrays ([expression, ...])
func JSON() Language {
return ljson
}
// Parentheses contains support for parentheses.
func Parentheses() Language {
return parentheses
}
// Ident contains support for variables and functions.
func Ident() Language {
return ident
}
// Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions
// It contains true, false, (floating point) number, string ("" or ``) and char ('') constants
func Base() Language {
return base
}
var full = NewLanguage(arithmetic, bitmask, text, propositionalLogic, ljson,
InfixOperator("in", inArray),
InfixShortCircuit("??", func(a interface{}) (interface{}, bool) {
v := reflect.ValueOf(a)
return a, a != nil && !v.IsZero()
}),
InfixOperator("??", func(a, b interface{}) (interface{}, error) {
if v := reflect.ValueOf(a); a == nil || v.IsZero() {
return b, nil
}
return a, nil
}),
PostfixOperator("?", parseIf),
Function("date", func(arguments ...interface{}) (interface{}, error) {
if len(arguments) != 1 {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
s, ok := arguments[0].(string)
if !ok {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
for _, format := range [...]string{
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.RFC3339,
time.RFC3339Nano,
"2006-01-02", // RFC 3339
"2006-01-02 15:04", // RFC 3339 with minutes
"2006-01-02 15:04:05", // RFC 3339 with seconds
"2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone
"2006-01-02T15Z0700", // ISO8601 with hour
"2006-01-02T15:04Z0700", // ISO8601 with minutes
"2006-01-02T15:04:05Z0700", // ISO8601 with seconds
"2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds
} {
ret, err := time.ParseInLocation(format, s, time.Local)
if err == nil {
return ret, nil
}
}
return nil, fmt.Errorf("date() could not parse %s", s)
}),
)
var ljson = NewLanguage(
PrefixExtension('[', parseJSONArray),
PrefixExtension('{', parseJSONObject),
)
var arithmetic = NewLanguage(
InfixNumberOperator("+", func(a, b float64) (interface{}, error) { return a + b, nil }),
InfixNumberOperator("-", func(a, b float64) (interface{}, error) { return a - b, nil }),
InfixNumberOperator("*", func(a, b float64) (interface{}, error) { return a * b, nil }),
InfixNumberOperator("/", func(a, b float64) (interface{}, error) { return a / b, nil }),
InfixNumberOperator("%", func(a, b float64) (interface{}, error) { return math.Mod(a, b), nil }),
InfixNumberOperator("**", func(a, b float64) (interface{}, error) { return math.Pow(a, b), nil }),
InfixNumberOperator(">", func(a, b float64) (interface{}, error) { return a > b, nil }),
InfixNumberOperator(">=", func(a, b float64) (interface{}, error) { return a >= b, nil }),
InfixNumberOperator("<", func(a, b float64) (interface{}, error) { return a < b, nil }),
InfixNumberOperator("<=", func(a, b float64) (interface{}, error) { return a <= b, nil }),
InfixNumberOperator("==", func(a, b float64) (interface{}, error) { return a == b, nil }),
InfixNumberOperator("!=", func(a, b float64) (interface{}, error) { return a != b, nil }),
base,
)
var decimalArithmetic = NewLanguage(
InfixDecimalOperator("+", func(a, b decimal.Decimal) (interface{}, error) { return a.Add(b), nil }),
InfixDecimalOperator("-", func(a, b decimal.Decimal) (interface{}, error) { return a.Sub(b), nil }),
InfixDecimalOperator("*", func(a, b decimal.Decimal) (interface{}, error) { return a.Mul(b), nil }),
InfixDecimalOperator("/", func(a, b decimal.Decimal) (interface{}, error) { return a.Div(b), nil }),
InfixDecimalOperator("%", func(a, b decimal.Decimal) (interface{}, error) { return a.Mod(b), nil }),
InfixDecimalOperator("**", func(a, b decimal.Decimal) (interface{}, error) { return a.Pow(b), nil }),
InfixDecimalOperator(">", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThan(b), nil }),
InfixDecimalOperator(">=", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThanOrEqual(b), nil }),
InfixDecimalOperator("<", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThan(b), nil }),
InfixDecimalOperator("<=", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThanOrEqual(b), nil }),
InfixDecimalOperator("==", func(a, b decimal.Decimal) (interface{}, error) { return a.Equal(b), nil }),
InfixDecimalOperator("!=", func(a, b decimal.Decimal) (interface{}, error) { return !a.Equal(b), nil }),
base,
//Base is before these overrides so that the Base options are overridden
PrefixExtension(scanner.Int, parseDecimal),
PrefixExtension(scanner.Float, parseDecimal),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return decimal.NewFromFloat(i).Neg(), nil
}),
)
var bitmask = NewLanguage(
InfixNumberOperator("^", func(a, b float64) (interface{}, error) { return float64(int64(a) ^ int64(b)), nil }),
InfixNumberOperator("&", func(a, b float64) (interface{}, error) { return float64(int64(a) & int64(b)), nil }),
InfixNumberOperator("|", func(a, b float64) (interface{}, error) { return float64(int64(a) | int64(b)), nil }),
InfixNumberOperator("<<", func(a, b float64) (interface{}, error) { return float64(int64(a) << uint64(b)), nil }),
InfixNumberOperator(">>", func(a, b float64) (interface{}, error) { return float64(int64(a) >> uint64(b)), nil }),
PrefixOperator("~", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected number", v)
}
return float64(^int64(i)), nil
}),
)
var text = NewLanguage(
InfixTextOperator("+", func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }),
InfixTextOperator("<", func(a, b string) (interface{}, error) { return a < b, nil }),
InfixTextOperator("<=", func(a, b string) (interface{}, error) { return a <= b, nil }),
InfixTextOperator(">", func(a, b string) (interface{}, error) { return a > b, nil }),
InfixTextOperator(">=", func(a, b string) (interface{}, error) { return a >= b, nil }),
InfixEvalOperator("=~", regEx),
InfixEvalOperator("!~", notRegEx),
base,
)
var propositionalLogic = NewLanguage(
PrefixOperator("!", func(c context.Context, v interface{}) (interface{}, error) {
b, ok := convertToBool(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected bool", v)
}
return !b, nil
}),
InfixShortCircuit("&&", func(a interface{}) (interface{}, bool) { return false, a == false }),
InfixBoolOperator("&&", func(a, b bool) (interface{}, error) { return a && b, nil }),
InfixShortCircuit("||", func(a interface{}) (interface{}, bool) { return true, a == true }),
InfixBoolOperator("||", func(a, b bool) (interface{}, error) { return a || b, nil }),
InfixBoolOperator("==", func(a, b bool) (interface{}, error) { return a == b, nil }),
InfixBoolOperator("!=", func(a, b bool) (interface{}, error) { return a != b, nil }),
base,
)
var parentheses = NewLanguage(
PrefixExtension('(', parseParentheses),
)
var ident = NewLanguage(
PrefixMetaPrefix(scanner.Ident, parseIdent),
)
var base = NewLanguage(
PrefixExtension(scanner.Int, parseNumber),
PrefixExtension(scanner.Float, parseNumber),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToFloat(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return -i, nil
}),
PrefixExtension(scanner.String, parseString),
PrefixExtension(scanner.Char, parseString),
PrefixExtension(scanner.RawString, parseString),
Constant("true", true),
Constant("false", false),
InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }),
InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }),
parentheses,
Precedence("??", 0),
Precedence("||", 20),
Precedence("&&", 21),
Precedence("==", 40),
Precedence("!=", 40),
Precedence(">", 40),
Precedence(">=", 40),
Precedence("<", 40),
Precedence("<=", 40),
Precedence("=~", 40),
Precedence("!~", 40),
Precedence("in", 40),
Precedence("^", 60),
Precedence("&", 60),
Precedence("|", 60),
Precedence("<<", 90),
Precedence(">>", 90),
Precedence("+", 120),
Precedence("-", 120),
Precedence("*", 150),
Precedence("/", 150),
Precedence("%", 150),
Precedence("**", 200),
ident,
)

View File

@@ -0,0 +1,281 @@
package gval
import (
"context"
"fmt"
"text/scanner"
"unicode"
"github.com/shopspring/decimal"
)
// Language is an expression language
type Language struct {
prefixes map[interface{}]extension
operators map[string]operator
operatorSymbols map[rune]struct{}
init extension
def extension
selector func(Evaluables) Evaluable
}
// NewLanguage returns the union of given Languages as new Language.
func NewLanguage(bases ...Language) Language {
l := newLanguage()
for _, base := range bases {
for i, e := range base.prefixes {
l.prefixes[i] = e
}
for i, e := range base.operators {
l.operators[i] = e.merge(l.operators[i])
l.operators[i].initiate(i)
}
for i := range base.operatorSymbols {
l.operatorSymbols[i] = struct{}{}
}
if base.init != nil {
l.init = base.init
}
if base.def != nil {
l.def = base.def
}
if base.selector != nil {
l.selector = base.selector
}
}
return l
}
func newLanguage() Language {
return Language{
prefixes: map[interface{}]extension{},
operators: map[string]operator{},
operatorSymbols: map[rune]struct{}{},
}
}
// NewEvaluable returns an Evaluable for given expression in the specified language
func (l Language) NewEvaluable(expression string) (Evaluable, error) {
return l.NewEvaluableWithContext(context.Background(), expression)
}
// NewEvaluableWithContext returns an Evaluable for given expression in the specified language using context
func (l Language) NewEvaluableWithContext(c context.Context, expression string) (Evaluable, error) {
p := newParser(expression, l)
eval, err := p.parse(c)
if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF {
err = p.camouflage
}
if err != nil {
pos := p.scanner.Pos()
return nil, fmt.Errorf("parsing error: %s - %d:%d %s", p.scanner.Position, pos.Line, pos.Column, err)
}
return eval, nil
}
// Evaluate given parameter with given expression
func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error) {
return l.EvaluateWithContext(context.Background(), expression, parameter)
}
// Evaluate given parameter with given expression using context
func (l Language) EvaluateWithContext(c context.Context, expression string, parameter interface{}) (interface{}, error) {
eval, err := l.NewEvaluableWithContext(c, expression)
if err != nil {
return nil, err
}
v, err := eval(c, parameter)
if err != nil {
return nil, fmt.Errorf("can not evaluate %s: %v", expression, err)
}
return v, nil
}
// Function returns a Language with given function.
// Function has no conversion for input types.
//
// If the function returns an error it must be the last return parameter.
//
// If the function has (without the error) more then one return parameter,
// it returns them as []interface{}.
func Function(name string, function interface{}) Language {
l := newLanguage()
l.prefixes[name] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
args := []Evaluable{}
scan := p.Scan()
switch scan {
case '(':
args, err = p.parseArguments(c)
if err != nil {
return nil, err
}
default:
p.Camouflage("function call", '(')
}
return p.callFunc(toFunc(function), args...), nil
}
return l
}
// Constant returns a Language with given constant
func Constant(name string, value interface{}) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
return p.Const(value), nil
}
return l
}
// PrefixExtension extends a Language
func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.prefixes[r] = ext
return l
}
// Init is a language that does no parsing, but invokes the given function when
// parsing starts. It is incumbent upon the function to call ParseExpression to
// continue parsing.
//
// This function can be used to customize the parser settings, such as
// whitespace or ident behavior.
func Init(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.init = ext
return l
}
// DefaultExtension is a language that runs the given function if no other
// prefix matches.
func DefaultExtension(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.def = ext
return l
}
// PrefixMetaPrefix chooses a Prefix to be executed
func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language {
l := newLanguage()
l.prefixes[r] = func(c context.Context, p *Parser) (Evaluable, error) {
call, alternative, err := ext(c, p)
if err != nil {
return nil, err
}
if prefix, ok := p.prefixes[l.makePrefixKey(call)]; ok {
return prefix(c, p)
}
return alternative()
}
return l
}
//PrefixOperator returns a Language with given prefix
func PrefixOperator(name string, e Evaluable) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseNextExpression(c)
if err != nil {
return nil, err
}
prefix := func(c context.Context, v interface{}) (interface{}, error) {
a, err := eval(c, v)
if err != nil {
return nil, err
}
return e(c, a)
}
if eval.IsConst() {
v, err := prefix(c, nil)
if err != nil {
return nil, err
}
prefix = p.Const(v)
}
return prefix, nil
}
return l
}
// PostfixOperator extends a Language.
func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language {
l := newLanguage()
l.operators[l.makeInfixKey(name)] = postfix{
f: func(c context.Context, p *Parser, eval Evaluable, pre operatorPrecedence) (Evaluable, error) {
return ext(c, p, eval)
},
}
return l
}
// InfixOperator for two arbitrary values.
func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{arbitrary: f})
}
// InfixShortCircuit operator is called after the left operand is evaluated.
func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language {
return newLanguageOperator(name, &infix{shortCircuit: f})
}
// InfixTextOperator for two text values.
func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{text: f})
}
// InfixNumberOperator for two number values.
func InfixNumberOperator(name string, f func(a, b float64) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{number: f})
}
// InfixDecimalOperator for two decimal values.
func InfixDecimalOperator(name string, f func(a, b decimal.Decimal) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{decimal: f})
}
// InfixBoolOperator for two bool values.
func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{boolean: f})
}
// Precedence of operator. The Operator with higher operatorPrecedence is evaluated first.
func Precedence(name string, operatorPrecendence uint8) Language {
return newLanguageOperator(name, operatorPrecedence(operatorPrecendence))
}
// InfixEvalOperator operates on the raw operands.
// Therefore it cannot be combined with operators for other operand types.
func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language {
return newLanguageOperator(name, directInfix{infixBuilder: f})
}
func newLanguageOperator(name string, op operator) Language {
op.initiate(name)
l := newLanguage()
l.operators[l.makeInfixKey(name)] = op
return l
}
func (l *Language) makePrefixKey(key string) interface{} {
runes := []rune(key)
if len(runes) == 1 && !unicode.IsLetter(runes[0]) {
return runes[0]
}
return key
}
func (l *Language) makeInfixKey(key string) string {
for _, r := range key {
l.operatorSymbols[r] = struct{}{}
}
return key
}
// VariableSelector returns a Language which uses given variable selector.
// It must be combined with a Language that uses the vatiable selector. E.g. gval.Base().
func VariableSelector(selector func(path Evaluables) Evaluable) Language {
l := newLanguage()
l.selector = selector
return l
}

View File

@@ -0,0 +1,391 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/shopspring/decimal"
)
type stage struct {
Evaluable
infixBuilder
operatorPrecedence
}
type stageStack []stage //operatorPrecedence in stacktStage is continuously, monotone ascending
func (s *stageStack) push(b stage) error {
for len(*s) > 0 && s.peek().operatorPrecedence >= b.operatorPrecedence {
a := s.pop()
eval, err := a.infixBuilder(a.Evaluable, b.Evaluable)
if err != nil {
return err
}
if a.IsConst() && b.IsConst() {
v, err := eval(nil, nil)
if err != nil {
return err
}
b.Evaluable = constant(v)
continue
}
b.Evaluable = eval
}
*s = append(*s, b)
return nil
}
func (s *stageStack) peek() stage {
return (*s)[len(*s)-1]
}
func (s *stageStack) pop() stage {
a := s.peek()
(*s) = (*s)[:len(*s)-1]
return a
}
type infixBuilder func(a, b Evaluable) (Evaluable, error)
func (l Language) isSymbolOperation(r rune) bool {
_, in := l.operatorSymbols[r]
return in
}
func (l Language) isOperatorPrefix(op string) bool {
for k := range l.operators {
if strings.HasPrefix(k, op) {
return true
}
}
return false
}
func (op *infix) initiate(name string) {
f := func(a, b interface{}) (interface{}, error) {
return nil, fmt.Errorf("invalid operation (%T) %s (%T)", a, name, b)
}
if op.arbitrary != nil {
f = op.arbitrary
}
for _, typeConvertion := range []bool{true, false} {
if op.text != nil && (!typeConvertion || op.arbitrary == nil) {
f = getStringOpFunc(op.text, f, typeConvertion)
}
if op.boolean != nil {
f = getBoolOpFunc(op.boolean, f, typeConvertion)
}
if op.number != nil {
f = getFloatOpFunc(op.number, f, typeConvertion)
}
if op.decimal != nil {
f = getDecimalOpFunc(op.decimal, f, typeConvertion)
}
}
if op.shortCircuit == nil {
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
return
}
shortF := op.shortCircuit
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
if r, ok := shortF(a); ok {
return r, nil
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
}
type opFunc func(a, b interface{}) (interface{}, error)
func getStringOpFunc(s func(a, b string) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
if a != nil && b != nil {
return s(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
s1, k := a.(string)
s2, l := b.(string)
if k && l {
return s(s1, s2)
}
return f(a, b)
}
}
func convertToBool(o interface{}) (bool, bool) {
if b, ok := o.(bool); ok {
return b, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return false, false
}
o = v.Interface()
}
if o == false || o == nil || o == "false" || o == "FALSE" {
return false, true
}
if o == true || o == "true" || o == "TRUE" {
return true, true
}
if f, ok := convertToFloat(o); ok {
return f != 0., true
}
return false, false
}
func getBoolOpFunc(o func(a, b bool) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToBool(a)
y, l := convertToBool(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(bool)
y, l := b.(bool)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToFloat(o interface{}) (float64, bool) {
if i, ok := o.(float64); ok {
return i, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return 0, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return float64(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return float64(v.Uint()), true
case reflect.Float32, reflect.Float64:
return v.Float(), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f, true
}
}
return 0, false
}
func getFloatOpFunc(o func(a, b float64) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToFloat(a)
y, l := convertToFloat(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(float64)
y, l := b.(float64)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToDecimal(o interface{}) (decimal.Decimal, bool) {
if i, ok := o.(decimal.Decimal); ok {
return i, true
}
if i, ok := o.(float64); ok {
return decimal.NewFromFloat(i), true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return decimal.Zero, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return decimal.NewFromInt(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return decimal.NewFromFloat(float64(v.Uint())), true
case reflect.Float32, reflect.Float64:
return decimal.NewFromFloat(v.Float()), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return decimal.NewFromFloat(f), true
}
}
return decimal.Zero, false
}
func getDecimalOpFunc(o func(a, b decimal.Decimal) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToDecimal(a)
y, l := convertToDecimal(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(decimal.Decimal)
y, l := b.(decimal.Decimal)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
type operator interface {
merge(operator) operator
precedence() operatorPrecedence
initiate(name string)
}
type operatorPrecedence uint8
func (pre operatorPrecedence) merge(op operator) operator {
if op, ok := op.(operatorPrecedence); ok {
if op > pre {
return op
}
return pre
}
if op == nil {
return pre
}
return op.merge(pre)
}
func (pre operatorPrecedence) precedence() operatorPrecedence {
return pre
}
func (pre operatorPrecedence) initiate(name string) {}
type infix struct {
operatorPrecedence
number func(a, b float64) (interface{}, error)
decimal func(a, b decimal.Decimal) (interface{}, error)
boolean func(a, b bool) (interface{}, error)
text func(a, b string) (interface{}, error)
arbitrary func(a, b interface{}) (interface{}, error)
shortCircuit func(a interface{}) (interface{}, bool)
builder infixBuilder
}
func (op infix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case *infix:
if op.number == nil {
op.number = op2.number
}
if op.decimal == nil {
op.decimal = op2.decimal
}
if op.boolean == nil {
op.boolean = op2.boolean
}
if op.text == nil {
op.text = op2.text
}
if op.arbitrary == nil {
op.arbitrary = op2.arbitrary
}
if op.shortCircuit == nil {
op.shortCircuit = op2.shortCircuit
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return &op
}
type directInfix struct {
operatorPrecedence
infixBuilder
}
func (op directInfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case operatorPrecedence:
op.operatorPrecedence = op2
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}
type extension func(context.Context, *Parser) (Evaluable, error)
type postfix struct {
operatorPrecedence
f func(context.Context, *Parser, Evaluable, operatorPrecedence) (Evaluable, error)
}
func (op postfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case postfix:
if op2.f != nil {
op.f = op2.f
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}

View File

@@ -0,0 +1,347 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"text/scanner"
"github.com/shopspring/decimal"
)
//ParseExpression scans an expression into an Evaluable.
func (p *Parser) ParseExpression(c context.Context) (eval Evaluable, err error) {
stack := stageStack{}
for {
eval, err = p.ParseNextExpression(c)
if err != nil {
return nil, err
}
if stage, err := p.parseOperator(c, &stack, eval); err != nil {
return nil, err
} else if err = stack.push(stage); err != nil {
return nil, err
}
if stack.peek().infixBuilder == nil {
return stack.pop().Evaluable, nil
}
}
}
//ParseNextExpression scans the expression ignoring following operators
func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err error) {
scan := p.Scan()
ex, ok := p.prefixes[scan]
if !ok {
if scan != scanner.EOF && p.def != nil {
return p.def(c, p)
}
return nil, p.Expected("extensions")
}
return ex(c, p)
}
// ParseSublanguage sets the next language for this parser to parse and calls
// its initialization function, usually ParseExpression.
func (p *Parser) ParseSublanguage(c context.Context, l Language) (Evaluable, error) {
if p.isCamouflaged() {
panic("can not ParseSublanguage() on camouflaged Parser")
}
curLang := p.Language
curWhitespace := p.scanner.Whitespace
curMode := p.scanner.Mode
curIsIdentRune := p.scanner.IsIdentRune
p.Language = l
p.resetScannerProperties()
defer func() {
p.Language = curLang
p.scanner.Whitespace = curWhitespace
p.scanner.Mode = curMode
p.scanner.IsIdentRune = curIsIdentRune
}()
return p.parse(c)
}
func (p *Parser) parse(c context.Context) (Evaluable, error) {
if p.init != nil {
return p.init(c, p)
}
return p.ParseExpression(c)
}
func parseString(c context.Context, p *Parser) (Evaluable, error) {
s, err := strconv.Unquote(p.TokenText())
if err != nil {
return nil, fmt.Errorf("could not parse string: %s", err)
}
return p.Const(s), nil
}
func parseNumber(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(n), nil
}
func parseDecimal(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(decimal.NewFromFloat(n)), nil
}
func parseParentheses(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return eval, nil
default:
return nil, p.Expected("parentheses", ')')
}
}
func (p *Parser) parseOperator(c context.Context, stack *stageStack, eval Evaluable) (st stage, err error) {
for {
scan := p.Scan()
op := p.TokenText()
mustOp := false
if p.isSymbolOperation(scan) {
scan = p.Peek()
for p.isSymbolOperation(scan) && p.isOperatorPrefix(op+string(scan)) {
mustOp = true
op += string(scan)
p.Next()
scan = p.Peek()
}
} else if scan != scanner.Ident {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
switch operator := p.operators[op].(type) {
case *infix:
return stage{
Evaluable: eval,
infixBuilder: operator.builder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case directInfix:
return stage{
Evaluable: eval,
infixBuilder: operator.infixBuilder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case postfix:
if err = stack.push(stage{
operatorPrecedence: operator.operatorPrecedence,
Evaluable: eval,
}); err != nil {
return stage{}, err
}
eval, err = operator.f(c, p, stack.pop().Evaluable, operator.operatorPrecedence)
if err != nil {
return
}
continue
}
if !mustOp {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
return stage{}, fmt.Errorf("unknown operator %s", op)
}
}
func parseIdent(c context.Context, p *Parser) (call string, alternative func() (Evaluable, error), err error) {
token := p.TokenText()
return token,
func() (Evaluable, error) {
fullname := token
keys := []Evaluable{p.Const(token)}
for {
scan := p.Scan()
switch scan {
case '.':
scan = p.Scan()
switch scan {
case scanner.Ident:
token = p.TokenText()
keys = append(keys, p.Const(token))
default:
return nil, p.Expected("field", scanner.Ident)
}
case '(':
args, err := p.parseArguments(c)
if err != nil {
return nil, err
}
return p.callEvaluable(fullname, p.Var(keys...), args...), nil
case '[':
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ']':
keys = append(keys, key)
default:
return nil, p.Expected("array key", ']')
}
default:
p.Camouflage("variable", '.', '(', '[')
return p.Var(keys...), nil
}
}
}, nil
}
func (p *Parser) parseArguments(c context.Context) (args []Evaluable, err error) {
if p.Scan() == ')' {
return
}
p.Camouflage("scan arguments", ')')
for {
arg, err := p.ParseExpression(c)
args = append(args, arg)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return args, nil
case ',':
default:
return nil, p.Expected("arguments", ')', ',')
}
}
}
func inArray(a, b interface{}) (interface{}, error) {
col, ok := b.([]interface{})
if !ok {
return nil, fmt.Errorf("expected type []interface{} for in operator but got %T", b)
}
for _, value := range col {
if reflect.DeepEqual(a, value) {
return true, nil
}
}
return false, nil
}
func parseIf(c context.Context, p *Parser, e Evaluable) (Evaluable, error) {
a, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
b := p.Const(nil)
switch p.Scan() {
case ':':
b, err = p.ParseExpression(c)
if err != nil {
return nil, err
}
case scanner.EOF:
default:
return nil, p.Expected("<> ? <> : <>", ':', scanner.EOF)
}
return func(c context.Context, v interface{}) (interface{}, error) {
x, err := e(c, v)
if err != nil {
return nil, err
}
if valX := reflect.ValueOf(x); x == nil || valX.IsZero() {
return b(c, v)
}
return a(c, v)
}, nil
}
func parseJSONArray(c context.Context, p *Parser) (Evaluable, error) {
evals := []Evaluable{}
for {
switch p.Scan() {
default:
p.Camouflage("array", ',', ']')
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, eval)
case ',':
case ']':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := make([]interface{}, len(evals))
for i, e := range evals {
eval, err := e(c, v)
if err != nil {
return nil, err
}
vs[i] = eval
}
return vs, nil
}, nil
}
}
}
func parseJSONObject(c context.Context, p *Parser) (Evaluable, error) {
type kv struct {
key Evaluable
value Evaluable
}
evals := []kv{}
for {
switch p.Scan() {
default:
p.Camouflage("object", ',', '}')
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
if p.Scan() != ':' {
if err != nil {
return nil, p.Expected("object", ':')
}
}
value, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, kv{key, value})
case ',':
case '}':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := map[string]interface{}{}
for _, e := range evals {
value, err := e.value(c, v)
if err != nil {
return nil, err
}
key, err := e.key.EvalString(c, v)
if err != nil {
return nil, err
}
vs[key] = value
}
return vs, nil
}, nil
}
}
}

View File

@@ -0,0 +1,147 @@
package gval
import (
"bytes"
"fmt"
"strings"
"text/scanner"
"unicode"
)
//Parser parses expressions in a Language into an Evaluable
type Parser struct {
scanner scanner.Scanner
Language
lastScan rune
camouflage error
}
func newParser(expression string, l Language) *Parser {
sc := scanner.Scanner{}
sc.Init(strings.NewReader(expression))
sc.Error = func(*scanner.Scanner, string) {}
sc.Filename = expression + "\t"
p := &Parser{scanner: sc, Language: l}
p.resetScannerProperties()
return p
}
func (p *Parser) resetScannerProperties() {
p.scanner.Whitespace = scanner.GoWhitespace
p.scanner.Mode = scanner.GoTokens
p.scanner.IsIdentRune = func(r rune, pos int) bool {
return unicode.IsLetter(r) || r == '_' || (pos > 0 && unicode.IsDigit(r))
}
}
// SetWhitespace sets the behavior of the whitespace matcher. The given
// characters must be less than or equal to 0x20 (' ').
func (p *Parser) SetWhitespace(chars ...rune) {
var mask uint64
for _, char := range chars {
mask |= 1 << char
}
p.scanner.Whitespace = mask
}
// SetMode sets the tokens that the underlying scanner will match.
func (p *Parser) SetMode(mode uint) {
p.scanner.Mode = mode
}
// SetIsIdentRuneFunc sets the function that matches ident characters in the
// underlying scanner.
func (p *Parser) SetIsIdentRuneFunc(fn func(ch rune, i int) bool) {
p.scanner.IsIdentRune = fn
}
// Scan reads the next token or Unicode character from source and returns it.
// It only recognizes tokens t for which the respective Mode bit (1<<-t) is set.
// It returns scanner.EOF at the end of the source.
func (p *Parser) Scan() rune {
if p.isCamouflaged() {
p.camouflage = nil
return p.lastScan
}
p.camouflage = nil
p.lastScan = p.scanner.Scan()
return p.lastScan
}
func (p *Parser) isCamouflaged() bool {
return p.camouflage != nil && p.camouflage != errCamouflageAfterNext
}
// Camouflage rewind the last Scan(). The Parser holds the camouflage error until
// the next Scan()
// Do not call Rewind() on a camouflaged Parser
func (p *Parser) Camouflage(unit string, expected ...rune) {
if p.isCamouflaged() {
panic(fmt.Errorf("can only Camouflage() after Scan(): %v", p.camouflage))
}
p.camouflage = p.Expected(unit, expected...)
}
// Peek returns the next Unicode character in the source without advancing
// the scanner. It returns EOF if the scanner's position is at the last
// character of the source.
// Do not call Peek() on a camouflaged Parser
func (p *Parser) Peek() rune {
if p.isCamouflaged() {
panic("can not Peek() on camouflaged Parser")
}
return p.scanner.Peek()
}
var errCamouflageAfterNext = fmt.Errorf("Camouflage() after Next()")
// Next reads and returns the next Unicode character.
// It returns EOF at the end of the source.
// Do not call Next() on a camouflaged Parser
func (p *Parser) Next() rune {
if p.isCamouflaged() {
panic("can not Next() on camouflaged Parser")
}
p.camouflage = errCamouflageAfterNext
return p.scanner.Next()
}
// TokenText returns the string corresponding to the most recently scanned token.
// Valid after calling Scan().
func (p *Parser) TokenText() string {
return p.scanner.TokenText()
}
//Expected returns an error signaling an unexpected Scan() result
func (p *Parser) Expected(unit string, expected ...rune) error {
return unexpectedRune{unit, expected, p.lastScan}
}
type unexpectedRune struct {
unit string
expected []rune
got rune
}
func (err unexpectedRune) Error() string {
exp := bytes.Buffer{}
runes := err.expected
switch len(runes) {
default:
for _, r := range runes[:len(runes)-2] {
exp.WriteString(scanner.TokenString(r))
exp.WriteString(", ")
}
fallthrough
case 2:
exp.WriteString(scanner.TokenString(runes[len(runes)-2]))
exp.WriteString(" or ")
fallthrough
case 1:
exp.WriteString(scanner.TokenString(runes[len(runes)-1]))
case 0:
return fmt.Sprintf("unexpected %s while scanning %s", scanner.TokenString(err.got), err.unit)
}
return fmt.Sprintf("unexpected %s while scanning %s expected %s", scanner.TokenString(err.got), err.unit, exp.String())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Sentimens Research Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,63 @@
# ctx
Composable utilities for Go contexts.
[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/SentimensRG/ctx)
[![Go Report Card](https://goreportcard.com/badge/github.com/SentimensRG/ctx?style=flat-square)](https://goreportcard.com/report/github.com/SentimensRG/ctx)
## Installation
```bash
go get -u github.com/SentimensRG/ctx
```
## Overview
The `ctx` package provides utilites for working with data structures satisfying
the `ctx.Doner` interface, most notably `context.Context`:
```go
type Doner interface {
Done() <-chan struct{}
}
```
The functions in `ctx` are appropriate for operations that do not preserve the
values in a context, e.g.: joining several contexts together.
## Subpackages
- [sigctx](https://github.com/SentimensRG/ctx/tree/master/sigctx): contexts for graceful shutdown
- [refctx](https://github.com/SentimensRG/ctx/tree/master/refctx): contexts linked to a reference-counter
- [mergectx](https://github.com/SentimensRG/ctx/tree/master/mergectx): utilities for merging `context.Context` instances while preserving values, errors and deadlines.
## RFC
If you find this useful please let me know: <l.thibault@sentimens.com>
Seriously, even if you just used it in your weekend project, I'd like to hear
about it :)
## License
The MIT License
Copyright (c) 2017 Sentimens Research Group, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,184 @@
package ctx
import (
"context"
"sync"
"time"
)
// Binder is the interface that wraps the basic Bind method.
// Bind executes logic until the Doner completes. Implementations of Bind must
// not return until the Doner has completed.
type Binder interface {
Bind(Doner)
}
// BindFunc is an adapter to allow the use of ordinary functions as Binders.
type BindFunc func(Doner)
// Bind executes logic until the Doner completes. It satisfies the Binder
// interface.
func (f BindFunc) Bind(d Doner) { f(d) }
// Doner can block until something is done
type Doner interface {
Done() <-chan struct{}
}
// C is a basic implementation of Doner
type C <-chan struct{}
// Background is the ctx analog to context.Background(). It never fires.
func Background() C {
return nil
}
// Done returns a channel that receives when an action is complete
func (dc C) Done() <-chan struct{} { return dc }
type ctx struct {
Doner
}
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (ctx) Deadline() (deadline time.Time, ok bool) {
return
}
func (c ctx) Err() error {
select {
case <-c.Done():
return context.Canceled
default:
return nil
}
}
func (c ctx) Value(interface{}) (v interface{}) {
return
}
// AsContext creates a context that fires when the Doner fires
func AsContext(d Doner) context.Context {
return ctx{d}
}
// After time time has elapsed, the Doner fires
func After(d time.Duration) C {
ch := make(chan struct{})
go func() {
<-time.After(d)
close(ch)
}()
return ch
}
// WithCancel returns a new Doner that can be cancelled via the associated
// function
func WithCancel(d Doner) (C, func()) {
var closer sync.Once
cq := make(chan struct{})
cancel := func() { closer.Do(func() { close(cq) }) }
go func() {
select {
case <-cq:
case <-d.Done():
cancel()
}
}()
return cq, cancel
}
// Tick returns a <-chan whose range ends when the underlying context cancels
func Tick(d Doner) <-chan struct{} {
c := make(chan struct{})
cq := d.Done()
go func() {
for {
select {
case <-cq:
close(c)
return
default:
select {
case c <- struct{}{}:
case <-cq:
}
}
}
}()
return c
}
// Defer guarantees that a function will be called after a context has cancelled
func Defer(d Doner, cb func()) {
go func() {
<-d.Done()
cb()
}()
}
// Link returns a channel that fires if ANY of the constituent Doners have fired
func Link(doners ...Doner) C {
c := make(chan struct{})
cancel := func() { close(c) }
var once sync.Once
for _, d := range doners {
Defer(d, func() { once.Do(cancel) })
}
return c
}
// Join returns a channel that receives when all constituent Doners have fired
func Join(doners ...Doner) C {
var wg sync.WaitGroup
wg.Add(len(doners))
for _, d := range doners {
Defer(d, wg.Done)
}
cq := make(chan struct{})
go func() {
wg.Wait()
close(cq)
}()
return cq
}
// FTick calls a function in a loop until the Doner has fired
func FTick(d Doner, f func()) {
for range Tick(d) {
f()
}
}
// FTickInterval calls a function repeatedly at a given internval, until the Doner
// has fired. Note that FTickInterval ignores the time spent executing a function,
// and instead guarantees an interval of `t` between of return of the previous
// function call and the invocation of the next function call.
func FTickInterval(d Doner, t time.Duration, f func()) {
for {
select {
case <-d.Done():
return
case <-time.After(t):
f()
}
}
}
// FDone returns a doner that fires when the function returns or panics
func FDone(f func()) C {
ch := make(chan struct{})
go func() {
defer close(ch)
f()
}()
return ch
}

View File

@@ -0,0 +1,63 @@
# sigctx
Package `sigctx` provides contexts for graceful shutdown.
The `sigctx` package provides a context that terminates when it receives a
SIGINT or SIGTERM. This provides a convenient mechanism for triggering
graceful application shutdown.
`sigctx.New` returns a `ctx.C`, which implements the ubiquitous `ctx.Doner`
interface. It fires when either SIGINT or SIGTERM is caught.
## Examples
```go
import (
"log"
"github.com/SentimensRG/ctx/sigctx"
)
func main() {
ctx := sigctx.New() // returns a regular context.Context
<-ctx.Done() // will unblock on SIGINT and SIGTERM
log.Println("exiting.")
}
```
`sigctx.Tick` can be used to react to streams of signals. For example, you can
implement a graceful shutdown attempt, followed by a forced shutdown.
```go
import (
"log"
"github.com/SentimensRG/ctx/sigctx"
"github.com/SentimensRG/ctx"
)
func main() {
t := sigctx.Tick()
d, cancel := ctx.WithCancel(ctx.Background())
go func() {
defer cancel()
go func() {
// business logic goes here
}()
<-t
log.Println("attempting graceful shutdown - press Ctrl + c again to force quit")
go func() {
defer cancel()
// cleanup logic goes here
}()
<-t
log.Println("forcing close")
}()
<-d.Done()
}
```

View File

@@ -0,0 +1,60 @@
// Package sigctx provides a context that expires when a SIGINT or SIGTERM is
// received.
package sigctx
import (
"os"
"os/signal"
"sync"
"syscall"
"github.com/SentimensRG/ctx"
)
var (
c ctx.C
sigCh chan os.Signal
initC, initSig sync.Once
)
func initSigCh() {
sigCh = make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
}
// New signal-bound ctx.C that terminates when either SIGINT or SIGTERM
// is caught.
func New() ctx.C {
initC.Do(func() {
initSig.Do(initSigCh)
dc := make(chan struct{})
c = dc
go func() {
select {
case <-sigCh:
close(dc)
case <-c.Done():
}
}()
})
return c
}
// Tick returns a channel that recvs each time a either SIGINT or SIGTERM are
// caught.
func Tick() <-chan struct{} {
initSig.Do(initSigCh)
dc := make(chan struct{})
go func() {
for {
<-sigCh
dc <- struct{}{}
}
}()
return dc
}

View File

@@ -0,0 +1,15 @@
bin/
.idea/
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

View File

@@ -0,0 +1,12 @@
language: go
dist: xenial
go:
- '1.10'
- '1.11'
- '1.12'
- '1.13'
- 'tip'
script:
- go test -coverpkg=./... -coverprofile=coverage.info -timeout=5s
- bash <(curl -s https://codecov.io/bash)

View File

@@ -0,0 +1,43 @@
# Contributor Code of Conduct
This project adheres to [The Code Manifesto](http://codemanifesto.com)
as its guidelines for contributor interactions.
## The Code Manifesto
We want to work in an ecosystem that empowers developers to reach their
potential — one that encourages growth and effective collaboration. A space
that is safe for all.
A space such as this benefits everyone that participates in it. It encourages
new developers to enter our field. It is through discussion and collaboration
that we grow, and through growth that we improve.
In the effort to create such a place, we hold to these values:
1. **Discrimination limits us.** This includes discrimination on the basis of
race, gender, sexual orientation, gender identity, age, nationality,
technology and any other arbitrary exclusion of a group of people.
2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort
levels. Remember that, and if brought to your attention, heed it.
3. **We are our biggest assets.** None of us were born masters of our trade.
Each of us has been helped along the way. Return that favor, when and where
you can.
4. **We are resources for the future.** As an extension of #3, share what you
know. Make yourself a resource to help those that come after you.
5. **Respect defines us.** Treat others as you wish to be treated. Make your
discussions, criticisms and debates from a position of respectfulness. Ask
yourself, is it true? Is it necessary? Is it constructive? Anything less is
unacceptable.
6. **Reactions require grace.** Angry responses are valid, but abusive language
and vindictive actions are toxic. When something happens that offends you,
handle it assertively, but be respectful. Escalate reasonably, and try to
allow the offender an opportunity to explain themselves, and possibly
correct the issue.
7. **Opinions are just that: opinions.** Each and every one of us, due to our
background and upbringing, have varying opinions. That is perfectly
acceptable. Remember this: if you respect your own opinions, you should
respect the opinions of others.
8. **To err is human.** You might not intend it, but mistakes do happen and
contribute to build experience. Tolerate honest mistakes, and don't
hesitate to apologize if you make one yourself.

View File

@@ -0,0 +1,63 @@
#### Support
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
#### What to contribute
If you don't know what to do, there are some features and functions that need to be done
- [ ] Refactor code
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
- [ ] Create actual list of contributors and projects that currently using this package
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
- [ ] Implement benchmarking
- [ ] Implement batch of examples
- [ ] Look at forks for new features and fixes
#### Advice
Feel free to create what you want, but keep in mind when you implement new features:
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
- Public functions must be documented and described in source file and added to README.md to the list of available functions
- There are must be unit-tests for any new functions and improvements
## Financial contributions
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/govalidator).
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
## Credits
### Contributors
Thank you to all the people who have already contributed to govalidator!
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
### Backers
Thank you to all our backers! [[Become a backer](https://opencollective.com/govalidator#backer)]
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
### Sponsors
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/govalidator#sponsor))
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2020 Alex Saskevich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,622 @@
govalidator
===========
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator)
[![Coverage](https://codecov.io/gh/asaskevich/govalidator/branch/master/graph/badge.svg)](https://codecov.io/gh/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [![Backers on Open Collective](https://opencollective.com/govalidator/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/govalidator/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_shield)
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
#### Installation
Make sure that Go is installed on your computer.
Type the following command in your terminal:
go get github.com/asaskevich/govalidator
or you can get specified release of the package with `gopkg.in`:
go get gopkg.in/asaskevich/govalidator.v10
After it the package is ready to use.
#### Import package in your project
Add following line in your `*.go` file:
```go
import "github.com/asaskevich/govalidator"
```
If you are unhappy to use long `govalidator`, you can do something like this:
```go
import (
valid "github.com/asaskevich/govalidator"
)
```
#### Activate behavior to require all fields have a validation tag by default
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
`SetNilPtrAllowedByRequired` causes validation to pass when struct fields marked by `required` are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between `nil` and `zero value` state can use this. If disabled, both `nil` and `zero` values cause validation errors.
```go
import "github.com/asaskevich/govalidator"
func init() {
govalidator.SetFieldsRequiredByDefault(true)
}
```
Here's some code to explain it:
```go
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
type exampleStruct struct {
Name string ``
Email string `valid:"email"`
}
// this, however, will only fail when Email is empty or an invalid email address:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email"`
}
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
type exampleStruct2 struct {
Name string `valid:"-"`
Email string `valid:"email,optional"`
}
```
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
##### Custom validator function signature
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
```go
import "github.com/asaskevich/govalidator"
// old signature
func(i interface{}) bool
// new signature
func(i interface{}, o interface{}) bool
```
##### Adding a custom validator
This was changed to prevent data races when accessing custom validators.
```go
import "github.com/asaskevich/govalidator"
// before
govalidator.CustomTypeTagMap["customByteArrayValidator"] = func(i interface{}, o interface{}) bool {
// ...
}
// after
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, o interface{}) bool {
// ...
})
```
#### List of functions:
```go
func Abs(value float64) float64
func BlackList(str, chars string) string
func ByteLength(str string, params ...string) bool
func CamelCaseToUnderscore(str string) string
func Contains(str, substring string) bool
func Count(array []interface{}, iterator ConditionIterator) int
func Each(array []interface{}, iterator Iterator)
func ErrorByField(e error, field string) string
func ErrorsByField(e error) map[string]string
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
func Find(array []interface{}, iterator ConditionIterator) interface{}
func GetLine(s string, index int) (string, error)
func GetLines(s string) []string
func HasLowerCase(str string) bool
func HasUpperCase(str string) bool
func HasWhitespace(str string) bool
func HasWhitespaceOnly(str string) bool
func InRange(value interface{}, left interface{}, right interface{}) bool
func InRangeFloat32(value, left, right float32) bool
func InRangeFloat64(value, left, right float64) bool
func InRangeInt(value, left, right interface{}) bool
func IsASCII(str string) bool
func IsAlpha(str string) bool
func IsAlphanumeric(str string) bool
func IsBase64(str string) bool
func IsByteLength(str string, min, max int) bool
func IsCIDR(str string) bool
func IsCRC32(str string) bool
func IsCRC32b(str string) bool
func IsCreditCard(str string) bool
func IsDNSName(str string) bool
func IsDataURI(str string) bool
func IsDialString(str string) bool
func IsDivisibleBy(str, num string) bool
func IsEmail(str string) bool
func IsExistingEmail(email string) bool
func IsFilePath(str string) (bool, int)
func IsFloat(str string) bool
func IsFullWidth(str string) bool
func IsHalfWidth(str string) bool
func IsHash(str string, algorithm string) bool
func IsHexadecimal(str string) bool
func IsHexcolor(str string) bool
func IsHost(str string) bool
func IsIP(str string) bool
func IsIPv4(str string) bool
func IsIPv6(str string) bool
func IsISBN(str string, version int) bool
func IsISBN10(str string) bool
func IsISBN13(str string) bool
func IsISO3166Alpha2(str string) bool
func IsISO3166Alpha3(str string) bool
func IsISO4217(str string) bool
func IsISO693Alpha2(str string) bool
func IsISO693Alpha3b(str string) bool
func IsIn(str string, params ...string) bool
func IsInRaw(str string, params ...string) bool
func IsInt(str string) bool
func IsJSON(str string) bool
func IsLatitude(str string) bool
func IsLongitude(str string) bool
func IsLowerCase(str string) bool
func IsMAC(str string) bool
func IsMD4(str string) bool
func IsMD5(str string) bool
func IsMagnetURI(str string) bool
func IsMongoID(str string) bool
func IsMultibyte(str string) bool
func IsNatural(value float64) bool
func IsNegative(value float64) bool
func IsNonNegative(value float64) bool
func IsNonPositive(value float64) bool
func IsNotNull(str string) bool
func IsNull(str string) bool
func IsNumeric(str string) bool
func IsPort(str string) bool
func IsPositive(value float64) bool
func IsPrintableASCII(str string) bool
func IsRFC3339(str string) bool
func IsRFC3339WithoutZone(str string) bool
func IsRGBcolor(str string) bool
func IsRegex(str string) bool
func IsRequestURI(rawurl string) bool
func IsRequestURL(rawurl string) bool
func IsRipeMD128(str string) bool
func IsRipeMD160(str string) bool
func IsRsaPub(str string, params ...string) bool
func IsRsaPublicKey(str string, keylen int) bool
func IsSHA1(str string) bool
func IsSHA256(str string) bool
func IsSHA384(str string) bool
func IsSHA512(str string) bool
func IsSSN(str string) bool
func IsSemver(str string) bool
func IsTiger128(str string) bool
func IsTiger160(str string) bool
func IsTiger192(str string) bool
func IsTime(str string, format string) bool
func IsType(v interface{}, params ...string) bool
func IsURL(str string) bool
func IsUTFDigit(str string) bool
func IsUTFLetter(str string) bool
func IsUTFLetterNumeric(str string) bool
func IsUTFNumeric(str string) bool
func IsUUID(str string) bool
func IsUUIDv3(str string) bool
func IsUUIDv4(str string) bool
func IsUUIDv5(str string) bool
func IsULID(str string) bool
func IsUnixTime(str string) bool
func IsUpperCase(str string) bool
func IsVariableWidth(str string) bool
func IsWhole(value float64) bool
func LeftTrim(str, chars string) string
func Map(array []interface{}, iterator ResultIterator) []interface{}
func Matches(str, pattern string) bool
func MaxStringLength(str string, params ...string) bool
func MinStringLength(str string, params ...string) bool
func NormalizeEmail(str string) (string, error)
func PadBoth(str string, padStr string, padLen int) string
func PadLeft(str string, padStr string, padLen int) string
func PadRight(str string, padStr string, padLen int) string
func PrependPathToErrors(err error, path string) error
func Range(str string, params ...string) bool
func RemoveTags(s string) string
func ReplacePattern(str, pattern, replace string) string
func Reverse(s string) string
func RightTrim(str, chars string) string
func RuneLength(str string, params ...string) bool
func SafeFileName(str string) string
func SetFieldsRequiredByDefault(value bool)
func SetNilPtrAllowedByRequired(value bool)
func Sign(value float64) float64
func StringLength(str string, params ...string) bool
func StringMatches(s string, params ...string) bool
func StripLow(str string, keepNewLines bool) string
func ToBoolean(str string) (bool, error)
func ToFloat(str string) (float64, error)
func ToInt(value interface{}) (res int64, err error)
func ToJSON(obj interface{}) (string, error)
func ToString(obj interface{}) string
func Trim(str, chars string) string
func Truncate(str string, length int, ending string) string
func TruncatingErrorf(str string, args ...interface{}) error
func UnderscoreToCamelCase(s string) string
func ValidateMap(inputMap map[string]interface{}, validationMap map[string]interface{}) (bool, error)
func ValidateStruct(s interface{}) (bool, error)
func WhiteList(str, chars string) string
type ConditionIterator
type CustomTypeValidator
type Error
func (e Error) Error() string
type Errors
func (es Errors) Error() string
func (es Errors) Errors() []error
type ISO3166Entry
type ISO693Entry
type InterfaceParamValidator
type Iterator
type ParamValidator
type ResultIterator
type UnsupportedTypeError
func (e *UnsupportedTypeError) Error() string
type Validator
```
#### Examples
###### IsURL
```go
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
```
###### IsType
```go
println(govalidator.IsType("Bob", "string"))
println(govalidator.IsType(1, "int"))
i := 1
println(govalidator.IsType(&i, "*int"))
```
IsType can be used through the tag `type` which is essential for map validation:
```go
type User struct {
Name string `valid:"type(string)"`
Age int `valid:"type(int)"`
Meta interface{} `valid:"type(string)"`
}
result, err := govalidator.ValidateStruct(User{"Bob", 20, "meta"})
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### ToString
```go
type User struct {
FirstName string
LastName string
}
str := govalidator.ToString(&User{"John", "Juan"})
println(str)
```
###### Each, Map, Filter, Count for slices
Each iterates over the slice/array and calls Iterator for every item
```go
data := []interface{}{1, 2, 3, 4, 5}
var fn govalidator.Iterator = func(value interface{}, index int) {
println(value.(int))
}
govalidator.Each(data, fn)
```
```go
data := []interface{}{1, 2, 3, 4, 5}
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
return value.(int) * 3
}
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
```
```go
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
return value.(int)%2 == 0
}
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
_ = govalidator.Count(data, fn) // result = 5
```
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
```go
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
return str == "duck"
})
```
For completely custom validators (interface-based), see below.
Here is a list of available validators for struct fields (validator - used function):
```go
"email": IsEmail,
"url": IsURL,
"dialstring": IsDialString,
"requrl": IsRequestURL,
"requri": IsRequestURI,
"alpha": IsAlpha,
"utfletter": IsUTFLetter,
"alphanum": IsAlphanumeric,
"utfletternum": IsUTFLetterNumeric,
"numeric": IsNumeric,
"utfnumeric": IsUTFNumeric,
"utfdigit": IsUTFDigit,
"hexadecimal": IsHexadecimal,
"hexcolor": IsHexcolor,
"rgbcolor": IsRGBcolor,
"lowercase": IsLowerCase,
"uppercase": IsUpperCase,
"int": IsInt,
"float": IsFloat,
"null": IsNull,
"uuid": IsUUID,
"uuidv3": IsUUIDv3,
"uuidv4": IsUUIDv4,
"uuidv5": IsUUIDv5,
"creditcard": IsCreditCard,
"isbn10": IsISBN10,
"isbn13": IsISBN13,
"json": IsJSON,
"multibyte": IsMultibyte,
"ascii": IsASCII,
"printableascii": IsPrintableASCII,
"fullwidth": IsFullWidth,
"halfwidth": IsHalfWidth,
"variablewidth": IsVariableWidth,
"base64": IsBase64,
"datauri": IsDataURI,
"ip": IsIP,
"port": IsPort,
"ipv4": IsIPv4,
"ipv6": IsIPv6,
"dns": IsDNSName,
"host": IsHost,
"mac": IsMAC,
"latitude": IsLatitude,
"longitude": IsLongitude,
"ssn": IsSSN,
"semver": IsSemver,
"rfc3339": IsRFC3339,
"rfc3339WithoutZone": IsRFC3339WithoutZone,
"ISO3166Alpha2": IsISO3166Alpha2,
"ISO3166Alpha3": IsISO3166Alpha3,
"ulid": IsULID,
```
Validators with parameters
```go
"range(min|max)": Range,
"length(min|max)": ByteLength,
"runelength(min|max)": RuneLength,
"stringlength(min|max)": StringLength,
"matches(pattern)": StringMatches,
"in(string1|string2|...|stringN)": IsIn,
"rsapub(keylength)" : IsRsaPub,
"minstringlength(int): MinStringLength,
"maxstringlength(int): MaxStringLength,
```
Validators with parameters for any type
```go
"type(type)": IsType,
```
And here is small example of usage:
```go
type Post struct {
Title string `valid:"alphanum,required"`
Message string `valid:"duck,ascii"`
Message2 string `valid:"animal(dog)"`
AuthorIP string `valid:"ipv4"`
Date string `valid:"-"`
}
post := &Post{
Title: "My Example Post",
Message: "duck",
Message2: "dog",
AuthorIP: "123.234.54.3",
}
// Add your own struct validation tags
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
return str == "duck"
})
// Add your own struct validation tags with parameter
govalidator.ParamTagMap["animal"] = govalidator.ParamValidator(func(str string, params ...string) bool {
species := params[0]
return str == species
})
govalidator.ParamTagRegexMap["animal"] = regexp.MustCompile("^animal\\((\\w+)\\)$")
result, err := govalidator.ValidateStruct(post)
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### ValidateMap [#2](https://github.com/asaskevich/govalidator/pull/338)
If you want to validate maps, you can use the map to be validated and a validation map that contain the same tags used in ValidateStruct, both maps have to be in the form `map[string]interface{}`
So here is small example of usage:
```go
var mapTemplate = map[string]interface{}{
"name":"required,alpha",
"family":"required,alpha",
"email":"required,email",
"cell-phone":"numeric",
"address":map[string]interface{}{
"line1":"required,alphanum",
"line2":"alphanum",
"postal-code":"numeric",
},
}
var inputMap = map[string]interface{}{
"name":"Bob",
"family":"Smith",
"email":"foo@bar.baz",
"address":map[string]interface{}{
"line1":"",
"line2":"",
"postal-code":"",
},
}
result, err := govalidator.ValidateMap(inputMap, mapTemplate)
if err != nil {
println("error: " + err.Error())
}
println(result)
```
###### WhiteList
```go
// Remove all characters from string ignoring characters between "a" and "z"
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
```
###### Custom validation functions
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
```go
import "github.com/asaskevich/govalidator"
type CustomByteArray [6]byte // custom types are supported and can be validated
type StructWithCustomByteArray struct {
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
Email string `valid:"email"`
CustomMinLength int `valid:"-"`
}
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", func(i interface{}, context interface{}) bool {
switch v := context.(type) { // you can type switch on the context interface being validated
case StructWithCustomByteArray:
// you can check and validate against some other field in the context,
// return early or not validate against the context at all – your choice
case SomeOtherType:
// ...
default:
// expecting some other type? Throw/panic here or continue
}
switch v := i.(type) { // type switch on the struct field being validated
case CustomByteArray:
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
if e != 0 {
return true
}
}
}
return false
})
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", func(i interface{}, context interface{}) bool {
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
case StructWithCustomByteArray:
return len(v.ID) >= v.CustomMinLength
}
return false
})
```
###### Loop over Error()
By default .Error() returns all errors in a single String. To access each error you can do this:
```go
if err != nil {
errs := err.(govalidator.Errors).Errors()
for _, e := range errs {
fmt.Println(e.Error())
}
}
```
###### Custom error messages
Custom error messages are supported via annotations by adding the `~` separator - here's an example of how to use it:
```go
type Ticket struct {
Id int64 `json:"id"`
FirstName string `json:"firstname" valid:"required~First name is blank"`
}
```
#### Notes
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
#### Support
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
#### What to contribute
If you don't know what to do, there are some features and functions that need to be done
- [ ] Refactor code
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
- [ ] Create actual list of contributors and projects that currently using this package
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
- [ ] Implement fuzzing testing
- [ ] Implement some struct/map/array utilities
- [ ] Implement map/array validation
- [ ] Implement benchmarking
- [ ] Implement batch of examples
- [ ] Look at forks for new features and fixes
#### Advice
Feel free to create what you want, but keep in mind when you implement new features:
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
- Public functions must be documented and described in source file and added to README.md to the list of available functions
- There are must be unit-tests for any new functions and improvements
## Credits
### Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
* [Daniel Lohse](https://github.com/annismckenzie)
* [Attila Oláh](https://github.com/attilaolah)
* [Daniel Korner](https://github.com/Dadie)
* [Steven Wilkin](https://github.com/stevenwilkin)
* [Deiwin Sarjas](https://github.com/deiwin)
* [Noah Shibley](https://github.com/slugmobile)
* [Nathan Davies](https://github.com/nathj07)
* [Matt Sanford](https://github.com/mzsanford)
* [Simon ccl1115](https://github.com/ccl1115)
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/govalidator#backer)]
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/govalidator#sponsor)]
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_large)

View File

@@ -0,0 +1,87 @@
package govalidator
// Iterator is the function that accepts element of slice/array and its index
type Iterator func(interface{}, int)
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
type ResultIterator func(interface{}, int) interface{}
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
type ConditionIterator func(interface{}, int) bool
// ReduceIterator is the function that accepts two element of slice/array and returns result of merging those values
type ReduceIterator func(interface{}, interface{}) interface{}
// Some validates that any item of array corresponds to ConditionIterator. Returns boolean.
func Some(array []interface{}, iterator ConditionIterator) bool {
res := false
for index, data := range array {
res = res || iterator(data, index)
}
return res
}
// Every validates that every item of array corresponds to ConditionIterator. Returns boolean.
func Every(array []interface{}, iterator ConditionIterator) bool {
res := true
for index, data := range array {
res = res && iterator(data, index)
}
return res
}
// Reduce boils down a list of values into a single value by ReduceIterator
func Reduce(array []interface{}, iterator ReduceIterator, initialValue interface{}) interface{} {
for _, data := range array {
initialValue = iterator(initialValue, data)
}
return initialValue
}
// Each iterates over the slice and apply Iterator to every item
func Each(array []interface{}, iterator Iterator) {
for index, data := range array {
iterator(data, index)
}
}
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
func Map(array []interface{}, iterator ResultIterator) []interface{} {
var result = make([]interface{}, len(array))
for index, data := range array {
result[index] = iterator(data, index)
}
return result
}
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
func Find(array []interface{}, iterator ConditionIterator) interface{} {
for index, data := range array {
if iterator(data, index) {
return data
}
}
return nil
}
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
var result = make([]interface{}, 0)
for index, data := range array {
if iterator(data, index) {
result = append(result, data)
}
}
return result
}
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
func Count(array []interface{}, iterator ConditionIterator) int {
count := 0
for index, data := range array {
if iterator(data, index) {
count = count + 1
}
}
return count
}

View File

@@ -0,0 +1,81 @@
package govalidator
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
)
// ToString convert the input to a string.
func ToString(obj interface{}) string {
res := fmt.Sprintf("%v", obj)
return res
}
// ToJSON convert the input to a valid JSON string
func ToJSON(obj interface{}) (string, error) {
res, err := json.Marshal(obj)
if err != nil {
res = []byte("")
}
return string(res), err
}
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
func ToFloat(value interface{}) (res float64, err error) {
val := reflect.ValueOf(value)
switch value.(type) {
case int, int8, int16, int32, int64:
res = float64(val.Int())
case uint, uint8, uint16, uint32, uint64:
res = float64(val.Uint())
case float32, float64:
res = val.Float()
case string:
res, err = strconv.ParseFloat(val.String(), 64)
if err != nil {
res = 0
}
default:
err = fmt.Errorf("ToInt: unknown interface type %T", value)
res = 0
}
return
}
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
func ToInt(value interface{}) (res int64, err error) {
val := reflect.ValueOf(value)
switch value.(type) {
case int, int8, int16, int32, int64:
res = val.Int()
case uint, uint8, uint16, uint32, uint64:
res = int64(val.Uint())
case float32, float64:
res = int64(val.Float())
case string:
if IsInt(val.String()) {
res, err = strconv.ParseInt(val.String(), 0, 64)
if err != nil {
res = 0
}
} else {
err = fmt.Errorf("ToInt: invalid numeric format %g", value)
res = 0
}
default:
err = fmt.Errorf("ToInt: unknown interface type %T", value)
res = 0
}
return
}
// ToBoolean convert the input string to a boolean.
func ToBoolean(str string) (bool, error) {
return strconv.ParseBool(str)
}

View File

@@ -0,0 +1,3 @@
package govalidator
// A package of validators and sanitizers for strings, structures and collections.

View File

@@ -0,0 +1,47 @@
package govalidator
import (
"sort"
"strings"
)
// Errors is an array of multiple errors and conforms to the error interface.
type Errors []error
// Errors returns itself.
func (es Errors) Errors() []error {
return es
}
func (es Errors) Error() string {
var errs []string
for _, e := range es {
errs = append(errs, e.Error())
}
sort.Strings(errs)
return strings.Join(errs, ";")
}
// Error encapsulates a name, an error and whether there's a custom error message or not.
type Error struct {
Name string
Err error
CustomErrorMessageExists bool
// Validator indicates the name of the validator that failed
Validator string
Path []string
}
func (e Error) Error() string {
if e.CustomErrorMessageExists {
return e.Err.Error()
}
errName := e.Name
if len(e.Path) > 0 {
errName = strings.Join(append(e.Path, e.Name), ".")
}
return errName + ": " + e.Err.Error()
}

View File

@@ -0,0 +1,100 @@
package govalidator
import (
"math"
)
// Abs returns absolute value of number
func Abs(value float64) float64 {
return math.Abs(value)
}
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
func Sign(value float64) float64 {
if value > 0 {
return 1
} else if value < 0 {
return -1
} else {
return 0
}
}
// IsNegative returns true if value < 0
func IsNegative(value float64) bool {
return value < 0
}
// IsPositive returns true if value > 0
func IsPositive(value float64) bool {
return value > 0
}
// IsNonNegative returns true if value >= 0
func IsNonNegative(value float64) bool {
return value >= 0
}
// IsNonPositive returns true if value <= 0
func IsNonPositive(value float64) bool {
return value <= 0
}
// InRangeInt returns true if value lies between left and right border
func InRangeInt(value, left, right interface{}) bool {
value64, _ := ToInt(value)
left64, _ := ToInt(left)
right64, _ := ToInt(right)
if left64 > right64 {
left64, right64 = right64, left64
}
return value64 >= left64 && value64 <= right64
}
// InRangeFloat32 returns true if value lies between left and right border
func InRangeFloat32(value, left, right float32) bool {
if left > right {
left, right = right, left
}
return value >= left && value <= right
}
// InRangeFloat64 returns true if value lies between left and right border
func InRangeFloat64(value, left, right float64) bool {
if left > right {
left, right = right, left
}
return value >= left && value <= right
}
// InRange returns true if value lies between left and right border, generic type to handle int, float32, float64 and string.
// All types must the same type.
// False if value doesn't lie in range or if it incompatible or not comparable
func InRange(value interface{}, left interface{}, right interface{}) bool {
switch value.(type) {
case int:
intValue, _ := ToInt(value)
intLeft, _ := ToInt(left)
intRight, _ := ToInt(right)
return InRangeInt(intValue, intLeft, intRight)
case float32, float64:
intValue, _ := ToFloat(value)
intLeft, _ := ToFloat(left)
intRight, _ := ToFloat(right)
return InRangeFloat64(intValue, intLeft, intRight)
case string:
return value.(string) >= left.(string) && value.(string) <= right.(string)
default:
return false
}
}
// IsWhole returns true if value is whole number
func IsWhole(value float64) bool {
return math.Remainder(value, 1) == 0
}
// IsNatural returns true if value is natural number (positive and whole)
func IsNatural(value float64) bool {
return IsWhole(value) && IsPositive(value)
}

View File

@@ -0,0 +1,113 @@
package govalidator
import "regexp"
// Basic regular expressions for validating strings
const (
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$"
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
ISBN13 string = "^(?:[0-9]{13})$"
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
Alpha string = "^[a-zA-Z]+$"
Alphanumeric string = "^[a-zA-Z0-9]+$"
Numeric string = "^[0-9]+$"
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
Hexadecimal string = "^[0-9a-fA-F]+$"
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
ASCII string = "^[\x00-\x7F]+$"
Multibyte string = "[^\x00-\x7F]"
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
PrintableASCII string = "^[\x20-\x7E]+$"
DataURI string = "^data:.+\\/(.+);base64$"
MagnetURI string = "^magnet:\\?xt=urn:[a-zA-Z0-9]+:[a-zA-Z0-9]{32,40}&dn=.+&tr=.+$"
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername string = `(\S+(:\S*)?@)`
URLPath string = `((\/|\?|#)[^\s]*)`
URLPort string = `(:(\d{1,5}))`
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
UnixPath string = `^(/[^/\x00]*)+/?$`
WinARPath string = `^(?:(?:[a-zA-Z]:|\\\\[a-z0-9_.$●-]+\\[a-z0-9_.$●-]+)\\|\\?[^\\/:*?"<>|\r\n]+\\?)(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
UnixARPath string = `^((\.{0,2}/)?([^/\x00]*))+/?$`
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
tagName string = "valid"
hasLowerCase string = ".*[[:lower:]]"
hasUpperCase string = ".*[[:upper:]]"
hasWhitespace string = ".*[[:space:]]"
hasWhitespaceOnly string = "^[[:space:]]+$"
IMEI string = "^[0-9a-f]{14}$|^\\d{15}$|^\\d{18}$"
IMSI string = "^\\d{14,15}$"
E164 string = `^\+?[1-9]\d{1,14}$`
)
// Used by IsFilePath func
const (
// Unknown is unresolved OS type
Unknown = iota
// Win is Windows type
Win
// Unix is *nix OS types
Unix
)
var (
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
rxEmail = regexp.MustCompile(Email)
rxCreditCard = regexp.MustCompile(CreditCard)
rxISBN10 = regexp.MustCompile(ISBN10)
rxISBN13 = regexp.MustCompile(ISBN13)
rxUUID3 = regexp.MustCompile(UUID3)
rxUUID4 = regexp.MustCompile(UUID4)
rxUUID5 = regexp.MustCompile(UUID5)
rxUUID = regexp.MustCompile(UUID)
rxAlpha = regexp.MustCompile(Alpha)
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
rxNumeric = regexp.MustCompile(Numeric)
rxInt = regexp.MustCompile(Int)
rxFloat = regexp.MustCompile(Float)
rxHexadecimal = regexp.MustCompile(Hexadecimal)
rxHexcolor = regexp.MustCompile(Hexcolor)
rxRGBcolor = regexp.MustCompile(RGBcolor)
rxASCII = regexp.MustCompile(ASCII)
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
rxMultibyte = regexp.MustCompile(Multibyte)
rxFullWidth = regexp.MustCompile(FullWidth)
rxHalfWidth = regexp.MustCompile(HalfWidth)
rxBase64 = regexp.MustCompile(Base64)
rxDataURI = regexp.MustCompile(DataURI)
rxMagnetURI = regexp.MustCompile(MagnetURI)
rxLatitude = regexp.MustCompile(Latitude)
rxLongitude = regexp.MustCompile(Longitude)
rxDNSName = regexp.MustCompile(DNSName)
rxURL = regexp.MustCompile(URL)
rxSSN = regexp.MustCompile(SSN)
rxWinPath = regexp.MustCompile(WinPath)
rxUnixPath = regexp.MustCompile(UnixPath)
rxARWinPath = regexp.MustCompile(WinARPath)
rxARUnixPath = regexp.MustCompile(UnixARPath)
rxSemver = regexp.MustCompile(Semver)
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
rxHasWhitespace = regexp.MustCompile(hasWhitespace)
rxHasWhitespaceOnly = regexp.MustCompile(hasWhitespaceOnly)
rxIMEI = regexp.MustCompile(IMEI)
rxIMSI = regexp.MustCompile(IMSI)
rxE164 = regexp.MustCompile(E164)
)

View File

@@ -0,0 +1,656 @@
package govalidator
import (
"reflect"
"regexp"
"sort"
"sync"
)
// Validator is a wrapper for a validator function that returns bool and accepts string.
type Validator func(str string) bool
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
type CustomTypeValidator func(i interface{}, o interface{}) bool
// ParamValidator is a wrapper for validator functions that accept additional parameters.
type ParamValidator func(str string, params ...string) bool
// InterfaceParamValidator is a wrapper for functions that accept variants parameters for an interface value
type InterfaceParamValidator func(in interface{}, params ...string) bool
type tagOptionsMap map[string]tagOption
func (t tagOptionsMap) orderedKeys() []string {
var keys []string
for k := range t {
keys = append(keys, k)
}
sort.Slice(keys, func(a, b int) bool {
return t[keys[a]].order < t[keys[b]].order
})
return keys
}
type tagOption struct {
name string
customErrorMessage string
order int
}
// UnsupportedTypeError is a wrapper for reflect.Type
type UnsupportedTypeError struct {
Type reflect.Type
}
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
// It implements the methods to sort by string.
type stringValues []reflect.Value
// InterfaceParamTagMap is a map of functions accept variants parameters for an interface value
var InterfaceParamTagMap = map[string]InterfaceParamValidator{
"type": IsType,
}
// InterfaceParamTagRegexMap maps interface param tags to their respective regexes.
var InterfaceParamTagRegexMap = map[string]*regexp.Regexp{
"type": regexp.MustCompile(`^type\((.*)\)$`),
}
// ParamTagMap is a map of functions accept variants parameters
var ParamTagMap = map[string]ParamValidator{
"length": ByteLength,
"range": Range,
"runelength": RuneLength,
"stringlength": StringLength,
"matches": StringMatches,
"in": IsInRaw,
"rsapub": IsRsaPub,
"minstringlength": MinStringLength,
"maxstringlength": MaxStringLength,
}
// ParamTagRegexMap maps param tags to their respective regexes.
var ParamTagRegexMap = map[string]*regexp.Regexp{
"range": regexp.MustCompile("^range\\((\\d+)\\|(\\d+)\\)$"),
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
"in": regexp.MustCompile(`^in\((.*)\)`),
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
"rsapub": regexp.MustCompile("^rsapub\\((\\d+)\\)$"),
"minstringlength": regexp.MustCompile("^minstringlength\\((\\d+)\\)$"),
"maxstringlength": regexp.MustCompile("^maxstringlength\\((\\d+)\\)$"),
}
type customTypeTagMap struct {
validators map[string]CustomTypeValidator
sync.RWMutex
}
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
tm.RLock()
defer tm.RUnlock()
v, ok := tm.validators[name]
return v, ok
}
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
tm.Lock()
defer tm.Unlock()
tm.validators[name] = ctv
}
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
// `type UUID [16]byte` (this would be handled as an array of bytes).
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
var TagMap = map[string]Validator{
"email": IsEmail,
"url": IsURL,
"dialstring": IsDialString,
"requrl": IsRequestURL,
"requri": IsRequestURI,
"alpha": IsAlpha,
"utfletter": IsUTFLetter,
"alphanum": IsAlphanumeric,
"utfletternum": IsUTFLetterNumeric,
"numeric": IsNumeric,
"utfnumeric": IsUTFNumeric,
"utfdigit": IsUTFDigit,
"hexadecimal": IsHexadecimal,
"hexcolor": IsHexcolor,
"rgbcolor": IsRGBcolor,
"lowercase": IsLowerCase,
"uppercase": IsUpperCase,
"int": IsInt,
"float": IsFloat,
"null": IsNull,
"notnull": IsNotNull,
"uuid": IsUUID,
"uuidv3": IsUUIDv3,
"uuidv4": IsUUIDv4,
"uuidv5": IsUUIDv5,
"creditcard": IsCreditCard,
"isbn10": IsISBN10,
"isbn13": IsISBN13,
"json": IsJSON,
"multibyte": IsMultibyte,
"ascii": IsASCII,
"printableascii": IsPrintableASCII,
"fullwidth": IsFullWidth,
"halfwidth": IsHalfWidth,
"variablewidth": IsVariableWidth,
"base64": IsBase64,
"datauri": IsDataURI,
"ip": IsIP,
"port": IsPort,
"ipv4": IsIPv4,
"ipv6": IsIPv6,
"dns": IsDNSName,
"host": IsHost,
"mac": IsMAC,
"latitude": IsLatitude,
"longitude": IsLongitude,
"ssn": IsSSN,
"semver": IsSemver,
"rfc3339": IsRFC3339,
"rfc3339WithoutZone": IsRFC3339WithoutZone,
"ISO3166Alpha2": IsISO3166Alpha2,
"ISO3166Alpha3": IsISO3166Alpha3,
"ISO4217": IsISO4217,
"IMEI": IsIMEI,
"ulid": IsULID,
}
// ISO3166Entry stores country codes
type ISO3166Entry struct {
EnglishShortName string
FrenchShortName string
Alpha2Code string
Alpha3Code string
Numeric string
}
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
var ISO3166List = []ISO3166Entry{
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
{"Angola", "Angola (l')", "AO", "AGO", "024"},
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
{"Australia", "Australie (l')", "AU", "AUS", "036"},
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
{"Canada", "Canada (le)", "CA", "CAN", "124"},
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
{"Chile", "Chili (le)", "CL", "CHL", "152"},
{"China", "Chine (la)", "CN", "CHN", "156"},
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
{"Cuba", "Cuba", "CU", "CUB", "192"},
{"Cyprus", "Chypre", "CY", "CYP", "196"},
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
{"France", "France (la)", "FR", "FRA", "250"},
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
{"Guam", "Guam", "GU", "GUM", "316"},
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
{"Haiti", "Haïti", "HT", "HTI", "332"},
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
{"India", "Inde (l')", "IN", "IND", "356"},
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
{"Israel", "Israël", "IL", "ISR", "376"},
{"Italy", "Italie (l')", "IT", "ITA", "380"},
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
{"Japan", "Japon (le)", "JP", "JPN", "392"},
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
{"Libya", "Libye (la)", "LY", "LBY", "434"},
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
{"Macao", "Macao", "MO", "MAC", "446"},
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
{"Mali", "Mali (le)", "ML", "MLI", "466"},
{"Malta", "Malte", "MT", "MLT", "470"},
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
{"Mauritius", "Maurice", "MU", "MUS", "480"},
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
{"Monaco", "Monaco", "MC", "MCO", "492"},
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
{"Oman", "Oman", "OM", "OMN", "512"},
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
{"Nauru", "Nauru", "NR", "NRU", "520"},
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
{"Aruba", "Aruba", "AW", "ABW", "533"},
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
{"Niue", "Niue", "NU", "NIU", "570"},
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
{"Panama", "Panama (le)", "PA", "PAN", "591"},
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
{"Peru", "Pérou (le)", "PE", "PER", "604"},
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
{"Poland", "Pologne (la)", "PL", "POL", "616"},
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
{"Singapore", "Singapour", "SG", "SGP", "702"},
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
{"Togo", "Togo (le)", "TG", "TGO", "768"},
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
{"Jersey", "Jersey", "JE", "JEY", "832"},
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
}
// ISO4217List is the list of ISO currency codes
var ISO4217List = []string{
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
"CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK",
"DJF", "DKK", "DOP", "DZD",
"EGP", "ERN", "ETB", "EUR",
"FJD", "FKP",
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
"HKD", "HNL", "HRK", "HTG", "HUF",
"IDR", "ILS", "INR", "IQD", "IRR", "ISK",
"JMD", "JOD", "JPY",
"KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT",
"LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN",
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD",
"OMR",
"PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
"QAR",
"RON", "RSD", "RUB", "RWF",
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "STN", "SVC", "SYP", "SZL",
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS",
"UAH", "UGX", "USD", "USN", "UYI", "UYU", "UYW", "UZS",
"VEF", "VES", "VND", "VUV",
"WST",
"XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX",
"YER",
"ZAR", "ZMW", "ZWL",
}
// ISO693Entry stores ISO language codes
type ISO693Entry struct {
Alpha3bCode string
Alpha2Code string
English string
}
//ISO693List based on http://data.okfn.org/data/core/language-codes/r/language-codes-3b2.json
var ISO693List = []ISO693Entry{
{Alpha3bCode: "aar", Alpha2Code: "aa", English: "Afar"},
{Alpha3bCode: "abk", Alpha2Code: "ab", English: "Abkhazian"},
{Alpha3bCode: "afr", Alpha2Code: "af", English: "Afrikaans"},
{Alpha3bCode: "aka", Alpha2Code: "ak", English: "Akan"},
{Alpha3bCode: "alb", Alpha2Code: "sq", English: "Albanian"},
{Alpha3bCode: "amh", Alpha2Code: "am", English: "Amharic"},
{Alpha3bCode: "ara", Alpha2Code: "ar", English: "Arabic"},
{Alpha3bCode: "arg", Alpha2Code: "an", English: "Aragonese"},
{Alpha3bCode: "arm", Alpha2Code: "hy", English: "Armenian"},
{Alpha3bCode: "asm", Alpha2Code: "as", English: "Assamese"},
{Alpha3bCode: "ava", Alpha2Code: "av", English: "Avaric"},
{Alpha3bCode: "ave", Alpha2Code: "ae", English: "Avestan"},
{Alpha3bCode: "aym", Alpha2Code: "ay", English: "Aymara"},
{Alpha3bCode: "aze", Alpha2Code: "az", English: "Azerbaijani"},
{Alpha3bCode: "bak", Alpha2Code: "ba", English: "Bashkir"},
{Alpha3bCode: "bam", Alpha2Code: "bm", English: "Bambara"},
{Alpha3bCode: "baq", Alpha2Code: "eu", English: "Basque"},
{Alpha3bCode: "bel", Alpha2Code: "be", English: "Belarusian"},
{Alpha3bCode: "ben", Alpha2Code: "bn", English: "Bengali"},
{Alpha3bCode: "bih", Alpha2Code: "bh", English: "Bihari languages"},
{Alpha3bCode: "bis", Alpha2Code: "bi", English: "Bislama"},
{Alpha3bCode: "bos", Alpha2Code: "bs", English: "Bosnian"},
{Alpha3bCode: "bre", Alpha2Code: "br", English: "Breton"},
{Alpha3bCode: "bul", Alpha2Code: "bg", English: "Bulgarian"},
{Alpha3bCode: "bur", Alpha2Code: "my", English: "Burmese"},
{Alpha3bCode: "cat", Alpha2Code: "ca", English: "Catalan; Valencian"},
{Alpha3bCode: "cha", Alpha2Code: "ch", English: "Chamorro"},
{Alpha3bCode: "che", Alpha2Code: "ce", English: "Chechen"},
{Alpha3bCode: "chi", Alpha2Code: "zh", English: "Chinese"},
{Alpha3bCode: "chu", Alpha2Code: "cu", English: "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"},
{Alpha3bCode: "chv", Alpha2Code: "cv", English: "Chuvash"},
{Alpha3bCode: "cor", Alpha2Code: "kw", English: "Cornish"},
{Alpha3bCode: "cos", Alpha2Code: "co", English: "Corsican"},
{Alpha3bCode: "cre", Alpha2Code: "cr", English: "Cree"},
{Alpha3bCode: "cze", Alpha2Code: "cs", English: "Czech"},
{Alpha3bCode: "dan", Alpha2Code: "da", English: "Danish"},
{Alpha3bCode: "div", Alpha2Code: "dv", English: "Divehi; Dhivehi; Maldivian"},
{Alpha3bCode: "dut", Alpha2Code: "nl", English: "Dutch; Flemish"},
{Alpha3bCode: "dzo", Alpha2Code: "dz", English: "Dzongkha"},
{Alpha3bCode: "eng", Alpha2Code: "en", English: "English"},
{Alpha3bCode: "epo", Alpha2Code: "eo", English: "Esperanto"},
{Alpha3bCode: "est", Alpha2Code: "et", English: "Estonian"},
{Alpha3bCode: "ewe", Alpha2Code: "ee", English: "Ewe"},
{Alpha3bCode: "fao", Alpha2Code: "fo", English: "Faroese"},
{Alpha3bCode: "fij", Alpha2Code: "fj", English: "Fijian"},
{Alpha3bCode: "fin", Alpha2Code: "fi", English: "Finnish"},
{Alpha3bCode: "fre", Alpha2Code: "fr", English: "French"},
{Alpha3bCode: "fry", Alpha2Code: "fy", English: "Western Frisian"},
{Alpha3bCode: "ful", Alpha2Code: "ff", English: "Fulah"},
{Alpha3bCode: "geo", Alpha2Code: "ka", English: "Georgian"},
{Alpha3bCode: "ger", Alpha2Code: "de", English: "German"},
{Alpha3bCode: "gla", Alpha2Code: "gd", English: "Gaelic; Scottish Gaelic"},
{Alpha3bCode: "gle", Alpha2Code: "ga", English: "Irish"},
{Alpha3bCode: "glg", Alpha2Code: "gl", English: "Galician"},
{Alpha3bCode: "glv", Alpha2Code: "gv", English: "Manx"},
{Alpha3bCode: "gre", Alpha2Code: "el", English: "Greek, Modern (1453-)"},
{Alpha3bCode: "grn", Alpha2Code: "gn", English: "Guarani"},
{Alpha3bCode: "guj", Alpha2Code: "gu", English: "Gujarati"},
{Alpha3bCode: "hat", Alpha2Code: "ht", English: "Haitian; Haitian Creole"},
{Alpha3bCode: "hau", Alpha2Code: "ha", English: "Hausa"},
{Alpha3bCode: "heb", Alpha2Code: "he", English: "Hebrew"},
{Alpha3bCode: "her", Alpha2Code: "hz", English: "Herero"},
{Alpha3bCode: "hin", Alpha2Code: "hi", English: "Hindi"},
{Alpha3bCode: "hmo", Alpha2Code: "ho", English: "Hiri Motu"},
{Alpha3bCode: "hrv", Alpha2Code: "hr", English: "Croatian"},
{Alpha3bCode: "hun", Alpha2Code: "hu", English: "Hungarian"},
{Alpha3bCode: "ibo", Alpha2Code: "ig", English: "Igbo"},
{Alpha3bCode: "ice", Alpha2Code: "is", English: "Icelandic"},
{Alpha3bCode: "ido", Alpha2Code: "io", English: "Ido"},
{Alpha3bCode: "iii", Alpha2Code: "ii", English: "Sichuan Yi; Nuosu"},
{Alpha3bCode: "iku", Alpha2Code: "iu", English: "Inuktitut"},
{Alpha3bCode: "ile", Alpha2Code: "ie", English: "Interlingue; Occidental"},
{Alpha3bCode: "ina", Alpha2Code: "ia", English: "Interlingua (International Auxiliary Language Association)"},
{Alpha3bCode: "ind", Alpha2Code: "id", English: "Indonesian"},
{Alpha3bCode: "ipk", Alpha2Code: "ik", English: "Inupiaq"},
{Alpha3bCode: "ita", Alpha2Code: "it", English: "Italian"},
{Alpha3bCode: "jav", Alpha2Code: "jv", English: "Javanese"},
{Alpha3bCode: "jpn", Alpha2Code: "ja", English: "Japanese"},
{Alpha3bCode: "kal", Alpha2Code: "kl", English: "Kalaallisut; Greenlandic"},
{Alpha3bCode: "kan", Alpha2Code: "kn", English: "Kannada"},
{Alpha3bCode: "kas", Alpha2Code: "ks", English: "Kashmiri"},
{Alpha3bCode: "kau", Alpha2Code: "kr", English: "Kanuri"},
{Alpha3bCode: "kaz", Alpha2Code: "kk", English: "Kazakh"},
{Alpha3bCode: "khm", Alpha2Code: "km", English: "Central Khmer"},
{Alpha3bCode: "kik", Alpha2Code: "ki", English: "Kikuyu; Gikuyu"},
{Alpha3bCode: "kin", Alpha2Code: "rw", English: "Kinyarwanda"},
{Alpha3bCode: "kir", Alpha2Code: "ky", English: "Kirghiz; Kyrgyz"},
{Alpha3bCode: "kom", Alpha2Code: "kv", English: "Komi"},
{Alpha3bCode: "kon", Alpha2Code: "kg", English: "Kongo"},
{Alpha3bCode: "kor", Alpha2Code: "ko", English: "Korean"},
{Alpha3bCode: "kua", Alpha2Code: "kj", English: "Kuanyama; Kwanyama"},
{Alpha3bCode: "kur", Alpha2Code: "ku", English: "Kurdish"},
{Alpha3bCode: "lao", Alpha2Code: "lo", English: "Lao"},
{Alpha3bCode: "lat", Alpha2Code: "la", English: "Latin"},
{Alpha3bCode: "lav", Alpha2Code: "lv", English: "Latvian"},
{Alpha3bCode: "lim", Alpha2Code: "li", English: "Limburgan; Limburger; Limburgish"},
{Alpha3bCode: "lin", Alpha2Code: "ln", English: "Lingala"},
{Alpha3bCode: "lit", Alpha2Code: "lt", English: "Lithuanian"},
{Alpha3bCode: "ltz", Alpha2Code: "lb", English: "Luxembourgish; Letzeburgesch"},
{Alpha3bCode: "lub", Alpha2Code: "lu", English: "Luba-Katanga"},
{Alpha3bCode: "lug", Alpha2Code: "lg", English: "Ganda"},
{Alpha3bCode: "mac", Alpha2Code: "mk", English: "Macedonian"},
{Alpha3bCode: "mah", Alpha2Code: "mh", English: "Marshallese"},
{Alpha3bCode: "mal", Alpha2Code: "ml", English: "Malayalam"},
{Alpha3bCode: "mao", Alpha2Code: "mi", English: "Maori"},
{Alpha3bCode: "mar", Alpha2Code: "mr", English: "Marathi"},
{Alpha3bCode: "may", Alpha2Code: "ms", English: "Malay"},
{Alpha3bCode: "mlg", Alpha2Code: "mg", English: "Malagasy"},
{Alpha3bCode: "mlt", Alpha2Code: "mt", English: "Maltese"},
{Alpha3bCode: "mon", Alpha2Code: "mn", English: "Mongolian"},
{Alpha3bCode: "nau", Alpha2Code: "na", English: "Nauru"},
{Alpha3bCode: "nav", Alpha2Code: "nv", English: "Navajo; Navaho"},
{Alpha3bCode: "nbl", Alpha2Code: "nr", English: "Ndebele, South; South Ndebele"},
{Alpha3bCode: "nde", Alpha2Code: "nd", English: "Ndebele, North; North Ndebele"},
{Alpha3bCode: "ndo", Alpha2Code: "ng", English: "Ndonga"},
{Alpha3bCode: "nep", Alpha2Code: "ne", English: "Nepali"},
{Alpha3bCode: "nno", Alpha2Code: "nn", English: "Norwegian Nynorsk; Nynorsk, Norwegian"},
{Alpha3bCode: "nob", Alpha2Code: "nb", English: "Bokmål, Norwegian; Norwegian Bokmål"},
{Alpha3bCode: "nor", Alpha2Code: "no", English: "Norwegian"},
{Alpha3bCode: "nya", Alpha2Code: "ny", English: "Chichewa; Chewa; Nyanja"},
{Alpha3bCode: "oci", Alpha2Code: "oc", English: "Occitan (post 1500); Provençal"},
{Alpha3bCode: "oji", Alpha2Code: "oj", English: "Ojibwa"},
{Alpha3bCode: "ori", Alpha2Code: "or", English: "Oriya"},
{Alpha3bCode: "orm", Alpha2Code: "om", English: "Oromo"},
{Alpha3bCode: "oss", Alpha2Code: "os", English: "Ossetian; Ossetic"},
{Alpha3bCode: "pan", Alpha2Code: "pa", English: "Panjabi; Punjabi"},
{Alpha3bCode: "per", Alpha2Code: "fa", English: "Persian"},
{Alpha3bCode: "pli", Alpha2Code: "pi", English: "Pali"},
{Alpha3bCode: "pol", Alpha2Code: "pl", English: "Polish"},
{Alpha3bCode: "por", Alpha2Code: "pt", English: "Portuguese"},
{Alpha3bCode: "pus", Alpha2Code: "ps", English: "Pushto; Pashto"},
{Alpha3bCode: "que", Alpha2Code: "qu", English: "Quechua"},
{Alpha3bCode: "roh", Alpha2Code: "rm", English: "Romansh"},
{Alpha3bCode: "rum", Alpha2Code: "ro", English: "Romanian; Moldavian; Moldovan"},
{Alpha3bCode: "run", Alpha2Code: "rn", English: "Rundi"},
{Alpha3bCode: "rus", Alpha2Code: "ru", English: "Russian"},
{Alpha3bCode: "sag", Alpha2Code: "sg", English: "Sango"},
{Alpha3bCode: "san", Alpha2Code: "sa", English: "Sanskrit"},
{Alpha3bCode: "sin", Alpha2Code: "si", English: "Sinhala; Sinhalese"},
{Alpha3bCode: "slo", Alpha2Code: "sk", English: "Slovak"},
{Alpha3bCode: "slv", Alpha2Code: "sl", English: "Slovenian"},
{Alpha3bCode: "sme", Alpha2Code: "se", English: "Northern Sami"},
{Alpha3bCode: "smo", Alpha2Code: "sm", English: "Samoan"},
{Alpha3bCode: "sna", Alpha2Code: "sn", English: "Shona"},
{Alpha3bCode: "snd", Alpha2Code: "sd", English: "Sindhi"},
{Alpha3bCode: "som", Alpha2Code: "so", English: "Somali"},
{Alpha3bCode: "sot", Alpha2Code: "st", English: "Sotho, Southern"},
{Alpha3bCode: "spa", Alpha2Code: "es", English: "Spanish; Castilian"},
{Alpha3bCode: "srd", Alpha2Code: "sc", English: "Sardinian"},
{Alpha3bCode: "srp", Alpha2Code: "sr", English: "Serbian"},
{Alpha3bCode: "ssw", Alpha2Code: "ss", English: "Swati"},
{Alpha3bCode: "sun", Alpha2Code: "su", English: "Sundanese"},
{Alpha3bCode: "swa", Alpha2Code: "sw", English: "Swahili"},
{Alpha3bCode: "swe", Alpha2Code: "sv", English: "Swedish"},
{Alpha3bCode: "tah", Alpha2Code: "ty", English: "Tahitian"},
{Alpha3bCode: "tam", Alpha2Code: "ta", English: "Tamil"},
{Alpha3bCode: "tat", Alpha2Code: "tt", English: "Tatar"},
{Alpha3bCode: "tel", Alpha2Code: "te", English: "Telugu"},
{Alpha3bCode: "tgk", Alpha2Code: "tg", English: "Tajik"},
{Alpha3bCode: "tgl", Alpha2Code: "tl", English: "Tagalog"},
{Alpha3bCode: "tha", Alpha2Code: "th", English: "Thai"},
{Alpha3bCode: "tib", Alpha2Code: "bo", English: "Tibetan"},
{Alpha3bCode: "tir", Alpha2Code: "ti", English: "Tigrinya"},
{Alpha3bCode: "ton", Alpha2Code: "to", English: "Tonga (Tonga Islands)"},
{Alpha3bCode: "tsn", Alpha2Code: "tn", English: "Tswana"},
{Alpha3bCode: "tso", Alpha2Code: "ts", English: "Tsonga"},
{Alpha3bCode: "tuk", Alpha2Code: "tk", English: "Turkmen"},
{Alpha3bCode: "tur", Alpha2Code: "tr", English: "Turkish"},
{Alpha3bCode: "twi", Alpha2Code: "tw", English: "Twi"},
{Alpha3bCode: "uig", Alpha2Code: "ug", English: "Uighur; Uyghur"},
{Alpha3bCode: "ukr", Alpha2Code: "uk", English: "Ukrainian"},
{Alpha3bCode: "urd", Alpha2Code: "ur", English: "Urdu"},
{Alpha3bCode: "uzb", Alpha2Code: "uz", English: "Uzbek"},
{Alpha3bCode: "ven", Alpha2Code: "ve", English: "Venda"},
{Alpha3bCode: "vie", Alpha2Code: "vi", English: "Vietnamese"},
{Alpha3bCode: "vol", Alpha2Code: "vo", English: "Volapük"},
{Alpha3bCode: "wel", Alpha2Code: "cy", English: "Welsh"},
{Alpha3bCode: "wln", Alpha2Code: "wa", English: "Walloon"},
{Alpha3bCode: "wol", Alpha2Code: "wo", English: "Wolof"},
{Alpha3bCode: "xho", Alpha2Code: "xh", English: "Xhosa"},
{Alpha3bCode: "yid", Alpha2Code: "yi", English: "Yiddish"},
{Alpha3bCode: "yor", Alpha2Code: "yo", English: "Yoruba"},
{Alpha3bCode: "zha", Alpha2Code: "za", English: "Zhuang; Chuang"},
{Alpha3bCode: "zul", Alpha2Code: "zu", English: "Zulu"},
}

View File

@@ -0,0 +1,270 @@
package govalidator
import (
"errors"
"fmt"
"html"
"math"
"path"
"regexp"
"strings"
"unicode"
"unicode/utf8"
)
// Contains checks if the string contains the substring.
func Contains(str, substring string) bool {
return strings.Contains(str, substring)
}
// Matches checks if string matches the pattern (pattern is regular expression)
// In case of error return false
func Matches(str, pattern string) bool {
match, _ := regexp.MatchString(pattern, str)
return match
}
// LeftTrim trims characters from the left side of the input.
// If second argument is empty, it will remove leading spaces.
func LeftTrim(str, chars string) string {
if chars == "" {
return strings.TrimLeftFunc(str, unicode.IsSpace)
}
r, _ := regexp.Compile("^[" + chars + "]+")
return r.ReplaceAllString(str, "")
}
// RightTrim trims characters from the right side of the input.
// If second argument is empty, it will remove trailing spaces.
func RightTrim(str, chars string) string {
if chars == "" {
return strings.TrimRightFunc(str, unicode.IsSpace)
}
r, _ := regexp.Compile("[" + chars + "]+$")
return r.ReplaceAllString(str, "")
}
// Trim trims characters from both sides of the input.
// If second argument is empty, it will remove spaces.
func Trim(str, chars string) string {
return LeftTrim(RightTrim(str, chars), chars)
}
// WhiteList removes characters that do not appear in the whitelist.
func WhiteList(str, chars string) string {
pattern := "[^" + chars + "]+"
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, "")
}
// BlackList removes characters that appear in the blacklist.
func BlackList(str, chars string) string {
pattern := "[" + chars + "]+"
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, "")
}
// StripLow removes characters with a numerical value < 32 and 127, mostly control characters.
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
func StripLow(str string, keepNewLines bool) string {
chars := ""
if keepNewLines {
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
} else {
chars = "\x00-\x1F\x7F"
}
return BlackList(str, chars)
}
// ReplacePattern replaces regular expression pattern in string
func ReplacePattern(str, pattern, replace string) string {
r, _ := regexp.Compile(pattern)
return r.ReplaceAllString(str, replace)
}
// Escape replaces <, >, & and " with HTML entities.
var Escape = html.EscapeString
func addSegment(inrune, segment []rune) []rune {
if len(segment) == 0 {
return inrune
}
if len(inrune) != 0 {
inrune = append(inrune, '_')
}
inrune = append(inrune, segment...)
return inrune
}
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
// Ex.: my_func => MyFunc
func UnderscoreToCamelCase(s string) string {
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
}
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
// Ex.: MyFunc => my_func
func CamelCaseToUnderscore(str string) string {
var output []rune
var segment []rune
for _, r := range str {
// not treat number as separate segment
if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
output = addSegment(output, segment)
segment = nil
}
segment = append(segment, unicode.ToLower(r))
}
output = addSegment(output, segment)
return string(output)
}
// Reverse returns reversed string
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// GetLines splits string by "\n" and return array of lines
func GetLines(s string) []string {
return strings.Split(s, "\n")
}
// GetLine returns specified line of multiline string
func GetLine(s string, index int) (string, error) {
lines := GetLines(s)
if index < 0 || index >= len(lines) {
return "", errors.New("line index out of bounds")
}
return lines[index], nil
}
// RemoveTags removes all tags from HTML string
func RemoveTags(s string) string {
return ReplacePattern(s, "<[^>]*>", "")
}
// SafeFileName returns safe string that can be used in file names
func SafeFileName(str string) string {
name := strings.ToLower(str)
name = path.Clean(path.Base(name))
name = strings.Trim(name, " ")
separators, err := regexp.Compile(`[ &_=+:]`)
if err == nil {
name = separators.ReplaceAllString(name, "-")
}
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
if err == nil {
name = legal.ReplaceAllString(name, "")
}
for strings.Contains(name, "--") {
name = strings.Replace(name, "--", "-", -1)
}
return name
}
// NormalizeEmail canonicalize an email address.
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
// normalized to @gmail.com.
func NormalizeEmail(str string) (string, error) {
if !IsEmail(str) {
return "", fmt.Errorf("%s is not an email", str)
}
parts := strings.Split(str, "@")
parts[0] = strings.ToLower(parts[0])
parts[1] = strings.ToLower(parts[1])
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
parts[1] = "gmail.com"
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
}
return strings.Join(parts, "@"), nil
}
// Truncate a string to the closest length without breaking words.
func Truncate(str string, length int, ending string) string {
var aftstr, befstr string
if len(str) > length {
words := strings.Fields(str)
before, present := 0, 0
for i := range words {
befstr = aftstr
before = present
aftstr = aftstr + words[i] + " "
present = len(aftstr)
if present > length && i != 0 {
if (length - before) < (present - length) {
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
}
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
}
}
}
return str
}
// PadLeft pads left side of a string if size of string is less then indicated pad length
func PadLeft(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, true, false)
}
// PadRight pads right side of a string if size of string is less then indicated pad length
func PadRight(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, false, true)
}
// PadBoth pads both sides of a string if size of string is less then indicated pad length
func PadBoth(str string, padStr string, padLen int) string {
return buildPadStr(str, padStr, padLen, true, true)
}
// PadString either left, right or both sides.
// Note that padding string can be unicode and more then one character
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
// When padded length is less then the current string size
if padLen < utf8.RuneCountInString(str) {
return str
}
padLen -= utf8.RuneCountInString(str)
targetLen := padLen
targetLenLeft := targetLen
targetLenRight := targetLen
if padLeft && padRight {
targetLenLeft = padLen / 2
targetLenRight = padLen - targetLenLeft
}
strToRepeatLen := utf8.RuneCountInString(padStr)
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
repeatedString := strings.Repeat(padStr, repeatTimes)
leftSide := ""
if padLeft {
leftSide = repeatedString[0:targetLenLeft]
}
rightSide := ""
if padRight {
rightSide = repeatedString[0:targetLenRight]
}
return leftSide + str + rightSide
}
// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
func TruncatingErrorf(str string, args ...interface{}) error {
n := strings.Count(str, "%s")
return fmt.Errorf(str, args[:n]...)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
box: golang
build:
steps:
- setup-go-workspace
- script:
name: go get
code: |
go version
go get -t ./...
- script:
name: go test
code: |
go test -race -v ./...

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Aymerick JEHANNE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,60 @@
package css
import "fmt"
// Declaration represents a parsed style property
type Declaration struct {
Property string
Value string
Important bool
}
// NewDeclaration instanciates a new Declaration
func NewDeclaration() *Declaration {
return &Declaration{}
}
// Returns string representation of the Declaration
func (decl *Declaration) String() string {
return decl.StringWithImportant(true)
}
// StringWithImportant returns string representation with optional !important part
func (decl *Declaration) StringWithImportant(option bool) string {
result := fmt.Sprintf("%s: %s", decl.Property, decl.Value)
if option && decl.Important {
result += " !important"
}
result += ";"
return result
}
// Equal returns true if both Declarations are equals
func (decl *Declaration) Equal(other *Declaration) bool {
return (decl.Property == other.Property) && (decl.Value == other.Value) && (decl.Important == other.Important)
}
//
// DeclarationsByProperty
//
// DeclarationsByProperty represents sortable style declarations
type DeclarationsByProperty []*Declaration
// Implements sort.Interface
func (declarations DeclarationsByProperty) Len() int {
return len(declarations)
}
// Implements sort.Interface
func (declarations DeclarationsByProperty) Swap(i, j int) {
declarations[i], declarations[j] = declarations[j], declarations[i]
}
// Implements sort.Interface
func (declarations DeclarationsByProperty) Less(i, j int) bool {
return declarations[i].Property < declarations[j].Property
}

View File

@@ -0,0 +1,230 @@
package css
import (
"fmt"
"strings"
)
const (
indentSpace = 2
)
// RuleKind represents a Rule kind
type RuleKind int
// Rule kinds
const (
QualifiedRule RuleKind = iota
AtRule
)
// At Rules than have Rules inside their block instead of Declarations
var atRulesWithRulesBlock = []string{
"@document", "@font-feature-values", "@keyframes", "@media", "@supports",
}
// Rule represents a parsed CSS rule
type Rule struct {
Kind RuleKind
// At Rule name (eg: "@media")
Name string
// Raw prelude
Prelude string
// Qualified Rule selectors parsed from prelude
Selectors []string
// Style properties
Declarations []*Declaration
// At Rule embedded rules
Rules []*Rule
// Current rule embedding level
EmbedLevel int
}
// NewRule instanciates a new Rule
func NewRule(kind RuleKind) *Rule {
return &Rule{
Kind: kind,
}
}
// Returns string representation of rule kind
func (kind RuleKind) String() string {
switch kind {
case QualifiedRule:
return "Qualified Rule"
case AtRule:
return "At Rule"
default:
return "WAT"
}
}
// EmbedsRules returns true if this rule embeds another rules
func (rule *Rule) EmbedsRules() bool {
if rule.Kind == AtRule {
for _, atRuleName := range atRulesWithRulesBlock {
if rule.Name == atRuleName {
return true
}
}
}
return false
}
// Equal returns true if both rules are equals
func (rule *Rule) Equal(other *Rule) bool {
if (rule.Kind != other.Kind) ||
(rule.Prelude != other.Prelude) ||
(rule.Name != other.Name) {
return false
}
if (len(rule.Selectors) != len(other.Selectors)) ||
(len(rule.Declarations) != len(other.Declarations)) ||
(len(rule.Rules) != len(other.Rules)) {
return false
}
for i, sel := range rule.Selectors {
if sel != other.Selectors[i] {
return false
}
}
for i, decl := range rule.Declarations {
if !decl.Equal(other.Declarations[i]) {
return false
}
}
for i, rule := range rule.Rules {
if !rule.Equal(other.Rules[i]) {
return false
}
}
return true
}
// Diff returns a string representation of rules differences
func (rule *Rule) Diff(other *Rule) []string {
result := []string{}
if rule.Kind != other.Kind {
result = append(result, fmt.Sprintf("Kind: %s | %s", rule.Kind.String(), other.Kind.String()))
}
if rule.Prelude != other.Prelude {
result = append(result, fmt.Sprintf("Prelude: \"%s\" | \"%s\"", rule.Prelude, other.Prelude))
}
if rule.Name != other.Name {
result = append(result, fmt.Sprintf("Name: \"%s\" | \"%s\"", rule.Name, other.Name))
}
if len(rule.Selectors) != len(other.Selectors) {
result = append(result, fmt.Sprintf("Selectors: %v | %v", strings.Join(rule.Selectors, ", "), strings.Join(other.Selectors, ", ")))
} else {
for i, sel := range rule.Selectors {
if sel != other.Selectors[i] {
result = append(result, fmt.Sprintf("Selector: \"%s\" | \"%s\"", sel, other.Selectors[i]))
}
}
}
if len(rule.Declarations) != len(other.Declarations) {
result = append(result, fmt.Sprintf("Declarations Nb: %d | %d", len(rule.Declarations), len(other.Declarations)))
} else {
for i, decl := range rule.Declarations {
if !decl.Equal(other.Declarations[i]) {
result = append(result, fmt.Sprintf("Declaration: \"%s\" | \"%s\"", decl.String(), other.Declarations[i].String()))
}
}
}
if len(rule.Rules) != len(other.Rules) {
result = append(result, fmt.Sprintf("Rules Nb: %d | %d", len(rule.Rules), len(other.Rules)))
} else {
for i, rule := range rule.Rules {
if !rule.Equal(other.Rules[i]) {
result = append(result, fmt.Sprintf("Rule: \"%s\" | \"%s\"", rule.String(), other.Rules[i].String()))
}
}
}
return result
}
// Returns the string representation of a rule
func (rule *Rule) String() string {
result := ""
if rule.Kind == QualifiedRule {
for i, sel := range rule.Selectors {
if i != 0 {
result += ", "
}
result += sel
}
} else {
// AtRule
result += fmt.Sprintf("%s", rule.Name)
if rule.Prelude != "" {
if result != "" {
result += " "
}
result += fmt.Sprintf("%s", rule.Prelude)
}
}
if (len(rule.Declarations) == 0) && (len(rule.Rules) == 0) {
result += ";"
} else {
result += " {\n"
if rule.EmbedsRules() {
for _, subRule := range rule.Rules {
result += fmt.Sprintf("%s%s\n", rule.indent(), subRule.String())
}
} else {
for _, decl := range rule.Declarations {
result += fmt.Sprintf("%s%s\n", rule.indent(), decl.String())
}
}
result += fmt.Sprintf("%s}", rule.indentEndBlock())
}
return result
}
// Returns identation spaces for declarations and rules
func (rule *Rule) indent() string {
result := ""
for i := 0; i < ((rule.EmbedLevel + 1) * indentSpace); i++ {
result += " "
}
return result
}
// Returns identation spaces for end of block character
func (rule *Rule) indentEndBlock() string {
result := ""
for i := 0; i < (rule.EmbedLevel * indentSpace); i++ {
result += " "
}
return result
}

View File

@@ -0,0 +1,25 @@
package css
// Stylesheet represents a parsed stylesheet
type Stylesheet struct {
Rules []*Rule
}
// NewStylesheet instanciate a new Stylesheet
func NewStylesheet() *Stylesheet {
return &Stylesheet{}
}
// Returns string representation of the Stylesheet
func (sheet *Stylesheet) String() string {
result := ""
for _, rule := range sheet.Rules {
if result != "" {
result += "\n"
}
result += rule.String()
}
return result
}

View File

@@ -0,0 +1,409 @@
package parser
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/gorilla/css/scanner"
"github.com/aymerick/douceur/css"
)
const (
importantSuffixRegexp = `(?i)\s*!important\s*$`
)
var (
importantRegexp *regexp.Regexp
)
// Parser represents a CSS parser
type Parser struct {
scan *scanner.Scanner // Tokenizer
// Tokens parsed but not consumed yet
tokens []*scanner.Token
// Rule embedding level
embedLevel int
}
func init() {
importantRegexp = regexp.MustCompile(importantSuffixRegexp)
}
// NewParser instanciates a new parser
func NewParser(txt string) *Parser {
return &Parser{
scan: scanner.New(txt),
}
}
// Parse parses a whole stylesheet
func Parse(text string) (*css.Stylesheet, error) {
result, err := NewParser(text).ParseStylesheet()
if err != nil {
return nil, err
}
return result, nil
}
// ParseDeclarations parses CSS declarations
func ParseDeclarations(text string) ([]*css.Declaration, error) {
result, err := NewParser(text).ParseDeclarations()
if err != nil {
return nil, err
}
return result, nil
}
// ParseStylesheet parses a stylesheet
func (parser *Parser) ParseStylesheet() (*css.Stylesheet, error) {
result := css.NewStylesheet()
// Parse BOM
if _, err := parser.parseBOM(); err != nil {
return result, err
}
// Parse list of rules
rules, err := parser.ParseRules()
if err != nil {
return result, err
}
result.Rules = rules
return result, nil
}
// ParseRules parses a list of rules
func (parser *Parser) ParseRules() ([]*css.Rule, error) {
result := []*css.Rule{}
inBlock := false
if parser.tokenChar("{") {
// parsing a block of rules
inBlock = true
parser.embedLevel++
parser.shiftToken()
}
for parser.tokenParsable() {
if parser.tokenIgnorable() {
parser.shiftToken()
} else if parser.tokenChar("}") {
if !inBlock {
errMsg := fmt.Sprintf("Unexpected } character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
parser.shiftToken()
parser.embedLevel--
// finished
break
} else {
rule, err := parser.ParseRule()
if err != nil {
return result, err
}
rule.EmbedLevel = parser.embedLevel
result = append(result, rule)
}
}
return result, parser.err()
}
// ParseRule parses a rule
func (parser *Parser) ParseRule() (*css.Rule, error) {
if parser.tokenAtKeyword() {
return parser.parseAtRule()
}
return parser.parseQualifiedRule()
}
// ParseDeclarations parses a list of declarations
func (parser *Parser) ParseDeclarations() ([]*css.Declaration, error) {
result := []*css.Declaration{}
if parser.tokenChar("{") {
parser.shiftToken()
}
for parser.tokenParsable() {
if parser.tokenIgnorable() {
parser.shiftToken()
} else if parser.tokenChar("}") {
// end of block
parser.shiftToken()
break
} else {
declaration, err := parser.ParseDeclaration()
if err != nil {
return result, err
}
result = append(result, declaration)
}
}
return result, parser.err()
}
// ParseDeclaration parses a declaration
func (parser *Parser) ParseDeclaration() (*css.Declaration, error) {
result := css.NewDeclaration()
curValue := ""
for parser.tokenParsable() {
if parser.tokenChar(":") {
result.Property = strings.TrimSpace(curValue)
curValue = ""
parser.shiftToken()
} else if parser.tokenChar(";") || parser.tokenChar("}") {
if result.Property == "" {
errMsg := fmt.Sprintf("Unexpected ; character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
if importantRegexp.MatchString(curValue) {
result.Important = true
curValue = importantRegexp.ReplaceAllString(curValue, "")
}
result.Value = strings.TrimSpace(curValue)
if parser.tokenChar(";") {
parser.shiftToken()
}
// finished
break
} else {
token := parser.shiftToken()
curValue += token.Value
}
}
// log.Printf("[parsed] Declaration: %s", result.String())
return result, parser.err()
}
// Parse an At Rule
func (parser *Parser) parseAtRule() (*css.Rule, error) {
// parse rule name (eg: "@import")
token := parser.shiftToken()
result := css.NewRule(css.AtRule)
result.Name = token.Value
for parser.tokenParsable() {
if parser.tokenChar(";") {
parser.shiftToken()
// finished
break
} else if parser.tokenChar("{") {
if result.EmbedsRules() {
// parse rules block
rules, err := parser.ParseRules()
if err != nil {
return result, err
}
result.Rules = rules
} else {
// parse declarations block
declarations, err := parser.ParseDeclarations()
if err != nil {
return result, err
}
result.Declarations = declarations
}
// finished
break
} else {
// parse prelude
prelude, err := parser.parsePrelude()
if err != nil {
return result, err
}
result.Prelude = prelude
}
}
// log.Printf("[parsed] Rule: %s", result.String())
return result, parser.err()
}
// Parse a Qualified Rule
func (parser *Parser) parseQualifiedRule() (*css.Rule, error) {
result := css.NewRule(css.QualifiedRule)
for parser.tokenParsable() {
if parser.tokenChar("{") {
if result.Prelude == "" {
errMsg := fmt.Sprintf("Unexpected { character: %s", parser.nextToken().String())
return result, errors.New(errMsg)
}
// parse declarations block
declarations, err := parser.ParseDeclarations()
if err != nil {
return result, err
}
result.Declarations = declarations
// finished
break
} else {
// parse prelude
prelude, err := parser.parsePrelude()
if err != nil {
return result, err
}
result.Prelude = prelude
}
}
result.Selectors = strings.Split(result.Prelude, ",")
for i, sel := range result.Selectors {
result.Selectors[i] = strings.TrimSpace(sel)
}
// log.Printf("[parsed] Rule: %s", result.String())
return result, parser.err()
}
// Parse Rule prelude
func (parser *Parser) parsePrelude() (string, error) {
result := ""
for parser.tokenParsable() && !parser.tokenEndOfPrelude() {
token := parser.shiftToken()
result += token.Value
}
result = strings.TrimSpace(result)
// log.Printf("[parsed] prelude: %s", result)
return result, parser.err()
}
// Parse BOM
func (parser *Parser) parseBOM() (bool, error) {
if parser.nextToken().Type == scanner.TokenBOM {
parser.shiftToken()
return true, nil
}
return false, parser.err()
}
// Returns next token without removing it from tokens buffer
func (parser *Parser) nextToken() *scanner.Token {
if len(parser.tokens) == 0 {
// fetch next token
nextToken := parser.scan.Next()
// log.Printf("[token] %s => %v", nextToken.Type.String(), nextToken.Value)
// queue it
parser.tokens = append(parser.tokens, nextToken)
}
return parser.tokens[0]
}
// Returns next token and remove it from the tokens buffer
func (parser *Parser) shiftToken() *scanner.Token {
var result *scanner.Token
result, parser.tokens = parser.tokens[0], parser.tokens[1:]
return result
}
// Returns tokenizer error, or nil if no error
func (parser *Parser) err() error {
if parser.tokenError() {
token := parser.nextToken()
return fmt.Errorf("Tokenizer error: %s", token.String())
}
return nil
}
// Returns true if next token is Error
func (parser *Parser) tokenError() bool {
return parser.nextToken().Type == scanner.TokenError
}
// Returns true if next token is EOF
func (parser *Parser) tokenEOF() bool {
return parser.nextToken().Type == scanner.TokenEOF
}
// Returns true if next token is a whitespace
func (parser *Parser) tokenWS() bool {
return parser.nextToken().Type == scanner.TokenS
}
// Returns true if next token is a comment
func (parser *Parser) tokenComment() bool {
return parser.nextToken().Type == scanner.TokenComment
}
// Returns true if next token is a CDO or a CDC
func (parser *Parser) tokenCDOorCDC() bool {
switch parser.nextToken().Type {
case scanner.TokenCDO, scanner.TokenCDC:
return true
default:
return false
}
}
// Returns true if next token is ignorable
func (parser *Parser) tokenIgnorable() bool {
return parser.tokenWS() || parser.tokenComment() || parser.tokenCDOorCDC()
}
// Returns true if next token is parsable
func (parser *Parser) tokenParsable() bool {
return !parser.tokenEOF() && !parser.tokenError()
}
// Returns true if next token is an At Rule keyword
func (parser *Parser) tokenAtKeyword() bool {
return parser.nextToken().Type == scanner.TokenAtKeyword
}
// Returns true if next token is given character
func (parser *Parser) tokenChar(value string) bool {
token := parser.nextToken()
return (token.Type == scanner.TokenChar) && (token.Value == value)
}
// Returns true if next token marks the end of a prelude
func (parser *Parser) tokenEndOfPrelude() bool {
return parser.tokenChar(";") || parser.tokenChar("{")
}

View File

@@ -0,0 +1,117 @@
package types
import (
"database/sql/driver"
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/sql"
"time"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Attachment struct {
ID uint64 `json:"attachmentID,string"`
OwnerID uint64 `json:"ownerID,string"`
Kind string `json:"-"`
Url string `json:"url,omitempty"`
PreviewUrl string `json:"previewUrl,omitempty"`
Name string `json:"name,omitempty"`
Meta AttachmentMeta `json:"meta"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
// AttachmentFilter is used for filtering and as a return value from Find
AttachmentFilter struct {
NamespaceID uint64 `json:"namespaceID,string"`
Kind string `json:"kind,omitempty"`
PageID uint64 `json:"pageID,string,omitempty"`
RecordID uint64 `json:"recordID,string,omitempty"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
FieldName string `json:"fieldName,omitempty"`
Filter string `json:"filter"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Attachment) (bool, error)
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
AttachmentImageMeta struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Animated bool `json:"animated"`
}
AttachmentFileMeta struct {
Size int64 `json:"size"`
Extension string `json:"ext"`
Mimetype string `json:"mimetype"`
Image *AttachmentImageMeta `json:"image,omitempty"`
}
AttachmentIconMeta struct {
Name string `json:"name"`
Library string `json:"library"`
}
AttachmentIconSvgMeta struct {
Src string `json:"src"`
}
AttachmentMeta struct {
Original AttachmentFileMeta `json:"original"`
Preview *AttachmentFileMeta `json:"preview,omitempty"`
Icon *AttachmentIconMeta `json:"icon,omitempty"`
IconSvg *AttachmentIconSvgMeta `json:"iconSvg,omitempty"`
}
)
const (
PageAttachment string = "page"
IconAttachment string = "icon"
RecordAttachment string = "record"
NamespaceAttachment string = "namespace"
)
func (a *Attachment) SetOriginalImageMeta(width, height int, animated bool) *AttachmentFileMeta {
a.imageMeta(&a.Meta.Original, width, height, animated)
return &a.Meta.Original
}
func (a *Attachment) SetPreviewImageMeta(width, height int, animated bool) *AttachmentFileMeta {
if a.Meta.Preview == nil {
a.Meta.Preview = &AttachmentFileMeta{}
}
a.imageMeta(a.Meta.Preview, width, height, animated)
return a.Meta.Preview
}
func (a *Attachment) imageMeta(in *AttachmentFileMeta, width, height int, animated bool) {
if in.Image == nil {
in.Image = &AttachmentImageMeta{}
}
if width > 0 && height > 0 {
in.Image.Animated = animated
in.Image.Width = width
in.Image.Height = height
}
}
func (meta *AttachmentMeta) Scan(src any) error { return sql.ParseJSON(src, meta) }
func (meta AttachmentMeta) Value() (driver.Value, error) { return json.Marshal(meta) }

View File

@@ -0,0 +1,312 @@
package types
import (
"database/sql/driver"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/spf13/cast"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Chart struct {
ID uint64 `json:"chartID,string"`
Handle string `json:"handle"`
Name string `json:"name"`
Config ChartConfig `json:"config"`
Labels map[string]string `json:"labels,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
ChartConfig struct {
Reports []*ChartConfigReport `json:"reports,omitempty"`
ColorScheme string `json:"colorScheme,omitempty"`
NoAnimation bool `json:"noAnimation,omitempty"`
Toolbox map[string]interface{} `json:"toolbox,omitempty"`
}
ChartConfigReport struct {
ReportID uint64 `json:"reportID,string,omitempty"`
Filter string `json:"filter"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
Metrics []map[string]interface{} `json:"metrics,omitempty"`
Dimensions []map[string]interface{} `json:"dimensions,omitempty"`
YAxis map[string]interface{} `json:"yAxis,omitempty"`
Legend map[string]interface{} `json:"legend,omitempty"`
Tooltip map[string]interface{} `json:"tooltip,omitempty"`
Offset map[string]interface{} `json:"offset,omitempty"`
Renderer struct {
Version string `json:"version,omitempty" `
} `json:"renderer,omitempty"`
}
ChartFilter struct {
NamespaceID uint64 `json:"namespaceID,string"`
ChartID []string `json:"chartID"`
Handle string `json:"handle"`
Name string `json:"name"`
Query string `json:"query"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Chart) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
func (c Chart) decodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
for i, report := range c.Config.Reports {
if report == nil {
continue
}
// apply translated label for YAxis
if aux = tt.FindByKey(LocaleKeyChartYAxisLabel.Path); aux != nil {
if c.Config.Reports[i].YAxis == nil {
c.Config.Reports[i].YAxis = make(map[string]interface{})
}
c.Config.Reports[i].YAxis["label"] = aux.Msg
}
// apply translated labels for metrics
report.WalkMetrics(func(metricID string, metric map[string]interface{}) {
mpl := strings.NewReplacer("{{metricID}}", metricID)
aux = tt.FindByKey(mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path))
if aux == nil {
return
}
metric["label"] = aux.Msg
})
// apply translated labels for each dimension/step
report.WalkDimensionSteps(func(dimensionID, stepID string, step map[string]interface{}) {
mpl := strings.NewReplacer(
"{{dimensionID}}", dimensionID,
"{{stepID}}", stepID,
)
aux = tt.FindByKey(mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path))
if aux == nil {
return
}
step["label"] = aux.Msg
})
}
}
func (c Chart) encodeTranslations() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 12)
for _, report := range c.Config.Reports {
// collect labels from chart config: YAxis
if _, ok := report.YAxis["label"]; ok {
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: LocaleKeyChartYAxisLabel.Path,
Msg: cast.ToString(report.YAxis["label"]),
})
}
// collect labels from chart config: metrics
report.WalkMetrics(func(metricID string, m map[string]interface{}) {
mpl := strings.NewReplacer(
"{{metricID}}", metricID,
)
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: mpl.Replace(LocaleKeyChartMetricsMetricIDLabel.Path),
Msg: cast.ToString(m["label"]),
})
})
// collect labels from chart config: dimensions/steps
report.WalkDimensionSteps(func(dimID, stepID string, step map[string]interface{}) {
mpl := strings.NewReplacer(
"{{dimensionID}}", dimID,
"{{stepID}}", stepID,
)
out = append(out, &locale.ResourceTranslation{
Resource: c.ResourceTranslation(),
Key: mpl.Replace(LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel.Path),
Msg: cast.ToString(step["label"]),
})
})
}
return
}
func (c *Chart) setValue(name string, pos uint, value any) (err error) {
pp := strings.Split(name, ".")
switch pp[0] {
case "Config":
if pp[1] == "Reports" {
return c.Config.Reports[cast.ToInt(pp[2])].setValue(pp[3], 0, value)
}
}
return
}
func (r *ChartConfigReport) setValue(name string, pos uint, value any) (err error) {
switch name {
case "moduleID", "ModuleID":
r.ModuleID = cast.ToUint64(value)
}
return
}
// FindByHandle finds chart by it's handle
func (set ChartSet) FindByHandle(handle string) *Chart {
for i := range set {
if set[i].Handle == handle {
return set[i]
}
}
return nil
}
func (cc *ChartConfig) Scan(src any) error { return sql.ParseJSON(src, cc) }
func (cc ChartConfig) Value() (driver.Value, error) { return json.Marshal(cc) }
func (r *ChartConfigReport) WalkMetrics(fn func(string, map[string]interface{})) {
for m := range r.Metrics {
metricID, ok := r.Metrics[m]["metricID"]
if !ok {
continue
}
if len(r.Metrics[m]) == 0 {
// avoid problems with nil maps
r.Metrics[m] = make(map[string]interface{})
}
fn(metricID.(string), r.Metrics[m])
}
}
func (r *ChartConfigReport) WalkDimensionSteps(fn func(string, string, map[string]interface{})) {
for d := range r.Dimensions {
dimensionID, ok := r.Dimensions[d]["dimensionID"]
if !ok {
continue
}
meta, is := r.Dimensions[d]["meta"].(map[string]interface{})
if !is {
continue
}
var steps []map[string]interface{}
switch aux := meta["steps"].(type) {
case []interface{}:
for _, i := range aux {
if kv, is := i.(map[string]interface{}); is {
steps = append(steps, kv)
}
}
case []map[string]interface{}:
steps = aux
}
for s := range steps {
stepID, has := steps[s]["stepID"]
if !has {
return
}
fn(dimensionID.(string), stepID.(string), steps[s])
}
}
}
func (c *ChartConfig) GenerateIDs(nextID func() uint64) {
// Ensure chart report IDs
for r := range c.Reports {
c.Reports[r].ReportID = nextID()
// Ensure chart report metric IDs
for m := range c.Reports[r].Metrics {
met := c.Reports[r].Metrics[m]
if _, has := met["metricID"]; has {
continue
}
met["metricID"] = strconv.FormatUint(nextID(), 10)
}
for d := range c.Reports[r].Dimensions {
dim := c.Reports[r].Dimensions[d]
if _, has := dim["dimensionID"]; !has {
dim["dimensionID"] = strconv.FormatUint(nextID(), 10)
}
meta, is := dim["meta"].(map[string]interface{})
if !is {
// no meta, no steps
continue
}
var steps []map[string]interface{}
switch aux := meta["steps"].(type) {
case []interface{}:
for _, i := range aux {
if kv, is := i.(map[string]interface{}); is {
steps = append(steps, kv)
}
}
case []map[string]interface{}:
steps = aux
}
for s := range steps {
_, has := steps[s]["stepID"]
if has {
continue
}
steps[s]["stepID"] = strconv.FormatUint(nextID(), 10)
}
meta["steps"] = steps
dim["meta"] = meta
}
}
}

View File

@@ -0,0 +1,38 @@
package types
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
)
type (
PrivacyModule struct {
Module PrivacyModuleMeta `json:"module"`
Namespace PrivacyNamespaceMeta `json:"namespace"`
ConnectionID uint64 `json:"-"`
Connection *sysTypes.DalConnection `json:"connection"`
}
PrivacyModuleMeta struct {
ID uint64 `json:"moduleID,string"`
Name string `json:"name"`
Handle string `json:"handle"`
Fields ModuleFieldSet `json:"fields"`
}
PrivacyNamespaceMeta struct {
ID uint64 `json:"namespaceID,string"`
Slug string `json:"slug"`
Name string `json:"name"`
}
PrivacyModuleFilter struct {
NamespaceID uint64 `json:"-"`
ConnectionID []string `json:"connectionID"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)

View File

@@ -0,0 +1,528 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"github.com/cortezaproject/corteza/server/pkg/cast2"
)
func (r Attachment) GetID() uint64 { return r.ID }
func (r *Attachment) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "id", "ID":
return r.ID, nil
case "kind", "Kind":
return r.Kind, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownerID", "OwnerID":
return r.OwnerID, nil
case "previewUrl", "PreviewUrl":
return r.PreviewUrl, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "url", "Url":
return r.Url, nil
}
return nil, nil
}
func (r *Attachment) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Attachment{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "kind", "Kind":
return cast2.String(value, &r.Kind)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownerID", "OwnerID":
return cast2.Uint64(value, &r.OwnerID)
case "previewUrl", "PreviewUrl":
return cast2.String(value, &r.PreviewUrl)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "url", "Url":
return cast2.String(value, &r.Url)
}
return nil
}
func (r Chart) GetID() uint64 { return r.ID }
func (r *Chart) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Chart) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Chart{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Module) GetID() uint64 { return r.ID }
func (r *Module) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Module) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Module{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
}
return nil
}
func (r ModuleField) GetID() uint64 { return r.ID }
func (r *ModuleField) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "id", "ID":
return r.ID, nil
case "kind", "Kind":
return r.Kind, nil
case "label", "Label":
return r.Label, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "multi", "Multi":
return r.Multi, nil
case "name", "Name":
return r.Name, nil
case "place", "Place":
return r.Place, nil
case "required", "Required":
return r.Required, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *ModuleField) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &ModuleField{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "kind", "Kind":
return cast2.String(value, &r.Kind)
case "label", "Label":
return cast2.String(value, &r.Label)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "multi", "Multi":
return cast2.Bool(value, &r.Multi)
case "name", "Name":
return cast2.String(value, &r.Name)
case "place", "Place":
return cast2.Int(value, &r.Place)
case "required", "Required":
return cast2.Bool(value, &r.Required)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Namespace) GetID() uint64 { return r.ID }
func (r *Namespace) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "enabled", "Enabled":
return r.Enabled, nil
case "id", "ID":
return r.ID, nil
case "name", "Name":
return r.Name, nil
case "slug", "Slug":
return r.Slug, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
}
return nil, nil
}
func (r *Namespace) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Namespace{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "enabled", "Enabled":
return cast2.Bool(value, &r.Enabled)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "name", "Name":
return cast2.String(value, &r.Name)
case "slug", "Slug":
return cast2.String(value, &r.Slug)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
}
return nil
}
func (r Page) GetID() uint64 { return r.ID }
func (r *Page) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "description", "Description":
return r.Description, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "selfID", "SelfID":
return r.SelfID, nil
case "title", "Title":
return r.Title, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "visible", "Visible":
return r.Visible, nil
case "weight", "Weight":
return r.Weight, nil
}
return nil, nil
}
func (r *Page) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Page{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "description", "Description":
return cast2.String(value, &r.Description)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "selfID", "SelfID":
return cast2.Uint64(value, &r.SelfID)
case "title", "Title":
return cast2.String(value, &r.Title)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "visible", "Visible":
return cast2.Bool(value, &r.Visible)
case "weight", "Weight":
return cast2.Int(value, &r.Weight)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r PageLayout) GetID() uint64 { return r.ID }
func (r *PageLayout) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "handle", "Handle":
return r.Handle, nil
case "id", "ID":
return r.ID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownedBy", "OwnedBy":
return r.OwnedBy, nil
case "pageID", "PageID":
return r.PageID, nil
case "parentID", "ParentID":
return r.ParentID, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "weight", "Weight":
return r.Weight, nil
default:
return r.getValue(name, pos)
}
return nil, nil
}
func (r *PageLayout) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &PageLayout{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "handle", "Handle":
return cast2.String(value, &r.Handle)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownedBy", "OwnedBy":
return cast2.Uint64(value, &r.OwnedBy)
case "pageID", "PageID":
return cast2.Uint64(value, &r.PageID)
case "parentID", "ParentID":
return cast2.Uint64(value, &r.ParentID)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "weight", "Weight":
return cast2.Int(value, &r.Weight)
default:
return r.setValue(name, pos, value)
}
return nil
}
func (r Record) GetID() uint64 { return r.ID }
func (r *Record) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return r.CreatedAt, nil
case "createdBy", "CreatedBy", "created_by":
return r.CreatedBy, nil
case "deletedAt", "DeletedAt":
return r.DeletedAt, nil
case "deletedBy", "DeletedBy", "deleted_by":
return r.DeletedBy, nil
case "id", "ID":
return r.ID, nil
case "meta", "Meta":
return r.Meta, nil
case "moduleID", "ModuleID":
return r.ModuleID, nil
case "namespaceID", "NamespaceID":
return r.NamespaceID, nil
case "ownedBy", "OwnedBy", "owned_by":
return r.OwnedBy, nil
case "revision", "Revision":
return r.Revision, nil
case "updatedAt", "UpdatedAt":
return r.UpdatedAt, nil
case "updatedBy", "UpdatedBy", "updated_by":
return r.UpdatedBy, nil
default:
return r.getValue(name, pos)
}
return nil, nil
}
func (r *Record) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &Record{}
}
switch name {
case "createdAt", "CreatedAt":
return cast2.Time(value, &r.CreatedAt)
case "createdBy", "CreatedBy", "created_by":
return cast2.Uint64(value, &r.CreatedBy)
case "deletedAt", "DeletedAt":
return cast2.TimePtr(value, &r.DeletedAt)
case "deletedBy", "DeletedBy", "deleted_by":
return cast2.Uint64(value, &r.DeletedBy)
case "id", "ID":
return cast2.Uint64(value, &r.ID)
case "meta", "Meta":
return cast2.Meta(value, &r.Meta)
case "moduleID", "ModuleID":
return cast2.Uint64(value, &r.ModuleID)
case "namespaceID", "NamespaceID":
return cast2.Uint64(value, &r.NamespaceID)
case "ownedBy", "OwnedBy", "owned_by":
return cast2.Uint64(value, &r.OwnedBy)
case "revision", "Revision":
return cast2.Int(value, &r.Revision)
case "updatedAt", "UpdatedAt":
return cast2.TimePtr(value, &r.UpdatedAt)
case "updatedBy", "UpdatedBy", "updated_by":
return cast2.Uint64(value, &r.UpdatedBy)
default:
return r.setValue(name, pos, value)
}
return nil
}

View File

@@ -0,0 +1,39 @@
package types
func (r *Record) GetValues(name string) ([]any, error) {
if r == nil {
return nil, nil
}
switch name {
case "createdAt", "CreatedAt":
return []any{r.CreatedAt}, nil
case "createdBy", "CreatedBy", "created_by":
return []any{r.CreatedBy}, nil
case "deletedAt", "DeletedAt":
return []any{r.DeletedAt}, nil
case "deletedBy", "DeletedBy", "deleted_by":
return []any{r.DeletedBy}, nil
case "id", "ID":
return []any{r.ID}, nil
case "meta", "Meta":
return []any{r.Meta}, nil
case "moduleID", "ModuleID":
return []any{r.ModuleID}, nil
case "namespaceID", "NamespaceID":
return []any{r.NamespaceID}, nil
case "ownedBy", "OwnedBy", "owned_by":
return []any{r.OwnedBy}, nil
case "revision", "Revision":
return []any{r.Revision}, nil
case "updatedAt", "UpdatedAt":
return []any{r.UpdatedAt}, nil
case "updatedBy", "UpdatedBy", "updated_by":
return []any{r.UpdatedBy}, nil
default:
return r.getValues(name)
}
return nil, nil
}

View File

@@ -0,0 +1,44 @@
package types
import (
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
Icon struct {
}
IconFilter struct {
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(icon *Icon) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
IconType string
)
const (
// IconTypeLink empty or "link" (default):
// Indicate that src will contain an absolute or relative link to an icon.
// Can also be used for inline images (storing "base64:" prefixed string in source).
// This type and reference is not validated by the backend.
IconTypeLink IconType = "link" // type, source
// IconTypeLibrary "library"
// Source references an icon from a library. Ref's value should be in the following
// notation: "font-awesome://<icon-identifier>".
// This type and source is not validated by the backend.
IconTypeLibrary IconType = "library" // type, library(font-awesome), icon name
// IconTypeInlineSvg "svg"
// SRC contains raw SVG document
IconTypeInlineSvg IconType = "inline-svg" // source
// IconTypeAttachment "attachment"
// Reference (ID) to an existing attachment in local Corteza instance is expected
// This type and reference must be validated by the backend.
IconTypeAttachment IconType = "attachment" // type, Icon, name
)

View File

@@ -0,0 +1,427 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/locale"
"strconv"
)
type (
LocaleKey struct {
Name string
Resource string
Path string
CustomHandler string
}
)
// Types and stuff
const (
ChartResourceTranslationType = "compose:chart"
ModuleResourceTranslationType = "compose:module"
ModuleFieldResourceTranslationType = "compose:module-field"
NamespaceResourceTranslationType = "compose:namespace"
PageResourceTranslationType = "compose:page"
PageLayoutResourceTranslationType = "compose:page-layout"
)
var (
// @todo can we remove LocaleKey struct for string constant?
LocaleKeyChartYAxisLabel = LocaleKey{Path: "yAxis.label"}
LocaleKeyChartMetricsMetricIDLabel = LocaleKey{Path: "metrics.{{metricID}}.label"}
LocaleKeyChartDimensionsDimensionIDMetaStepsStepIDLabel = LocaleKey{Path: "dimensions.{{dimensionID}}.meta.steps.{{stepID}}.label"}
LocaleKeyModuleName = LocaleKey{Path: "name"}
LocaleKeyModuleFieldLabel = LocaleKey{Path: "label"}
LocaleKeyModuleFieldMetaDescriptionView = LocaleKey{Path: "meta.description.view"}
LocaleKeyModuleFieldMetaDescriptionEdit = LocaleKey{Path: "meta.description.edit"}
LocaleKeyModuleFieldMetaHintView = LocaleKey{Path: "meta.hint.view"}
LocaleKeyModuleFieldMetaHintEdit = LocaleKey{Path: "meta.hint.edit"}
LocaleKeyModuleFieldExpressionValidatorValidatorIDError = LocaleKey{Path: "expression.validator.{{validatorID}}.error"}
LocaleKeyModuleFieldMetaOptionsValueText = LocaleKey{Path: "meta.options.{{value}}.text"}
LocaleKeyModuleFieldMetaBoolValueLabel = LocaleKey{Path: "meta.bool.{{value}}.label"}
LocaleKeyNamespaceName = LocaleKey{Path: "name"}
LocaleKeyNamespaceMetaSubtitle = LocaleKey{Path: "meta.subtitle"}
LocaleKeyNamespaceMetaDescription = LocaleKey{Path: "meta.description"}
LocaleKeyPageTitle = LocaleKey{Path: "title"}
LocaleKeyPageDescription = LocaleKey{Path: "description"}
LocaleKeyPagePageBlockBlockIDTitle = LocaleKey{Path: "pageBlock.{{blockID}}.title"}
LocaleKeyPagePageBlockBlockIDDescription = LocaleKey{Path: "pageBlock.{{blockID}}.description"}
LocaleKeyPagePageBlockBlockIDButtonButtonIDLabel = LocaleKey{Path: "pageBlock.{{blockID}}.button.{{buttonID}}.label"}
LocaleKeyPagePageBlockBlockIDContentBody = LocaleKey{Path: "pageBlock.{{blockID}}.content.body"}
LocaleKeyPageLayoutMetaTitle = LocaleKey{Path: "meta.title"}
LocaleKeyPageLayoutMetaDescription = LocaleKey{Path: "meta.description"}
LocaleKeyPageLayoutConfigButtonsNewLabel = LocaleKey{Path: "config.buttons.new.label"}
LocaleKeyPageLayoutConfigButtonsEditLabel = LocaleKey{Path: "config.buttons.edit.label"}
LocaleKeyPageLayoutConfigButtonsSubmitLabel = LocaleKey{Path: "config.buttons.submit.label"}
LocaleKeyPageLayoutConfigButtonsDeleteLabel = LocaleKey{Path: "config.buttons.delete.label"}
LocaleKeyPageLayoutConfigButtonsCloneLabel = LocaleKey{Path: "config.buttons.clone.label"}
LocaleKeyPageLayoutConfigButtonsBackLabel = LocaleKey{Path: "config.buttons.back.label"}
LocaleKeyPageLayoutConfigActionsActionIDMetaLabel = LocaleKey{Path: "config.actions.{{actionID}}.meta.label"}
)
// ResourceTranslation returns string representation of Locale resource for Chart by calling ChartResourceTranslation fn
//
// Locale resource is in "compose:chart/..." format
//
// This function is auto-generated
func (r Chart) ResourceTranslation() string {
return ChartResourceTranslation(r.NamespaceID, r.ID)
}
// ChartResourceTranslation returns string representation of Locale resource for Chart
//
// Locale resource is in the compose:chart/... format
//
// This function is auto-generated
func ChartResourceTranslation(NamespaceID uint64, ID uint64) string {
cpts := []interface{}{
ChartResourceTranslationType,
strconv.FormatUint(NamespaceID, 10),
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(ChartResourceTranslationTpl(), cpts...)
}
func ChartResourceTranslationTpl() string {
return "%s/%s/%s"
}
func (r *Chart) DecodeTranslations(tt locale.ResourceTranslationIndex) {
r.decodeTranslations(tt)
}
func (r *Chart) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, r.encodeTranslations()...)
return out
}
// ResourceTranslation returns string representation of Locale resource for Module by calling ModuleResourceTranslation fn
//
// Locale resource is in "compose:module/..." format
//
// This function is auto-generated
func (r Module) ResourceTranslation() string {
return ModuleResourceTranslation(r.NamespaceID, r.ID)
}
// ModuleResourceTranslation returns string representation of Locale resource for Module
//
// Locale resource is in the compose:module/... format
//
// This function is auto-generated
func ModuleResourceTranslation(NamespaceID uint64, ID uint64) string {
cpts := []interface{}{
ModuleResourceTranslationType,
strconv.FormatUint(NamespaceID, 10),
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(ModuleResourceTranslationTpl(), cpts...)
}
func ModuleResourceTranslationTpl() string {
return "%s/%s/%s"
}
func (r *Module) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleName.Path); aux != nil {
r.Name = aux.Msg
}
r.decodeTranslations(tt)
}
func (r *Module) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyModuleName.Path,
Msg: locale.SanitizeMessage(r.Name),
})
out = append(out, r.encodeTranslations()...)
return out
}
// ResourceTranslation returns string representation of Locale resource for ModuleField by calling ModuleFieldResourceTranslation fn
//
// Locale resource is in "compose:module-field/..." format
//
// This function is auto-generated
func (r ModuleField) ResourceTranslation() string {
return ModuleFieldResourceTranslation(r.NamespaceID, r.ModuleID, r.ID)
}
// ModuleFieldResourceTranslation returns string representation of Locale resource for ModuleField
//
// Locale resource is in the compose:module-field/... format
//
// This function is auto-generated
func ModuleFieldResourceTranslation(NamespaceID uint64, ModuleID uint64, ID uint64) string {
cpts := []interface{}{
ModuleFieldResourceTranslationType,
strconv.FormatUint(NamespaceID, 10),
strconv.FormatUint(ModuleID, 10),
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(ModuleFieldResourceTranslationTpl(), cpts...)
}
func ModuleFieldResourceTranslationTpl() string {
return "%s/%s/%s/%s"
}
func (r *ModuleField) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleFieldLabel.Path); aux != nil {
r.Label = aux.Msg
}
r.decodeTranslationsMetaDescriptionView(tt)
r.decodeTranslationsMetaDescriptionEdit(tt)
r.decodeTranslationsMetaHintView(tt)
r.decodeTranslationsMetaHintEdit(tt)
r.decodeTranslationsExpressionValidatorValidatorIDError(tt)
r.decodeTranslationsMetaOptionsValueText(tt)
r.decodeTranslationsMetaBoolValueLabel(tt)
}
func (r *ModuleField) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyModuleFieldLabel.Path,
Msg: locale.SanitizeMessage(r.Label),
})
out = append(out, r.encodeTranslationsMetaDescriptionView()...)
out = append(out, r.encodeTranslationsMetaDescriptionEdit()...)
out = append(out, r.encodeTranslationsMetaHintView()...)
out = append(out, r.encodeTranslationsMetaHintEdit()...)
out = append(out, r.encodeTranslationsExpressionValidatorValidatorIDError()...)
out = append(out, r.encodeTranslationsMetaOptionsValueText()...)
out = append(out, r.encodeTranslationsMetaBoolValueLabel()...)
return out
}
// ResourceTranslation returns string representation of Locale resource for Namespace by calling NamespaceResourceTranslation fn
//
// Locale resource is in "compose:namespace/..." format
//
// This function is auto-generated
func (r Namespace) ResourceTranslation() string {
return NamespaceResourceTranslation(r.ID)
}
// NamespaceResourceTranslation returns string representation of Locale resource for Namespace
//
// Locale resource is in the compose:namespace/... format
//
// This function is auto-generated
func NamespaceResourceTranslation(ID uint64) string {
cpts := []interface{}{
NamespaceResourceTranslationType,
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(NamespaceResourceTranslationTpl(), cpts...)
}
func NamespaceResourceTranslationTpl() string {
return "%s/%s"
}
func (r *Namespace) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyNamespaceName.Path); aux != nil {
r.Name = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyNamespaceMetaSubtitle.Path); aux != nil {
r.Meta.Subtitle = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyNamespaceMetaDescription.Path); aux != nil {
r.Meta.Description = aux.Msg
}
}
func (r *Namespace) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyNamespaceName.Path,
Msg: locale.SanitizeMessage(r.Name),
})
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyNamespaceMetaSubtitle.Path,
Msg: locale.SanitizeMessage(r.Meta.Subtitle),
})
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyNamespaceMetaDescription.Path,
Msg: locale.SanitizeMessage(r.Meta.Description),
})
return out
}
// ResourceTranslation returns string representation of Locale resource for Page by calling PageResourceTranslation fn
//
// Locale resource is in "compose:page/..." format
//
// This function is auto-generated
func (r Page) ResourceTranslation() string {
return PageResourceTranslation(r.NamespaceID, r.ID)
}
// PageResourceTranslation returns string representation of Locale resource for Page
//
// Locale resource is in the compose:page/... format
//
// This function is auto-generated
func PageResourceTranslation(NamespaceID uint64, ID uint64) string {
cpts := []interface{}{
PageResourceTranslationType,
strconv.FormatUint(NamespaceID, 10),
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(PageResourceTranslationTpl(), cpts...)
}
func PageResourceTranslationTpl() string {
return "%s/%s/%s"
}
func (r *Page) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyPageTitle.Path); aux != nil {
r.Title = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageDescription.Path); aux != nil {
r.Description = aux.Msg
}
r.decodeTranslations(tt)
}
func (r *Page) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyPageTitle.Path,
Msg: locale.SanitizeMessage(r.Title),
})
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyPageDescription.Path,
Msg: locale.SanitizeMessage(r.Description),
})
out = append(out, r.encodeTranslations()...)
return out
}
// ResourceTranslation returns string representation of Locale resource for PageLayout by calling PageLayoutResourceTranslation fn
//
// Locale resource is in "compose:page-layout/..." format
//
// This function is auto-generated
func (r PageLayout) ResourceTranslation() string {
return PageLayoutResourceTranslation(r.NamespaceID, r.PageID, r.ID)
}
// PageLayoutResourceTranslation returns string representation of Locale resource for PageLayout
//
// Locale resource is in the compose:page-layout/... format
//
// This function is auto-generated
func PageLayoutResourceTranslation(NamespaceID uint64, PageID uint64, ID uint64) string {
cpts := []interface{}{
PageLayoutResourceTranslationType,
strconv.FormatUint(NamespaceID, 10),
strconv.FormatUint(PageID, 10),
strconv.FormatUint(ID, 10),
}
return fmt.Sprintf(PageLayoutResourceTranslationTpl(), cpts...)
}
func PageLayoutResourceTranslationTpl() string {
return "%s/%s/%s/%s"
}
func (r *PageLayout) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyPageLayoutMetaTitle.Path); aux != nil {
r.Meta.Title = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutMetaDescription.Path); aux != nil {
r.Meta.Description = aux.Msg
}
r.decodeTranslations(tt)
}
func (r *PageLayout) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyPageLayoutMetaTitle.Path,
Msg: locale.SanitizeMessage(r.Meta.Title),
})
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: LocaleKeyPageLayoutMetaDescription.Path,
Msg: locale.SanitizeMessage(r.Meta.Description),
})
out = append(out, r.encodeTranslations()...)
return out
}

View File

@@ -0,0 +1,191 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/jmoiron/sqlx/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
)
type (
Module struct {
ID uint64 `json:"moduleID,string"`
Handle string `json:"handle"`
// collection of configurations for various subsystems that
// use this module and how it affects their behaviour
Config ModuleConfig `json:"config"`
// @todo should be removed and placed into a separate subsystem
// mostly because we want to allow client apps to store
// application configs away from the module config
// using separate access-control
Meta types.JSONText `json:"meta"`
Fields ModuleFieldSet `json:"fields"`
Labels map[string]string `json:"labels,omitempty"`
Issues []dal.Issue `json:"issues,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Name string `json:"name"`
}
ModuleConfig struct {
// How and where the records of this module are stored in the database
DAL ModuleConfigDAL `json:"dal"`
// Record data privacy settings
Privacy ModuleConfigDataPrivacy `json:"privacy"`
Discovery ModuleConfigDiscovery `json:"discovery"`
RecordRevisions ModuleConfigRecordRevisions `json:"recordRevisions"`
// RecordDeDup value duplicate detection settings
RecordDeDup ModuleConfigRecordDeDup `json:"recordDeDup"`
}
ModuleConfigDAL struct {
ConnectionID uint64 `json:"connectionID,string"`
Constraints map[string][]any `json:"constraints"`
// model identifier (table, collection on the database)
// can contain {{placeholders}}
Ident string `json:"ident"`
SystemFieldEncoding SystemFieldEncoding `json:"systemFieldEncoding"`
}
ModuleConfigRecordRevisions struct {
// enable or disable revisions
Enabled bool `json:"enabled"`
// where are record revisions stored
Ident string `json:"ident"`
}
ModuleConfigDataPrivacy struct {
// Define the highest sensitivity level which
// can be configured on the module fields
SensitivityLevelID uint64 `json:"sensitivityLevelID,string,omitempty"`
UsageDisclosure string `json:"usageDisclosure"`
}
ModuleConfigRecordDeDup struct {
// strictly restrict record saving
// otherwise show a warning with list of duplicated records
Strict bool `json:"-"`
// list of duplicate detection rules applied to module's fields
Rules DeDupRuleSet `json:"rules,omitempty"`
}
ModuleConfigDiscovery struct {
Public DiscoveryResult `json:"public"`
Private DiscoveryResult `json:"private"`
Protected DiscoveryResult `json:"protected"`
}
DiscoveryResult struct {
Result []struct {
Lang string `json:"lang"`
Fields []string `json:"fields"`
} `json:"result"`
}
ModuleFilter struct {
ModuleID []string `json:"moduleID"`
NamespaceID uint64 `json:"namespaceID,string"`
Query string `json:"query"`
Handle string `json:"handle"`
Name string `json:"name"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Module) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
func (m Module) Clone() *Module {
c := &m
c.Fields = m.Fields.Clone()
return c
}
func (m Module) HasIssues() bool {
return len(m.Issues) > 0
}
// We won't worry about fields at this point
func (m *Module) decodeTranslations(tt locale.ResourceTranslationIndex) {
return
}
// We won't worry about fields at this point
func (m *Module) encodeTranslations() (out locale.ResourceTranslationSet) {
return
}
func (m *Module) ModelRef() dal.ModelRef {
return dal.ModelRef{
ConnectionID: m.Config.DAL.ConnectionID,
ResourceID: m.ID,
ResourceType: ModuleResourceType,
// @todo will use this for now but should probably change
Resource: m.RbacResource(),
}
}
// FindByHandle finds module by it's handle
func (set ModuleSet) FindByHandle(handle string) *Module {
for i := range set {
if set[i].Handle == handle {
return set[i]
}
}
return nil
}
func (c *ModuleConfig) Scan(src any) error { return sql.ParseJSON(src, c) }
func (c ModuleConfig) Value() (driver.Value, error) { return json.Marshal(c) }
func ParseModuleConfig(ss []string) (m ModuleConfig, err error) {
if len(ss) == 0 {
return
}
err = json.Unmarshal([]byte(ss[0]), &m)
return
}

View File

@@ -0,0 +1,600 @@
package types
import (
"database/sql/driver"
"encoding/json"
"sort"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/spf13/cast"
)
type (
// Modules - CRM module definitions
ModuleField struct {
ID uint64 `json:"fieldID,string"`
NamespaceID uint64 `json:"namespaceID,string"`
ModuleID uint64 `json:"moduleID,string"`
Place int `json:"-"`
Kind string `json:"kind"`
Name string `json:"name"`
// Options relevant to field type
Options ModuleFieldOptions `json:"options"`
// Configuration - how sub-services and sub-systems like DAL and record revisions
// are configured to work with this field
Config ModuleFieldConfig `json:"config"`
Required bool `json:"isRequired"`
Multi bool `json:"isMulti"`
DefaultValue RecordValueSet `json:"defaultValue"`
Expressions ModuleFieldExpr `json:"expressions"`
Labels map[string]string `json:"labels,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Label string `json:"label"`
}
ModuleFieldConfig struct {
DAL ModuleFieldConfigDAL `json:"dal"`
Privacy ModuleFieldConfigDataPrivacy `json:"privacy"`
RecordRevisions ModuleFieldConfigRecordRevisions `json:"recordRevisions"`
}
// ModuleFieldConfigDAL holds DAL configuration for a specific field
//
// If strategy is not set for a specific field (nil)
// then a default strategy is used
ModuleFieldConfigDAL struct {
EncodingStrategy *EncodingStrategy `json:"encodingStrategy"`
}
ModuleFieldConfigDataPrivacy struct {
// Define the highest sensitivity level which
// can be configured on the module fields
SensitivityLevelID uint64 `json:"sensitivityLevelID,string,omitempty"`
UsageDisclosure string `json:"usageDisclosure"`
}
ModuleFieldConfigRecordRevisions struct {
// when true, skip record revisions for this field
Skip bool `json:"enabled"`
}
// SystemFieldEncoding holds configuration for encoding record system fields
//
// If strategy is not set for a specific field (nil)
// then a default strategy is used, assuming system field/column presence
SystemFieldEncoding struct {
ID *EncodingStrategy `json:"id"`
ModuleID *EncodingStrategy `json:"moduleID"`
NamespaceID *EncodingStrategy `json:"namespaceID"`
Revision *EncodingStrategy `json:"revision"`
Meta *EncodingStrategy `json:"meta"`
OwnedBy *EncodingStrategy `json:"ownedBy"`
CreatedAt *EncodingStrategy `json:"createdAt"`
CreatedBy *EncodingStrategy `json:"createdBy"`
UpdatedAt *EncodingStrategy `json:"updatedAt"`
UpdatedBy *EncodingStrategy `json:"updatedBy"`
DeletedAt *EncodingStrategy `json:"deletedAt"`
DeletedBy *EncodingStrategy `json:"deletedBy"`
}
// EncodingStrategy is used by both: Module (for system fields) and ModuleField
//
EncodingStrategy struct {
//Type string `json:"type"`
//TypeParams map[string]any `json:"typeParams"`
Omit bool `json:"omit,omitempty"`
*EncodingStrategyAlias `json:"alias,omitempty"`
*EncodingStrategyJSON `json:"json,omitempty"`
*EncodingStrategyPlain `json:"plain,omitempty"`
}
EncodingStrategyAlias struct {
Ident string `json:"ident"`
}
EncodingStrategyJSON struct {
Ident string `json:"ident"`
}
EncodingStrategyPlain struct{}
ModuleFieldFilter struct {
ModuleID []uint64
Deleted filter.State
Limit uint
}
)
var (
_ sort.Interface = &ModuleFieldSet{}
)
func (f *ModuleField) SelectOptions() (out []string) {
if f.Kind != "Select" {
return
}
var (
options, has = f.Options["options"]
)
if !has {
return
}
switch oo := options.(type) {
case []string:
out = oo
case []interface{}:
for _, o := range oo {
switch c := o.(type) {
case string:
out = append(out, c)
case map[string]string:
if value, has := c["value"]; has {
out = append(out, value)
}
case map[string]interface{}:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
out = append(out, value)
}
}
case ModuleFieldOptions:
if value, has := c["value"]; has {
if value, ok := value.(string); ok {
out = append(out, value)
}
}
}
}
}
return
}
func (f *ModuleField) decodeTranslationsExpressionValidatorValidatorIDError(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
for i, e := range f.Expressions.Validators {
validatorID := locale.ContentID(e.ValidatorID, i)
rpl := strings.NewReplacer(
"{{validatorID}}", strconv.FormatUint(validatorID, 10),
)
if aux = tt.FindByKey(rpl.Replace(LocaleKeyModuleFieldExpressionValidatorValidatorIDError.Path)); aux != nil {
f.Expressions.Validators[i].Error = aux.Msg
}
}
}
func (f *ModuleField) decodeTranslationsMetaDescriptionView(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleFieldMetaDescriptionView.Path); aux != nil {
f.setOptionKey(aux.Msg, "description", "view")
}
}
func (f *ModuleField) decodeTranslationsMetaDescriptionEdit(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleFieldMetaDescriptionEdit.Path); aux != nil {
f.setOptionKey(aux.Msg, "description", "edit")
}
}
func (f *ModuleField) decodeTranslationsMetaHintView(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleFieldMetaHintView.Path); aux != nil {
f.setOptionKey(aux.Msg, "hint", "view")
}
}
func (f *ModuleField) decodeTranslationsMetaHintEdit(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(LocaleKeyModuleFieldMetaHintEdit.Path); aux != nil {
f.setOptionKey(aux.Msg, "hint", "edit")
}
}
// Decodes translations and modifies options
//
// Why "options-option-texts"? Because we're translating option txts under options key-value
func (f *ModuleField) decodeTranslationsMetaOptionsValueText(tt locale.ResourceTranslationIndex) {
var (
tr *locale.ResourceTranslation
)
optsUnknown, has := f.Options["options"]
if !has {
return
}
optsSlice, is := optsUnknown.([]interface{})
if !is {
return
}
for i, optUnknown := range optsSlice {
outOpt := map[string]any{
"value": "",
"text": "",
"style": map[string]any{
"textColor": "",
"backgroundColor": "",
},
}
// what is this we're dealing with? slice of strings (values) or map (value+text)
switch opt := optUnknown.(type) {
case string:
// cast string (value) into map (value+text)
// and use value as text (as a fallback in case
// of missing translation)
outOpt["value"] = opt
outOpt["text"] = opt
case map[string]interface{}:
outOpt["value"], is = opt["value"].(string)
if !is {
continue
}
outOpt["text"], _ = opt["text"].(string)
if style, ok := opt["style"].(map[string]any); ok {
outOpt["style"] = style
}
}
// find the translation for that value
// and update the option (effectively overwriting
// the original text value (in case of map option)
trKey := strings.NewReplacer("{{value}}", outOpt["value"].(string)).Replace(LocaleKeyModuleFieldMetaOptionsValueText.Path)
if tr = tt.FindByKey(trKey); tr != nil {
outOpt["text"] = tr.Msg
}
// Update slice item with translated option
optsSlice[i] = outOpt
}
}
// Decodes translations and modifies options
func (f *ModuleField) decodeTranslationsMetaBoolValueLabel(tt locale.ResourceTranslationIndex) {
if f.Kind != "Bool" {
return
}
var aux *locale.ResourceTranslation
if aux = tt.FindByKey(strings.NewReplacer("{{value}}", "true").Replace(LocaleKeyModuleFieldMetaBoolValueLabel.Path)); aux != nil {
f.setOptionKey(aux.Msg, "trueLabel")
}
if aux = tt.FindByKey(strings.NewReplacer("{{value}}", "false").Replace(LocaleKeyModuleFieldMetaBoolValueLabel.Path)); aux != nil {
f.setOptionKey(aux.Msg, "falseLabel")
}
}
func (m *ModuleField) encodeTranslationsExpressionValidatorValidatorIDError() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 3)
// Module field expressions
for i, e := range m.Expressions.Validators {
validatorID := locale.ContentID(e.ValidatorID, i)
rpl := strings.NewReplacer(
"{{validatorID}}", strconv.FormatUint(validatorID, 10),
)
out = append(out, &locale.ResourceTranslation{
Resource: m.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyModuleFieldExpressionValidatorValidatorIDError.Path),
Msg: e.Error,
})
}
return
}
func (f *ModuleField) encodeTranslationsMetaDescriptionView() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
t := &locale.ResourceTranslation{
Resource: f.ResourceTranslation(),
Key: LocaleKeyModuleFieldMetaDescriptionView.Path,
}
if v := f.getOptionKey("description", "view"); v != nil {
t.Msg = cast.ToString(v)
}
out = append(out, t)
return out
}
func (f *ModuleField) encodeTranslationsMetaDescriptionEdit() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
t := &locale.ResourceTranslation{
Resource: f.ResourceTranslation(),
Key: LocaleKeyModuleFieldMetaDescriptionEdit.Path,
}
if v := f.getOptionKey("description", "edit"); v != nil {
t.Msg = cast.ToString(v)
}
out = append(out, t)
return out
}
func (f *ModuleField) encodeTranslationsMetaHintView() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
t := &locale.ResourceTranslation{
Resource: f.ResourceTranslation(),
Key: LocaleKeyModuleFieldMetaHintView.Path,
}
if v := f.getOptionKey("hint", "view"); v != nil {
t.Msg = cast.ToString(v)
}
out = append(out, t)
return out
}
func (f *ModuleField) encodeTranslationsMetaHintEdit() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
t := &locale.ResourceTranslation{
Resource: f.ResourceTranslation(),
Key: LocaleKeyModuleFieldMetaHintEdit.Path,
}
if v := f.getOptionKey("hint", "edit"); v != nil {
t.Msg = cast.ToString(v)
}
out = append(out, t)
return out
}
// extracts option texts and converts (encodes) them to translations
func (f *ModuleField) encodeTranslationsMetaOptionsValueText() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 3)
optsUnknown, has := f.Options["options"]
if !has {
return
}
optsSlice, is := optsUnknown.([]interface{})
if !is {
return
}
for _, optUnknown := range optsSlice {
// we only care about maps (items with value & text)
switch opt := optUnknown.(type) {
case map[string]interface{}:
value, _ := opt["value"].(string)
text, _ := opt["text"].(string)
out = append(out, &locale.ResourceTranslation{
Resource: f.ResourceTranslation(),
Key: strings.NewReplacer("{{value}}", value).
Replace(LocaleKeyModuleFieldMetaOptionsValueText.Path),
Msg: text,
})
}
}
return
}
func (m *ModuleField) encodeTranslationsMetaBoolValueLabel() (out locale.ResourceTranslationSet) {
if m.Kind != "Bool" {
return
}
out = make(locale.ResourceTranslationSet, 0, 3)
out = append(out, &locale.ResourceTranslation{
Resource: m.ResourceTranslation(),
Key: strings.NewReplacer("{{value}}", "true").Replace(LocaleKeyModuleFieldMetaBoolValueLabel.Path),
Msg: m.Options.String("trueLabel"),
})
out = append(out, &locale.ResourceTranslation{
Resource: m.ResourceTranslation(),
Key: strings.NewReplacer("{{value}}", "false").Replace(LocaleKeyModuleFieldMetaBoolValueLabel.Path),
Msg: m.Options.String("falseLabel"),
})
return
}
func (f ModuleField) Clone() *ModuleField {
return &f
}
func (f ModuleField) setOptionKey(v interface{}, kk ...string) {
opt := f.Options
if opt == nil {
opt = ModuleFieldOptions{}
}
for _, k := range kk[0 : len(kk)-1] {
_, ok := opt[k]
if !ok {
opt = map[string]interface{}{k: make(map[string]interface{})}
}
var aux ModuleFieldOptions
switch c := opt[k].(type) {
case map[string]interface{}:
aux = ModuleFieldOptions(c)
case ModuleFieldOptions:
aux = c
}
opt = aux
}
k := kk[len(kk)-1]
opt[k] = v
}
func (f ModuleField) getOptionKey(kk ...string) interface{} {
if len(f.Options) == 0 {
return nil
}
opt := f.Options
for _, k := range kk[0 : len(kk)-1] {
_, ok := opt[k]
if !ok {
opt = map[string]interface{}{k: make(map[string]interface{})}
}
var aux ModuleFieldOptions
switch c := opt[k].(type) {
case map[string]interface{}:
aux = ModuleFieldOptions(c)
case ModuleFieldOptions:
aux = c
}
opt = aux
}
return opt[kk[len(kk)-1]]
}
func (set ModuleFieldSet) Clone() (out ModuleFieldSet) {
out = make([]*ModuleField, len(set))
for i := range set {
out[i] = set[i].Clone()
}
return out
}
func (set *ModuleFieldSet) Scan(src any) error { return sql.ParseJSON(src, set) }
func (set ModuleFieldSet) Value() (driver.Value, error) { return json.Marshal(set) }
func (set ModuleFieldSet) Names() (names []string) {
names = make([]string, len(set))
for i := range set {
names[i] = set[i].Name
}
return
}
func (set ModuleFieldSet) HasName(name string) bool {
for i := range set {
if name == set[i].Name {
return true
}
}
return false
}
func (set ModuleFieldSet) FindByName(name string) *ModuleField {
for i := range set {
if name == set[i].Name {
return set[i]
}
}
return nil
}
func (set ModuleFieldSet) FilterByModule(moduleID uint64) (ff ModuleFieldSet) {
for i := range set {
if set[i].ModuleID == moduleID {
ff = append(ff, set[i])
}
}
return
}
func (set ModuleFieldSet) Len() int {
return len(set)
}
func (set ModuleFieldSet) Less(i, j int) bool {
return set[i].Place < set[j].Place
}
func (set ModuleFieldSet) Swap(i, j int) {
set[i], set[j] = set[j], set[i]
}
func (f ModuleField) IsBoolean() bool {
return f.Kind == "Bool"
}
func (f ModuleField) IsNumeric() bool {
return f.Kind == "Number"
}
func (f ModuleField) IsDateTime() bool {
return f.Kind == "DateTime" && !f.IsDateOnly() && !f.IsTimeOnly()
}
func (f ModuleField) IsDateOnly() bool {
return f.Kind == "DateTime" && f.Options.Bool("onlyDate")
}
func (f ModuleField) IsTimeOnly() bool {
return f.Kind == "DateTime" && f.Options.Bool("onlyTime")
}
// IsRef tells us if value of this field be a reference to something
// (another record, file , user)?
func (f ModuleField) IsRef() bool {
return f.Kind == "Record" || f.Kind == "User" || f.Kind == "File"
}
func (f ModuleField) IsSensitive() bool {
return f.Config.Privacy.SensitivityLevelID > 0
}
func (f *ModuleField) setValue(name string, pos uint, value any) (err error) {
switch name {
// @todo consider moving this to the .cue definition; figure out why it wasn't yet
case "NamespaceID", "namespaceID":
f.NamespaceID = cast.ToUint64(value)
case "Options.ModuleID":
f.Options["moduleID"] = cast.ToString(value)
}
return
}
func (p *ModuleFieldConfig) Scan(src any) error { return sql.ParseJSON(src, p) }
func (p ModuleFieldConfig) Value() (driver.Value, error) { return json.Marshal(p) }

View File

@@ -0,0 +1,30 @@
package types
import (
"database/sql/driver"
"encoding/json"
"github.com/cortezaproject/corteza/server/pkg/sql"
)
type (
ModuleFieldExpr struct {
ValueExpr string `json:"value,omitempty"`
Sanitizers []string `json:"sanitizers,omitempty"`
Validators []ModuleFieldValidator `json:"validators,omitempty"`
DisableDefaultValidators bool `json:"disableDefaultValidators,omitempty"`
Formatters []string `json:"formatters,omitempty"`
DisableDefaultFormatters bool `json:"disableDefaultFormatters,omitempty"`
}
ModuleFieldValidator struct {
ValidatorID uint64 `json:"validatorID,string,omitempty"`
Test string `json:"test,omitempty"`
Error string `json:"error,omitempty"`
}
)
func (opt *ModuleFieldExpr) Scan(src any) error { return sql.ParseJSON(src, opt) }
func (opt ModuleFieldExpr) Value() (driver.Value, error) { return json.Marshal(opt) }

View File

@@ -0,0 +1,183 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/spf13/cast"
"strconv"
)
type (
ModuleFieldOptions map[string]interface{}
ModuleFieldOptionIndex struct {
// Applicable to ref field kinds where we use nested/object type
// and we want to control how deep do we want to follow the references
NestingDepth map[string]bool `json:"nestingDepth,omitempty"`
// When embedding this field into a parent doc, do we skip it?
SkipNested bool `json:"skipNested,omitempty"`
// @todo we'll probably define aggregation/buckets params here
// so that we can handle faceting properly?
}
)
const (
moduleFieldOptionExpression = "expression"
moduleFieldOptionIsUnique = "isUnique"
moduleFieldOptionIsUniqueMultiValue = "isUniqueMultiValue"
moduleFieldOptionIndex = "index"
moduleFieldOptionOptions = "options"
moduleFieldNumberOptionPrecision = "precision"
moduleFieldNumberOptionPrecisionMin uint = 0
moduleFieldNumberOptionPrecisionMax uint = 6
)
func (opt *ModuleFieldOptions) Scan(src any) error { return sql.ParseJSON(src, opt) }
func (opt ModuleFieldOptions) Value() (driver.Value, error) { return json.Marshal(opt) }
// Bool returns option value for key as boolean true or false
//
// Invalid, non-existing are returned as false
func (opt ModuleFieldOptions) Bool(key string) bool {
if _, has := opt[key]; has {
if v, ok := opt[key].(bool); ok {
return v
}
}
return false
}
func (opt ModuleFieldOptions) UInt64(key string) uint64 {
return opt.UInt64Def(key, 0)
}
func (opt ModuleFieldOptions) UInt64Def(key string, def uint64) uint64 {
if val, has := opt[key]; has {
v, err := cast.ToUint64E(val)
if err != nil {
return def
}
return v
}
return def
}
func (opt ModuleFieldOptions) Int64(key string) int64 {
return opt.Int64Def(key, 0)
}
func (opt ModuleFieldOptions) Int64Def(key string, def int64) int64 {
if val, has := opt[key]; has {
switch conv := val.(type) {
case int:
return int64(conv)
case int64:
return conv
default:
// to avoid covering every possible type, just convert value into string
strVal := fmt.Sprintf("%v", val)
if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil {
return intVal
}
if floatVal, err := strconv.ParseFloat(strVal, 64); err == nil {
return int64(floatVal)
}
}
}
return def
}
// Uint64 returns option value for key casted to uint64
func (opt ModuleFieldOptions) Uint64(key string) uint64 {
if val, has := opt[key]; has {
return cast.ToUint64(val)
}
return 0
}
// Strings returns option value for key as slice of strings
//
// Invalid, non-existing are returned as nil
func (opt ModuleFieldOptions) Strings(key string) []string {
if _, has := opt[key]; has {
if v, ok := opt[key].([]string); ok {
return v
}
}
return nil
}
// String returns option value for key as single string
//
// Invalid, non-existing are returned as empty string ("")
func (opt ModuleFieldOptions) String(key string) string {
if _, has := opt[key]; has {
if v, ok := opt[key].(string); ok {
return v
}
}
return ""
}
// IsUnique - should value in this field be unique across records?
func (opt ModuleFieldOptions) IsUnique() bool {
return opt.Bool(moduleFieldOptionIsUnique)
}
// SetIsUnique - should value in this field be unique across records?
func (opt ModuleFieldOptions) SetIsUnique(value bool) {
opt[moduleFieldOptionIsUnique] = value
}
// IsUniqueMultiValue - should value in this field be unique in the multi-value set?
func (opt ModuleFieldOptions) IsUniqueMultiValue() bool {
return opt.Bool(moduleFieldOptionIsUniqueMultiValue)
}
func (opt ModuleFieldOptions) SetIsUniqueMultiValue(value bool) {
// SetIsUniqueMultiValue - should value in this field be unique in the multi-value set?
opt[moduleFieldOptionIsUniqueMultiValue] = value
}
func (opt ModuleFieldOptions) Precision() (p uint) {
p = uint(opt.Int64(moduleFieldNumberOptionPrecision))
if p < moduleFieldNumberOptionPrecisionMin {
p = moduleFieldNumberOptionPrecisionMin
} else if p > moduleFieldNumberOptionPrecisionMax {
p = moduleFieldNumberOptionPrecisionMax
}
return
}
func (opt ModuleFieldOptions) SetPrecision(p uint) {
opt[moduleFieldNumberOptionPrecision] = p
}
// IsUnique - should value in this field be unique across records?
func (opt ModuleFieldOptions) Index() *ModuleFieldOptionIndex {
if val, has := opt[moduleFieldOptionIndex]; has {
if i, is := val.(*ModuleFieldOptionIndex); is {
return i
}
}
return nil
}

View File

@@ -0,0 +1,93 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/sql"
)
type (
Namespace struct {
ID uint64 `json:"namespaceID,string"`
Slug string `json:"slug"`
Enabled bool `json:"enabled"`
Meta NamespaceMeta `json:"meta"`
Labels map[string]string `json:"labels,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Name string `json:"name"`
}
NamespaceFilter struct {
NamespaceID []string `json:"namespaceID"`
Query string `json:"query"`
Slug string `json:"slug"`
Name string `json:"name"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Namespace) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
NamespaceMeta struct {
// Temporary icon & logo URLs
// @todo rework this when we rework attachment management
Icon string `json:"icon,omitempty"`
IconID uint64 `json:"iconID,string"`
Logo string `json:"logo,omitempty"`
LogoID uint64 `json:"logoID,string"`
LogoEnabled bool `json:"logoEnabled,omitempty"`
HideSidebar bool `json:"hideSidebar"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Subtitle string `json:"subtitle,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Description string `json:"description,omitempty"`
}
)
func (n Namespace) Clone() *Namespace {
c := &n
return c
}
// FindByHandle finds namespace by it's handle/slug
func (set NamespaceSet) FindByHandle(handle string) *Namespace {
for i := range set {
if set[i].Slug == handle {
return set[i]
}
}
return nil
}
func (nm *NamespaceMeta) Scan(src any) error { return sql.ParseJSON(src, nm) }
func (nm NamespaceMeta) Value() (driver.Value, error) { return json.Marshal(nm) }

View File

@@ -0,0 +1,13 @@
package types
type (
EmailNotification struct {
To []string
Cc []string
ReplyTo string
Subject string
ContentPlain string
ContentHTML string
RemoteAttachments []string
}
)

View File

@@ -0,0 +1,488 @@
package types
import (
"database/sql/driver"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/modern-go/reflect2"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/spf13/cast"
)
type (
Page struct {
ID uint64 `json:"pageID,string"`
SelfID uint64 `json:"selfID,string"`
NamespaceID uint64 `json:"namespaceID,string"`
ModuleID uint64 `json:"moduleID,string"`
Handle string `json:"handle"`
Config PageConfig `json:"config"`
Blocks PageBlocks `json:"blocks"`
Meta PageMeta `json:"meta"`
Children PageSet `json:"children,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Visible bool `json:"visible"`
Weight int `json:"weight"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Title string `json:"title"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Description string `json:"description"`
}
PageBlocks []PageBlock
PageBlock struct {
BlockID uint64 `json:"blockID,string,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
Style PageBlockStyle `json:"style,omitempty"`
Kind string `json:"kind"`
XYWH [4]int `json:"xywh"` // x,y,w,h
Meta map[string]any `json:"meta,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Title string `json:"title,omitempty"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Description string `json:"description,omitempty"`
}
PageMeta struct {
AllowPersonalLayouts bool `json:"allowPersonalLayouts"`
Notifications map[string]any `json:"notifications,omitempty"`
}
PageBlockStyle struct {
Variants map[string]string `json:"variants,omitempty"`
Wrap map[string]string `json:"wrap,omitempty"`
Border map[string]interface{} `json:"border,omitempty"`
}
PageConfig struct {
// How page is presented in the navigation
NavItem struct {
// Expanded menu
Expanded bool `json:"expanded"`
Icon *PageConfigIcon `json:"icon,omitempty"`
} `json:"navItem"`
}
PageConfigIcon struct {
// Icon types and sources
//
// Note that backed does not enforce or validate all src value (types due to a limited
// awareness of capabilities and
//
// Type: empty or "link" (default):
// Indicate that src will contain an absolute or relative link to an icon.
// Can also be used for inline images (storing "base64:" prefixed string in source).
// This type and reference is not validated by the backend.
//
// Type: "library"
// Source references an icon from a library. Ref's value should be in the following
// notation: "font-awesome://<icon-identifier>".
// This type and source is not validated by the backend.
//
// Type: "svg"
// SRC contains raw SVG document
// //////////////////////////////////////////////////////////////////////////////////////////////////////
// Other types that might be implemented in the future:
// "attachment"
// Reference (ID) to an existing attachment in local Corteza instance is expected
// This type and reference must be validated by the backend.
Type IconType `json:"type,omitempty"`
Src string `json:"src"`
// Any custom styling that should be applied to the icon
Style map[string]string `json:"style,omitempty"`
}
PageFilter struct {
NamespaceID uint64 `json:"namespaceID"`
PageID []string `json:"pageID,string"`
ParentID uint64 `json:"parentID,string,omitempty"`
ModuleID uint64 `json:"moduleID,string,omitempty"`
Root bool `json:"root,omitempty"`
Handle string `json:"handle"`
Title string `json:"title"`
Query string `json:"query"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Page) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
PageChildrenDeleteStrategy string
)
const (
PageChildrenOnDeleteAbort PageChildrenDeleteStrategy = "abort"
PageChildrenOnDeleteForce PageChildrenDeleteStrategy = "force"
PageChildrenOnDeleteRebase PageChildrenDeleteStrategy = "rebase"
PageChildrenOnDeleteCascade PageChildrenDeleteStrategy = "cascade"
)
func (m Page) Clone() *Page {
c := &m
return c
}
func (p *Page) decodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
for i, block := range p.Blocks {
blockID := locale.ContentID(block.BlockID, i)
rpl := strings.NewReplacer(
"{{blockID}}", strconv.FormatUint(blockID, 10),
)
// - generic page block stuff
if aux = tt.FindByKey(rpl.Replace(LocaleKeyPagePageBlockBlockIDTitle.Path)); aux != nil {
p.Blocks[i].Title = aux.Msg
}
if aux = tt.FindByKey(rpl.Replace(LocaleKeyPagePageBlockBlockIDDescription.Path)); aux != nil {
p.Blocks[i].Description = aux.Msg
}
// - automation page block stuff
switch block.Kind {
case "Automation":
bb, _ := block.Options["buttons"].([]interface{})
p.decodeRecordListButtons(tt, bb, blockID)
case "RecordList":
bb, _ := block.Options["selectionButtons"].([]interface{})
p.decodeRecordListButtons(tt, bb, blockID)
case "Content":
if aux = tt.FindByKey(rpl.Replace(LocaleKeyPagePageBlockBlockIDContentBody.Path)); aux != nil {
block.Options["body"] = aux.Msg
}
}
}
}
func (p *Page) decodeRecordListButtons(tt locale.ResourceTranslationIndex, bb []interface{}, blockID uint64) {
var aux *locale.ResourceTranslation
for j, auxBtn := range bb {
btn := auxBtn.(map[string]interface{})
buttonID := uint64(0)
if aux, ok := btn["buttonID"]; ok {
buttonID = cast.ToUint64(aux)
}
buttonID = locale.ContentID(buttonID, j)
rpl := strings.NewReplacer(
"{{blockID}}", strconv.FormatUint(blockID, 10),
"{{buttonID}}", strconv.FormatUint(buttonID, 10),
)
if aux = tt.FindByKey(rpl.Replace(LocaleKeyPagePageBlockBlockIDButtonButtonIDLabel.Path)); aux != nil {
btn["label"] = aux.Msg
}
}
}
func (p *Page) encodeTranslations() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 12)
// Button translations don't need to happen here as we don't do anything with buttons at the moment
// @todo when we add custom buttons this should change
// Page blocks
for i, block := range p.Blocks {
blockID := locale.ContentID(block.BlockID, i)
rpl := strings.NewReplacer(
"{{blockID}}", strconv.FormatUint(uint64(blockID), 10),
)
// - generic page block stuff
out = append(out, &locale.ResourceTranslation{
Resource: p.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyPagePageBlockBlockIDTitle.Path),
Msg: block.Title,
})
out = append(out, &locale.ResourceTranslation{
Resource: p.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyPagePageBlockBlockIDDescription.Path),
Msg: block.Description,
})
// - automation page block stuff
switch block.Kind {
case "Automation":
bb, _ := block.Options["buttons"].([]interface{})
out = append(out, p.encodeRecordListButtons(bb, blockID)...)
case "RecordList":
bb, _ := block.Options["selectionButtons"].([]interface{})
out = append(out, p.encodeRecordListButtons(bb, blockID)...)
case "Content":
body, _ := block.Options["body"].(string)
out = append(out, &locale.ResourceTranslation{
Resource: p.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyPagePageBlockBlockIDContentBody.Path),
Msg: body,
})
}
}
return
}
func (p *Page) encodeRecordListButtons(bb []interface{}, blockID uint64) (out locale.ResourceTranslationSet) {
for j, auxBtn := range bb {
btn := auxBtn.(map[string]interface{})
if _, ok := btn["label"]; !ok {
continue
}
buttonID := uint64(0)
if aux, ok := btn["buttonID"]; ok {
buttonID = cast.ToUint64(aux)
}
buttonID = locale.ContentID(buttonID, j)
rpl := strings.NewReplacer(
"{{blockID}}", strconv.FormatUint(blockID, 10),
"{{buttonID}}", strconv.FormatUint(buttonID, 10),
)
out = append(out, &locale.ResourceTranslation{
Resource: p.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyPagePageBlockBlockIDButtonButtonIDLabel.Path),
Msg: btn["label"].(string),
})
}
return
}
func (p *Page) setValue(name string, pos uint, value any) (err error) {
pp := strings.Split(name, ".")
switch pp[0] {
case "Blocks":
return p.Blocks[cast.ToUint(pp[1])].setValue(strings.Join(pp[2:], "."), pos, value)
}
return
}
func (p *Page) Prune(rt string) {
for i := len(p.Blocks) - 1; i >= 0; i-- {
b := p.Blocks[i]
switch b.Kind {
// Implement the rest when support is needed
case "Automation":
if rt != "corteza::automation:workflow" {
continue
}
p.removeBlock(i)
}
}
}
// Page block utilities
func (p *Page) removeBlock(i int) {
// do the swap remove thing
p.Blocks[i] = p.Blocks[len(p.Blocks)-1]
p.Blocks = p.Blocks[:len(p.Blocks)-1]
}
func (b *PageBlock) setValue(name string, pos uint, value any) (err error) {
pp := strings.Split(name, ".")
switch pp[0] {
case "Options":
return b.setOptionValue(pp[1:], pos, value)
}
return
}
func (b *PageBlock) setOptionValue(path []string, pos uint, value any) (err error) {
setProgressValue := func(k string) {
v := make(map[string]any)
if !reflect2.IsNil(b.Options[k]) {
v = b.Options[k].(map[string]any)
}
v["moduleID"] = cast.ToString(value)
b.Options[k] = v
}
switch path[0] {
case "feeds":
// Get the feed on the correct index in the correct type
feed := (b.Options["feeds"].([]any))[cast.ToInt(path[1])].(map[string]any)
// Getthe feed options in the correct type
options := feed["options"].(map[string]any)
// Set the value
options["moduleID"] = cast.ToString(value)
case "metrics":
// Get the metric on the correct index in the correct type
metric := (b.Options["metrics"].([]any))[cast.ToInt(path[1])].(map[string]any)
metric["moduleID"] = cast.ToString(value)
case "minValue":
setProgressValue("minValue")
case "maxValue":
setProgressValue("maxValue")
case "value":
setProgressValue("value")
case "moduleID", "ModuleID":
b.Options["moduleID"] = cast.ToString(value)
case "chartID", "ChartID":
b.Options["chartID"] = cast.ToString(value)
case "buttons":
switch path[2] {
case "workflowID", "WorkflowID":
button := (b.Options["buttons"].([]any))[cast.ToInt(path[1])].(map[string]interface{})
button["workflowID"] = cast.ToString(value)
delete(button, "workflow")
}
}
return
}
// FindByHandle finds page by it's handle
func (set PageSet) FindByHandle(handle string) *Page {
for i := range set {
if set[i].Handle == handle {
return set[i]
}
}
return nil
}
func (bb *PageBlocks) Scan(src any) error { return sql.ParseJSON(src, bb) }
func (bb PageBlocks) Value() (driver.Value, error) { return json.Marshal(bb) }
func (bb *PageMeta) Scan(src any) error { return sql.ParseJSON(src, bb) }
func (bb PageMeta) Value() (driver.Value, error) { return json.Marshal(bb) }
// Helper to extract old encoding to new one
func (b *PageBlock) UnmarshalJSON(data []byte) (err error) {
type internalPageBlock PageBlock
i := struct {
internalPageBlock
X int `json:"x,omitempty"`
Y int `json:"y,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}{}
if err = json.Unmarshal(data, &i); err != nil {
return
}
*b = PageBlock(i.internalPageBlock)
if i.XYWH[0]+i.XYWH[1]+i.XYWH[2]+i.XYWH[3] > 0 {
return nil
}
if i.X+i.Y+i.Width+i.Height > 0 {
// Cast old x,y,w,h structure to this:
b.XYWH = [4]int{i.X, i.Y, i.Width, i.Height}
}
return nil
}
func (set PageSet) FindByParent(parentID uint64) (out PageSet) {
out = PageSet{}
for i := range set {
if set[i].SelfID == parentID {
out = append(out, set[i])
}
}
return
}
// RecursiveWalk through all child pages
func (set PageSet) RecursiveWalk(parent *Page, fn func(c *Page, parent *Page) error) (err error) {
if parent == nil {
return
}
for _, page := range set {
if page.SelfID != parent.ID {
continue
}
if err = fn(page, parent); err != nil {
return
}
if err = set.RecursiveWalk(page, fn); err != nil {
return
}
}
return
}
func (bb *PageConfig) Scan(src any) error { return sql.ParseJSON(src, bb) }
func (bb PageConfig) Value() (driver.Value, error) { return json.Marshal(bb) }

View File

@@ -0,0 +1,240 @@
package types
import (
"database/sql/driver"
"encoding/json"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/cast2"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
PageLayout struct {
ID uint64 `json:"pageLayoutID,string"`
NamespaceID uint64 `json:"namespaceID,string"`
PageID uint64 `json:"pageID,string"`
ParentID uint64 `json:"parentID,string"`
Handle string `json:"handle"`
Primary bool `json:"primary"`
Weight int `json:"weight"`
Meta PageLayoutMeta `json:"meta,omitempty"`
Config PageLayoutConfig `json:"config"`
Blocks PageLayoutBlocks `json:"blocks,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
OwnedBy uint64 `json:"ownedBy,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
PageLayoutBlocks []PageLayoutBlock
PageLayoutBlock struct {
BlockID uint64 `json:"blockID,string,omitempty" yaml:"blockID"`
XYWH [4]int `json:"xywh" yaml:"xywh"`
Meta map[string]any `json:"meta,omitempty"`
}
PageLayoutMeta struct {
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Title string `json:"title"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Description string `json:"description"`
Style map[string]any `json:"style,omitempty"`
}
PageLayoutButton struct {
Enabled bool `json:"enabled"`
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Label string `json:"label"`
}
PageLayoutButtonConfig struct {
New PageLayoutButton `json:"new"`
Edit PageLayoutButton `json:"edit"`
Submit PageLayoutButton `json:"submit"`
Delete PageLayoutButton `json:"delete"`
Clone PageLayoutButton `json:"clone"`
Back PageLayoutButton `json:"back"`
}
PageLayoutConfig struct {
Visibility PageLayoutVisibility `json:"visibility"`
Buttons PageLayoutButtonConfig `json:"buttons"`
Actions []PageLayoutAction `json:"actions,omitempty"`
UseTitle bool `json:"useTitle"`
}
PageLayoutVisibility struct {
Expression string `json:"expression"`
Roles []string `json:"roles,omitempty"`
}
PageLayoutAction struct {
ActionID uint64 `json:"actionID,string"`
Placement string `json:"placement"`
Meta PageLayoutActionMeta `json:"meta"`
Enabled bool `json:"enabled"`
// Kind and Params specify the action's behavior and the parameters it
// can use for execution
Kind string `json:"kind"`
Params any `json:"params"`
}
PageLayoutActionMeta struct {
// Warning: value of this field is now handled via resource-translation facility
// struct field is kept for the convenience for now since it allows us
// easy encoding/decoding of the outgoing/incoming values
Label string `json:"label"`
Style map[string]any `json:"style,omitempty"`
}
PageLayoutFilter struct {
PageLayoutID []string `json:"pageLayoutID"`
NamespaceID uint64 `json:"namespaceID,string"`
PageID uint64 `json:"pageID,string,omitempty"`
ParentID uint64 `json:"ParentID,string,omitempty"`
Handle string `json:"handle"`
Name string `json:"name"`
Query string `json:"query"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*PageLayout) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
func (m PageLayout) Clone() *PageLayout {
c := &m
return c
}
func (p *PageLayout) decodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
// @note not doing blocks because they are simply copied from the page's index
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsNewLabel.Path); aux != nil {
p.Config.Buttons.New.Label = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsEditLabel.Path); aux != nil {
p.Config.Buttons.Edit.Label = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsSubmitLabel.Path); aux != nil {
p.Config.Buttons.Submit.Label = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsDeleteLabel.Path); aux != nil {
p.Config.Buttons.Delete.Label = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsCloneLabel.Path); aux != nil {
p.Config.Buttons.Clone.Label = aux.Msg
}
if aux = tt.FindByKey(LocaleKeyPageLayoutConfigButtonsBackLabel.Path); aux != nil {
p.Config.Buttons.Back.Label = aux.Msg
}
for i, action := range p.Config.Actions {
actionID := locale.ContentID(action.ActionID, i)
rpl := strings.NewReplacer(
"{{actionID}}", strconv.FormatUint(actionID, 10),
)
if aux = tt.FindByKey(rpl.Replace(LocaleKeyPageLayoutConfigActionsActionIDMetaLabel.Path)); aux != nil {
p.Config.Actions[i].Meta.Label = aux.Msg
}
}
}
func (p *PageLayout) encodeTranslations() (out locale.ResourceTranslationSet) {
out = make(locale.ResourceTranslationSet, 0, 8)
// @note not doing blocks because they are simply copied from the page's index
// Actions
for i, action := range p.Config.Actions {
actionID := locale.ContentID(action.ActionID, i)
rpl := strings.NewReplacer(
"{{actionID}}", strconv.FormatUint(actionID, 10),
)
out = append(out, &locale.ResourceTranslation{
Resource: p.ResourceTranslation(),
Key: rpl.Replace(LocaleKeyPageLayoutConfigActionsActionIDMetaLabel.Path),
Msg: action.Meta.Label,
})
}
return
}
// FindByHandle finds pageLayout by it's handle
func (set PageLayoutSet) FindByHandle(handle string) *PageLayout {
for i := range set {
if set[i].Handle == handle {
return set[i]
}
}
return nil
}
func (r *PageLayout) getValue(name string, pos uint) (any, error) {
switch name {
case "selfD", "SelfD":
return r.PageID, nil
}
return nil, nil
}
func (r *PageLayout) setValue(name string, pos uint, value any) (err error) {
switch name {
case "selfID", "SelfID":
return cast2.Uint64(value, &r.PageID)
}
return nil
}
func (bb *PageLayoutConfig) Scan(src any) error { return sql.ParseJSON(src, &bb) }
func (bb PageLayoutConfig) Value() (driver.Value, error) { return json.Marshal(bb) }
func (vv *PageLayoutMeta) Scan(src any) error { return sql.ParseJSON(src, &vv) }
func (vv PageLayoutMeta) Value() (driver.Value, error) { return json.Marshal(vv) }
func (bb *PageLayoutBlocks) Scan(src any) error { return sql.ParseJSON(src, bb) }
func (bb PageLayoutBlocks) Value() (driver.Value, error) { return json.Marshal(bb) }

View File

@@ -0,0 +1,21 @@
package types
import "encoding/json"
func ParsePageLayoutMeta(ss []string) (p PageLayoutMeta, err error) {
p = PageLayoutMeta{}
return p, parseStringsInput(ss, &p)
}
func ParsePageMeta(ss []string) (p PageMeta, err error) {
p = PageMeta{}
return p, parseStringsInput(ss, &p)
}
func parseStringsInput(ss []string, p interface{}) (err error) {
if len(ss) == 0 {
return
}
return json.Unmarshal([]byte(ss[0]), &p)
}

View File

@@ -0,0 +1,311 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
import (
"fmt"
"strconv"
)
type (
// Component struct serves as a virtual resource type for the compose component
//
// This struct is auto-generated
Component struct{}
)
var (
_ = fmt.Printf
_ = strconv.FormatUint
)
// RbacResource returns string representation of RBAC resource for Chart by calling ChartRbacResource fn
//
// RBAC resource is in the corteza::compose:chart/... format
//
// This function is auto-generated
func (r Chart) RbacResource() string {
return ChartRbacResource(r.NamespaceID, r.ID)
}
// ChartRbacResource returns string representation of RBAC resource for Chart
//
// RBAC resource is in the corteza::compose:chart/... format
//
// This function is auto-generated
func ChartRbacResource(namespaceID uint64, id uint64) string {
cpts := []interface{}{ChartResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(ChartRbacResourceTpl(), cpts...)
}
func ChartRbacResourceTpl() string {
return "%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for Module by calling ModuleRbacResource fn
//
// RBAC resource is in the corteza::compose:module/... format
//
// This function is auto-generated
func (r Module) RbacResource() string {
return ModuleRbacResource(r.NamespaceID, r.ID)
}
// ModuleRbacResource returns string representation of RBAC resource for Module
//
// RBAC resource is in the corteza::compose:module/... format
//
// This function is auto-generated
func ModuleRbacResource(namespaceID uint64, id uint64) string {
cpts := []interface{}{ModuleResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(ModuleRbacResourceTpl(), cpts...)
}
func ModuleRbacResourceTpl() string {
return "%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for ModuleField by calling ModuleFieldRbacResource fn
//
// RBAC resource is in the corteza::compose:module-field/... format
//
// This function is auto-generated
func (r ModuleField) RbacResource() string {
return ModuleFieldRbacResource(r.NamespaceID, r.ModuleID, r.ID)
}
// ModuleFieldRbacResource returns string representation of RBAC resource for ModuleField
//
// RBAC resource is in the corteza::compose:module-field/... format
//
// This function is auto-generated
func ModuleFieldRbacResource(namespaceID uint64, moduleID uint64, id uint64) string {
cpts := []interface{}{ModuleFieldResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if moduleID != 0 {
cpts = append(cpts, strconv.FormatUint(moduleID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(ModuleFieldRbacResourceTpl(), cpts...)
}
func ModuleFieldRbacResourceTpl() string {
return "%s/%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for Namespace by calling NamespaceRbacResource fn
//
// RBAC resource is in the corteza::compose:namespace/... format
//
// This function is auto-generated
func (r Namespace) RbacResource() string {
return NamespaceRbacResource(r.ID)
}
// NamespaceRbacResource returns string representation of RBAC resource for Namespace
//
// RBAC resource is in the corteza::compose:namespace/... format
//
// This function is auto-generated
func NamespaceRbacResource(id uint64) string {
cpts := []interface{}{NamespaceResourceType}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(NamespaceRbacResourceTpl(), cpts...)
}
func NamespaceRbacResourceTpl() string {
return "%s/%s"
}
// RbacResource returns string representation of RBAC resource for Page by calling PageRbacResource fn
//
// RBAC resource is in the corteza::compose:page/... format
//
// This function is auto-generated
func (r Page) RbacResource() string {
return PageRbacResource(r.NamespaceID, r.ID)
}
// PageRbacResource returns string representation of RBAC resource for Page
//
// RBAC resource is in the corteza::compose:page/... format
//
// This function is auto-generated
func PageRbacResource(namespaceID uint64, id uint64) string {
cpts := []interface{}{PageResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(PageRbacResourceTpl(), cpts...)
}
func PageRbacResourceTpl() string {
return "%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for PageLayout by calling PageLayoutRbacResource fn
//
// RBAC resource is in the corteza::compose:page-layout/... format
//
// This function is auto-generated
func (r PageLayout) RbacResource() string {
return PageLayoutRbacResource(r.NamespaceID, r.PageID, r.ID)
}
// PageLayoutRbacResource returns string representation of RBAC resource for PageLayout
//
// RBAC resource is in the corteza::compose:page-layout/... format
//
// This function is auto-generated
func PageLayoutRbacResource(namespaceID uint64, pageID uint64, id uint64) string {
cpts := []interface{}{PageLayoutResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if pageID != 0 {
cpts = append(cpts, strconv.FormatUint(pageID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(PageLayoutRbacResourceTpl(), cpts...)
}
func PageLayoutRbacResourceTpl() string {
return "%s/%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for Record by calling RecordRbacResource fn
//
// RBAC resource is in the corteza::compose:record/... format
//
// This function is auto-generated
func (r Record) RbacResource() string {
return RecordRbacResource(r.NamespaceID, r.ModuleID, r.ID)
}
// RecordRbacResource returns string representation of RBAC resource for Record
//
// RBAC resource is in the corteza::compose:record/... format
//
// This function is auto-generated
func RecordRbacResource(namespaceID uint64, moduleID uint64, id uint64) string {
cpts := []interface{}{RecordResourceType}
if namespaceID != 0 {
cpts = append(cpts, strconv.FormatUint(namespaceID, 10))
} else {
cpts = append(cpts, "*")
}
if moduleID != 0 {
cpts = append(cpts, strconv.FormatUint(moduleID, 10))
} else {
cpts = append(cpts, "*")
}
if id != 0 {
cpts = append(cpts, strconv.FormatUint(id, 10))
} else {
cpts = append(cpts, "*")
}
return fmt.Sprintf(RecordRbacResourceTpl(), cpts...)
}
func RecordRbacResourceTpl() string {
return "%s/%s/%s/%s"
}
// RbacResource returns string representation of RBAC resource for Component by calling ComponentRbacResource fn
//
// RBAC resource is in the corteza::compose/... format
//
// This function is auto-generated
func (r Component) RbacResource() string {
return ComponentRbacResource()
}
// ComponentRbacResource returns string representation of RBAC resource for Component
//
// RBAC resource is in the corteza::compose/ format
//
// This function is auto-generated
func ComponentRbacResource() string {
return ComponentResourceType + "/"
}
func ComponentRbacResourceTpl() string {
return "%s"
}

View File

@@ -0,0 +1,485 @@
package types
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/modern-go/reflect2"
"github.com/spf13/cast"
)
type (
bulkPreProcess func(m, r *Record) (*Record, error)
OperationType string
RecordBulkOperation struct {
Record *Record
RecordID uint64
NamespaceID uint64
ModuleID uint64
LinkBy string
Operation OperationType
ID string
}
RecordBulkOperationResult struct {
Record *Record
Error error
ValueError *RecordValueErrorSet
DuplicationError *RecordValueErrorSet
}
RecordBulk struct {
RefField string `json:"refField,omitempty"`
IDPrefix string `json:"idPrefix,omitempty"`
Set RecordSet `json:"set,omitempty"`
}
RecordBulkSet []*RecordBulk
// Record is a stored row in the `record` table
Record struct {
ID uint64 `json:"recordID,string"`
ModuleID uint64 `json:"moduleID,string"`
Revision int `json:"revision,omitempty"`
module *Module
Values RecordValueSet `json:"values,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
NamespaceID uint64 `json:"namespaceID,string"`
OwnedBy uint64 `json:"ownedBy,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
CreatedBy uint64 `json:"createdBy,string" `
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
UpdatedBy uint64 `json:"updatedBy,string,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
DeletedBy uint64 `json:"deletedBy,string,omitempty"`
}
RecordSummary struct {
Name string
Min float64
Max float64
Avg float64
Sum float64
EmptyCount int
NotEmptyCount int
UniqueCount int
Earliest time.Time
Latest time.Time
Count int
}
RecordSummaryReq struct {
Name string `json:"name"`
Field string `json:"field"`
}
RecordFilter struct {
Summaries []RecordSummaryReq `json:"summaries,omitempty"`
ModuleID uint64 `json:"moduleID,string"`
NamespaceID uint64 `json:"namespaceID,string"`
Query string `json:"query"`
Meta map[string]any `json:"meta,omitempty"`
Deleted filter.State `json:"deleted"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Record) (bool, error) `json:"-"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
SensitiveRecord struct {
RecordID uint64
Values []map[string]any
}
SensitiveRecordSet struct {
// Contextual metadata
ConnectionID uint64
Module *Module
Namespace *Namespace
Records []SensitiveRecord
}
)
const (
OperationTypeCreate OperationType = "create"
OperationTypeUpdate OperationType = "update"
OperationTypeDelete OperationType = "delete"
OperationTypePatch OperationType = "patch"
OperationTypeUndelete OperationType = "undelete"
DateOnlyLayout = "2006-01-02"
TimeOnlyLayout = "15:04:05"
)
func (f RecordFilter) ToConstraintedFilter(c map[string][]any) filter.Filter {
return filter.Generic(
// combine constraints with namespace and module
filter.WithConstraints(c),
filter.WithExpression(f.Query),
filter.WithOrderBy(f.Sort),
filter.WithLimit(f.Limit),
filter.WithCursor(f.PageCursor),
filter.WithMetaConstraints(f.Meta),
filter.WithStateConstraint("deletedAt", f.Deleted),
)
}
// UserIDs returns a slice of user IDs from all items in the set
func (set RecordSet) UserIDs() (IDs []uint64) {
IDs = make([]uint64, 0)
loop:
for i := range set {
for _, id := range IDs {
if id == set[i].OwnedBy {
continue loop
}
}
IDs = append(IDs, set[i].OwnedBy)
}
return
}
// Sets/updates module ptr
//
// Only if not previously set and if matches record specs
func (r *Record) SetModule(m *Module) {
if m == nil {
return
}
if (r.module == nil || r.module.ID == m.ID) && r.ModuleID == m.ID {
r.module = m
}
}
func (r *Record) GetModule() *Module {
return r.module
}
func (r Record) Clone() *Record {
c := &r
c.Values = r.Values.Clone()
return c
}
func (r *Record) getValue(name string, pos uint) (any, error) {
if r.Values == nil {
return nil, nil
}
if val := r.Values.Get(name, pos); val != nil {
return val.Value, nil
}
return nil, nil
}
func (r *Record) getValues(name string) ([]any, error) {
if r.Values == nil {
return nil, nil
}
if val := r.Values.GetAll(name); val != nil {
aux := make([]any, 0, len(val))
for _, v := range val {
aux = append(aux, v.Value)
}
return aux, nil
}
return nil, nil
}
// CountValues returns how many values per field are there
func (r *Record) CountValues() (pos map[string]uint) {
var (
mod = r.GetModule()
)
pos = map[string]uint{
"ID": 1,
"moduleID": 1,
"namespaceID": 1,
"meta": 1,
"revision": 1,
"createdAt": 1,
"createdBy": 1,
"updatedAt": 1,
"updatedBy": 1,
"deletedAt": 1,
"deletedBy": 1,
"ownedBy": 1,
}
// if mod == nil {
// // count record values
// // only when module is known
// return
// }
if mod != nil {
for _, f := range mod.Fields {
pos[f.Name] = 0
}
}
for _, val := range r.Values {
pos[val.Name]++
}
return
}
func (r *Record) setValue(name string, pos uint, value any) (err error) {
if reflect2.IsNil(value) {
r.Values, _ = r.Values.Filter(func(rv *RecordValue) (bool, error) {
if rv.Name == name && rv.Place == pos {
return false, nil
}
return true, nil
})
return
}
rv := &RecordValue{Name: name, Place: pos}
if cv, ok := value.(*RecordValue); ok {
rv = cv
} else {
var auxv string
switch aux := value.(type) {
case *time.Time:
auxv = aux.Format(time.RFC3339)
case time.Time:
auxv = aux.Format(time.RFC3339)
default:
auxv, err = cast.ToStringE(aux)
if r.module != nil {
f := r.module.Fields.FindByName(name)
if f != nil {
switch f.Kind {
case "DateTime":
// @note temporary solution to make timestamps consistent; we should handle
// timezones (or the lack of) more properly
parseAndFormat := func(value interface{}, format string) string {
auxt, err := cast.ToTimeE(value)
if err != nil || auxt.IsZero() {
return ""
}
return auxt.Format(format)
}
switch {
case f.IsDateOnly():
auxv = parseAndFormat(auxv, DateOnlyLayout)
case f.IsTimeOnly():
auxt, err := time.Parse(TimeOnlyLayout, auxv)
if err != nil || auxt.IsZero() {
auxv = ""
}
default:
auxv = parseAndFormat(auxv, time.RFC3339)
}
}
}
}
}
if err != nil {
return
}
rv.Value = auxv
}
// Try to utilize the module when possible
// It can be omitted for some cases for easier test cases
if r.module != nil {
f := r.module.Fields.FindByName(name)
if f != nil {
switch f.Kind {
case "Record", "User", "File":
rv.Ref = cast.ToUint64(value)
}
}
}
r.Values = r.Values.Set(rv)
return
}
func (r Record) Dict() map[string]interface{} {
dict := map[string]interface{}{
"ID": r.ID,
"recordID": r.ID,
"moduleID": r.ModuleID,
"revision": r.Revision,
"meta": r.Meta,
"namespaceID": r.NamespaceID,
"ownedBy": r.OwnedBy,
"createdAt": r.CreatedAt,
"createdBy": r.CreatedBy,
"updatedAt": r.UpdatedAt,
"updatedBy": r.UpdatedBy,
"deletedAt": r.DeletedAt,
"deletedBy": r.DeletedBy,
}
if r.module != nil {
dict["values"] = r.Values.Dict(r.module.Fields)
}
return dict
}
// UnmarshalJSON for custom record deserialization
//
// Due to https://github.com/golang/go/issues/21092, we should manually reset the given record value set.
// If this is skipped there is a chance of data corruption; ie. wrong value is removed/edited
func (r *Record) UnmarshalJSON(data []byte) error {
// Reset value set
r.Values = nil
// Deserialize to r (*Record) via auxRecord auxiliary record type alias
//
// This prevents inf. loop where json.Unmarshal directly on Record type
// calls this function
type auxRecord Record
return json.Unmarshal(data, &struct{ *auxRecord }{auxRecord: (*auxRecord)(r)})
}
func (m RecordSummary) MarshalJSON() ([]byte, error) {
out := map[string]any{
"name": m.Name,
}
switch m.Name {
case "min":
out["value"] = cast.ToString(m.Min)
case "max":
out["value"] = cast.ToString(m.Max)
case "avg":
out["value"] = cast.ToString(m.Avg)
case "sum":
out["value"] = cast.ToString(m.Sum)
case "emptyCount":
out["value"] = cast.ToString(m.EmptyCount)
case "notEmptyCount":
out["value"] = cast.ToString(m.NotEmptyCount)
case "count":
out["value"] = cast.ToString(m.Count)
case "uniqueCount":
out["value"] = cast.ToString(m.UniqueCount)
case "earliest":
out["value"] = cast.ToString(m.Earliest)
case "latest":
out["value"] = cast.ToString(m.Latest)
}
return json.Marshal(out)
}
// ToBulkOperations converts BulkRecordSet to a list of BulkRecordOperations
func (set RecordBulkSet) ToBulkOperations(dftModule uint64, dftNamespace uint64) (oo []*RecordBulkOperation, err error) {
for _, br := range set {
// can't use for loop's index, since some records can already have an ID
i := 0
for _, rr := range br.Set {
// No use in allowing cross-namespace record creation.
rr.NamespaceID = dftNamespace
// default module
if rr.ModuleID == 0 {
rr.ModuleID = dftModule
}
b := &RecordBulkOperation{
Record: rr,
Operation: OperationTypeUpdate,
LinkBy: br.RefField,
}
if rr.ID == 0 {
b.ID = fmt.Sprintf("%s:%d", br.IDPrefix, i)
i++
} else {
b.ID = strconv.FormatUint(rr.ID, 10)
}
// If no RecordID is defined, we should create it
if rr.ID == 0 {
b.Operation = OperationTypeCreate
}
if rr.DeletedAt != nil {
b.Operation = OperationTypeDelete
}
oo = append(oo, b)
}
}
return
}
// GetValuesByName filters values for records by names
func (set RecordSet) GetValuesByName(names ...string) (out map[uint64]RecordValueSet) {
nameMap := make(map[string]bool)
for _, n := range names {
if len(n) > 0 {
nameMap[n] = true
}
}
out = make(map[uint64]RecordValueSet)
err := set.Walk(func(rec *Record) error {
_ = rec.Values.Walk(func(val *RecordValue) error {
if val != nil && nameMap[val.Name] {
val.RecordID = rec.ID
out[val.RecordID] = append(out[val.RecordID], val)
}
return nil
})
return nil
})
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,346 @@
package types
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/str"
"github.com/spf13/cast"
)
type (
deDup struct {
ls localeService
}
localeService interface {
T(ctx context.Context, ns, key string, rr ...string) string
}
DeDupRule struct {
Name DeDupRuleName `json:"name"`
Strict bool `json:"strict"`
ErrorMessage string `json:"errorMessage"`
ConstraintSet DeDupRuleConstraintSet `json:"constraints"`
}
DeDupRuleConstraint struct {
Attribute string `json:"attribute"`
Modifier DeDupValueModifier `json:"modifier"`
MultiValue DeDupMultiValueConstraint `json:"multiValue"`
}
DeDupRuleConstraintSet []*DeDupRuleConstraint
// DeDupRuleName represent the identifier for duplicate detection rule
DeDupRuleName string
// DeDupValueModifier represent the algorithm used to check value string
DeDupValueModifier string
// DeDupMultiValueConstraint for matching multi values accordingly
DeDupMultiValueConstraint string
// DeDupIssueKind based on strict mode rule or duplication config
DeDupIssueKind string
)
const (
ignoreCase DeDupValueModifier = "ignore-case"
caseSensitive DeDupValueModifier = "case-sensitive"
fuzzyMatch DeDupValueModifier = "fuzzy-match"
soundsLike DeDupValueModifier = "sounds-like"
oneOf DeDupMultiValueConstraint = "one-of"
equal DeDupMultiValueConstraint = "equal"
deDupWarning DeDupIssueKind = "duplication_warning"
deDupError DeDupIssueKind = "duplication_error"
)
func DeDup() *deDup {
return &deDup{
ls: locale.Global(),
}
}
func (d deDup) CheckDuplication(ctx context.Context, rules DeDupRuleSet, rec Record, rr RecordSet) (out *RecordValueErrorSet, err error) {
out = &RecordValueErrorSet{}
err = rules.Walk(func(rule *DeDupRule) error {
if rule.HasAttributes() {
values := rr.GetValuesByName(distinct(rule.Attributes())...)
for valID, value := range values {
if valID == rec.ID {
continue
}
set := rule.validateValue(ctx, d.ls, rec, value)
if !set.IsValid() {
out.Push(set.Set...)
}
}
}
return nil
})
if err != nil {
return
}
if out.IsValid() {
out = nil
}
return
}
func (rule DeDupIssueKind) String() string {
return string(rule)
}
func (rule DeDupRule) HasAttributes() bool {
return len(rule.ConstraintSet) > 0 && len(rule.Attributes()) > 0
}
func (rule DeDupRule) Attributes() (out []string) {
for _, c := range rule.ConstraintSet {
out = append(out, c.Attribute)
}
return
}
func (rule DeDupRule) IsStrict() bool {
return rule.Strict
}
func (rule DeDupRule) IssueKind() string {
out := deDupWarning
if rule.Strict {
out = deDupError
}
return out.String()
}
func (rule DeDupRule) IssueMessage() (out string) {
return "record-field.errors.duplicateValue"
}
func (rule DeDupRule) IssueMultivalueMessage() (out string) {
return "record-field.errors.duplicateValueInSet"
}
func (rule DeDupRule) String() string {
return fmt.Sprintf("%s duplicate detection on `%s` field", rule.Name, strings.Join(rule.Attributes(), ", "))
}
// validateValue will check duplicate detection based on rules name
func (rule DeDupRule) validateValue(ctx context.Context, ls localeService, rec Record, vv RecordValueSet) (out *RecordValueErrorSet) {
return rule.checkDuplication(ctx, ls, rec, vv)
}
func (rule DeDupRule) checkDuplication(ctx context.Context, ls localeService, rec Record, vv RecordValueSet) (out *RecordValueErrorSet) {
var (
recVal = rec.Values
)
out = &RecordValueErrorSet{}
for _, c := range rule.ConstraintSet {
rvv := recVal.FilterByName(c.Attribute)
if rvv.Len() == 0 {
continue
}
var (
valErr = &RecordValueErrorSet{}
)
existingVv := vv.FilterByName(c.Attribute)
moduleField := rec.module.Fields.FindByName(c.Attribute)
if moduleField.Multi && c.IsAllEqual() {
multiValErr := rule.multiValueAllEqual(ctx, ls, c, rvv, existingVv)
out.Push(multiValErr)
} else {
rvvValueMap := make(map[string]*RecordValue)
_ = rvv.Walk(func(rv *RecordValue) error {
if len(rv.Value) > 0 {
rvvValueMap[rv.Value] = rv
}
return nil
})
_ = vv.Walk(func(v *RecordValue) error {
if v.RecordID == rec.ID {
return nil
}
if rv, exists := rvvValueMap[v.Value]; exists && matchValue(c.Modifier, rv.Value, v.Value) {
valErr.Push(RecordValueError{
Kind: rule.IssueKind(),
Message: ls.T(ctx, "compose", rule.IssueMessage()),
Meta: map[string]interface{}{
"field": v.Name,
"value": v.Value,
"dupValueField": rv.Name,
"recordID": cast.ToString(v.RecordID),
"rule": rule.String(),
},
})
}
// 1. multiValue is empty, then all value needs to be a match then return error/warning
// 2. multiValue is oneOf, then one or more value needs to be a match then return error/warning
// 3. multiValue is equal, then all value needs to be a match then return error/warning
if (!valErr.IsValid() && (!c.HasMultiValue() || c.IsAllEqual()) && valErr.Len() == rvv.Len()) || (c.IsOneOf() && valErr.Len() > 0) {
out.Push(valErr.Set...)
}
return nil
})
}
}
return
}
func (rule DeDupRule) multiValueAllEqual(ctx context.Context, ls localeService, c *DeDupRuleConstraint, rvv RecordValueSet, existingVv RecordValueSet) (out RecordValueError) {
var valErr = RecordValueError{}
if rvv.Len() != existingVv.Len() {
return valErr
}
rvvmap := make(map[string]int)
existingVvmap := make(map[string]int)
dupValues := recordValueFrequencyMap(rvvmap, c.Modifier, rvv)
_ = recordValueFrequencyMap(existingVvmap, c.Modifier, existingVv)
if matchRecordValueFrequencyMap(rvvmap, existingVvmap) {
valErr = RecordValueError{
Kind: rule.IssueKind(),
Message: ls.T(ctx, "compose", rule.IssueMultivalueMessage()),
Meta: map[string]interface{}{
"field": c.Attribute,
"value": strings.Join(dupValues, ", "),
"dupValueField": c.Attribute,
"rule": rule.String(),
},
}
return valErr
}
return valErr
}
func (dr DeDupRuleSet) Validate() (err error) {
return dr.Walk(func(rule *DeDupRule) (err error) {
if !rule.HasAttributes() {
err = fmt.Errorf("deduplication not valid (no constraints)")
return
}
for _, a := range rule.Attributes() {
if a == "" {
err = fmt.Errorf("deduplication not valid (invalid field)")
return
}
}
return
})
}
func (c DeDupRuleConstraint) HasMultiValue() bool {
switch c.MultiValue {
case oneOf, equal:
return true
default:
return false
}
}
func (c DeDupRuleConstraint) IsAllEqual() bool {
return c.MultiValue == equal
}
func (c DeDupRuleConstraint) IsOneOf() bool {
return c.MultiValue == oneOf
}
func (v *RecordValueErrorSet) SetMetaID(id uint64) {
if v.IsValid() {
return
}
for _, val := range v.Set {
if val.Meta != nil {
if _, ok := val.Meta["id"]; !ok {
val.Meta["id"] = cast.ToString(id)
}
}
}
}
func (v *RecordValueErrorSet) HasStrictErrors() bool {
return v.HasKind(deDupError.String())
}
// distinct only list the different (distinct) values
func distinct(input []string) (out []string) {
keys := make(map[string]bool)
for _, val := range input {
if _, ok := keys[val]; !ok {
keys[val] = true
out = append(out, val)
}
}
return
}
// matchValue will check if the input matches with target string as per the modifier
func matchValue(modifier DeDupValueModifier, input string, target string) bool {
switch modifier {
case ignoreCase:
return str.Match(input, target, str.CaseInSensitiveMatch)
case caseSensitive:
return str.Match(input, target, str.CaseSensitiveMatch)
case fuzzyMatch:
return str.Match(input, target, str.LevenshteinDistance)
case soundsLike:
return str.Match(input, target, str.Soundex)
default:
// ignoreCase as default, if not specified
return str.Match(input, target, str.CaseInSensitiveMatch)
}
}
func recordValueFrequencyMap(rvFreqMap map[string]int, c DeDupValueModifier, vv RecordValueSet) (values []string) {
for _, v := range vv {
values = append(values, v.Value)
if c == ignoreCase {
v.Value = strings.ToLower(v.Value)
}
rvFreqMap[v.Value]++
}
return values
}
func matchRecordValueFrequencyMap(a, b map[string]int) (ok bool) {
for k, keyCount := range a {
_, ok = b[k]
if !ok || keyCount != b[k] {
return false
}
}
return true
}

View File

@@ -0,0 +1,444 @@
package types
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/cortezaproject/corteza/server/pkg/sql"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/spf13/cast"
)
type (
// RecordValue is a stored row in the `record_value` table
RecordValue struct {
RecordID uint64 `json:"-"`
Name string `json:"name"`
Value string `json:"value,omitempty"`
Ref uint64 `json:"-"`
Place uint `json:"place,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
Updated bool `json:"-"`
OldValue string `json:"-"`
}
RecordValueFilter struct {
RecordID []uint64
Deleted filter.State `json:"deleted"`
Limit uint
}
)
func (v RecordValue) IsUpdated() bool {
return v.Updated
}
func (v RecordValue) IsDeleted() bool {
return v.DeletedAt != nil
}
func (v RecordValue) Clone() *RecordValue {
return &RecordValue{
RecordID: v.RecordID,
Name: v.Name,
Value: v.Value,
Ref: v.Ref,
Place: v.Place,
DeletedAt: v.DeletedAt,
Updated: v.Updated,
OldValue: v.OldValue,
}
}
func (v RecordValue) Cast(f *ModuleField) (interface{}, error) {
if f == nil {
// safe fallback to string
return v.Value, nil
}
switch {
case f.IsRef():
if v.Ref == 0 && len(v.Value) > 0 {
// Cover cases when we Ref is not set but Value is
// This happens when RVS is transferred as JSON
v.Ref, _ = strconv.ParseUint(v.Value, 10, 64)
}
return v.Ref, nil
case f.IsDateTime():
if v.Value == "" {
return nil, nil
}
return cast.ToTimeE(v.Value)
case f.IsDateOnly():
if v.Value == "" {
return nil, nil
}
return time.Parse(DateOnlyLayout, v.Value)
case f.IsTimeOnly():
if v.Value == "" {
return nil, nil
}
return time.Parse(TimeOnlyLayout, v.Value)
case f.IsBoolean():
return expr.CastToBoolean(v.Value)
case f.IsNumeric():
// @todo !! This is temporary; precision and scale require a rework
if f.Options.Precision() == 0 {
return cast.ToInt64E(v.Value)
}
return cast.ToFloat64E(v.Value)
default:
return v.Value, nil
}
}
func (set RecordValueSet) Clone() (vv RecordValueSet) {
vv = make(RecordValueSet, len(set))
for i := range set {
vv[i] = set[i].Clone()
}
return
}
func (set RecordValueSet) FilterByName(name string) (vv RecordValueSet) {
for i := range set {
if set[i].Name == name {
vv = append(vv, set[i])
}
}
return
}
func (set RecordValueSet) FilterByRecordID(recordID uint64) (vv RecordValueSet) {
// Make sure we never return nil
vv = RecordValueSet{}
for i := range set {
if set[i].RecordID == recordID {
vv = append(vv, set[i])
}
}
return
}
// Replace existing values, remove extra
func (set RecordValueSet) Replace(name string, values ...string) (vv RecordValueSet) {
for i := range set {
if set[i].Name != name {
// copy values from other fields
vv = append(vv, set[i])
}
}
for p, v := range values {
vv = append(vv, &RecordValue{
Name: name,
Value: v,
Place: uint(p),
Updated: true,
})
}
return
}
// Set updates existing value or creates a new one
func (set RecordValueSet) Set(v *RecordValue) RecordValueSet {
for i := range set {
if set[i].Name != v.Name {
continue
}
if set[i].Place != v.Place {
continue
}
// Update existing entry
set[i] = v
return set
}
// Append new value
return append(set, v)
}
// Get value set?
func (set RecordValueSet) Get(name string, place uint) *RecordValue {
for i := range set {
if set[i].Name != name {
continue
}
if set[i].Place != place {
continue
}
return set[i]
}
return nil
}
func (set RecordValueSet) GetAll(name string) (out []*RecordValue) {
for i := range set {
if set[i].Name != name {
continue
}
out = append(out, set[i])
}
return
}
// Has value set?
func (set RecordValueSet) Has(name string, place uint) bool {
for i := range set {
if set[i].Name != name {
continue
}
if set[i].Place != place {
continue
}
return true
}
return false
}
func (set RecordValueSet) SetRecordID(recordID uint64) {
for i := range set {
set[i].RecordID = recordID
}
}
func (set RecordValueSet) SetUpdatedFlag(updated bool) {
for i := range set {
set[i].Updated = updated
}
}
func (set RecordValueSet) GetUpdated() (out RecordValueSet) {
out = make([]*RecordValue, 0, len(set))
for i := range set {
if !set[i].Updated {
continue
}
out = append(out, set[i])
}
// Append new value
return out
}
func (set RecordValueSet) GetClean() (out RecordValueSet) {
out = make([]*RecordValue, 0, len(set))
for s := range set {
if set[s].DeletedAt != nil {
continue
}
out = append(out, set[s].Clone())
}
return out
}
// Merge merges old value set with new one and expects unchanged values to be in the new set
func (set RecordValueSet) Merge(mfs ModuleFieldSet, new RecordValueSet, canAccessField func(f *ModuleField) bool) (out RecordValueSet) {
new, _ = new.Filter(func(v *RecordValue) (bool, error) {
return mfs.HasName(v.Name), nil
})
// Value merge process does not know anything about permissions so
// in case when new values are missing but do exist in the old set and their update/read is denied
// we need to copy them to ensure value merge process them correctly
for _, f := range mfs {
if len(new.FilterByName(f.Name)) == 0 && !canAccessField(f) {
// copy all fields from old to new
new = append(new, set.FilterByName(f.Name).GetClean()...)
}
}
return set.merge(new)
}
// Raw merge of old and one and new values, skipping unchanged
//
// This satisfies current requirements where record values are always
// manipulated as a whole (not partial)
func (set RecordValueSet) merge(new RecordValueSet) (out RecordValueSet) {
if len(set) == 0 {
// Empty set, copy all new values and return them
for i := range new {
new[i].Updated = true
}
return new
}
out = make([]*RecordValue, 0)
for s := range set {
// Mark all old as updated
out = append(out, &RecordValue{
Name: set[s].Name,
Value: set[s].Value,
Ref: set[s].Ref,
Place: set[s].Place,
DeletedAt: &time.Time{},
Updated: true,
OldValue: set[s].Value,
})
}
for n := range new {
if ex := out.Get(new[n].Name, new[n].Place); ex != nil {
// Reset deleted flag
ex.DeletedAt = new[n].DeletedAt
if ex.OldValue == new[n].Value {
// Value is the same
ex.Updated = false
} else if !ex.Updated {
// Value changed and old one was not marked as updated before
// See if values really changed and update old value on existing value
ex.Updated = ex.Value != new[n].Value
ex.OldValue = ex.Value
}
ex.Value = new[n].Value
ex.Ref = new[n].Ref
} else {
// Value not previously set, make new
out = append(out, &RecordValue{
Name: new[n].Name,
Value: new[n].Value,
Ref: new[n].Ref,
Place: new[n].Place,
Updated: true,
// verbose & explicit for clarity
OldValue: "",
DeletedAt: nil,
})
}
}
return out
}
func (set *RecordValueSet) Scan(src any) error { return sql.ParseJSON(src, set) }
func (set RecordValueSet) Value() (driver.Value, error) { return json.Marshal(set) }
// Simple RVS as string output utility fn that
// can help with debugging
func (set RecordValueSet) String() (o string) {
if set == nil {
return "<RecordValueSet = nil>"
}
is := func(in interface{}) string {
switch in := in.(type) {
case bool:
if in {
return "✔"
}
case *time.Time:
if in != nil {
return "✔"
}
}
return "x"
}
o += "━━━━━━━━━━━┳━━━━┳━━━┳━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
o += "name ┃ ## ┃ u ┃ d ┃ value ┃ ref ┃ old value \n"
o += "━━━━━━━━━━━╋━━━━╋━━━╋━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
const tpl = "%-10s ┃ %2d ┃ %s ┃ %s ┃ %-25s ┃ %-20d ┃ %-25s\n"
for _, v := range set {
if v == nil {
o += "<--------> ┃ -- ┃ - ┃ - ┃ <------------------------> ┃ <------------------> ┃ <------------------> \n"
continue
}
o += fmt.Sprintf(
tpl,
v.Name,
v.Place,
is(v.Updated),
is(v.DeletedAt),
v.Value,
v.Ref,
v.OldValue,
)
}
o += "━━━━━━━━━━━┻━━━━┻━━━┻━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
return o
}
// Returns structured representation of values casted to the appropriate types
func (set RecordValueSet) Dict(fields ModuleFieldSet) map[string]interface{} {
var (
rval = make(map[string]interface{})
)
if len(fields) == 0 {
return rval
}
_ = fields.Walk(func(f *ModuleField) error {
// make sure all fields are set at least to nil
rval[f.Name] = nil
if f.Multi {
var (
rv = set.FilterByName(f.Name)
vv = make([]interface{}, len(rv))
)
for i, val := range rv {
vv[i], _ = val.Cast(f)
}
rval[f.Name] = vv
} else if v := set.Get(f.Name, 0); v != nil {
rval[f.Name], _ = v.Cast(f)
}
return nil
})
return rval
}
func (set RecordValueSet) Len() int { return len(set) }
func (set RecordValueSet) Swap(i, j int) { set[i], set[j] = set[j], set[i] }
func (set RecordValueSet) Less(i, j int) bool {
n := strings.Compare(set[i].Name, set[j].Name)
if n != 0 {
return n < 0
}
return set[i].Place < set[j].Place
}

View File

@@ -0,0 +1,20 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
const (
AttachmentResourceType = "corteza::compose:attachment"
ChartResourceType = "corteza::compose:chart"
ModuleResourceType = "corteza::compose:module"
ModuleFieldResourceType = "corteza::compose:module-field"
NamespaceResourceType = "corteza::compose:namespace"
PageResourceType = "corteza::compose:page"
PageLayoutResourceType = "corteza::compose:page-layout"
RecordResourceType = "corteza::compose:record"
RecordRevisionResourceType = "corteza::compose:record-revision"
ComponentResourceType = "corteza::compose"
)

View File

@@ -0,0 +1,153 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// compose/types/types.yaml
// SetLabel adds new label to label map
func (m *Chart) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m Chart) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Chart) LabelResourceKind() string {
return "compose:chart"
}
// GetLabels adds new label to label map
func (m Chart) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Module) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m Module) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Module) LabelResourceKind() string {
return "compose:module"
}
// GetLabels adds new label to label map
func (m Module) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *ModuleField) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m ModuleField) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (ModuleField) LabelResourceKind() string {
return "compose:module:field"
}
// GetLabels adds new label to label map
func (m ModuleField) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Namespace) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m Namespace) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Namespace) LabelResourceKind() string {
return "compose:namespace"
}
// GetLabels adds new label to label map
func (m Namespace) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Page) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m Page) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Page) LabelResourceKind() string {
return "compose:page"
}
// GetLabels adds new label to label map
func (m Page) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *PageLayout) SetLabel(key string, value string) {
if m.Labels == nil {
m.Labels = make(map[string]string)
}
m.Labels[key] = value
}
// GetLabels adds new label to label map
func (m PageLayout) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (PageLayout) LabelResourceKind() string {
return "compose:page-layout"
}
// GetLabels adds new label to label map
func (m PageLayout) LabelResourceID() uint64 {
return m.ID
}

View File

@@ -0,0 +1,640 @@
package types
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
// Definitions file that controls how this file is generated:
// compose/types/types.yaml
type (
// AttachmentSet slice of Attachment
//
// This type is auto-generated.
AttachmentSet []*Attachment
// ChartSet slice of Chart
//
// This type is auto-generated.
ChartSet []*Chart
// DeDupRuleSet slice of DeDupRule
//
// This type is auto-generated.
DeDupRuleSet []*DeDupRule
// IconSet slice of Icon
//
// This type is auto-generated.
IconSet []*Icon
// ModuleSet slice of Module
//
// This type is auto-generated.
ModuleSet []*Module
// ModuleFieldSet slice of ModuleField
//
// This type is auto-generated.
ModuleFieldSet []*ModuleField
// NamespaceSet slice of Namespace
//
// This type is auto-generated.
NamespaceSet []*Namespace
// PageSet slice of Page
//
// This type is auto-generated.
PageSet []*Page
// PageLayoutSet slice of PageLayout
//
// This type is auto-generated.
PageLayoutSet []*PageLayout
// PrivacyModuleSet slice of PrivacyModule
//
// This type is auto-generated.
PrivacyModuleSet []*PrivacyModule
// RecordSet slice of Record
//
// This type is auto-generated.
RecordSet []*Record
// RecordValueSet slice of RecordValue
//
// This type is auto-generated.
RecordValueSet []*RecordValue
)
// Walk iterates through every slice item and calls w(Attachment) err
//
// This function is auto-generated.
func (set AttachmentSet) Walk(w func(*Attachment) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Attachment) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set AttachmentSet) Filter(f func(*Attachment) (bool, error)) (out AttachmentSet, err error) {
var ok bool
out = AttachmentSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set AttachmentSet) FindByID(ID uint64) *Attachment {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set AttachmentSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(Chart) err
//
// This function is auto-generated.
func (set ChartSet) Walk(w func(*Chart) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Chart) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ChartSet) Filter(f func(*Chart) (bool, error)) (out ChartSet, err error) {
var ok bool
out = ChartSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ChartSet) FindByID(ID uint64) *Chart {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ChartSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(DeDupRule) err
//
// This function is auto-generated.
func (set DeDupRuleSet) Walk(w func(*DeDupRule) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(DeDupRule) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set DeDupRuleSet) Filter(f func(*DeDupRule) (bool, error)) (out DeDupRuleSet, err error) {
var ok bool
out = DeDupRuleSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// Walk iterates through every slice item and calls w(Icon) err
//
// This function is auto-generated.
func (set IconSet) Walk(w func(*Icon) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Icon) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set IconSet) Filter(f func(*Icon) (bool, error)) (out IconSet, err error) {
var ok bool
out = IconSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// Walk iterates through every slice item and calls w(Module) err
//
// This function is auto-generated.
func (set ModuleSet) Walk(w func(*Module) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Module) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ModuleSet) Filter(f func(*Module) (bool, error)) (out ModuleSet, err error) {
var ok bool
out = ModuleSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ModuleSet) FindByID(ID uint64) *Module {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ModuleSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(ModuleField) err
//
// This function is auto-generated.
func (set ModuleFieldSet) Walk(w func(*ModuleField) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(ModuleField) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ModuleFieldSet) Filter(f func(*ModuleField) (bool, error)) (out ModuleFieldSet, err error) {
var ok bool
out = ModuleFieldSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ModuleFieldSet) FindByID(ID uint64) *ModuleField {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ModuleFieldSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(Namespace) err
//
// This function is auto-generated.
func (set NamespaceSet) Walk(w func(*Namespace) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Namespace) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set NamespaceSet) Filter(f func(*Namespace) (bool, error)) (out NamespaceSet, err error) {
var ok bool
out = NamespaceSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set NamespaceSet) FindByID(ID uint64) *Namespace {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set NamespaceSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(Page) err
//
// This function is auto-generated.
func (set PageSet) Walk(w func(*Page) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Page) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set PageSet) Filter(f func(*Page) (bool, error)) (out PageSet, err error) {
var ok bool
out = PageSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set PageSet) FindByID(ID uint64) *Page {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set PageSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(PageLayout) err
//
// This function is auto-generated.
func (set PageLayoutSet) Walk(w func(*PageLayout) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(PageLayout) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set PageLayoutSet) Filter(f func(*PageLayout) (bool, error)) (out PageLayoutSet, err error) {
var ok bool
out = PageLayoutSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set PageLayoutSet) FindByID(ID uint64) *PageLayout {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set PageLayoutSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(PrivacyModule) err
//
// This function is auto-generated.
func (set PrivacyModuleSet) Walk(w func(*PrivacyModule) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(PrivacyModule) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set PrivacyModuleSet) Filter(f func(*PrivacyModule) (bool, error)) (out PrivacyModuleSet, err error) {
var ok bool
out = PrivacyModuleSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// Walk iterates through every slice item and calls w(Record) err
//
// This function is auto-generated.
func (set RecordSet) Walk(w func(*Record) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Record) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set RecordSet) Filter(f func(*Record) (bool, error)) (out RecordSet, err error) {
var ok bool
out = RecordSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set RecordSet) FindByID(ID uint64) *Record {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set RecordSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(RecordValue) err
//
// This function is auto-generated.
func (set RecordValueSet) Walk(w func(*RecordValue) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(RecordValue) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set RecordValueSet) Filter(f func(*RecordValue) (bool, error)) (out RecordValueSet, err error) {
var ok bool
out = RecordValueSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}

View File

@@ -0,0 +1,23 @@
types:
Namespace:
labelResourceType: compose:namespace
Attachment: {}
Module:
labelResourceType: compose:module
ModuleField:
labelResourceType: compose:module:field
Page:
labelResourceType: compose:page
Icon:
noIdField: true
PageLayout:
labelResourceType: compose:page-layout
Chart:
labelResourceType: compose:chart
Record: {}
RecordValue:
noIdField: true
PrivacyModule:
noIdField: true
DeDupRule:
noIdField: true

View File

@@ -0,0 +1,104 @@
package types
import (
"encoding/json"
"errors"
"fmt"
)
type (
RecordValueError struct {
Kind string `json:"kind"`
Message string `json:"message"`
Meta map[string]interface{} `json:"meta"`
}
RecordValueErrorSet struct {
Set []RecordValueError `json:"set"`
}
)
// safe to show details of this error
func (RecordValueErrorSet) Safe() bool { return true }
func (v *RecordValueErrorSet) Push(err ...RecordValueError) {
v.Set = append(v.Set, err...)
}
func (v *RecordValueErrorSet) IsValid() bool {
return v == nil || len(v.Set) == 0
}
func (v *RecordValueErrorSet) Len() int {
if v == nil {
return 0
}
return len(v.Set)
}
func (v *RecordValueErrorSet) Error() string {
var no = 0
if v != nil {
no = len(v.Set)
}
return fmt.Sprintf("%d issue(s) found", no)
}
func (v RecordValueErrorSet) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Message string `json:"message"`
Set []RecordValueError `json:"set,omitempty"`
}{
Message: v.Error(),
Set: v.Set,
})
}
func (v *RecordValueErrorSet) HasKind(kind string) bool {
if v == nil || v.IsValid() {
return false
}
for _, e := range v.Set {
if e.Kind == kind {
return true
}
}
return false
}
func (v *RecordValueErrorSet) Merge(errs ...*RecordValueErrorSet) {
if v == nil {
return
}
for _, e := range errs {
if e == nil || e.IsValid() {
continue
}
v.Push(e.Set...)
}
}
// IsRecordValueErrorSet tests if given error is RecordValueErrorSet (or it wraps it) and it has errors
// If not is not (or !IsValid), it return nil!
func IsRecordValueErrorSet(err error) *RecordValueErrorSet {
for {
if err == nil {
return nil
}
if rve, ok := err.(*RecordValueErrorSet); ok {
if rve.IsValid() {
return nil
}
return rve
}
err = errors.Unwrap(err)
}
}

View File

@@ -0,0 +1,24 @@
package api
import (
"context"
"net/http"
)
// Debug context
type ctxKeyDebug struct{}
// Packs remote address to context
func DebugToContext(production bool) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ctxKeyDebug{}, !production)))
})
}
}
// DebugFromContext returns remote IP address from context
func DebugFromContext(ctx context.Context) bool {
debug, ok := ctx.Value(ctxKeyDebug{}).(bool)
return ok && debug
}

View File

@@ -0,0 +1,29 @@
package api
import (
"context"
"net/http"
)
// Key to use when setting the request ID.
type ctxKeyRemoteAddr int
// RemoteAddrKey is the key that holds th unique request ID in a request context.
const remoteAddrKey ctxKeyRemoteAddr = 0
// Packs remote address to context
func RemoteAddrToContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), remoteAddrKey, req.RemoteAddr)))
})
}
// RemoteAddrFromContext returns remote IP address from context
func RemoteAddrFromContext(ctx context.Context) string {
v := ctx.Value(remoteAddrKey)
if str, ok := v.(string); ok {
return str
}
return ""
}

View File

@@ -0,0 +1,170 @@
package api
// This code is modified version from
// https://github.com/titpetric/factory/tree/master/resputil
//
// Parts of the code are rewritten to allow greater flexibility
// but the general logic stays the same for now
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/errors"
"net/http"
)
type (
successWrap struct {
Success struct {
Message string `json:"message"`
} `json:"success"`
}
CallFrame struct {
Function string `json:"function"`
File string `json:"file"`
Line int `json:"line"`
}
ErrorPayload struct {
Message string `json:"message"`
Context map[string]interface{} `json:"context,omitempty"`
Callstack []*CallFrame `json:"callstack,omitempty"`
}
)
// Success returns a structured success message for API responses
func Success(success ...string) *successWrap {
response := &successWrap{}
response.Success.Message = "OK"
if len(success) > 0 {
response.Success.Message = success[0]
}
return response
}
// OK returns the default Success message
func OK() *successWrap {
return Success()
}
// Writes response, according to type
//
// Primarily this function encodes given payload (directly or indirectly) as compact JSON
//
// In some specific scenarios, when:
// - debug mode is enabled,
// - no explicit accept header with /json mime-type is sent
// and,
//
// if payload is an error:
// it outputs formatted error with extended info
//
// if payload is non-error:
// it outputs formatted and indented JSON
//
func encode(w http.ResponseWriter, r *http.Request, payload interface{}) {
var (
err error
enc = json.NewEncoder(w)
)
switch c := payload.(type) {
case error:
err = c
case *successWrap:
// main key is "success"
w.Header().Add("Content-Type", "application/json")
if err = enc.Encode(c); err != nil {
err = fmt.Errorf("failed to encode response: %w", err)
}
default:
w.Header().Add("Content-Type", "application/json")
// main key is "response"
aux := struct {
Response interface{} `json:"response"`
}{c}
if err = enc.Encode(aux); err != nil {
err = fmt.Errorf("failed to encode response: %w", err)
}
}
if err != nil {
if err, is := err.(*errors.Error); is {
// trim out the base stack we don't care about...
_ = err.Apply(errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"))
}
errors.ServeHTTP(w, r, err, !DebugFromContext(r.Context()))
return
}
}
// Send handles first non-nil (and non-empty) payload and encodes it or it's results (when fn)
//
// See encode() for details
func Send(w http.ResponseWriter, r *http.Request, rr ...interface{}) {
for _, rsp := range rr {
switch c := rsp.(type) {
case nil:
// this will match a nil error
continue
case *successWrap:
encode(w, r, c)
case func(http.ResponseWriter, *http.Request):
c(w, r)
case func() ([]byte, error):
result, err := c()
Send(w, r, err, result)
case func() (interface{}, error):
result, err := c()
Send(w, r, err, result)
case func() error:
err := c()
if err == nil {
continue
}
encode(w, r, err)
case error:
encode(w, r, c)
case []byte:
if len(c) == 0 {
continue
}
if _, err := w.Write(c); err != nil {
panic(err)
}
return
case string:
if c == "" {
continue
}
encode(w, r, c)
case bool:
if !c {
continue
}
encode(w, r, c)
default:
encode(w, r, c)
}
// Exit on the first output...
return
}
encode(w, r, false)
}

View File

@@ -0,0 +1,13 @@
package cast2
import "github.com/spf13/cast"
func Bool(in any, out *bool) error {
aux, err := cast.ToBoolE(in)
if err != nil {
return err
}
*out = aux
return nil
}

View File

@@ -0,0 +1,33 @@
package cast2
import "github.com/spf13/cast"
func Uint64(in any, out *uint64) error {
aux, err := cast.ToUint64E(in)
if err != nil {
return err
}
*out = aux
return nil
}
func Uint(in any, out *uint) error {
aux, err := cast.ToUintE(in)
if err != nil {
return err
}
*out = aux
return nil
}
func Int(in any, out *int) error {
aux, err := cast.ToIntE(in)
if err != nil {
return err
}
*out = aux
return nil
}

View File

@@ -0,0 +1,32 @@
package cast2
import (
"encoding/json"
"fmt"
"github.com/modern-go/reflect2"
)
// Meta casts a value to a map[string]any
func Meta(in any, out *map[string]any) (err error) {
if reflect2.IsNil(in) {
*out = nil
return nil
}
switch aux := in.(type) {
case []byte:
err = json.Unmarshal(aux, out)
case string:
err = json.Unmarshal([]byte(aux), out)
case map[string]any:
*out = aux
default:
err = fmt.Errorf("unsupported type: %T", in)
}
if err == nil {
return
}
return fmt.Errorf("can not cast to Meta: %w", err)
}

View File

@@ -0,0 +1,12 @@
package cast2
// Anys converts any kinds of values to a []any slice
func Anys[C any](in ...C) (out []any) {
out = make([]any, len(in))
for i := range in {
out[i] = in[i]
}
return
}

View File

@@ -0,0 +1,13 @@
package cast2
import "github.com/spf13/cast"
func String(in any, out *string) error {
aux, err := cast.ToStringE(in)
if err != nil {
return err
}
*out = aux
return nil
}

View File

@@ -0,0 +1,40 @@
package cast2
import (
"github.com/modern-go/reflect2"
"github.com/spf13/cast"
"time"
)
func Time(in any, out *time.Time) error {
if reflect2.IsNil(in) {
if out != nil {
*out = time.Time{}
}
return nil
}
aux, err := cast.ToTimeE(in)
if err != nil {
return err
}
*out = aux
return nil
}
func TimePtr(in any, out **time.Time) error {
if reflect2.IsNil(in) {
*out = nil
return nil
}
aux, err := cast.ToTimeE(in)
if err != nil {
return err
}
*out = &aux
return nil
}

View File

@@ -0,0 +1,95 @@
package cli
import (
"github.com/cortezaproject/corteza/server/pkg/version"
"github.com/spf13/cobra"
)
var (
rootCommand = &cobra.Command{
Use: "corteza-server",
Aliases: []string{"corteza", "server"},
TraverseChildren: true,
}
serveApiCommand = &cobra.Command{
Use: "serve-api",
Aliases: []string{"serve"},
Short: "Start HTTP server with REST API",
}
upgradeCommand = &cobra.Command{
Use: "upgrade",
Short: "Upgrade tasks",
}
provisionCommand = &cobra.Command{
Use: "provision",
Short: "Provision tasks",
}
versionCommand = &cobra.Command{
Use: "version",
Short: "Version",
}
// List of commands that do not
// run initialization (db connection, etc...)
light = map[string]bool{
"help": true,
versionCommand.Use: true,
}
)
// RootCommand creates root command with a simple persistent-pre-run callback
//
// Callback is called when not executed with help subcommand
func RootCommand(ppRunEfn func() error) *cobra.Command {
rootCommand.PersistentPreRunE = func(cmd *cobra.Command, args []string) (err error) {
if light[cmd.Name()] {
// Do not run hooks on parts on help command
return nil
}
if ppRunEfn != nil {
return ppRunEfn()
}
return nil
}
return rootCommand
}
func ServeCommand(runEfn func() error) *cobra.Command {
serveApiCommand.RunE = func(cmd *cobra.Command, args []string) (err error) {
return runEfn()
}
return serveApiCommand
}
func UpgradeCommand(dbfn func() error) *cobra.Command {
upgradeCommand.RunE = func(cmd *cobra.Command, args []string) (err error) {
return dbfn()
}
return upgradeCommand
}
func ProvisionCommand(cffn func() error) *cobra.Command {
provisionCommand.RunE = func(cmd *cobra.Command, args []string) (err error) {
return cffn()
}
return provisionCommand
}
func VersionCommand() *cobra.Command {
versionCommand.Run = func(cmd *cobra.Command, args []string) {
cmd.Println(version.Version)
}
return versionCommand
}

View File

@@ -0,0 +1,16 @@
package cli
import (
"context"
ctxwrap "github.com/SentimensRG/ctx"
"github.com/SentimensRG/ctx/sigctx"
)
// Context is small wrapper that returns sig-term bound context
//
// This can be used as (proper) background context that properly terminates
// all subroutines.
func Context() context.Context {
return ctxwrap.AsContext(sigctx.New())
}

View File

@@ -0,0 +1,57 @@
package cli
import (
"os"
"path"
"sort"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/joho/godotenv"
"github.com/spf13/cobra"
)
// LoadEnv loads env-variables (KEY=VAL) from provided files and paths (by searching for .env file in the given path)
//
// Please note that loaded values
// DO NOT OVERRIDE the existing
// environmental variables
func LoadEnv(pp ...string) error {
// preparse the input and try to figure out if .env should be appended
var checked = make([]string, 0, len(pp))
for _, p := range pp {
if s, err := os.Stat(p); err != nil {
return err
} else if s.IsDir() {
chk := path.Join(p, ".env")
if _, err = os.Stat(chk); err == nil {
// make sure only .env files
checked = append(checked, chk)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
} else {
checked = append(checked, p)
}
}
if len(checked) == 0 {
return nil
}
return godotenv.Load(checked...)
}
// EnvCommand outputs all loaded env variables
func EnvCommand() *cobra.Command {
return &cobra.Command{
Use: "env",
Long: "Outputs list (sorted by key) of of all environmental variables. Can be used for diagnosis and debugging",
Run: func(cmd *cobra.Command, args []string) {
kv := os.Environ()
sort.Strings(kv)
for _, l := range kv {
cmd.Println(l)
}
},
}
}

View File

@@ -0,0 +1,28 @@
package cli
import (
"errors"
"fmt"
"os"
)
func HandleError(err error) {
if err = unwrapGeneric(err); err == nil {
return
}
_, _ = fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
func unwrapGeneric(err error) error {
for {
g, ok := err.(interface{ IsGeneric() bool })
if ok && g != nil && g.IsGeneric() {
err = errors.Unwrap(err)
continue
}
return err
}
}

View File

@@ -0,0 +1,359 @@
package dal
import (
"context"
"fmt"
"math"
"strings"
"github.com/cortezaproject/corteza/server/pkg/ql"
"github.com/modern-go/reflect2"
"github.com/spf13/cast"
)
type (
// aggregator performs value aggregation primarily used for the aggregate step
// @todo consider moving into a separate package
//
// The aggregator performs the requested value expressions as well as
// aggregation operations over the evaluated values.
//
// The aggregator computes the values on the fly (for the ones it can).
aggregator struct {
// aggregates holds output aggregation values
// @note we'll use float64 for all values, but it might make more sense to split it up
// in the future. For now, it'll be ok.
aggregates []float64
// counts holds the number of values for each aggregate including multi value fields.
// Counts are currently only used for average.
counts []int
// def provides a list of aggregates.
//
// Each index corresponds to the aggregates and counts slices
def []aggregateDef
// scanned indicates whether the aggregator has been scanned since
// we need to block writes after the first scan.
scanned bool
}
// aggregateDef is a wrapper for the provided value aggregation
aggregateDef struct {
outIdent string
aggOp string
inIdent string
eval evaluator
}
)
var (
// aggregateFunctionIndex specifies all of the registered aggregate functions
//
// @todo consider making this expandable via some registry/plugin/...
aggregateFunctionIndex = map[string]bool{
"count": true,
"sum": true,
"min": true,
"max": true,
"avg": true,
}
)
// Aggregator initializes a new aggregator for the given set of mappings
//
// The aggregator is not routine safe; consider defining multiple aggregators
// and then combining them together.
func Aggregator() *aggregator {
return &aggregator{
aggregates: make([]float64, 0, 16),
counts: make([]int, 0, 16),
}
}
// AddAggregateE adds a new aggregate from a raw expression
func (a *aggregator) AddAggregateE(ident, expr string) (err error) {
// @note the converter is reused so we can safely do this
n, err := newConverterGval().Parse(expr)
if err != nil {
return
}
return a.AddAggregate(ident, n)
}
// AddAggregate adds a new aggregate from an already parsed expression
func (a *aggregator) AddAggregate(ident string, expr *ql.ASTNode) (err error) {
def := aggregateDef{
outIdent: ident,
}
inIdent, expr, err := unpackMappingSource(expr)
if err != nil {
return
}
// Take it from the source
if inIdent != "" {
def.inIdent = inIdent
}
// Take it from the expression
// - agg. op.
def.aggOp, expr, err = unpackExpressionNode(expr)
if err != nil {
return
}
// Prepare a runner in case we're not simply copying values
if inIdent == "" {
// - make evaluator
if expr != nil {
def.eval, err = newRunnerGvalParsed(expr)
if err != nil {
return
}
}
}
a.aggregates = append(a.aggregates, 0)
a.counts = append(a.counts, 0)
a.def = append(a.def, def)
return
}
// Aggregate aggregates the given value
func (a *aggregator) Aggregate(ctx context.Context, v ValueGetter) (err error) {
if a.scanned {
// If we attempt to add data to the aggregator, the previous data might
// no longer be valid.
return fmt.Errorf("cannot call Aggregate on an already scanned aggregator")
}
// @todo consider throwing all of the aggregates into a routine.
// I think (for now) performance gains will be negligible.
for i, attr := range a.def {
err = a.aggregate(ctx, attr, i, v)
if err != nil {
return
}
}
return nil
}
// Scan scans the aggregated values into the setter
func (a *aggregator) Scan(s ValueSetter) (err error) {
// On first scan, complete partial aggregates
if !a.scanned {
a.completePartials()
}
a.scanned = true
// Set the values
for i, attr := range a.def {
// @note each aggregated value can be at most one so no need for multi-value
// suport here.
err = s.SetValue(attr.outIdent, 0, a.aggregates[i])
if err != nil {
return
}
}
return
}
// aggregate applies the provided value into the requested aggregate
func (a *aggregator) aggregate(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
switch attr.aggOp {
case "count":
return a.count(ctx, attr, i, v)
case "sum":
return a.sum(ctx, attr, i, v)
case "min":
return a.min(ctx, attr, i, v)
case "max":
return a.max(ctx, attr, i, v)
case "avg":
return a.avg(ctx, attr, i, v)
}
return fmt.Errorf("unsupported aggregate function: %s", attr.aggOp)
}
// walkValues traverses the available values for the specified attribute
func (a *aggregator) walkValues(ctx context.Context, r ValueGetter, cc map[string]uint, attr aggregateDef, run func(v any, isNil bool)) (err error) {
var out any
if attr.inIdent == "" {
if attr.eval != nil {
out, err = attr.eval.Eval(ctx, r)
if err != nil {
return
}
} else {
out = r
}
run(out, reflect2.IsNil(out))
return nil
}
for i := uint(0); i < cc[attr.inIdent]; i++ {
v, err := r.GetValue(attr.inIdent, i)
if err != nil {
return err
}
run(v, reflect2.IsNil(v))
}
return nil
}
// Aggregate methods
func (a *aggregator) count(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
if isNil {
return
}
a.aggregates[i]++
a.counts[i]++
})
if err != nil {
return
}
return
}
func (a *aggregator) sum(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
if isNil {
return
}
a.aggregates[i] += cast.ToFloat64(v)
a.counts[i]++
})
if err != nil {
return
}
return
}
func (a *aggregator) min(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
if isNil {
return
}
if a.counts[i] == 0 {
a.aggregates[i] = cast.ToFloat64(v)
} else {
a.aggregates[i] = math.Min(a.aggregates[i], cast.ToFloat64(v))
}
a.counts[i]++
})
if err != nil {
return
}
return
}
func (a *aggregator) max(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
if isNil {
return
}
if a.counts[i] == 0 {
a.aggregates[i] = cast.ToFloat64(v)
} else {
a.aggregates[i] = math.Max(a.aggregates[i], cast.ToFloat64(v))
}
a.counts[i]++
})
if err != nil {
return
}
return
}
func (a *aggregator) avg(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
if isNil {
return
}
a.aggregates[i] += cast.ToFloat64(v)
a.counts[i]++
})
if err != nil {
return
}
return
}
func (a *aggregator) completePartials() {
a.completeAverage()
}
func (a *aggregator) completeAverage() {
for i, attr := range a.def {
if attr.aggOp == "avg" {
if a.counts[i] == 0 {
return
}
a.aggregates[i] = a.aggregates[i] / float64(a.counts[i])
}
}
}
// Utilities
func unpackMappingSource(n *ql.ASTNode) (ident string, expr *ql.ASTNode, err error) {
// Check if first arg of agg. fnc. is an attr.
if len(n.Args) == 1 && n.Args[0].Symbol != "" {
return n.Args[0].Symbol, n, nil
}
expr = n
return
}
func unpackExpressionNode(n *ql.ASTNode) (aggOp string, expr *ql.ASTNode, err error) {
if n.Ref != "" {
aggOp = strings.ToLower(n.Ref)
}
if !aggregateFunctionIndex[aggOp] {
err = fmt.Errorf("root expression must be an aggregate function")
return
}
if len(n.Args) > 0 {
expr = n.Args[0]
}
return
}
// reset is a benchmarking utility to reset the aggregator
//
// Don't use it in production code.
func (a *aggregator) reset() {
for i := 0; i < len(a.aggregates); i++ {
a.aggregates[i] = 0
a.counts[i] = 0
}
a.scanned = false
}

View File

@@ -0,0 +1,370 @@
package dal
import (
"encoding/json"
"fmt"
)
type (
Alteration struct {
ID uint64
BatchID uint64
DependsOn uint64
Resource string
ResourceType string
ConnectionID uint64
AttributeAdd *AttributeAdd
AttributeDelete *AttributeDelete
AttributeReType *AttributeReType
AttributeReEncode *AttributeReEncode
ModelAdd *ModelAdd
ModelDelete *ModelDelete
}
AlterationSet []*Alteration
AttributeAdd struct {
Attr *Attribute `json:"attr"`
}
AttributeDelete struct {
Attr *Attribute `json:"attr"`
}
AttributeReType struct {
Attr *Attribute `json:"attr"`
To Type `json:"to"`
}
AttributeReEncode struct {
Attr *Attribute `json:"attr"`
To Codec `json:"to"`
}
ModelAdd struct {
Model *Model `json:"model"`
}
ModelDelete struct {
Model *Model `json:"model"`
}
// auxAttributeType is a helper struct used for marshaling/unmarshaling
//
// This is required since the Type inside the Attribute is an interface and we
// need to help the encoding/json a bit.
auxAttributeReType struct {
Attr *Attribute `json:"attr"`
To *auxAttributeType `json:"to"`
}
// auxAttributeReEncode is a helper struct used for marshaling/unmarshaling
//
// This is required since the Codec inside the Attribute is an interface and we
// need to help the encoding/json a bit.
auxAttributeReEncode struct {
Attr *Attribute `json:"attr"`
To *auxStoreCodec `json:"to"`
}
)
// Merge merges the two alteration slices
func (aa AlterationSet) Merge(bb AlterationSet) (cc AlterationSet) {
// @todo the Merge function currently just merges the two slices together
// and removes any duplicates that would occur due to the merge.
// We should also handle overlapping/transitive alterations to reduce
// the amount of needles processing.
//
// A quick list of overlapping alterations:
// * attribute A added and then renamed from A to A'
// * attribute A renamed to A' and then renamed to A''
// * attribute A deleted and then created
aux := append(aa, bb...)
seen := make(map[int]bool, len(aux)/2)
for i, a := range aux {
if seen[i] {
continue
}
found := false
for j := i + 1; j < len(aux); j++ {
if seen[j] {
continue
}
if a.compare(*aux[j]) {
if !found {
cc = append(cc, aux[j])
}
seen[j] = true
found = true
}
}
if !found {
cc = append(cc, a)
}
}
return
}
func (a AttributeReType) MarshalJSON() ([]byte, error) {
aux := auxAttributeReType{
Attr: a.Attr,
To: &auxAttributeType{},
}
switch t := a.To.(type) {
case *TypeID:
aux.To.Type = "ID"
aux.To.ID = t
case *TypeRef:
aux.To.Type = "Ref"
aux.To.Ref = t
case *TypeTimestamp:
aux.To.Type = "Timestamp"
aux.To.Timestamp = t
case *TypeTime:
aux.To.Type = "Time"
aux.To.Time = t
case *TypeDate:
aux.To.Type = "Date"
aux.To.Date = t
case *TypeNumber:
aux.To.Type = "Number"
aux.To.Number = t
case *TypeText:
aux.To.Type = "Text"
aux.To.Text = t
case *TypeBoolean:
aux.To.Type = "Boolean"
aux.To.Boolean = t
case *TypeEnum:
aux.To.Type = "Enum"
aux.To.Enum = t
case *TypeGeometry:
aux.To.Type = "Geometry"
aux.To.Geometry = t
case *TypeJSON:
aux.To.Type = "JSON"
aux.To.JSON = t
case *TypeBlob:
aux.To.Type = "Blob"
aux.To.Blob = t
case *TypeUUID:
aux.To.Type = "UUID"
aux.To.UUID = t
}
return json.Marshal(aux)
}
func (a *AttributeReType) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttributeReType{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = AttributeReType{}
}
a.Attr = aux.Attr
switch aux.To.Type {
case "ID":
a.To = aux.To.ID
case "Ref":
a.To = aux.To.Ref
case "Timestamp":
a.To = aux.To.Timestamp
case "Time":
a.To = aux.To.Time
case "Date":
a.To = aux.To.Date
case "Number":
a.To = aux.To.Number
case "Text":
a.To = aux.To.Text
case "Boolean":
a.To = aux.To.Boolean
case "Enum":
a.To = aux.To.Enum
case "Geometry":
a.To = aux.To.Geometry
case "JSON":
a.To = aux.To.JSON
case "Blob":
a.To = aux.To.Blob
case "UUID":
a.To = aux.To.UUID
}
return
}
func (a AttributeReEncode) MarshalJSON() ([]byte, error) {
aux := auxAttributeReEncode{
Attr: a.Attr,
To: &auxStoreCodec{},
}
switch t := a.To.(type) {
case *CodecPlain:
aux.To.Type = "CodecPlain"
aux.To.CodecPlain = t
case *CodecRecordValueSetJSON:
aux.To.Type = "CodecRecordValueSetJSON"
aux.To.CodecRecordValueSetJSON = t
case *CodecAlias:
aux.To.Type = "CodecAlias"
aux.To.CodecAlias = t
}
return json.Marshal(aux)
}
func (a *AttributeReEncode) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttributeReEncode{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = AttributeReEncode{}
}
a.Attr = aux.Attr
switch aux.To.Type {
case "CodecPlain":
a.To = aux.To.CodecPlain
case "CodecRecordValueSetJSON":
a.To = aux.To.CodecRecordValueSetJSON
case "CodecAlias":
a.To = aux.To.CodecAlias
}
return
}
func (a Alteration) compare(b Alteration) (cmp bool) {
if a.AttributeAdd == nil && b.AttributeAdd != nil {
return false
}
if a.AttributeDelete == nil && b.AttributeDelete != nil {
return false
}
if a.AttributeReType == nil && b.AttributeReType != nil {
return false
}
if a.AttributeReEncode == nil && b.AttributeReEncode != nil {
return false
}
if a.ModelAdd == nil && b.ModelAdd != nil {
return false
}
if a.ModelDelete == nil && b.ModelDelete != nil {
return false
}
switch {
case a.AttributeAdd != nil:
return a.compareAttributeAdd(b)
case a.AttributeDelete != nil:
return a.compareAttributeDelete(b)
case a.AttributeReType != nil:
return a.compareAttributeReType(b)
case a.AttributeReEncode != nil:
return a.compareAttributeReEncode(b)
case a.ModelAdd != nil:
return a.compareModelAdd(b)
case a.ModelDelete != nil:
return a.compareModelDelete(b)
}
panic(fmt.Sprintf("unsupported alteration type %v", a))
}
func (a Alteration) compareAttributeAdd(b Alteration) bool {
if a.AttributeAdd == nil || b.AttributeAdd == nil {
return a.AttributeAdd == b.AttributeAdd
}
return a.AttributeAdd.Attr.Compare(b.AttributeAdd.Attr)
}
func (a Alteration) compareAttributeDelete(b Alteration) bool {
if a.AttributeDelete == nil || b.AttributeDelete == nil {
return a.AttributeDelete == b.AttributeDelete
}
return a.AttributeDelete.Attr.Compare(b.AttributeDelete.Attr)
}
func (a Alteration) compareAttributeReType(b Alteration) bool {
if !a.AttributeReType.Attr.Compare(b.AttributeReType.Attr) {
return false
}
if a.AttributeReType == nil || b.AttributeReType == nil {
return a.AttributeReType == b.AttributeReType
}
return a.AttributeReType.To.Type() == b.AttributeReType.To.Type()
}
func (a Alteration) compareAttributeReEncode(b Alteration) bool {
if !a.AttributeReEncode.Attr.Compare(b.AttributeReEncode.Attr) {
return false
}
if a.AttributeReEncode == nil || b.AttributeReEncode == nil {
return a.AttributeReEncode == b.AttributeReEncode
}
return a.AttributeReEncode.To.Type() == b.AttributeReEncode.To.Type()
}
func (a Alteration) compareModelAdd(b Alteration) bool {
if a.ModelAdd == nil || b.ModelAdd == nil {
return a.ModelAdd == b.ModelAdd
}
return a.ModelAdd.Model.Compare(*b.ModelAdd.Model)
}
func (a Alteration) compareModelDelete(b Alteration) bool {
if a.ModelDelete == nil || b.ModelDelete == nil {
return a.ModelDelete == b.ModelDelete
}
return a.ModelDelete.Model.Compare(*b.ModelDelete.Model)
}

View File

@@ -0,0 +1,69 @@
package dal
type (
AttributeCodecType string
// Codec defines how values for a specific model attribute
// are retrieved or stored
//
// If attribute does not have store strategy set
// store driver should use attribute name to determinate
// source/destination of the value (table column, json document property)
Codec interface {
Type() AttributeCodecType
SingleValueOnly() bool
}
CodecPlain struct{}
// CodecRecordValueSetJSON defines that values are encoded/decoded into
// a JSON simple document { [_: string]: Array<unknown> }
//
// This only solves
// Attribute{Ident: "foo", Store: StoreCodecRecordValueJSON{ Ident: "bar" }
// => "bar"->'foo'->0
CodecRecordValueSetJSON struct {
Ident string
}
// handling complex JSON documents
//StoreCodecJSON struct {
// Ident string
// Path []any
//}
//
// { "@value": ... "@type": .... }
// StoreCodecJSONLD struct { Ident string; Path []any }
// StoreCodecXML
//StoreCodecXML struct {}
// CodecAlias defines case when value is not stored
// under the same column (in case of an SQL table)
//
// Value of CodecAlias.Ident is used as base
// and value of Attribute.Ident holding CodecAlias is used
// as an alias!
//
// Attribute{Ident: "foo", Store: CodecAlias{ Ident: "bar" }
// => "bar" as "foo"
CodecAlias struct {
Ident string
}
)
const (
AttributeCodecPlain AttributeCodecType = "corteza::dal:attribute-codec:plain"
AttributeCodecRecordValueSetJSON AttributeCodecType = "corteza::dal:attribute-codec:record-value-set-json"
AttributeCodecAlias AttributeCodecType = "corteza::dal:attribute-codec:alias"
)
func (*CodecPlain) Type() AttributeCodecType { return AttributeCodecPlain }
func (*CodecRecordValueSetJSON) Type() AttributeCodecType {
return AttributeCodecRecordValueSetJSON
}
func (*CodecAlias) Type() AttributeCodecType { return AttributeCodecAlias }
func (*CodecPlain) SingleValueOnly() bool { return true }
func (*CodecRecordValueSetJSON) SingleValueOnly() bool { return false }
func (*CodecAlias) SingleValueOnly() bool { return true }

View File

@@ -0,0 +1,203 @@
package dal
type (
AttributeType string
// temp
Type interface {
Type() AttributeType
IsNullable() bool
}
// TypeID handles ID (uint64) coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, json ...)
//
// It is always Corteza ID
TypeID struct {
GeneratedByStore bool
Nullable bool
HasDefault bool
DefaultValue uint64
}
// TypeRef handles ID (uint64) coding + reference info
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, json ...)
TypeRef struct {
// defaults to ID
RefAttribute string
RefModel *ModelRef
Nullable bool
HasDefault bool
DefaultValue uint64
}
// TypeTimestamp handles timestamp coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, json ...)
TypeTimestamp struct {
Timezone bool
Precision int
Nullable bool
DefaultCurrentTimestamp bool
}
// TypeTime handles time coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, json ...)
TypeTime struct {
Timezone bool
TimezonePrecision bool
Precision int
Nullable bool
DefaultCurrentTimestamp bool
}
// TypeDate handles date coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, jsonb...)
TypeDate struct {
//
Nullable bool
DefaultCurrentTimestamp bool
}
// TypeNumber handles number coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, jsonb...)
TypeNumber struct {
Precision int
Scale int
Nullable bool
HasDefault bool
DefaultValue float64
Meta map[string]any
}
// TypeText handles string coding
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, jsonb...)
TypeText struct {
Length uint
Nullable bool
HasDefault bool
DefaultValue string
}
// TypeBoolean
TypeBoolean struct {
//
Nullable bool
HasDefault bool
DefaultValue bool
}
// TypeEnum
TypeEnum struct {
Values []string
Nullable bool
HasDefault bool
DefaultValue string
}
// TypeGeometry
TypeGeometry struct {
//
Nullable bool
}
// TypeJSON handles coding of arbitrary data into JSON structure
// NOT TO BE CONFUSED with encodedField
//
// Encoding/decoding might be different depending on
// 1) underlying store (and dialect)
// 2) value codec (raw, json ...)
TypeJSON struct {
//
Nullable bool
HasDefault bool
DefaultValue any
}
// TypeBlob store/return data as
TypeBlob struct {
//
Nullable bool
}
TypeUUID struct {
Nullable bool
}
TypeNumberStoreNativeType string
)
const (
AttributeTypeID AttributeType = "corteza::dal:attribute-type:id"
AttributeTypeRef AttributeType = "corteza::dal:attribute-type:ref"
AttributeTypeTimestamp AttributeType = "corteza::dal:attribute-type:timestamp"
AttributeTypeTime AttributeType = "corteza::dal:attribute-type:time"
AttributeTypeDate AttributeType = "corteza::dal:attribute-type:date"
AttributeTypeNumber AttributeType = "corteza::dal:attribute-type:number"
AttributeTypeText AttributeType = "corteza::dal:attribute-type:text"
AttributeTypeBoolean AttributeType = "corteza::dal:attribute-type:boolean"
AttributeTypeEnum AttributeType = "corteza::dal:attribute-type:enum"
AttributeTypeGeometry AttributeType = "corteza::dal:attribute-type:geometry"
AttributeTypejson AttributeType = "corteza::dal:attribute-type:json"
AttributeTypeBlob AttributeType = "corteza::dal:attribute-type:blob"
AttributeTypeUUID AttributeType = "corteza::dal:attribute-type:uuid"
)
func (t TypeID) IsNullable() bool { return t.Nullable }
func (t TypeRef) IsNullable() bool { return t.Nullable }
func (t TypeTimestamp) IsNullable() bool { return t.Nullable }
func (t TypeTime) IsNullable() bool { return t.Nullable }
func (t TypeDate) IsNullable() bool { return t.Nullable }
func (t TypeNumber) IsNullable() bool { return t.Nullable }
func (t TypeText) IsNullable() bool { return t.Nullable }
func (t TypeBoolean) IsNullable() bool { return t.Nullable }
func (t TypeEnum) IsNullable() bool { return t.Nullable }
func (t TypeGeometry) IsNullable() bool { return t.Nullable }
func (t TypeJSON) IsNullable() bool { return t.Nullable }
func (t TypeBlob) IsNullable() bool { return t.Nullable }
func (t TypeUUID) IsNullable() bool { return t.Nullable }
func (t TypeID) Type() AttributeType { return AttributeTypeID }
func (t TypeRef) Type() AttributeType { return AttributeTypeRef }
func (t TypeTimestamp) Type() AttributeType { return AttributeTypeTimestamp }
func (t TypeTime) Type() AttributeType { return AttributeTypeTime }
func (t TypeDate) Type() AttributeType { return AttributeTypeDate }
func (t TypeNumber) Type() AttributeType { return AttributeTypeNumber }
func (t TypeText) Type() AttributeType { return AttributeTypeText }
func (t TypeBoolean) Type() AttributeType { return AttributeTypeBoolean }
func (t TypeEnum) Type() AttributeType { return AttributeTypeEnum }
func (t TypeGeometry) Type() AttributeType { return AttributeTypeGeometry }
func (t TypeJSON) Type() AttributeType { return AttributeTypejson }
func (t TypeBlob) Type() AttributeType { return AttributeTypeBlob }
func (t TypeUUID) Type() AttributeType { return AttributeTypeUUID }

View File

@@ -0,0 +1,36 @@
package dal
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// OrderedBuffer provides the buffered data in the specified order
OrderedBuffer interface {
Buffer
// InOrder makes the buffer provide the stored data in the specified order
InOrder(ss ...*filter.SortExpr) (err error)
}
// Buffer provides a place where you can buffer the data provided by DAL
// @note Buffers are currently primarily used for testing
Buffer interface {
// Seek moves the index pointer to the specified location
// After the Seek call, a Next() call is required
Seek(context.Context, int) error
// Len returns the number of elements in the buffer
Len() int
Iterator
Adder
}
Adder interface {
// Add adds a new ValueGetter to the buffer
Add(context.Context, ValueGetter) (err error)
}
)

View File

@@ -0,0 +1,42 @@
package dal
import "regexp"
type (
ConnectionWrap struct {
ID uint64
connection Connection
params ConnectionParams
Config ConnectionConfig
}
ConnectionConfig struct {
SensitivityLevelID uint64
Label string
// When model does not specify the ident (table name for example), fallback to this
ModelIdent string
// when a new model is added on a connection, it's ident
// is verified against this regexp
//
// ident is considered valid if it matches one of the expressions
// or if the list of checks is empty
ModelIdentCheck []*regexp.Regexp
}
)
func checkIdent(ident string, rr ...*regexp.Regexp) bool {
if len(rr) == 0 {
return true
}
for _, r := range rr {
if r.MatchString(ident) {
return true
}
}
return false
}

View File

@@ -0,0 +1,299 @@
package dal
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
)
type (
// Aggregate produces a series of aggregated rows from the provided sources based
// on the specified group.
Aggregate struct {
Ident string
RelSource string
Filter filter.Filter
filter internalFilter
Group []AggregateAttr
OutAttributes []AggregateAttr
SourceAttributes []AttributeMapping
rel PipelineStep
plan aggregatePlan
analysis map[string]OpAnalysis
}
// aggregatePlan outlines how the optimizer determined the dataset should be
// aggregated
aggregatePlan struct {
// partialScan indicates we can partially pull data from the two sources
// as the data is provided in the correct order.
partialScan bool
}
groupKey []any
aggregateGroup struct {
key groupKey
agg *aggregator
}
// AggregateAttr is a simple wrapper to outline aggregated attribute definitions
AggregateAttr struct {
MultiValue bool
Key bool
// @todo change; temporary for compose service
RawExpr string
Identifier string
Label string
Expression *ql.ASTNode
Type Type
Store Codec
}
)
func (def *Aggregate) Identifier() string {
return def.Ident
}
func (def *Aggregate) Sources() []string {
return []string{def.RelSource}
}
func (def *Aggregate) Attributes() [][]AttributeMapping {
aa := append(def.Group, def.OutAttributes...)
out := make([]AttributeMapping, 0, len(aa))
for _, a := range aa {
out = append(out, a.toSimpleAttr())
}
return [][]AttributeMapping{out}
}
func (def *Aggregate) Analyze(ctx context.Context) (err error) {
// @todo proper analysis; for now we'll leave this as defaults
def.analysis = map[string]OpAnalysis{
OpAnalysisIterate: {
ScanCost: CostUnknown,
SearchCost: CostUnknown,
FilterCost: CostUnknown,
SortCost: CostUnknown,
OutputSize: SizeUnknown,
},
}
return
}
func (def *Aggregate) Analysis() map[string]OpAnalysis {
return def.analysis
}
func (def *Aggregate) Optimize(reqFilter internalFilter) (rspFilter internalFilter, err error) {
err = fmt.Errorf("not implemented")
return
}
// iterator initializes an iterator based on the provided pipeline step definition
func (def *Aggregate) iterator(ctx context.Context, src Iterator) (out Iterator, err error) {
exec, err := def.init(ctx, src)
if err != nil {
return
}
return exec, exec.init(ctx)
}
// dryrun performs step execution without interacting with the data
// @todo consider rewording this
func (def *Aggregate) dryrun(ctx context.Context) (err error) {
_, err = def.init(ctx, nil)
return
}
func (def *Aggregate) init(ctx context.Context, src Iterator) (exec *aggregate, err error) {
exec = &aggregate{
source: src,
}
// Convert the provided filter into an internal filter
if def.Filter != nil {
def.filter, err = toInternalFilter(def.Filter)
if err != nil {
return
}
}
// Collect attributes from the underlaying step in case own are not provided
if len(def.SourceAttributes) == 0 {
def.SourceAttributes = collectAttributes(def.rel)
}
// Index source attributes for group/aggregate definition validation
srcAttrs := indexAttrs(def.SourceAttributes...)
pp := newQlParser(func(ident ql.Ident) (_ ql.Ident, err error) {
if _, ok := srcAttrs[ident.Value]; !ok {
return ident, fmt.Errorf("unknown attribute %s", ident.Value)
}
return ident, nil
})
prepAttr := func(attr AggregateAttr) (_ AggregateAttr, err error) {
if attr.RawExpr != "" {
// Parse (it already validates) raw expressions
attr.Expression, err = pp.Parse(attr.RawExpr)
if err != nil {
return
}
} else {
// Manually validate already parsed expressions
err = attr.Expression.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
if a.Symbol == "" {
return true, a, nil
}
if _, ok := srcAttrs[a.Symbol]; !ok {
return false, nil, fmt.Errorf("unknown attribute %s", a.Symbol)
}
return true, a, nil
})
}
if err != nil {
return
}
return def.determineAttrType(attr, def.SourceAttributes)
}
// Convert & validate group definitions
// - groups
outAttrs := make(map[string]bool, len(def.Group)+len(def.OutAttributes))
for i, attr := range def.Group {
attr, err = prepAttr(attr)
if err != nil {
return
}
def.Group[i] = attr
idtf := attr.Identifier
exec.groupDefs = append(exec.groupDefs, attr)
outAttrs[idtf] = true
}
// - aggregates
for i, attr := range def.OutAttributes {
// @todo change when needed; currently, all aggregates are numbers
attr.Type = &TypeNumber{}
attr, err = prepAttr(attr)
if err != nil {
return
}
def.OutAttributes[i] = attr
idtf := attr.Identifier
exec.aggregateDefs = append(exec.aggregateDefs, attr)
outAttrs[idtf] = true
}
// Generic validation
if len(def.Group) == 0 {
err = fmt.Errorf("no group attributes specified")
return
}
if len(def.OutAttributes) == 0 {
err = fmt.Errorf("no output attributes specified")
return
}
if len(def.SourceAttributes) == 0 {
err = fmt.Errorf("no source attributes specified")
return
}
// order
for _, s := range def.filter.OrderBy() {
if _, ok := outAttrs[s.Column]; !ok {
err = fmt.Errorf("order by attribute %s does not exist", s.Column)
return
}
}
// Finishup
exec.filter = def.filter
exec.def = *def
return
}
// determineAttrType determines the type of the AggregateAttr based on it's definition
// and source attributes
func (def *Aggregate) determineAttrType(base AggregateAttr, ss []AttributeMapping) (out AggregateAttr, err error) {
out = base
if out.Type != nil {
return
}
var root *ql.ASTNode
var t Type
// If we have a symbol, then we'll use it to determine the type.
// All current operations should return the same output type as the input one.
//
// In case of a function, use the output type of the root most function which has
// a known type.
//
// Note, some refs (group and add for example) may not know their types so we need
// to dig deeper.
base.Expression.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
if a.Symbol != "" {
root = a
return false, a, nil
}
if a.Ref != "" {
tmp := refToGvalExp[strings.ToLower(a.Ref)]
if tmp == nil || tmp.OutType == nil || tmp.OutTypeUnknown {
return true, a, nil
}
if tmp.OutType != nil {
t = tmp.OutType
return false, a, nil
}
}
return true, a, nil
})
if root != nil {
for _, s := range ss {
if s.Identifier() == root.Symbol {
t = s.Properties().Type
break
}
}
}
out.Type = t
return
}
func (a AggregateAttr) toSimpleAttr() SimpleAttr {
return SimpleAttr{
Ident: a.Identifier,
Props: MapProperties{
Label: a.Label,
IsPrimary: a.Key,
Type: a.Type,
},
// @todo won't matter for now
Expr: "",
Src: "",
}
}

View File

@@ -0,0 +1,362 @@
package dal
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
)
type (
// Datasource is a simple passthrough step for underlaying datasources.
// It exists primarily to make operations consistent.
Datasource struct {
Ident string
Filter filter.Filter
ModelRef ModelRef
filter internalFilter
OutAttributes []AttributeMapping
analysis map[string]OpAnalysis
connection *ConnectionWrap
model *Model
// clobbered lists all of the steps that are offloaded into the datasource.
// The list is provided in order; the first step is the first step which should execute
// in the report pipeline.
//
// @todo change to generic step; doing this because I can for now
clobbered []*Aggregate
// provided in the init step so we can omit some code in the exec step
// @todo consider removing this
auxIter Iterator
}
)
func (def *Datasource) Identifier() string {
return def.Ident
}
func (def *Datasource) Sources() []string {
return []string{}
}
func (def *Datasource) Attributes() [][]AttributeMapping {
if len(def.clobbered) > 0 {
return def.offloadedAttributes()
}
return def.ownAttributes()
}
func (def *Datasource) Analyze(ctx context.Context) (err error) {
a, err := def.connection.connection.Analyze(ctx, def.model)
if err != nil {
return
}
def.analysis = map[string]OpAnalysis{
OpAnalysisIterate: {
ScanCost: CostUnknown,
SearchCost: CostUnknown,
FilterCost: CostUnknown,
SortCost: CostUnknown,
OutputSize: SizeUnknown,
},
}
if _, ok := a[OpAnalysisAggregate]; ok {
def.analysis[OpAnalysisAggregate] = a[OpAnalysisAggregate]
}
if _, ok := a[OpAnalysisJoin]; ok {
def.analysis[OpAnalysisJoin] = a[OpAnalysisJoin]
}
return
}
func (def *Datasource) Analysis() map[string]OpAnalysis {
return def.analysis
}
func (def *Datasource) Optimize(req internalFilter) (res internalFilter, err error) {
return internalFilter{}, fmt.Errorf("optimization not implemented")
}
func (def *Datasource) init(ctx context.Context) (err error) {
if def.model == nil {
return fmt.Errorf("cannot initialize datasource: model not set")
}
if def.connection == nil {
return fmt.Errorf("cannot initialize datasource: connection not set")
}
if def.Filter != nil {
def.filter, err = toInternalFilter(def.Filter)
if err != nil {
return
}
}
if len(def.OutAttributes) == 0 {
def.OutAttributes = def.outAttrsFromModel(def.model)
}
pp := make([]string, 0, len(def.OutAttributes)/2+1)
for _, a := range def.OutAttributes {
if a.Properties().IsPrimary {
pp = append(pp, a.Identifier())
}
}
// Assure and attempt to correct the provided sort to conform with the data set and the
// paging cursor (if any)
def.filter, err = assureSort(def.filter, pp)
if err != nil {
return
}
// Firstly validate the base
err = def.validate()
if err != nil {
return
}
if len(def.clobbered) > 0 {
// @todo currently, we can only do one; change this when we tweak the DB
// offloading code.
ag := def.clobbered[0]
// Invoke the aggregation's init to perform the validation and preparation logic
var wa *aggregate
wa, err = ag.init(ctx, nil)
if err != nil {
return
}
// Preprocess the filters to conform to connection API
f, having, err := def.getAggregationFilters(def.filter, wa.filter)
if err != nil {
return err
}
def.auxIter, err = def.connection.connection.Aggregate(ctx, def.model, f, wa.groupDefs, wa.aggregateDefs, having)
return err
}
def.auxIter, err = def.connection.connection.Search(ctx, def.model, def.filter)
return err
}
func (def *Datasource) exec(ctx context.Context) (out Iterator, err error) {
if def.auxIter == nil {
return nil, fmt.Errorf("datasource not initialized")
}
return def.auxIter, nil
}
func (def *Datasource) validate() (err error) {
err = func() (err error) {
if len(def.OutAttributes) == 0 {
return fmt.Errorf("no attributes specified")
}
return
}()
if err != nil {
return fmt.Errorf("invalid definition: %v", err)
}
return
}
func (def *Datasource) outAttrsFromModel(model *Model) (attrs []AttributeMapping) {
for _, attr := range model.Attributes {
if attr.Type == nil {
panic(fmt.Sprintf("impossible state: attribute %s has no type", attr.Ident))
}
attrs = append(attrs, SimpleAttr{
Ident: attr.Ident,
Src: attr.Ident,
Props: MapProperties{
IsPrimary: attr.PrimaryKey,
IsSystem: attr.System,
IsFilterable: attr.Filterable,
IsSortable: attr.Sortable,
Nullable: attr.Type.IsNullable(),
Type: attr.Type,
},
})
}
return
}
func (def *Datasource) shouldClobberAggregation() bool {
costs, ok := def.analysis[OpAnalysisAggregate]
if !ok {
return false
}
// @todo more to it; check and compare costs; for now we know that all rdbms
// offloads will be faster.
_ = costs
return true
}
func (def *Datasource) ownAttributes() [][]AttributeMapping {
return [][]AttributeMapping{def.OutAttributes}
}
func (def *Datasource) offloadedAttributes() [][]AttributeMapping {
// The last offloaded step's attributes is what this step's iterator returns
return def.clobbered[len(def.clobbered)-1].Attributes()
}
func (def *Datasource) clobber(s PipelineStep) (ok bool) {
switch cs := s.(type) {
case *Aggregate:
if !def.shouldClobberAggregation() {
return false
}
def.clobbered = append(def.clobbered, cs)
return true
}
return
}
// getAggregationFilters transforms the base and the aggregation filters to conform
// to the store/dal's model.Aggregate API
//
// - The full filter.Filter return parameter is applied to the base dataset
// - The QL node return parameter is applied only to the aggregated dataset
//
// @todo should we change the store's API to accept 2x filter.Filter? I think
// that would allow the underlaying driver to decide how to handle them
// instead of relying on what SQL does.
func (def *Datasource) getAggregationFilters(base, agg internalFilter) (filter internalFilter, having *ql.ASTNode, err error) {
var typedV expr.TypedValue
filter = base
// Move the aggregation's order and limit to the base filter because those
// are applied to the final output (per SQL)
//
// Leave base filtering parameters (filter, constraints, ...) as those are apploed
// to the dataset before aggregation (which is correct).
filter.orderBy = agg.orderBy
filter.limit = agg.limit
// Convert the rest of the filtering parameters defined on the aggregation's filter
// to the QL node.
// ALl of that is applied after the aggregation which is correct.
var nConstraints *ql.ASTNode
if len(agg.constraints) > 0 {
nConstraints = &ql.ASTNode{
Ref: "and",
}
for k, c := range agg.constraints {
arg := &ql.ASTNode{
Ref: "or",
}
for _, v := range c {
typedV, err = expr.Typify(v)
if err != nil {
return
}
arg.Args = append(arg.Args, &ql.ASTNode{
Symbol: k,
Value: ql.WrapValue(typedV),
})
}
nConstraints.Args = append(nConstraints.Args, &ql.ASTNode{
Ref: "group",
Args: ql.ASTNodeSet{arg},
})
}
}
var nStateConstraints *ql.ASTNode
if len(agg.stateConstraints) > 0 {
nStateConstraints = &ql.ASTNode{
Ref: "and",
}
for k, c := range agg.stateConstraints {
typedV, err = expr.Typify(c)
if err != nil {
return
}
nStateConstraints.Args = append(nStateConstraints.Args, &ql.ASTNode{
Symbol: k,
Value: ql.WrapValue(typedV),
})
}
}
var nMetaConstraints *ql.ASTNode
if len(agg.stateConstraints) > 0 {
nMetaConstraints = &ql.ASTNode{
Ref: "and",
}
for k, c := range agg.stateConstraints {
typedV, err = expr.Typify(c)
if err != nil {
return
}
nMetaConstraints.Args = append(nMetaConstraints.Args, &ql.ASTNode{
Symbol: k,
Value: ql.WrapValue(typedV),
})
}
}
nExpression := agg.expParsed
var nCursor *ql.ASTNode
if agg.cursor != nil {
nCursor, err = agg.cursor.ToAST(nil, nil)
if err != nil {
return
}
}
having = &ql.ASTNode{
Ref: "and",
}
if nConstraints != nil {
having.Args = append(having.Args, nConstraints)
}
if nStateConstraints != nil {
having.Args = append(having.Args, nStateConstraints)
}
if nMetaConstraints != nil {
having.Args = append(having.Args, nMetaConstraints)
}
if nExpression != nil {
having.Args = append(having.Args, nExpression)
}
if nCursor != nil {
having.Args = append(having.Args, nCursor)
}
// In case everything is empty, no need for the having part
if len(having.Ref) == 0 {
having = nil
}
return
}

View File

@@ -0,0 +1,218 @@
package dal
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// Join produces a series of joined rows from the provided sources based
// on the JoinPredicate.
//
// The join step produces an SQL left join-like output where all right rows
// have a corresponding left row.
Join struct {
Ident string
RelLeft string
RelRight string
// @todo allow multiple join predicates; for now (for easier indexing)
// only allow one (this is the same as we had before)
On JoinPredicate
Filter filter.Filter
filter internalFilter
OutAttributes []AttributeMapping
LeftAttributes []AttributeMapping
RightAttributes []AttributeMapping
relLeft PipelineStep
relRight PipelineStep
plan joinPlan
analysis map[string]OpAnalysis
}
// JoinPredicate determines the attributes the two datasets should get joined on
JoinPredicate struct {
Left string
Right string
}
// joinPlan outlines how the optimizer determined the two datasets should be
// joined on.
joinPlan struct {
// @todo add strategy when we have different strategies implemented
// strategy string
// partialScan indicates we can partially pull data from the two sources
// as the data is provided in the correct order.
partialScan bool
}
)
func (def *Join) Identifier() string {
return def.Ident
}
func (def *Join) Sources() []string {
return []string{def.RelLeft, def.RelRight}
}
func (def *Join) Attributes() [][]AttributeMapping {
return [][]AttributeMapping{def.OutAttributes}
}
func (def *Join) Analyze(ctx context.Context) (err error) {
// @todo proper analysis; for now we'll leave this as defaults
def.analysis = map[string]OpAnalysis{
OpAnalysisIterate: {
ScanCost: CostUnknown,
SearchCost: CostUnknown,
FilterCost: CostUnknown,
SortCost: CostUnknown,
OutputSize: SizeUnknown,
},
}
return
}
func (def *Join) Analysis() map[string]OpAnalysis {
return def.analysis
}
func (def *Join) Optimize(req internalFilter) (res internalFilter, err error) {
err = fmt.Errorf("not implemented")
return
}
// iterator initializes an iterator based on the provided pipeline step definition
func (def *Join) iterator(ctx context.Context, left, right Iterator) (out Iterator, err error) {
exec, err := def.init(ctx, left, right)
if err != nil {
return
}
// Get pred. type
// @todo should validate that both pred. types are the same/compatible
var pt Type
for _, a := range def.LeftAttributes {
if a.Identifier() == def.On.Left {
pt = a.Properties().Type
break
}
}
return exec, exec.init(ctx, pt)
}
// dryrun performs step execution without interacting with the data
// @todo consider rewording this
func (def *Join) dryrun(ctx context.Context) (err error) {
_, err = def.init(ctx, nil, nil)
return
}
func (def *Join) init(ctx context.Context, left, right Iterator) (exec *joinLeft, err error) {
exec = &joinLeft{
leftSource: left,
rightSource: right,
}
// Convert the provided filter into an internal filter
if def.Filter != nil {
def.filter, err = toInternalFilter(def.Filter)
if err != nil {
return
}
}
// Collect attributes from the underlaying step in case own are not provided
if len(def.LeftAttributes) == 0 {
def.LeftAttributes = collectAttributes(def.relLeft)
}
if len(def.RightAttributes) == 0 {
def.RightAttributes = collectAttributes(def.relRight)
}
// When the output attributes are not provided, we determine them based on the input
// attributes.
//
// The join step prefixes each attribute with the identifier of the source they came from.
// This avoids name collisions.
// @todo consider improving this to only prefix when there is a name collision
// and allow the nested attr. to be more permissive; for example, instead of
// requiring a.b.c only b.c or c would be enough.
if len(def.OutAttributes) == 0 {
ins := func(srcIdent string, aa []AttributeMapping) {
for _, a := range aa {
p := a.Properties()
def.OutAttributes = append(def.OutAttributes, SimpleAttr{
Ident: fmt.Sprintf("%s%s%s", srcIdent, attributeNestingSeparator, a.Identifier()),
Expr: a.Expression(),
Src: a.Identifier(),
Props: p,
})
}
}
def.OutAttributes = make([]AttributeMapping, 0, len(def.LeftAttributes)+len(def.RightAttributes))
ins(def.RelLeft, def.LeftAttributes)
ins(def.RelRight, def.RightAttributes)
}
// Assure and attempt to correct the provided sort to conform with the data set and the
// paging cursor (if any)
def.filter, err = assureSort(def.filter, exec.collectPrimaryAttributes(def.OutAttributes))
if err != nil {
return
}
// Index attrs for validations
leftSrcAttrs := indexAttrs(def.LeftAttributes...)
rightSrcAttrs := indexAttrs(def.RightAttributes...)
outAttrs := indexAttrs(def.OutAttributes...)
for _, a := range def.OutAttributes {
if !leftSrcAttrs[a.Source()] && !rightSrcAttrs[a.Source()] {
return nil, fmt.Errorf("unknown attribute %s", a.Source())
}
}
// Generic validation
if !leftSrcAttrs[def.On.Left] {
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Left)
}
if !rightSrcAttrs[def.On.Right] {
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Right)
}
if len(def.OutAttributes) == 0 {
return nil, fmt.Errorf("no attributes specified")
}
if len(def.LeftAttributes) == 0 {
return nil, fmt.Errorf("no left attributes specified")
}
if len(def.RightAttributes) == 0 {
return nil, fmt.Errorf("no right attributes specified")
}
if def.On.Left == "" {
return nil, fmt.Errorf("no left attribute in the join predicate specified")
}
if def.On.Right == "" {
return nil, fmt.Errorf("no right attribute in the join predicate specified")
}
// order
for _, s := range def.filter.OrderBy() {
if _, ok := outAttrs[s.Column]; !ok {
return nil, fmt.Errorf("order attribute %s does not exist", s.Column)
}
}
exec.filter = def.filter
exec.def = *def
return
}

View File

@@ -0,0 +1,289 @@
package dal
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// Link produces a series of left and corresponding right rows based on the
// provided sources and the LinkPredicate.
//
// The Link step produces an SQL left join-like output where left and right
// rows are served separately and the left rows are not duplicated.
Link struct {
Ident string
RelLeft string
RelRight string
// @todo allow multiple link predicates; for now (for easier indexing)
// only allow one (this is the same as we had before)
On LinkPredicate
// @todo consider splitting filter into left and right filter
filter internalFilter
Filter filter.Filter
OutLeftAttributes []AttributeMapping
OutRightAttributes []AttributeMapping
LeftAttributes []AttributeMapping
RightAttributes []AttributeMapping
relLeft PipelineStep
relRight PipelineStep
plan linkPlan
analysis map[string]OpAnalysis
}
// LinkPredicate determines the attributes the two datasets should get joined on
LinkPredicate struct {
Left string
Right string
}
// linkPlan outlines how the optimizer determined the two datasets should be
// joined on.
linkPlan struct {
// @todo add strategy when we have different strategies implemented
// strategy string
// partialScan indicates we can partially pull data from the two sources
// as the data is provided in the correct order.
partialScan bool
}
rowLink struct {
a, b ValueGetter
}
)
const (
LinkRefIdent = "$sys.ref"
)
func (def *Link) Identifier() string {
return def.Ident
}
func (def *Link) Sources() []string {
return []string{def.RelLeft, def.RelRight}
}
func (def *Link) Attributes() [][]AttributeMapping {
return [][]AttributeMapping{def.OutLeftAttributes, def.OutRightAttributes}
}
func (def *Link) Analyze(ctx context.Context) (err error) {
// @todo proper analysis; for now we'll leave this as defaults
def.analysis = map[string]OpAnalysis{
OpAnalysisIterate: {
ScanCost: CostUnknown,
SearchCost: CostUnknown,
FilterCost: CostUnknown,
SortCost: CostUnknown,
OutputSize: SizeUnknown,
},
}
return
}
func (def *Link) Analysis() map[string]OpAnalysis {
return def.analysis
}
func (def *Link) Optimize(req internalFilter) (res internalFilter, err error) {
err = fmt.Errorf("not implemented")
return
}
// iterator initializes an iterator based on the provided pipeline step definition
func (def *Link) iterator(ctx context.Context, left, right Iterator) (out Iterator, err error) {
x, err := def.init(ctx, left, right)
if err != nil {
return
}
// Get pred. type
// @todo should validate that both pred. types are the same/compatible
var pt Type
for _, a := range def.LeftAttributes {
if a.Identifier() == def.On.Left {
pt = a.Properties().Type
break
}
}
// Collect order by attributes which appear in the right source
rightAttrs := indexAttrs(def.RightAttributes...)
rightOrderAttrs := make([]string, 0, len(def.filter.OrderBy()))
for _, o := range def.filter.OrderBy() {
if rightAttrs[o.Column] {
rightOrderAttrs = append(rightOrderAttrs, o.Column)
}
}
return x, x.init(ctx, pt, rightOrderAttrs)
}
// dryrun performs step execution without interacting with the data
// @todo consider rewording this
func (def *Link) dryrun(ctx context.Context) (err error) {
_, err = def.init(ctx, nil, nil)
return
}
func (def *Link) init(ctx context.Context, left, right Iterator) (exec *linkLeft, err error) {
exec = &linkLeft{
leftSource: left,
rightSource: right,
}
// Convert the provided filter into an internal filter
if def.Filter != nil {
def.filter, err = toInternalFilter(def.Filter)
if err != nil {
return
}
}
// Collect attributes from the underlaying step in case own are not provided
if len(def.LeftAttributes) == 0 {
def.LeftAttributes = collectAttributes(def.relLeft)
}
if len(def.RightAttributes) == 0 {
def.RightAttributes = collectAttributes(def.relRight)
}
// @todo this isn't quite ok -- the ident of left/right must become the src of the out.
// An edge-case but it should be covered.
if len(def.OutLeftAttributes) == 0 {
def.OutLeftAttributes = def.LeftAttributes
}
if len(def.OutRightAttributes) == 0 {
def.OutRightAttributes = def.RightAttributes
}
// Index source attributes for group/aggregate definition validation
leftSrcAttrs := indexAttrs(def.LeftAttributes...)
rightSrcAttrs := indexAttrs(def.RightAttributes...)
outAttrs := indexAttrs(def.OutLeftAttributes...)
indexAttrsInto(outAttrs, def.OutRightAttributes...)
// Check if source attributes are present
for _, a := range append(def.OutLeftAttributes, def.OutRightAttributes...) {
if !leftSrcAttrs[a.Source()] && !rightSrcAttrs[a.Source()] {
return nil, fmt.Errorf("unknown attribute %s", a.Source())
}
}
// Check link predicates
if !leftSrcAttrs[def.On.Left] {
return nil, fmt.Errorf("left link predicate %s does not exist", def.On.Left)
}
if !rightSrcAttrs[def.On.Right] {
return nil, fmt.Errorf("right link predicate %s does not exist", def.On.Right)
}
// General validation
if len(def.OutLeftAttributes) == 0 {
return nil, fmt.Errorf("no left output attributes specified")
}
if len(def.OutRightAttributes) == 0 {
return nil, fmt.Errorf("no right output attributes specified")
}
if len(def.LeftAttributes) == 0 {
return nil, fmt.Errorf("no left attributes specified")
}
if len(def.RightAttributes) == 0 {
return nil, fmt.Errorf("no right attributes specified")
}
if def.On.Left == "" {
return nil, fmt.Errorf("no left attribute in the link predicate specified")
}
if def.On.Right == "" {
return nil, fmt.Errorf("no right attribute in the link predicate specified")
}
for _, s := range def.filter.OrderBy() {
if _, ok := outAttrs[s.Column]; !ok {
return nil, fmt.Errorf("order attribute %s does not exist", s.Column)
}
}
exec.filter = def.filter
exec.def = *def
return
}
func (r *rowLink) SelectGVal(ctx context.Context, k string) (interface{}, error) {
// @todo this way of determining multivalue is not entirely ok but we can get away with it
// since we have special functions to work with those.
// We need to somehow inform this bit as to what fields are multi value.
row, counts := r.getGetter(k)
if row == nil {
return nil, nil
}
cc := counts[k]
if cc == 0 {
return nil, nil
}
if cc == 1 {
return row.GetValue(k, 0)
}
out := make([]any, cc)
for i := uint(0); i < cc; i++ {
out[i], _ = row.GetValue(k, i)
}
return out, nil
}
func (r *rowLink) GetValue(name string, pos uint) (v any, err error) {
row, a := r.getGetter(name)
if row == nil {
return
}
if cc, ok := a[name]; ok {
if pos >= cc {
return nil, nil
}
return row.GetValue(name, pos)
}
return
}
func (r *rowLink) CountValues() (out map[string]uint) {
out = make(map[string]uint)
for k, c := range r.a.CountValues() {
out[k] = c
}
for k, c := range r.b.CountValues() {
out[k] = c
}
return
}
// getGetter returns the ValueGetter based on if the value is in a or b
// If neither, it returns nil
func (r *rowLink) getGetter(k string) (row ValueGetter, cc map[string]uint) {
cc = r.a.CountValues()
if _, ok := cc[k]; ok {
return r.a, cc
}
cc = r.b.CountValues()
if _, ok := cc[k]; ok {
return r.b, cc
}
return nil, nil
}

View File

@@ -0,0 +1,189 @@
package dal
type (
modelDiffType string
// ModelDiff defines one identified missmatch between two models
ModelDiff struct {
Type modelDiffType
// Original will be nil when a new attribute is being added
Original *Attribute
// Asserted will be nil wen an existing attribute is being removed
Asserted *Attribute
}
ModelDiffSet []*ModelDiff
)
const (
AttributeMissing modelDiffType = "attributeMissing"
AttributeTypeMissmatch modelDiffType = "typeMissmatch"
AttributeSensitivityMismatch modelDiffType = "sensitivityMismatch"
AttributeCodecMismatch modelDiffType = "codecMismatch"
)
// Diff calculates the diff between models a and b where a is used as base
func (a *Model) Diff(b *Model) (out ModelDiffSet) {
if a == nil {
a = &Model{}
}
if b == nil {
b = &Model{}
}
bIndex := make(map[string]struct {
found bool
attr *Attribute
})
for _, _attr := range b.Attributes {
attr := _attr
bIndex[attr.Ident] = struct {
found bool
attr *Attribute
}{
attr: attr,
}
}
aIndex := make(map[string]struct {
found bool
attr *Attribute
})
for _, _attr := range a.Attributes {
attr := _attr
aIndex[attr.Ident] = struct {
found bool
attr *Attribute
}{
attr: attr,
}
}
// Deleted and update ones
for _, _attrA := range a.Attributes {
attrA := _attrA
// store is an interface to something that could be a pointer.
// we need to copy it to make sure we don't get a nil pointer
// make sure not to modify this since it would modify the original
attrA.Store = _attrA.Store
// Missmatches
attrBAux, ok := bIndex[attrA.Ident]
if !ok {
out = append(out, &ModelDiff{
Type: AttributeMissing,
Original: attrA,
})
continue
}
// Typecheck
if attrA.Type.Type() != attrBAux.attr.Type.Type() {
out = append(out, &ModelDiff{
Type: AttributeTypeMissmatch,
Original: attrA,
Asserted: attrBAux.attr,
})
}
// Other stuff
// @todo improve; for now it'll do
if attrA.SensitivityLevelID != attrBAux.attr.SensitivityLevelID {
out = append(out, &ModelDiff{
Type: AttributeSensitivityMismatch,
Original: attrA,
Asserted: attrBAux.attr,
})
}
if attrA.Store.Type() != attrBAux.attr.Store.Type() {
out = append(out, &ModelDiff{
Type: AttributeCodecMismatch,
Original: attrA,
Asserted: attrBAux.attr,
})
}
}
// New
for _, _attrB := range b.Attributes {
attrB := _attrB
// Missmatches
_, ok := aIndex[attrB.Ident]
if !ok {
out = append(out, &ModelDiff{
Type: AttributeMissing,
Original: nil,
Asserted: attrB,
})
continue
}
}
return
}
func (dd ModelDiffSet) Alterations() (out []*Alteration) {
add := func(a *Alteration) {
out = append(out, a)
}
for _, d := range dd {
switch d.Type {
case AttributeMissing:
if d.Asserted == nil {
// @todo if this was the last attribute we can consider dropping this column
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
break
}
add(&Alteration{
AttributeDelete: &AttributeDelete{
Attr: d.Original,
},
})
} else {
if d.Asserted.Store.Type() == AttributeCodecRecordValueSetJSON {
add(&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: d.Asserted.StoreIdent(),
Type: &TypeJSON{Nullable: false},
Store: &CodecPlain{},
},
},
})
} else {
add(&Alteration{
AttributeAdd: &AttributeAdd{
Attr: d.Asserted,
},
})
}
}
case AttributeTypeMissmatch:
// @todo we might have to do some validation earlier on
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
break
}
add(&Alteration{
AttributeReType: &AttributeReType{
Attr: d.Asserted,
To: d.Asserted.Type,
},
})
case AttributeCodecMismatch:
add(&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: d.Asserted,
To: d.Asserted.Store,
},
})
}
}
return
}

View File

@@ -0,0 +1,257 @@
package dal
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
"go.uber.org/zap"
)
type (
PKValues map[string]any
ConnectionParams struct {
Type string `json:"type"`
Params map[string]any `json:"params"`
}
Connection interface {
// Meta
// Models returns all the models the underlying connection already supports
//
// This is useful when adding support for new models since we can find out what
// can work out of the box.
Models(context.Context) (ModelSet, error)
// Operations returns all of the operations the given store supports
Operations() OperationSet
// Can returns true if this store can handle the given operations
Can(operations ...Operation) bool
// DML stuff
// Create stores the given data into the underlying database
Create(ctx context.Context, m *Model, rr ...ValueGetter) error
// Update updates the given value in the underlying connection
Update(ctx context.Context, m *Model, r ValueGetter) error
// Lookup returns one bit of data
Lookup(context.Context, *Model, ValueGetter, ValueSetter) error
// Search returns an iterator which can be used to access all if the bits
Search(context.Context, *Model, filter.Filter) (Iterator, error)
// Count returns the total number of rows matching the filter
Count(context.Context, *Model, filter.Filter) (uint, error)
// Analyze returns the operation analysis the connection can perform for the model
Analyze(ctx context.Context, m *Model) (map[string]OpAnalysis, error)
// Aggregate returns the iterator with aggregated data from the base model
Aggregate(ctx context.Context, m *Model, f filter.Filter, groupBy []AggregateAttr, aggrExpr []AggregateAttr, having *ql.ASTNode) (i Iterator, _ error)
// Delete deletes the given value
Delete(ctx context.Context, m *Model, pkv ValueGetter) error
// Truncate deletes all the data for the given model
Truncate(ctx context.Context, m *Model) error
// DDL stuff
// // returns all attribute types that driver supports
// AttributeTypes() []data.AttributeType
// CreateModel adds support for the given models to the underlying database
//
// The operation returns an error if any of the models already exists.
CreateModel(context.Context, ...*Model) error
// DeleteModel removes support for the given model from the underlying database
DeleteModel(context.Context, ...*Model) error
// UpdateModel requests for metadata changes to the existing model
//
// Only metadata (such as idents) are affected; attributes can not be changed here
UpdateModel(ctx context.Context, old *Model, new *Model) error
// AssertSchemaAlterations returns a new set of Alterations based on what the underlying
// schema already provides -- it discards alterations for column additions that already exist, etc.
AssertSchemaAlterations(ctx context.Context, sch *Model, aa ...*Alteration) ([]*Alteration, error)
// ApplyAlteration applies the given alterations to the underlying schema
ApplyAlteration(ctx context.Context, sch *Model, aa ...*Alteration) []error
}
ConnectionCloser interface {
// Close closes the store connection allowing the driver to perform potential
// cleanup operations
Close(ctx context.Context) error
}
// Store provides an interface which CRS uses to interact with the underlying database
ValueGetter interface {
CountValues() map[string]uint
GetValue(string, uint) (any, error)
}
ValueSetter interface {
SetValue(string, uint, any) error
}
ConnectorFn func(ctx context.Context, dsn string) (Connection, error)
DriverConnectionParam struct {
Key string `json:"key"`
ValueType string `json:"valueType"`
MultiValue bool `json:"multiValue"`
}
DriverConnectionConfig struct {
Type string `json:"type"`
Params []DriverConnectionParam `json:"params"`
}
Driver struct {
Type string `json:"type"`
Connection DriverConnectionConfig `json:"connection"`
Operations OperationSet `json:"operations"`
}
)
var (
registeredConnectors = make(map[string]ConnectorFn)
registeredDrivers = make(map[string]Driver)
)
func (pkv PKValues) CountValues() map[string]uint {
c := make(map[string]uint)
for k := range pkv {
c[k] = 1
}
return c
}
func (pkv PKValues) GetValue(key string, _ uint) (any, error) {
if val, has := pkv[key]; has {
return val, nil
} else {
return nil, fmt.Errorf("missing")
}
}
// RegisterConnector registers a new connector for the given DSN schema
//
// In case of a duplicate schema the latter overwrites the prior
func RegisterConnector(fn ConnectorFn, tt ...string) {
for _, t := range tt {
registeredConnectors[t] = fn
}
}
func RegisterDriver(d Driver) {
registeredDrivers[d.Type] = d
}
// connect opens a new StoreConnection for the given CRS
func connect(ctx context.Context, log *zap.Logger, isDevelopment bool, cp ConnectionParams) (Connection, error) {
if cp.Type != "corteza::dal:connection:dsn" {
return nil, fmt.Errorf("cannot open connection: only DSN connections supported (got: %q)", cp.Type)
}
if cp.Params == nil {
return nil, fmt.Errorf("cannot open connection: connection parameters not defined")
}
if _, ok := cp.Params["dsn"]; !ok {
return nil, fmt.Errorf("cannot open connection: DSN not provided")
}
dsn := cp.Params["dsn"].(string)
if isDevelopment {
if strings.Contains(dsn, "{version}") {
log.Warn("You're using DB_DSN with {version}, It is still in EXPERIMENTAL phase")
log.Warn("Should be used only for development mode")
log.Warn("You may experience instability")
}
expr := regexp.MustCompile(`[.\-]+`)
version := expr.ReplaceAllString(os.Getenv("BUILD_VERSION"), "_")
dsn = strings.Replace(dsn, "{version}", version, 1)
}
var storeType = strings.SplitN(dsn, "://", 2)[0]
if storeType == "" {
// Backward compatibility
storeType = "mysql"
}
if conn, ok := registeredConnectors[storeType]; ok {
return conn(ctx, dsn)
} else {
return nil, fmt.Errorf("unknown store type used: %q (check your database configuration)", storeType)
}
}
func NewDSNDriverConnectionConfig() DriverConnectionConfig {
return DriverConnectionConfig{
Type: "corteza::dal:connection:dsn",
Params: []DriverConnectionParam{{
Key: "dsn",
ValueType: "string",
}},
}
}
//func NewHTTPDriverConnectionConfig() DriverConnectionConfig {
// panic("not implemented NewHTTPDriverConnectionConfig")
// return DriverConnectionConfig{
// Type: "corteza::dal:connection:http",
// Params: []DriverConnectionParam{{}},
// }
//}
//func NewFederatedNodeDriverConnectionConfig() DriverConnectionConfig {
// panic("not implemented NewFederatedNodeDriverConnectionConfig")
// return DriverConnectionConfig{
// Type: "corteza::dal:connection:federated-node",
// Params: []DriverConnectionParam{{}},
// }
//}
//func NewDSNConnection(dsn string) ConnectionParams {
// return ConnectionParams{
// Type: "corteza::dal:connection:dsn",
// Params: map[string]any{
// "dsn": dsn,
// },
// }
//}
//func NewHTTPConnection(url string, headers, query map[string][]string) ConnectionParams {
// return ConnectionParams{
// Type: "corteza::dal:connection:http",
// Params: map[string]any{
// "url": url,
// "headers": headers,
// "query": query,
// },
// }
//}
//
//func NewFederatedNodeConnection(url string, pairToken, authToken string) ConnectionParams {
// return ConnectionParams{
// Type: "corteza::dal:connection:federation-node",
// Params: map[string]any{
// "baseURL": url,
// "pairToken": pairToken,
// "authToken": authToken,
// },
// }
//}

View File

@@ -0,0 +1,166 @@
package dal
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/errors"
)
// Generic errors
func errModelNotFound(modelID uint64) error {
return errors.NotFound("model %d does not exist", modelID)
}
func errConnectionNotFound(connectionID uint64) error {
return errors.NotFound("connection %d does not exist", connectionID)
}
// Sensitivity level errors
// - remove
func errSensitivityLevelRemoveNotFound(sensitivityLevelID uint64) error {
return fmt.Errorf("cannot remove sensitivity level %d: sensitivity level does not exist", sensitivityLevelID)
}
// Connection errors
// - create
func errConnectionCreateMissingSensitivityLevel(connectionID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot create connection %d: sensitivity level does not exist %d", connectionID, sensitivityLevelID)
}
func errConnectionCreateConnectionFailed(connectionID uint64, err error) error {
return fmt.Errorf("cannot create connection %d: connection failed: %v", connectionID, err)
}
func errConnectionDeleteNotFound(connectionID uint64) error {
return fmt.Errorf("cannot delete connection %d: connection does not exist", connectionID)
}
func errConnectionDeleteCloserFailed(connectionID uint64, err error) error {
return fmt.Errorf("cannot delete connection %d: connection's driver failed to close: %v", connectionID, err)
}
// - update
func errConnectionUpdateNotFound(connectionID uint64) error {
return fmt.Errorf("cannot update connection %d: connection does not exist", connectionID)
}
func errConnectionUpdateMissingSensitivityLevel(connectionID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot update connection %d: sensitivity level %d does not exist", connectionID, sensitivityLevelID)
}
// Model errors
// - create
func errModelCreateProblematicConnection(connectionID, modelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: connection has issues", modelID, connectionID)
}
func errModelCreateMissingConnection(connectionID, modelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: connection does not exist", modelID, connectionID)
}
func errModelCreateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
}
func errModelCreateGreaterSensitivityLevel(connectionID, modelID, modelSensitivityLevelID, connSensitivityLevelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: sensitivity level %d exceeds connection supported sensitivity level %d", modelID, connectionID, modelSensitivityLevelID, connSensitivityLevelID)
}
func errModelCreateMissingAttributeSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: attribute sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
}
func errModelCreateGreaterAttributeSensitivityLevel(connectionID, modelID, attrSensitivityLevelID, modelSensitivityLevelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: attribute sensitivity level %d exceeds model supported sensitivity level %d", modelID, connectionID, attrSensitivityLevelID, modelSensitivityLevelID)
}
func errModelCreateConnectionModelUnsupported(connectionID, modelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: model already exists for connection but is not compatible with provided definition", modelID, connectionID)
}
func errModelCreateInvalidIdent(connectionID, modelID uint64, ident string) error {
return fmt.Errorf("cannot create model %d on connection %d: malformed model ident %s", modelID, connectionID, ident)
}
// - update
func errModelUpdateProblematicConnection(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: connection has issues", modelID, connectionID)
}
func errModelUpdateMissingConnection(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: connection does not exist", modelID, connectionID)
}
func errModelUpdateConnectionModelUnsupported(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: model already exists for connection but is not compatible with provided definition", modelID, connectionID)
}
func errModelUpdateMissingOldModel(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: model does not exist", modelID, connectionID)
}
func errModelUpdateDuplicate(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: model already exists", modelID, connectionID)
}
func errModelUpdateConnectionMissmatch(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: cannot change model connection", modelID, connectionID)
}
func errModelUpdateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
}
func errModelUpdateGreaterSensitivityLevel(connectionID, modelID, modelSensitivityLevelID, connSensitivityLevelID uint64) error {
return fmt.Errorf("cannot update model %d on connection %d: sensitivity level %d exceeds connection supported sensitivity level %d", modelID, connectionID, modelSensitivityLevelID, connSensitivityLevelID)
}
// - alterations
func errModelRequiresAlteration(connectionID, modelID, batchID uint64) error {
return fmt.Errorf("model %d on connection %d requires schema alterations: alteration batchID %d", modelID, connectionID, batchID)
}
// Attribute errors
// - Update
func errAttributeUpdateProblematicConnection(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update attribute for model %d on connection %d: connection has issues", modelID, connectionID)
}
func errAttributeUpdateMissingModel(connectionID, modelID uint64) error {
return fmt.Errorf("cannot update attribute for model %d on connection %d: model does not exist", modelID, connectionID)
}
func errAttributeUpdateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
return fmt.Errorf("cannot update attribute for model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
}
func errAttributeUpdateGreaterSensitivityLevel(connectionID, modelID, attrSensitivityLevelID, modelSensitivityLevelID uint64) error {
return fmt.Errorf("cannot update attribute for model %d on connection %d: sensitivity level %d exceeds model supported sensitivity level %d", modelID, connectionID, attrSensitivityLevelID, modelSensitivityLevelID)
}
// Record errors
func errRecordOpProblematicConnection(connectionID uint64) error {
return fmt.Errorf("cannot perform record operation: connection %d has issues", connectionID)
}
func errRecordOpProblematicModel(modelID uint64) error {
return fmt.Errorf("cannot perform record operation: model %d has issues", modelID)
}
// func errModelHigherSensitivity(model, connection string) error {
// return errors.New(
// errors.KindSensitiveData,
// "model sensitivity surpasses connection sensitivity",
// errors.Meta("type", "invalid sensitivity"),
// // Translation namespace & key
// errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
// errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.model-exceeds-connection"),
// errors.Meta("model", model),
// errors.Meta("connection", connection),
// errors.StackSkip(1),
// errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
// )
// }
// func errAttributeHigherSensitivity(model, attribute string) error {
// return errors.New(
// errors.KindSensitiveData,
// "attribute sensitivity surpasses model sensitivity",
// errors.Meta("type", "invalid sensitivity"),
// // Translation namespace & key
// errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
// errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.attribute-exceeds-model"),
// errors.Meta("model", model),
// errors.Meta("attribute", attribute),
// errors.StackSkip(1),
// errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
// )
// }

View File

@@ -0,0 +1,589 @@
package dal
import (
"context"
"fmt"
"sort"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
"github.com/tidwall/btree"
)
type (
aggregate struct {
def Aggregate
filter internalFilter
source Iterator
err error
scanRow *Row
planned bool
groupDefs []AggregateAttr
aggregateDefs []AggregateAttr
rowTester tester
keyWalker keyWalker
// Keep track of the registered groupIndex so we can easier implement the iterator.
// Simple index tracking won't be enough, because btree is self-balancing -- things change
//
// @todo worth considering using the inmembuffer and fork it around...
groupIndex *btree.Generic[*aggregateGroup]
groups []*aggregateGroup
i int
ctr int
}
keyWalker func(context.Context, ValueGetter, func(context.Context, groupKey, ValueGetter) error) error
)
// init initializes the execution step's state
func (xs *aggregate) init(ctx context.Context) (err error) {
// Initialize state variables
xs.scanRow = xs.initScanRow()
xs.groupIndex = btree.NewGeneric[*aggregateGroup](xs.compareGroupKeys)
xs.groups = make([]*aggregateGroup, 0, 128)
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
// Initialize the key maker
kk := make([]*ql.ASTNode, 0, len(xs.groupDefs))
for _, a := range xs.groupDefs {
kk = append(kk, a.Expression)
}
xs.keyWalker, err = aggregateGroupKeyWalker(kk...)
if err != nil {
return
}
// Apply the decided aggregation plan
return xs.applyPlan(ctx)
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Iterator methods
func (xs *aggregate) Next(ctx context.Context) (more bool) {
// Assure aggregation plan was taken into account
xs.err = xs.applyPlan(ctx)
if xs.err != nil {
return false
}
// Go next...
more, xs.err = xs.next(ctx)
return
}
// next prepares the next scannable row
//
// The method is adjusted based on the defined aggregation plan.
// The method performs appropriate filtering/sorting (if needed).
func (xs *aggregate) next(ctx context.Context) (more bool, err error) {
var g *aggregateGroup
for {
if xs.limitExceeded() {
return false, nil
}
// Make sure it's cleared out and ready for fresh data
xs.scanRow.Reset()
// Make sure we have data
err = xs.prepareNext(ctx)
if err != nil {
return false, err
}
// Next group
g, err = xs.nextGroup(ctx)
if err != nil || g == nil {
return false, err
}
// Make sure the key is in there
// @todo potential optimization where we don't scan entire composited keys
err = xs.scanKey(g, xs.scanRow)
if err != nil {
return
}
// Scan the data
err = g.agg.Scan(xs.scanRow)
if err != nil {
return
}
// Check if we want to keep it
k, err := xs.keep(ctx, xs.scanRow)
if err != nil {
return false, err
}
if !k {
continue
}
xs.ctr++
break
}
return true, nil
}
func (xs *aggregate) limitExceeded() bool {
return xs.filter.limit > 0 && xs.ctr >= int(xs.filter.limit)
}
func (xs *aggregate) More(limit uint, v ValueGetter) (err error) {
// Redo the cursor
xs.filter.cursor, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes()...)
if err != nil {
return
}
// Redo the row tester
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
// Redo the state
// @todo adjust based on aggregation plan; reuse buffered, etc.
xs.scanRow.Reset()
xs.groupIndex = btree.NewGeneric[*aggregateGroup](xs.compareGroupKeys)
xs.groups = make([]*aggregateGroup, 0, 128)
xs.planned = false
xs.i = 0
return
}
func (s *aggregate) Err() error { return s.err }
func (s *aggregate) Scan(dst ValueSetter) (err error) {
if s.i < 0 {
return fmt.Errorf("@todo err not initialized; next first")
}
var v any
for name, cc := range s.scanRow.CountValues() {
for i := uint(0); i < cc; i++ {
// omitting err here since it won't happen
v, _ = s.scanRow.GetValue(name, i)
err = dst.SetValue(name, i, v)
if err != nil {
return err
}
}
}
return nil
}
func (s *aggregate) Close() error {
if s.source != nil {
return s.source.Close()
}
return nil
}
func (s *aggregate) BackCursor(v ValueGetter) (*filter.PagingCursor, error) {
c, err := filter.PagingCursorFrom(s.filter.OrderBy(), v, s.collectPrimaryAttributes()...)
if err != nil {
return nil, err
}
c.ROrder = true
c.LThen = s.filter.OrderBy().Reversed()
return c, nil
}
func (s *aggregate) ForwardCursor(v ValueGetter) (*filter.PagingCursor, error) {
c, err := filter.PagingCursorFrom(s.filter.OrderBy(), v, s.collectPrimaryAttributes()...)
if err != nil {
return nil, err
}
return c, nil
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// applyPlan runs additional initialization code based on the defined aggregation plan
func (xs *aggregate) applyPlan(ctx context.Context) (err error) {
if xs.planned || xs.err != nil {
return xs.err
}
xs.planned = true
switch {
case !xs.def.plan.partialScan:
return xs.pullEntireSource(ctx)
}
return
}
func (xs *aggregate) prepareNext(ctx context.Context) (err error) {
// Pull next chunk from source if not entirely buffered
if xs.def.plan.partialScan {
err = xs.pullNextChunk(ctx)
if err != nil {
return
}
}
return
}
// pullEntireSource pulls the entire source into aggregators
//
// The entire source should be pulled when underlaying datasources can't provide
// more appropriately ordered data due to costs.
func (xs *aggregate) pullEntireSource(ctx context.Context) (err error) {
if xs.source == nil {
return fmt.Errorf("unable to pull data: no source defined")
}
// @todo consider pre-populating the hashmaps
r := &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
// Drain the source
for xs.source.Next(ctx) {
err = xs.source.Scan(r)
if err != nil {
return
}
// Get the key for this row
// @todo we probably can reuse the key or at least cache keys and avoid re-computation.
// My fairly hacky attempt boosted performance by ~20%
err = xs.keyWalker(ctx, r, xs.addToGroup)
if err != nil {
return
}
r.Reset()
}
xs.err = xs.source.Err()
if xs.err != nil {
return xs.err
}
xs.sortGroups()
return
}
// pullNextChunk pulls the next chunk into the aggregators
//
// Source should be pulled in chunks when the underlaying datasource can provide
// more appropriately ordered data.
func (xs *aggregate) pullNextChunk(ctx context.Context) (err error) {
return fmt.Errorf("not implemented")
}
func (s *aggregate) addToGroup(ctx context.Context, key groupKey, r ValueGetter) (err error) {
if s.groupIndex == nil {
s.groupIndex = btree.NewGeneric[*aggregateGroup](s.compareGroupKeys)
}
// Try to get the existing one; if it doesn't exist, push a new one
// @todo this causes a bit of a memory pressure; investigate
g, ok := s.groupIndex.Get(&aggregateGroup{key: key})
if !ok {
g, err = s.wrapGroup(ctx, key)
if err != nil {
return err
}
s.groupIndex.Set(g)
s.groups = append(s.groups, g)
}
// if it exists/was added, add it to the aggregator
err = g.agg.Aggregate(ctx, r)
if err != nil {
return
}
return
}
// Group key comparator
// @todo can this be moved to the struct's Less method?
func (t *aggregate) compareGroupKeys(a, b *aggregateGroup) (out bool) {
for i := range a.key {
va := a.key[i]
vb := b.key[i]
out = out || compareValues(va, vb) < 0
}
return
}
func (s *aggregate) wrapGroup(ctx context.Context, key groupKey) (g *aggregateGroup, err error) {
agg := Aggregator()
for _, a := range s.aggregateDefs {
err = agg.AddAggregate(a.Identifier, a.Expression)
if err != nil {
return
}
}
g = &aggregateGroup{
key: key,
agg: agg,
}
return
}
func (xs *aggregate) nextGroup(ctx context.Context) (_ *aggregateGroup, err error) {
if xs.i >= len(xs.groups) {
return
}
xs.i++
return xs.groups[xs.i-1], nil
}
func (xs *aggregate) scanKey(g *aggregateGroup, dst *Row) (err error) {
for i, attr := range xs.groupDefs {
// @todo multi value support?
// omitting err; internal row won't raise them
dst.SetValue(attr.Identifier, 0, g.key[i])
}
return nil
}
func (s *aggregate) keep(ctx context.Context, r *Row) (bool, error) {
if s.rowTester == nil {
return true, nil
}
return s.rowTester.Test(ctx, r)
}
// Each group key is a PK candidate; all together form a composite key
func (s *aggregate) collectPrimaryAttributes() (out []string) {
out = make([]string, 0, 2)
for _, m := range s.def.Group {
out = append(out, m.Identifier)
}
return
}
func (s *aggregate) sortGroups() {
sort.SliceStable(s.groups, func(i, j int) bool {
ga := s.groups[i]
gb := s.groups[j]
var (
va any
vb any
)
for _, o := range s.filter.OrderBy() {
x := inKeys(s.def.Group, o.Column)
if x > -1 {
va = ga.key[x]
} else {
x := inKeys(s.def.OutAttributes, o.Column)
va = ga.agg.aggregates[x]
}
x = inKeys(s.def.Group, o.Column)
if x > -1 {
vb = gb.key[x]
} else {
x := inKeys(s.def.OutAttributes, o.Column)
vb = gb.agg.aggregates[x]
}
cmp := compareValues(va, vb)
if cmp != 0 {
if o.Descending {
return cmp > 0
}
return cmp < 0
}
}
return false
})
}
func inKeys(kk []AggregateAttr, ident string) int {
for i, k := range kk {
if k.Identifier == ident {
return i
}
}
return -1
}
func (xs *aggregate) initScanRow() (out *Row) {
// base
out = &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
// pre-populate with known attrs
for _, attr := range append(xs.def.Group, xs.def.OutAttributes...) {
out.values[attr.Identifier] = make([]any, 0, 2)
out.counters[attr.Identifier] = 0
}
return
}
// aggregateGroupKeyWalker prepares a function which runs the provided function over all group keys
//
// This is required to handle multi-value attributes when used in group keys.
//
// Algorithm TL;DR
//
// We're going backwards in the slice of idents we need to handle (going the other way around)
// should also work but I chose to do it like so.
//
// For every ident, keep track of the number of elements and the index of the last unhandled ident.
//
// For every iteration, collect all of the values pointed to by the unused index.
// After the key is processed, increment the counter for the current ident.
// If the counter exceeds the number of elements, reset the counter for the current ident and move
// the ident pointer backwards (repeat if that ident's counter also exceeds the limit).
//
// When we find an indent which still has some items to process (counter doesn't exceed limit),
// reset the ident pointer to the end of the slice and repeat the whole thing.
func aggregateGroupKeyWalker(kk ...*ql.ASTNode) (out keyWalker, err error) {
// @todo option to copy constants and idents
runners, err := makeExprRunners(kk...)
if err != nil {
return
}
// We'll sort the idents to keep the output consistent; the order doesn't matter
// but it will simplify testing.
idents, hasConstants := keysFromExpr(kk...)
sort.Strings(idents)
out = func(ctx context.Context, vg ValueGetter, run func(context.Context, groupKey, ValueGetter) error) error {
// Edgecase for when all of the expressions return constant values
if len(idents) == 0 {
// This should be impossible but better safe then sorry
if !hasConstants {
return nil
}
k, err := makeGroupKey(ctx, runners, vg)
if err != nil {
return err
}
return run(ctx, k, vg)
}
// For every value combination of idents used in agg. key expressions
// construct a key and run the runner.
limits := vg.CountValues()
ptr := len(idents) - 1
counts := make(map[string]uint, len(limits))
for k := range limits {
counts[k] = 0
}
handle := func() (err error) {
aux := make(map[string]any, len(limits))
for k, i := range counts {
aux[k], err = vg.GetValue(k, i)
if err != nil {
return
}
}
kk, err := makeGroupKey(ctx, runners, aux)
if err != nil {
return err
}
return run(ctx, kk, vg)
}
outer:
for ptr >= 0 {
handle()
counts[idents[ptr]]++
for {
// There are still values to process so we can skip the rest
if counts[idents[ptr]] < limits[idents[ptr]] {
continue outer
}
// Reset the counter for the current ptr since we'll move back.
// The outer loop will have this reset all of the counters after the current ptr.
counts[idents[ptr]] = 0
ptr--
if ptr < 0 {
break outer
}
counts[idents[ptr]]++
if counts[idents[ptr]] >= limits[idents[ptr]] {
continue
}
// We need to reset to the end so the next ident gets all of the values
// that appear after it.
ptr = len(idents) - 1
break
}
}
return nil
}
return
}
func makeExprRunners(kk ...*ql.ASTNode) (out []*runnerGval, err error) {
out = make([]*runnerGval, len(kk))
for i, k := range kk {
out[i], err = newRunnerGvalParsed(k)
if err != nil {
return
}
}
return
}
func makeGroupKey(ctx context.Context, runners []*runnerGval, vals any) (gk groupKey, err error) {
gk = make(groupKey, len(runners))
for i, r := range runners {
v, err := r.Eval(ctx, vals)
if err != nil {
return nil, err
}
gk[i] = v
}
return gk, nil
}

View File

@@ -0,0 +1,417 @@
package dal
import (
"context"
"fmt"
"io"
"strings"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/tidwall/btree"
)
type (
// Considerations for optimizations
// - Rework how values are stored internally.
// Potentially rework the pipeline input/output interfaces, how ValueGetter/Setter work.
// - Skip initial left row scan/sort when sorting not requested.
// - Rework row struct to use slices instead of hashmaps.
// With how values are now the performance gain is not that impactful and the
// rework complexity is a bit too high for now.
// - Use `.More` to filter rows by key.
// - When data is provided in a satisfactory order, use that to pull data in chunks.
joinLeft struct {
def Join
filter internalFilter
leftSource Iterator
rightSource Iterator
err error
scanRow ValueGetter
planned bool
filtered bool
rowTester tester
// Index to keep track of related rows
relIndex *relIndex
// Output placeholder for sorted rows
// @todo consider a generic slice for cases when sorting is not needed.
// This will probably save up on memory/time since we don't even need
// to pull everything.
outSorted *btree.Generic[ValueGetter]
i int
}
)
func (xs *joinLeft) init(ctx context.Context, joinPredType Type) (err error) {
xs.relIndex, err = newRelIndex(joinPredType)
if err != nil {
return
}
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
// @note careful here if you throw routines into the mix; see the NoLocks flag.
// Enabling locks does have a performance impact so you might be better off by
// constructing multiple of these but then you'll also need to complicate
// the .Next methods a bit.
xs.outSorted = btree.NewGenericOptions[ValueGetter](makeRowComparator(xs.filter.OrderBy()...), btree.Options{NoLocks: true})
return xs.applyPlan(ctx)
}
func (xs *joinLeft) Next(ctx context.Context) (more bool) {
xs.err = xs.applyPlan(ctx)
if xs.err != nil {
return false
}
more, xs.err = xs.next(ctx)
return
}
func (xs *joinLeft) More(limit uint, v ValueGetter) (err error) {
xs.filter.cursor, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
if err != nil {
return
}
// Redo row tester
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
// Redo the state
// @todo adjust based on aggregation plan; reuse buffered, etc.
xs.relIndex.Clear()
xs.outSorted = btree.NewGenericOptions[ValueGetter](makeRowComparator(xs.filter.OrderBy()...), btree.Options{NoLocks: true})
xs.scanRow = nil
xs.planned = false
xs.i = 0
return
}
func (xs *joinLeft) Err() error { return xs.err }
func (xs *joinLeft) Scan(s ValueSetter) (err error) {
for k, cc := range xs.scanRow.CountValues() {
for i := uint(0); i < cc; i++ {
// @note internal row won't raise errors so we can safely omit them
v, _ := xs.scanRow.GetValue(k, i)
err = s.SetValue(k, i, v)
if err != nil {
return
}
}
}
return
}
func (xs *joinLeft) Close() (err error) {
if xs == nil {
return
}
cc := []io.Closer{
xs.leftSource,
xs.rightSource,
}
for _, c := range cc {
if c != nil {
err = c.Close()
if err != nil {
return err
}
}
}
return
}
func (xs *joinLeft) BackCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
if err != nil {
return nil, err
}
pc.ROrder = true
pc.LThen = xs.filter.OrderBy().Reversed()
return
}
func (xs *joinLeft) ForwardCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
if err != nil {
return nil, err
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility methods
// next prepares the next scan row based on the defined join plan
func (xs *joinLeft) next(ctx context.Context) (more bool, err error) {
if xs.limitExceeded() {
return false, nil
}
more, err = xs.pullNext(ctx)
if !more || err != nil {
return more, err
}
xs.scanRow, _ = xs.outSorted.GetAt(xs.i)
xs.i++
return true, nil
}
func (xs *joinLeft) limitExceeded() bool {
return xs.filter.limit > 0 && xs.i >= int(xs.filter.limit)
}
// pullNext pulls additional data so we can produce more
//
// This step may be omitted based on the join plan.
func (xs *joinLeft) pullNext(ctx context.Context) (more bool, err error) {
// Pull next chunk from source if not entirely buffered
if xs.def.plan.partialScan {
// @todo this case is currently not implemented so we're erroring it out
return false, fmt.Errorf("partialScan join plan strategy not implemented")
}
// Check if buffer has more
if xs.i >= xs.outSorted.Len() {
return false, nil
}
return true, nil
}
// applyPlan runs plan specific logic to prepare the state
func (xs *joinLeft) applyPlan(ctx context.Context) (err error) {
if xs.planned || xs.err != nil {
return
}
xs.planned = true
switch {
case !xs.def.plan.partialScan:
return xs.pullEntireSource(ctx)
}
return
}
// pullEntireSource pulls both sources into memory and indexes them for later use
func (xs *joinLeft) pullEntireSource(ctx context.Context) (err error) {
// This bit does the filtering so just mark it of as such here
xs.filtered = true
// First the right source
err = xs.pullEntireRightSource(ctx)
if err != nil {
return
}
// Next the left source
err = xs.pullEntireLeftSource(ctx)
if err != nil {
return
}
return
}
// pullEntireRightSource pulls and indexes all of the right bits
func (xs *joinLeft) pullEntireRightSource(ctx context.Context) (err error) {
for xs.rightSource.Next(ctx) {
r := &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
err = xs.rightSource.Scan(r)
if err != nil {
return
}
err = xs.indexRightRow(r)
if err != nil {
return
}
}
return xs.rightSource.Err()
}
// pullEntireLeftSource pulls left bits and attempts to do as much of the joining
// work on this stage
func (xs *joinLeft) pullEntireLeftSource(ctx context.Context) (err error) {
for xs.leftSource.Next(ctx) {
l := &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
err = xs.leftSource.Scan(l)
if err != nil {
return
}
err = xs.joinRight(ctx, l)
if err != nil {
return
}
}
return xs.leftSource.Err()
}
// joinRight finds related right rows for the given left row and matches them up
//
// @note for sorting, we use a b-tree as it's self sorting.
// Benchmarking shows that using a slice is negligibly faster if faster at all.
func (xs *joinLeft) joinRight(ctx context.Context, left *Row) (err error) {
bb, ok, err := xs.getRelatedBuffers(left)
if err != nil || !ok {
return
}
for _, b := range bb {
for _, right := range b.rows {
// Merge the two
xs.mergeRows(xs.def.OutAttributes, right, left, right)
// Assert if we want to keep
k, err := xs.keep(ctx, right)
if err != nil {
return err
}
if !k {
continue
}
xs.outSorted.Set(right)
}
}
return
}
// getRelatedBuffers returns all of the right rows corresponding to the given left row
func (xs *joinLeft) getRelatedBuffers(l *Row) (out []*relIndexBuffer, ok bool, err error) {
var aux *relIndexBuffer
for c := uint(0); c < l.CountValues()[xs.def.On.Left]; c++ {
// @note internal Row struct never errors
v, _ := l.GetValue(xs.def.On.Left, c)
aux, ok = xs.relIndex.Get(v)
if !ok {
continue
}
out = append(out, aux)
}
return
}
// indexRightRow pushes the provided row onto the rel index
// @todo consider moving most of this logic to the relIndex struct.
func (xs *joinLeft) indexRightRow(r *Row) (err error) {
for i := uint(0); i < r.CountValues()[xs.def.On.Right]; i++ {
v, err := r.GetValue(xs.def.On.Right, i)
if err != nil {
return err
}
xs.relIndex.Add(v, r)
}
return
}
// keep checks if the row should be kept or discarded
func (xs *joinLeft) keep(ctx context.Context, r *Row) (bool, error) {
if xs.rowTester == nil {
return true, nil
}
return xs.rowTester.Test(ctx, r)
}
// collectPrimaryAttributes returns all of the attributes of the composited key
//
// For joins, all primary attributes from both of the sides should be in here
// since that is what always uniquely identifies a joined row.
//
// @todo consider applying PK candidates and filter out some of these. I don't
// think it'll provide much of a performance boost but worth a shot later on.
func (xs *joinLeft) collectPrimaryAttributes(mm []AttributeMapping) (out []string) {
out = make([]string, 0, 2)
for _, m := range mm {
if m.Properties().IsPrimary {
out = append(out, m.Identifier())
}
}
return
}
// mergeRows merges the left and right rows based on the provided attrs
func (xs *joinLeft) mergeRows(attrs []AttributeMapping, out, left, right *Row) {
var (
identSide int
srcIdent string
)
for _, attr := range attrs {
srcIdent = attr.Identifier()
identSide = xs.identSide(srcIdent)
if identSide == -1 {
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, left)
} else if identSide == 1 {
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, right)
} else {
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, left, right)
}
}
}
func (xs *joinLeft) mergeValuesFrom(inIdent, outIdent string, out *Row, sources ...*Row) {
var (
aux any
)
for _, src := range sources {
for c := uint(0); c < src.CountValues()[inIdent]; c++ {
aux, _ = src.GetValue(inIdent, c)
out.SetValue(outIdent, c, aux)
}
}
}
// identSide returns -1 if the ident belongs to the left source, 1 if it belongs
// to the right side, and 0 if it's either.
//
// @todo consider adding an additional flag to identify what side it is on.
// For now, this should be fine.
func (xs *joinLeft) identSide(ident string) int {
pp := strings.Split(ident, attributeNestingSeparator)
if len(pp) > 1 {
if pp[0] == xs.def.RelLeft {
return -1
} else if pp[0] == xs.def.RelRight {
return 1
}
}
return 0
}

View File

@@ -0,0 +1,505 @@
package dal
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/pkg/filter"
"io"
"sort"
"strconv"
)
type (
linkLeft struct {
def Link
filter internalFilter
leftSource Iterator
rightSource Iterator
err error
scanRow *Row
planned bool
filtered bool
rightSortAttrs []string
rowTester tester
// Buffer to keep track of pulled left rows
leftRows []*Row
relIndex *relIndex
keepLeft bool
leftIndex int
rightIndex int
// Some helper fields for temporary data
leftRow *Row
rightRow *Row
relScanBuffer *relIndexBuffer
}
)
func (xs *linkLeft) init(ctx context.Context, linkPredType Type, rightSort []string) (err error) {
// @note the index is keeping track of the right source attributes so we can
// simplify the sorting logic.
xs.relIndex, err = newRelIndex(linkPredType, rightSort...)
if err != nil {
return
}
xs.rightSortAttrs = rightSort
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
return xs.applyPlan(ctx)
}
func (xs *linkLeft) Next(ctx context.Context) (more bool) {
xs.err = xs.applyPlan(ctx)
if xs.err != nil {
return false
}
more, xs.err = xs.next(ctx)
return
}
func (xs *linkLeft) More(limit uint, v ValueGetter) (err error) {
xs.filter.cursor, err = xs.ForwardCursor(v)
if err != nil {
return
}
// Redo row tester
xs.rowTester, err = prepareGenericRowTester(xs.filter)
if err != nil {
return
}
// Redo the state
// @todo adjust based on aggregation plan; reuse buffered, etc.
xs.relIndex.Clear()
xs.leftRows = make([]*Row, 0, 128)
xs.scanRow = nil
xs.planned = false
xs.keepLeft = false
xs.leftIndex = 0
xs.rightIndex = 0
return
}
func (xs *linkLeft) Err() error { return xs.err }
func (xs *linkLeft) Scan(s ValueSetter) (err error) {
for k, cc := range xs.scanRow.CountValues() {
for i := uint(0); i < cc; i++ {
// @note internal row won't raise errors so we can safely omit them
v, _ := xs.scanRow.GetValue(k, i)
err = s.SetValue(k, i, v)
if err != nil {
return
}
}
}
return
}
func (xs *linkLeft) Close() (err error) {
if xs == nil {
return
}
cc := []io.Closer{
xs.leftSource,
xs.rightSource,
}
for _, c := range cc {
if c != nil {
err = c.Close()
if err != nil {
return err
}
}
}
return
}
func (xs *linkLeft) BackCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
g := &rowLink{
a: xs.leftRow,
b: v,
}
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), g, xs.collectPrimaryAttributes()...)
if err != nil {
return nil, err
}
pc.ROrder = true
pc.LThen = xs.filter.OrderBy().Reversed()
return
}
func (xs *linkLeft) ForwardCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
g := &rowLink{
a: xs.leftRow,
b: v,
}
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), g, xs.collectPrimaryAttributes()...)
if err != nil {
return nil, err
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility methods
// next prepares the next scan row based on the defined link plan
func (xs *linkLeft) next(ctx context.Context) (more bool, err error) {
more, err = xs.pullNext(ctx)
if !more || err != nil {
return more, err
}
return xs.nextBuffered()
}
// pullNext pulls additional data so we can produce more
//
// This step may be omitted based on the join plan.
func (xs *linkLeft) pullNext(ctx context.Context) (more bool, err error) {
// Pull next chunk from source if not entirely buffered
if xs.def.plan.partialScan {
return false, fmt.Errorf("partialScan join plan strategy not implemented")
}
// Check if buffer has more
// We need to check for gt because the right bits may still be relevant
if xs.leftIndex > len(xs.leftRows) {
return false, nil
}
return true, nil
}
// nextBuffered prepares the next scan row from the buffers
func (xs *linkLeft) nextBuffered() (more bool, err error) {
// keepLeft indicates if we should keep the left row and move onto the next
// in the right buffers.
//
// If we're keeping it, take the next row from the other side, else take the
// next left one and reset right counters.
var (
ok bool
)
for {
if !xs.keepLeft {
// Go for the next left row
if xs.leftIndex >= len(xs.leftRows) {
return
}
xs.leftRow = xs.leftRows[xs.leftIndex]
xs.leftIndex++
// Go for the next right buffer
xs.relScanBuffer, ok, err = xs.getRelatedBuffer(xs.leftRow)
if !ok || err != nil {
return
}
// if len(rel) > 1 {
// // @todo implement this; not entirely sure how it should be so I'll block it for now
// // @todo move this check futher up
// return false, fmt.Errorf("multi-value link predicates not supported")
// }
// xs.relScanBuffer = rel[0]
xs.rightIndex = 0
xs.scanRow = xs.leftRow
xs.keepLeft = true
return true, nil
}
// Related buffer done
if xs.rightIndex >= len(xs.relScanBuffer.rows) {
xs.keepLeft = false
continue
}
xs.rightRow = xs.relScanBuffer.rows[xs.rightIndex]
xs.rightIndex++
xs.scanRow = xs.rightRow
// Sys defined value to indicate the source
// @todo consider using some wrap struct instead
xs.scanRow.SetValue(LinkRefIdent, 0, xs.def.RelRight)
break
}
return true, nil
}
// getRelatedBuffer returns all of the right rows corresponding to the given left row
func (xs *linkLeft) getRelatedBuffer(l *Row) (out *relIndexBuffer, ok bool, err error) {
// @todo mv link predicate attrs
v, _ := l.GetValue(xs.def.On.Left, 0)
out, ok = xs.relIndex.Get(v)
return
}
// applyPlan runs plan specific logic to prepare the state
func (xs *linkLeft) applyPlan(ctx context.Context) (err error) {
if xs.planned || xs.err != nil {
return
}
xs.planned = true
switch {
case !xs.def.plan.partialScan:
return xs.pullEntireSource(ctx)
}
return
}
// pullEntireSource pulls both sources into memory and indexes them for later use
func (xs *linkLeft) pullEntireSource(ctx context.Context) (err error) {
// This bit does the filtering so just mark it of as such here
xs.filtered = true
// First the right source
err = xs.pullEntireRightSource(ctx)
if err != nil {
return
}
// Next the left source
err = xs.pullEntireLeftSource(ctx)
if err != nil {
return
}
// Sort if needed
return xs.sortLeftRows()
}
// pullEntireRightSource pulls and indexes all of the right bits
func (xs *linkLeft) pullEntireRightSource(ctx context.Context) (err error) {
// Drain the source
for xs.rightSource.Next(ctx) {
r := &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
err = xs.rightSource.Scan(r)
if err != nil {
return
}
err = xs.indexRightRow(r)
if err != nil {
return
}
}
return xs.rightSource.Err()
}
// pullEntireLeftSource pulls and indexes all of the left bits
func (xs *linkLeft) pullEntireLeftSource(ctx context.Context) (err error) {
var (
rel *relIndexBuffer
ok bool
)
// Drain the source
for xs.leftSource.Next(ctx) {
l := &Row{
counters: make(map[string]uint),
values: make(valueSet),
}
err = xs.leftSource.Scan(l)
if err != nil {
return
}
rel, ok, err = xs.getRelatedBuffer(l)
if err != nil {
return
}
if !ok {
continue
}
k, err := xs.keep(ctx, l, rel)
if err != nil {
return err
}
if !k {
continue
}
xs.leftRows = append(xs.leftRows, l)
}
return xs.leftSource.Err()
}
// indexRightRow pushes the provided row onto the rel index
// @todo consider moving most of this logic to the relIndex struct.
func (xs *linkLeft) indexRightRow(r *Row) (err error) {
// @todo mv link predicate attrs; should be prevented higher up for now
v, err := r.GetValue(xs.def.On.Right, 0)
if err != nil {
return err
}
xs.relIndex.Add(v, r)
return
}
// sortLeftRows sorts the left rows into the correct order
//
// Algorithm outline:
// Compare the two left rows based on the defined sort order.
// If the two left rows match up, or we're primarily sorting using right rows,
// check the min/max stats values produced by the index buffer.
//
// If some min/max of a chunk is greater/lesser then the other, no row in the other
// can appear before/after the prior.
func (xs *linkLeft) sortLeftRows() (err error) {
var (
leftRelBufferA *relIndexBuffer
leftRelBufferB *relIndexBuffer
a, b any
)
// Use stable sort just so we don't needlesly messup the initial order if we decide
// to preserve sorts produced by further steps.
sort.SliceStable(xs.leftRows, func(i, j int) bool {
if err != nil {
return false
}
for _, s := range xs.filter.OrderBy() {
// Prepare the data
leftRowA := xs.leftRows[i]
leftRowB := xs.leftRows[j]
leftRowA = convertStringToIntForColumn(s.Column, leftRowA)
leftRowB = convertStringToIntForColumn(s.Column, leftRowB)
leftRelBufferA, _, err = xs.getRelatedBuffer(leftRowA)
if err != nil {
return false
}
leftRelBufferB, _, err = xs.getRelatedBuffer(leftRowB)
if err != nil {
return false
}
if !xs.isRightSortAttr(s.Column) {
// This bit here orders based on the left attributes
less, skip := evalCmpResult(compareGetters(leftRowA, leftRowB, leftRowA.counters, leftRowB.counters, s.Column), s)
if !skip {
return less
}
} else {
// This bit here orders based on the right attributes
// Check the stats of the buffer; make sure to adjust based on direction
// Use chunk's values
if !s.Descending {
a = leftRelBufferA.min[s.Column]
b = leftRelBufferB.min[s.Column]
} else {
a = leftRelBufferA.max[s.Column]
b = leftRelBufferB.max[s.Column]
}
less, skip := evalCmpResult(compareValues(a, b), s)
if !skip {
return less
}
}
}
return false
})
return
}
func (xs *linkLeft) isRightSortAttr(ident string) bool {
for _, i := range xs.rightSortAttrs {
if i == ident {
return true
}
}
return false
}
// keep checks if the row should be kept or discarded
//
// Link's keep is a bit more complicated and it looks at the related buffer as well.
func (xs *linkLeft) keep(ctx context.Context, left *Row, buffer *relIndexBuffer) (keep bool, err error) {
// If no buffer, we won't keep -- left inner join like behavior
if buffer == nil {
return false, nil
}
// No tester include all ok rows
if xs.rowTester == nil {
return true, nil
}
ch := &rowLink{a: left}
for _, ch.b = range buffer.rows {
k, err := xs.rowTester.Test(ctx, ch)
if err != nil {
return false, err
}
if !k {
return false, nil
}
}
return true, nil
}
func (xs *linkLeft) collectPrimaryAttributes() (out []string) {
out = make([]string, 0, 2)
for _, m := range xs.def.OutLeftAttributes {
if m.Properties().IsPrimary {
out = append(out, m.Identifier())
}
}
for _, m := range xs.def.OutRightAttributes {
if m.Properties().IsPrimary {
out = append(out, m.Identifier())
}
}
return
}
// convertStringToIntForColumn converts the string value that have numeric data in the specified column
// and updates the Row value datatype to int, if the conversion is successful
func convertStringToIntForColumn(column string, row *Row) *Row {
if strValue, ok := row.values[column][0].(string); ok {
if rowVal, err := strconv.Atoi(strValue); err == nil {
row.values[column][0] = rowVal
}
}
return row
}

View File

@@ -0,0 +1,189 @@
package dal
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
)
type (
// internalFilter is a wrap to simplify interactions for cases where we might
// want to interact with the received filters; i.e. when optimizing pipeline steps
internalFilter struct {
constraints map[string][]any
stateConstraints map[string]filter.State
metaConstraints map[string]any
expression string
expParsed *ql.ASTNode
orderBy filter.SortExprSet
limit uint
cursor *filter.PagingCursor
}
parsedFilter interface {
ExpressionParsed() *ql.ASTNode
}
)
func (f internalFilter) Constraints() map[string][]any { return f.constraints }
func (f internalFilter) StateConstraints() map[string]filter.State { return f.stateConstraints }
func (f internalFilter) MetaConstraints() map[string]any { return f.metaConstraints }
func (f internalFilter) Expression() string { return f.expression }
func (f internalFilter) ExpressionParsed() *ql.ASTNode { return f.expParsed }
func (f internalFilter) OrderBy() filter.SortExprSet { return f.orderBy }
func (f internalFilter) Limit() uint { return f.limit }
func (f internalFilter) Cursor() *filter.PagingCursor { return f.cursor }
// toInternalFilter converts filter.Filter to internalFilter for easier manipulation
//
// nil filters are returned as zeroed-out internalFilters.
func toInternalFilter(f filter.Filter) (out internalFilter, err error) {
if f == nil {
return
}
// Base
out = internalFilter{
constraints: f.Constraints(),
stateConstraints: f.StateConstraints(),
expression: f.Expression(),
orderBy: f.OrderBy(),
limit: f.Limit(),
cursor: f.Cursor(),
}
pf, ok := f.(parsedFilter)
if ok {
out.expParsed = pf.ExpressionParsed()
// In case the filter was already provided, we need to make sure the idents
// are wrapped
out.expParsed.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
a.Symbol = wrapNestedGvalIdent(a.Symbol)
return true, a, nil
})
}
if out.expParsed != nil {
// We can't mix the two so just clear it out to avoid confusion
out.expression = ""
}
// Parse expression for later use
if out.expression != "" {
out.expParsed, err = newQlParser().Parse(out.expression)
if err != nil {
return
}
}
return
}
func FilterFromExpr(n *ql.ASTNode) internalFilter {
// @todo consider adding string expr for consistency
return internalFilter{
expParsed: n,
}
}
// MergeFilters returns a new filter based on a overwritten by b
func (a internalFilter) MergeFilters(b filter.Filter) (c internalFilter, err error) {
// In case we got a generic b filter, convert it to the internal one for easier handling
aux, ok := b.(internalFilter)
if !ok {
var err error
aux, err = toInternalFilter(b)
if err != nil {
return c, err
}
}
return a.mergeFilters(aux), nil
}
func (a internalFilter) mergeFilters(b internalFilter) (c internalFilter) {
c = a
// expression
if b.expParsed != nil {
if c.expParsed == nil {
c.expParsed = b.expParsed
c.expression = b.expression
} else {
parsedA := a.expParsed
rawA := a.expression
if parsedA.Ref != "group" {
parsedA = &ql.ASTNode{
Ref: "group",
Args: []*ql.ASTNode{
parsedA,
},
}
rawA = fmt.Sprintf("(%s)", rawA)
}
parsedB := b.expParsed
rawB := b.expression
if parsedB.Ref != "group" {
parsedB = &ql.ASTNode{
Ref: "group",
Args: []*ql.ASTNode{
parsedB,
},
}
rawB = fmt.Sprintf("(%s)", rawB)
}
c.expParsed = &ql.ASTNode{
Ref: "and",
Args: ql.ASTNodeSet{parsedA, parsedB},
}
c.expression = fmt.Sprintf("%s && %s", rawA, rawB)
}
}
// constraints
if b.constraints != nil {
if c.constraints == nil {
c.constraints = make(map[string][]any)
}
for k, v := range b.constraints {
c.constraints[k] = append(c.constraints[k], v...)
}
}
// state constraints
if b.stateConstraints != nil {
if c.stateConstraints == nil {
c.stateConstraints = make(map[string]filter.State)
}
for k, v := range b.stateConstraints {
c.stateConstraints[k] = v
}
}
// order by
if b.orderBy != nil {
c.orderBy = append(c.orderBy, b.orderBy...)
}
// cursor
// always use the latest paging cursor
c.cursor = b.cursor
return
}
// empty reports if the filter is completely empty or not
func (f internalFilter) empty() bool {
return f.constraints == nil &&
f.stateConstraints == nil &&
f.expression == "" &&
f.orderBy == nil &&
f.limit == 0 &&
f.cursor == nil
}

View File

@@ -0,0 +1,212 @@
package dal
import (
"context"
"fmt"
"sort"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// inmemBuffer isa simple DAL buffer which holds all of the data in memory
//
// This buffer should be used for small datasets
inmemBuffer struct {
rows []ValueGetter
ctrs []map[string]uint
i int
cap int
closed bool
more bool
err error
sort filter.SortExprSet
}
)
// InMemoryBuffer initializes a new DAL buffer where the data is kept in memory
func InMemoryBuffer() *inmemBuffer {
return &inmemBuffer{
// @note we'll buffer the value counters along side the actual value counter
// because constant hashmap initialization is quite memory intensive and introduces
// a bit of a bottleneck.
//
// @todo investigate if this can be optimized at a lower level
rows: make([]ValueGetter, 0, 100),
ctrs: make([]map[string]uint, 0, 100),
// i starts off as -1 so we don't need an extra state flag when doing the first Next
i: -1,
more: true,
}
}
// InMemoryBufferWith returns a new buffer with the given value getters
func InMemoryBufferWith(ctx context.Context, vv ...ValueGetter) (Buffer, error) {
b := InMemoryBuffer()
for _, v := range vv {
if err := b.Add(ctx, v); err != nil {
return nil, err
}
}
return b, nil
}
func (b *inmemBuffer) InOrder(ss ...*filter.SortExpr) (err error) {
b.sort = ss
return nil
}
// Single makes the buffer only keep one element at the time
// @todo make this option convert the buffer into a circular buffer
func (b *inmemBuffer) Single() {
b.cap = 1
b.rows = make([]ValueGetter, 1)
b.ctrs = make([]map[string]uint, 1)
b.i = -1
}
func (b *inmemBuffer) Seek(_ context.Context, i int) (err error) {
// we go one level further and start at -1
b.i = i - 1
b.more = i < b.Len()
return
}
func (b *inmemBuffer) isSingle() bool {
return b.cap == 1
}
func (b *inmemBuffer) isSorted() bool {
return len(b.sort) > 0
}
func (b *inmemBuffer) Add(ctx context.Context, v ValueGetter) (err error) {
if b.i != -1 && b.isSorted() {
return fmt.Errorf("cannot buffer items after an access occurred and the buffer is ordered")
}
if b.isSingle() {
b.rows[0] = v
b.ctrs[0] = v.CountValues()
} else {
b.rows = append(b.rows, v)
b.ctrs = append(b.ctrs, v.CountValues())
}
b.more = true
return
}
func (b *inmemBuffer) Next(ctx context.Context) bool {
if !b.more {
return false
}
if b.closed {
panic("cannot call Next on closed buffer")
}
// @todo use something like a b-tree to sort while inserting
if b.i == -1 && len(b.sort) > 0 {
sort.Sort(b)
}
b.i++
if b.isSingle() {
b.i = b.i % b.cap
}
b.more = !b.isSingle() && b.i+1 < b.Len()
return b.i < b.Len()
}
func (b *inmemBuffer) More(uint, ValueGetter) error {
return nil
}
func (b *inmemBuffer) Err() error {
return b.err
}
func (b *inmemBuffer) Scan(s ValueSetter) (err error) {
if b.closed {
panic("cannot call Scan on closed buffer")
}
r, ctrs, err := b.rowAt(b.i)
if err != nil {
return
}
var v any
for name, count := range ctrs {
for i := uint(0); i < count; i++ {
v, err = r.GetValue(name, i)
if err != nil {
return
}
err = s.SetValue(name, i, v)
if err != nil {
return
}
}
}
return
}
// @todo should this get a context for things like IO ops
func (b *inmemBuffer) Close() error {
b.ctrs = nil
b.rows = nil
b.closed = true
return nil
}
func (b *inmemBuffer) BackCursor(ValueGetter) (*filter.PagingCursor, error) {
return nil, fmt.Errorf("not supported")
}
func (b *inmemBuffer) ForwardCursor(ValueGetter) (*filter.PagingCursor, error) {
return nil, fmt.Errorf("not supported")
}
// rowAt returns the row at a given index
func (b *inmemBuffer) rowAt(i int) (ValueGetter, map[string]uint, error) {
if i >= b.Len() {
return nil, nil, nil
}
return b.rows[i], b.ctrs[i], nil
}
// sort.Interface methods
func (b *inmemBuffer) Len() int {
return len(b.rows)
}
func (b *inmemBuffer) Less(i, j int) bool {
ra, _, err := b.rowAt(i)
if err != nil {
panic(err)
}
rb, _, err := b.rowAt(j)
if err != nil {
panic(err)
}
return makeRowComparator(b.sort...)(ra, rb)
}
func (b *inmemBuffer) Swap(i, j int) {
b.rows[i], b.rows[j] = b.rows[j], b.rows[i]
b.ctrs[i], b.ctrs[j] = b.ctrs[j], b.ctrs[i]
}

View File

@@ -0,0 +1,163 @@
package dal
import "go.uber.org/zap"
type (
Issue struct {
err error
Issue string `json:"issue,omitempty"`
Kind issueKind `json:"kind,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
issueSet []Issue
issueHelper struct {
// these two will be used to help clear out unneeded errors
connections []uint64
models []uint64
connectionIssues dalIssueIndex
modelIssues dalIssueIndex
}
issueKind string
dalIssueIndex map[uint64]issueSet
)
const (
connectionIssue issueKind = "connection"
modelIssue issueKind = "model"
)
func newIssueHelper() *issueHelper {
return &issueHelper{
connectionIssues: make(dalIssueIndex),
modelIssues: make(dalIssueIndex),
}
}
func (svc *service) SearchConnectionIssues(connectionID uint64) (out []Issue) {
return svc.connectionIssues[connectionID]
}
func (svc *service) SearchModelIssues(resourceID uint64) (out []Issue) {
return svc.modelIssues[resourceID]
}
func (svc *service) SearchResourceIssues(resourceType, resource string) (out []Issue) {
var m *Model
for _, ax := range svc.models {
m = ax.FindByResourceIdent(resourceType, resource)
}
if m == nil {
return
}
return svc.modelIssues[m.ResourceID]
}
func (svc *service) hasConnectionIssues(connectionID uint64) bool {
return len(svc.SearchConnectionIssues(connectionID)) > 0
}
func (svc *service) hasModelIssues(modelID uint64) bool {
return len(svc.SearchModelIssues(modelID)) > 0
}
func (svc *service) updateIssues(issues *issueHelper) {
for _, connectionID := range issues.connections {
delete(svc.connectionIssues, connectionID)
}
for connectionID, issues := range issues.connectionIssues {
svc.connectionIssues[connectionID] = issues
}
for _, modelID := range issues.models {
delete(svc.modelIssues, modelID)
}
for modelID, issues := range issues.modelIssues {
svc.modelIssues[modelID] = issues
}
}
func (svc *service) clearModelIssues() {
svc.modelIssues = make(dalIssueIndex)
}
func (rd *issueHelper) addConnection(connectionID uint64) *issueHelper {
rd.connections = append(rd.connections, connectionID)
return rd
}
func (rd *issueHelper) addModel(modelID uint64) *issueHelper {
rd.models = append(rd.models, modelID)
return rd
}
func (rd *issueHelper) addConnectionIssue(connectionID uint64, i Issue) {
i.Kind = connectionIssue
i.Issue = i.err.Error()
rd.connectionIssues[connectionID] = append(rd.connectionIssues[connectionID], i)
}
func (rd *issueHelper) addModelIssue(resourceID uint64, i Issue) {
i.Kind = modelIssue
i.Issue = i.err.Error()
rd.modelIssues[resourceID] = append(rd.modelIssues[resourceID], i)
}
func (rd *issueHelper) hasConnectionIssues() bool {
return len(rd.connectionIssues) > 0
}
func (rd *issueHelper) hasModelIssues() bool {
return len(rd.modelIssues) > 0
}
func (a *issueHelper) mergeWith(b *issueHelper) {
if b == nil {
return
}
for connectionID, issues := range b.connectionIssues {
a.connectionIssues[connectionID] = append(a.connectionIssues[connectionID], issues...)
}
for modelID, issues := range b.modelIssues {
a.modelIssues[modelID] = append(a.modelIssues[modelID], issues...)
}
}
// Op check utils
func (svc *service) canOpData(ref ModelRef) (err error) {
if svc.hasConnectionIssues(ref.ConnectionID) {
for _, i := range svc.connectionIssues[ref.ConnectionID] {
svc.logger.Debug(
"can not perform data operation due to connection issue: "+i.err.Error(),
zap.Any("ref", ref.ResourceID),
)
}
return errRecordOpProblematicConnection(ref.ConnectionID)
}
var mod = svc.FindModelByRef(ref)
if mod == nil {
return errModelNotFound(ref.ResourceID)
}
if svc.hasModelIssues(mod.ResourceID) {
for _, i := range svc.modelIssues[mod.ResourceID] {
svc.logger.Debug(
"can not perform data operation due to model issue: "+i.err.Error(),
zap.Any("ref", ref.ResourceID),
)
}
return errRecordOpProblematicModel(mod.ResourceID)
}
return nil
}

View File

@@ -0,0 +1,119 @@
package dal
import (
"context"
"encoding/json"
"io"
"github.com/cortezaproject/corteza/server/pkg/filter"
)
type (
// Iterator provides an interface for loading data from the underlying store
Iterator interface {
Next(ctx context.Context) bool
More(uint, ValueGetter) error
Err() error
Scan(ValueSetter) error
Close() error
BackCursor(ValueGetter) (*filter.PagingCursor, error)
ForwardCursor(ValueGetter) (*filter.PagingCursor, error)
// // -1 means unknown
// Total() int
// Cursor() any
// // ... do we need anything else here?
}
iterator interface {
Preload(context.Context, uint, *filter.PagingCursor) error
Sorting() filter.SortExprSet
}
)
// IteratorEncodeJSON helper function that encodes each item from the iterator as JSON
// and writes it to th given io.Writer.
//
// target initialization function is intentionally used to avoid use of reflection
func IteratorEncodeJSON(ctx context.Context, w io.Writer, iter Iterator, initTarget func() ValueSetter) (err error) {
var (
target ValueSetter
firstOut = false
)
for iter.Next(ctx) {
if err = iter.Err(); err != nil {
return
}
if firstOut {
if _, err = w.Write([]byte(`,`)); err != nil {
return
}
}
firstOut = true
target = initTarget()
if err = iter.Scan(target); err != nil {
return
}
err = json.NewEncoder(w).Encode(target)
if err != nil {
return
}
}
return
}
// PreLoadCursor into iterator and check it exist then return the cursor
// @todo this should be move to the Iterator
func PreLoadCursor(ctx context.Context, iter Iterator, limit uint, reverse bool, r ValueGetter, fx func(Iterator) (bool, error)) (out *filter.PagingCursor, err error) {
makeCursor := func() (*filter.PagingCursor, error) {
if reverse {
return iter.BackCursor(r)
} else {
return iter.ForwardCursor(r)
}
}
out, err = makeCursor()
if err != nil {
return
}
err = iter.(iterator).Preload(ctx, limit, out)
if err != nil {
return nil, nil
}
for {
if !iter.Next(ctx) {
out = nil
return
}
ok, err := fx(iter)
if err != nil {
return nil, err
}
if ok {
return out, err
// // @todo Skip the things we don't have access to; could cause some edge cases so probably not
// // It adds some performance since we skip unneeded stuff but could some records change in the mean time?
// // return makeCursor()
}
}
}
// IteratorSorting return iterator sorting
// @todo this should be move to the Iterator
func IteratorSorting(iter Iterator) filter.SortExprSet {
return iter.(iterator).Sorting()
}

View File

@@ -0,0 +1,578 @@
package dal
import (
"encoding/json"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/handle"
"github.com/modern-go/reflect2"
)
type (
// ModelRef is used to retrieve a model from the DAL based on given params
ModelRef struct {
ConnectionID uint64
ResourceID uint64
ResourceType string
Resource string
Refs map[string]any
}
// Model describes the underlying data and its shape
Model struct {
ConnectionID uint64 `json:"connectionID"`
Ident string `json:"ident"`
Label string `json:"label"`
Resource string `json:"resource"`
ResourceID uint64 `json:"resourceID"`
ResourceType string `json:"resourceType"`
// Refs is an arbitrary map to identify a model
// @todo consider reworking this; I'm not the biggest fan
Refs map[string]any `json:"refs"`
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
Attributes AttributeSet `json:"attributes"`
Constraints map[string][]any `json:"constraints"`
Indexes IndexSet `json:"indexes"`
}
ModelSet []*Model
// Attribute describes a specific value of the dataset
Attribute struct {
Ident string `json:"ident"`
Label string `json:"label"`
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
MultiValue bool `json:"multiValue"`
PrimaryKey bool `json:"primaryKey"`
// If attribute has SoftDeleteFlag=true we use it
// when filtering out deleted items
SoftDeleteFlag bool `json:"softDeleteFlag"`
// System indicates the attribute was defined by the system
System bool `json:"system"`
// Is attribute sortable?
// Note: all primary keys are sortable
Sortable bool `json:"sortable"`
// Can attribute be used in query expression?
Filterable bool `json:"filterable"`
// Store describes the strategy the underlying storage system should
// apply to the underlying value
Store Codec `json:"store"`
// Type describes what the value represents and how it should be
// encoded/decoded
Type Type `json:"type"`
}
// auxAttribute is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttribute struct {
Ident string `json:"ident"`
Label string `json:"label"`
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
MultiValue bool `json:"multiValue"`
PrimaryKey bool `json:"primaryKey"`
SoftDeleteFlag bool `json:"softDeleteFlag"`
System bool `json:"system"`
Sortable bool `json:"sortable"`
Filterable bool `json:"filterable"`
Store *auxAttributeStore `json:"store"`
Type *auxAttributeType `json:"type"`
}
// auxAttributeStore is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttributeStore struct {
Type string `json:"type"`
Plain *CodecPlain `json:"plain,omitempty"`
RecordValueSetJSON *CodecRecordValueSetJSON `json:"recordValueSetJSON,omitempty"`
Alias *CodecAlias `json:"alias,omitempty"`
}
// auxAttributeType is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttributeType struct {
Type string `json:"type"`
ID *TypeID `json:"id,omitempty"`
Ref *TypeRef `json:"ref,omitempty"`
Timestamp *TypeTimestamp `json:"timestamp,omitempty"`
Time *TypeTime `json:"time,omitempty"`
Date *TypeDate `json:"date,omitempty"`
Number *TypeNumber `json:"number,omitempty"`
Text *TypeText `json:"text,omitempty"`
Boolean *TypeBoolean `json:"boolean,omitempty"`
Enum *TypeEnum `json:"enum,omitempty"`
Geometry *TypeGeometry `json:"geometry,omitempty"`
JSON *TypeJSON `json:"jSON,omitempty"`
Blob *TypeBlob `json:"blob,omitempty"`
UUID *TypeUUID `json:"uuid,omitempty"`
}
// auxStoreCodec is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxStoreCodec struct {
Type string `json:"type"`
CodecPlain *CodecPlain `json:"codecPlain"`
CodecRecordValueSetJSON *CodecRecordValueSetJSON `json:"codecRecordValueSetJSON"`
CodecAlias *CodecAlias `json:"codecAlias"`
}
AttributeSet []*Attribute
Index struct {
Ident string
Type string
Unique bool
Fields []*IndexField
Predicate string
}
IndexField struct {
AttributeIdent string
Modifiers []IndexFieldModifier
Sort IndexFieldSort
Nulls IndexFieldNulls
}
IndexSet []*Index
IndexFieldModifier string
IndexFieldSort int
IndexFieldNulls int
)
const (
IndexFieldSortDesc IndexFieldSort = -1
IndexFieldSortAsc IndexFieldSort = 1
IndexFieldNullsLast IndexFieldNulls = -1
IndexFieldNullsFirst IndexFieldNulls = 1
IndexFieldModifierLower = "LOWERCASE"
)
func PrimaryAttribute(ident string, codec Codec) *Attribute {
out := FullAttribute(ident, TypeID{}, codec)
out.Type = &TypeID{}
out.PrimaryKey = true
return out
}
func FullAttribute(ident string, at Type, codec Codec) *Attribute {
return &Attribute{
Ident: ident,
Label: ident,
Sortable: true,
Filterable: true,
Store: codec,
Type: at,
}
}
func (a *Attribute) WithSoftDelete() *Attribute {
a.SoftDeleteFlag = true
return a
}
func (a *Attribute) WithMultiValue() *Attribute {
a.MultiValue = true
return a
}
func (a *Attribute) StoreIdent() string {
switch s := a.Store.(type) {
case *CodecRecordValueSetJSON:
return s.Ident
case *CodecAlias:
return s.Ident
default:
return a.Ident
}
}
// Compare the two attributes
func (a *Attribute) Compare(b *Attribute) bool {
if a == nil || b == nil {
return a == b
}
out := true
out = out && a.Ident == b.Ident
out = out && a.Label == b.Label
out = out && a.SensitivityLevelID == b.SensitivityLevelID
out = out && a.MultiValue == b.MultiValue
out = out && a.PrimaryKey == b.PrimaryKey
out = out && a.SoftDeleteFlag == b.SoftDeleteFlag
out = out && a.System == b.System
out = out && a.Sortable == b.Sortable
out = out && a.Filterable == b.Filterable
if a.Store == nil || b.Store == nil {
out = out && a.Store == b.Store
} else {
out = out && a.Store.Type() == b.Store.Type()
}
if a.Type == nil || b.Type == nil {
out = out && a.Type == b.Type
} else {
out = out && a.Type.Type() == b.Type.Type()
}
return out
}
func (mm ModelSet) FindByResourceID(resourceID uint64) *Model {
for _, m := range mm {
if m.ResourceID == resourceID {
return m
}
}
return nil
}
func (mm ModelSet) FindByResourceIdent(resourceType, resourceIdent string) *Model {
for _, m := range mm {
if m.ResourceType != resourceType {
continue
}
if m.Resource != resourceIdent {
continue
}
return m
}
return nil
}
func (mm ModelSet) FindByIdent(ident string) *Model {
for _, m := range mm {
if m.Ident == ident {
return m
}
}
return nil
}
// FindByRefs returns the first Model that matches the given refs
func (mm ModelSet) FindByRefs(refs map[string]any) *Model {
for _, model := range mm {
for k, v := range refs {
ref, ok := model.Refs[k]
if !ok {
goto skip
}
if v != ref {
goto skip
}
}
return model
skip:
}
return nil
}
// FilterByReferenced returns all of the models that reference b
func (aa ModelSet) FilterByReferenced(b *Model) (out ModelSet) {
for _, aModel := range aa {
if aModel.Resource == b.Resource {
continue
}
for _, aAttribute := range aModel.Attributes {
switch casted := aAttribute.Type.(type) {
case *TypeRef:
if casted.RefModel.Resource == b.Resource {
out = append(out, aModel)
}
}
}
}
return
}
func (m Model) ToFilter() ModelRef {
return ModelRef{
ConnectionID: m.ConnectionID,
ResourceID: m.ResourceID,
ResourceType: m.ResourceType,
Resource: m.Resource,
}
}
// HasAttribute returns true when the model includes the specified attribute
func (m Model) HasAttribute(ident string) bool {
return m.Attributes.FindByIdent(ident) != nil
}
func (aa AttributeSet) FindByIdent(ident string) *Attribute {
for _, a := range aa {
if strings.EqualFold(a.Ident, ident) {
return a
}
}
return nil
}
func (aa AttributeSet) FindByStoreIdent(ident string) *Attribute {
for _, a := range aa {
if strings.EqualFold(a.StoreIdent(), ident) {
return a
}
}
return nil
}
// Validate performs a base model validation before it is passed down
func (m Model) Validate() error {
if m.Resource == "" {
return fmt.Errorf("resource not defined")
}
seen := make(map[string]bool)
for _, attr := range m.Attributes {
if attr.Ident == "" {
return fmt.Errorf("invalid attribute ident: ident must not be empty")
}
if !handle.IsValid(attr.Ident) {
return fmt.Errorf("invalid attribute ident: %s is not a valid handle", attr.Ident)
}
if seen[attr.Ident] {
return fmt.Errorf("invalid attribute %s: duplicate attributes are not allowed", attr.Ident)
}
seen[attr.Ident] = true
if reflect2.IsNil(attr.Type) {
return fmt.Errorf("attribute does not define a type: %s", attr.Ident)
}
}
return nil
}
// Compare the two models
//
// This only checks model metadata, the attributes are excluded.
func (a Model) Compare(b Model) bool {
out := true
out = out && a.ConnectionID == b.ConnectionID
out = out && a.Ident == b.Ident
out = out && a.Label == b.Label
out = out && a.Resource == b.Resource
out = out && a.ResourceID == b.ResourceID
out = out && a.ResourceType == b.ResourceType
out = out && a.SensitivityLevelID == b.SensitivityLevelID
return out
}
func (a *Attribute) MarshalJSON() ([]byte, error) {
aux := &auxAttribute{
Ident: a.Ident,
Label: a.Label,
SensitivityLevelID: a.SensitivityLevelID,
MultiValue: a.MultiValue,
PrimaryKey: a.PrimaryKey,
SoftDeleteFlag: a.SoftDeleteFlag,
System: a.System,
Sortable: a.Sortable,
Filterable: a.Filterable,
Store: &auxAttributeStore{},
Type: &auxAttributeType{},
}
switch s := a.Store.(type) {
case *CodecPlain:
aux.Store.Type = "plain"
aux.Store.Plain = s
case *CodecRecordValueSetJSON:
aux.Store.Type = "recordValueSetJSON"
aux.Store.RecordValueSetJSON = s
case *CodecAlias:
aux.Store.Type = "alias"
aux.Store.Alias = s
default:
return nil, fmt.Errorf("unknown store codec type: %T", s)
}
switch t := a.Type.(type) {
case *TypeID:
aux.Type.Type = "ID"
aux.Type.ID = t
case *TypeRef:
aux.Type.Type = "Ref"
aux.Type.Ref = t
case *TypeTimestamp:
aux.Type.Type = "Timestamp"
aux.Type.Timestamp = t
case *TypeTime:
aux.Type.Type = "Time"
aux.Type.Time = t
case *TypeDate:
aux.Type.Type = "Date"
aux.Type.Date = t
case *TypeNumber:
aux.Type.Type = "Number"
aux.Type.Number = t
case *TypeText:
aux.Type.Type = "Text"
aux.Type.Text = t
case *TypeBoolean:
aux.Type.Type = "Boolean"
aux.Type.Boolean = t
case *TypeEnum:
aux.Type.Type = "Enum"
aux.Type.Enum = t
case *TypeGeometry:
aux.Type.Type = "Geometry"
aux.Type.Geometry = t
case *TypeJSON:
aux.Type.Type = "JSON"
aux.Type.JSON = t
case *TypeBlob:
aux.Type.Type = "Blob"
aux.Type.Blob = t
case *TypeUUID:
aux.Type.Type = "UUID"
aux.Type.UUID = t
default:
return nil, fmt.Errorf("unknown attribute type type: %T", t)
}
return json.Marshal(aux)
}
func (a *Attribute) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttribute{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = Attribute{}
}
a.Ident = aux.Ident
a.Label = aux.Label
a.SensitivityLevelID = aux.SensitivityLevelID
a.MultiValue = aux.MultiValue
a.PrimaryKey = aux.PrimaryKey
a.SoftDeleteFlag = aux.SoftDeleteFlag
a.System = aux.System
a.Sortable = aux.Sortable
a.Filterable = aux.Filterable
switch aux.Store.Type {
case "plain":
a.Store = aux.Store.Plain
case "recordValueSetJSON":
a.Store = aux.Store.RecordValueSetJSON
case "alias":
a.Store = aux.Store.Alias
}
switch aux.Type.Type {
case "ID":
a.Type = aux.Type.ID
case "Ref":
a.Type = aux.Type.Ref
case "Timestamp":
a.Type = aux.Type.Timestamp
case "Time":
a.Type = aux.Type.Time
case "Date":
a.Type = aux.Type.Date
case "Number":
a.Type = aux.Type.Number
case "Text":
a.Type = aux.Type.Text
case "Boolean":
a.Type = aux.Type.Boolean
case "Enum":
a.Type = aux.Type.Enum
case "Geometry":
a.Type = aux.Type.Geometry
case "JSON":
a.Type = aux.Type.JSON
case "Blob":
a.Type = aux.Type.Blob
case "UUID":
a.Type = aux.Type.UUID
default:
return fmt.Errorf("unknown attribute type type: %s", aux.Type.Type)
}
return
}

View File

@@ -0,0 +1,54 @@
package dal
import "strings"
// NormalizeAttrNames normalizes system identifiers to attribute identifiers
// @todo this is temporary until we re-visit field referencing
//
// The normalization step must exist since legacy versions allowed multiple
// identifier variations for the same system field, such as created_at and createdAt.
func NormalizeAttrNames(name string) string {
switch strings.ToLower(name) {
case
"recordid",
"record_id",
"id":
return "ID"
case "moduleid",
"module_id",
"module":
return "moduleID"
case "owned_by",
"ownedby":
return "ownedBy"
case "created_by",
"createdby":
return "createdBy"
case "updated_by",
"updatedby":
return "updatedBy"
case "deleted_by",
"deletedby":
return "deletedBy"
case "created_at",
"createdat":
return "createdAt"
case "updated_at",
"updatedat":
return "updatedAt"
case "deleted_at",
"deletedat":
return "deletedAt"
}
return name
}

View File

@@ -0,0 +1,155 @@
package dal
type (
Operation string
OperationSet []Operation
)
const (
Create Operation = "corteza::dal:operation:create"
Update Operation = "corteza::dal:operation:update"
Delete Operation = "corteza::dal:operation:delete"
Search Operation = "corteza::dal:operation:search"
Lookup Operation = "corteza::dal:operation:lookup"
Paging Operation = "corteza::dal:operation:paging"
Sorting Operation = "corteza::dal:operation:sorting"
)
var (
full = OperationSet{
Create,
Update,
Delete,
Search,
Lookup,
Paging,
Sorting,
}
createOperations = OperationSet{
Create,
}
updateOperations = OperationSet{
Update,
}
deleteOperations = OperationSet{
Update,
}
searchOperations = OperationSet{
Search,
Paging,
Sorting,
}
lookupOperations = OperationSet{
Lookup,
}
)
// FullOperations returns all base system defined operations
func FullOperations() (cc OperationSet) {
// Doing an union just to make a fresh copy
return full.Union(nil)
}
// CreateOperations returns only requested operations used for Create operations
func CreateOperations(requested ...Operation) (required OperationSet) {
return common(createOperations, requested)
}
// UpdateOperations returns only requested operations used for Update operations
func UpdateOperations(requested ...Operation) (required OperationSet) {
return common(updateOperations, requested)
}
// DeleteOperations returns only requested operations used for delete operations
func DeleteOperations(requested ...Operation) (required OperationSet) {
return common(deleteOperations, requested)
}
// SearchOperations returns only requested operations used for Search operations
func SearchOperations(requested ...Operation) (required OperationSet) {
return common(searchOperations, requested)
}
// LookupOperations returns only requested operations used for Search operations
func LookupOperations(requested ...Operation) (required OperationSet) {
return common(lookupOperations, requested)
}
func common(aa, bb OperationSet) OperationSet {
return aa.Intersect(bb)
}
// ---
// IsSuperset is inverse IsSubset
//
// IsSuperset checks if all bb operations are inside aa
func (aa OperationSet) IsSuperset(bb ...Operation) bool {
return OperationSet(bb).IsSubset(aa...)
}
// IsSubset checks if all aa operations are inside bb
func (aa OperationSet) IsSubset(bb ...Operation) bool {
if len(aa) > len(bb) {
return false
}
// When A is subset of B, the difference between the two must be 0
return len(aa.Diff(bb)) == 0
}
// Intersect returns the intersection between the two sets
func (aa OperationSet) Intersect(bb OperationSet) (cc OperationSet) {
cc = make(OperationSet, 0, len(aa))
for _, a := range aa {
for _, b := range bb {
if a == b {
cc = append(cc, a)
break
}
}
}
return
}
// Intersect returns the union between the two sets
//
// Duplicates are omitted
func (aa OperationSet) Union(bb OperationSet) (cc OperationSet) {
ix := make(map[Operation]bool)
for _, c := range append(aa, bb...) {
if !ix[c] {
ix[c] = true
cc = append(cc, c)
}
}
return
}
// Diff calculates the difference between the two operation sets
//
// The diff uses aa as base
func (aa OperationSet) Diff(bb OperationSet) (cc OperationSet) {
for _, a := range aa {
found := false
for _, b := range bb {
found = a == b
if found {
break
}
}
if found {
continue
}
cc = append(cc, a)
}
return
}

Some files were not shown because too many files have changed in this diff Show More