Skip to content

Commit

Permalink
feat: restore mapping (#331)
Browse files Browse the repository at this point in the history
* feat: restore mapping

* feat: Add test on very big transaction

* chore: lint

* fix: tests
  • Loading branch information
gfyrag authored Sep 15, 2022
1 parent 3af802f commit 74ee50b
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 10 deletions.
22 changes: 21 additions & 1 deletion pkg/api/controllers/mapping_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package controllers_test

import (
"context"
"encoding/json"
"net/http"
"testing"

"github.com/numary/ledger/pkg/api"
"github.com/numary/ledger/pkg/api/internal"
"github.com/numary/ledger/pkg/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/fx"
)

Expand All @@ -21,6 +23,14 @@ func TestMapping(t *testing.T) {
{
Name: "default",
Account: "*",
Expr: &core.ExprGt{
Op1: core.VariableExpr{
Name: "balance",
},
Op2: core.ConstantExpr{
Value: 0,
},
},
},
},
}
Expand All @@ -32,7 +42,17 @@ func TestMapping(t *testing.T) {

m2, _ := internal.DecodeSingleResponse[core.Mapping](t, rsp.Body)

assert.EqualValues(t, m, m2)
data, err := json.Marshal(m)
require.NoError(t, err)
m1AsMap := make(map[string]any)
require.NoError(t, json.Unmarshal(data, &m1AsMap))

data, err = json.Marshal(m2)
require.NoError(t, err)
m2AsMap := make(map[string]any)
require.NoError(t, json.Unmarshal(data, &m2AsMap))

assert.EqualValues(t, m1AsMap, m2AsMap)
return nil
},
})
Expand Down
25 changes: 25 additions & 0 deletions pkg/core/contract.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
package core

import (
"encoding/json"
"regexp"
"strings"
)

type Contract struct {
Name string `json:"name"`
Account string `json:"account"`
Expr Expr `json:"expr"`
}

func (c *Contract) UnmarshalJSON(data []byte) error {
type AuxContract Contract
type Aux struct {
AuxContract
Expr map[string]interface{} `json:"expr"`
}
aux := Aux{}
err := json.Unmarshal(data, &aux)
if err != nil {
return err
}
expr, err := ParseRuleExpr(aux.Expr)
if err != nil {
return err
}
*c = Contract{
Expr: expr,
Account: aux.Account,
Name: aux.Name,
}
return nil
}

func (c Contract) Match(addr string) bool {
Expand Down
291 changes: 291 additions & 0 deletions pkg/core/expr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
package core

import (
"encoding/json"
"errors"
"fmt"
"math"
"reflect"
"strings"
)

type EvalContext struct {
Variables map[string]interface{}
Metadata Metadata
Asset string
}

type Expr interface {
Eval(EvalContext) bool
}

type Value interface {
eval(ctx EvalContext) interface{}
}

type ExprOr []Expr

func (o ExprOr) Eval(ctx EvalContext) bool {
for _, e := range o {
if e.Eval(ctx) {
return true
}
}
return false
}

func (e ExprOr) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$or": []Expr(e),
})
}

type ExprAnd []Expr

func (o ExprAnd) Eval(ctx EvalContext) bool {
for _, e := range o {
if !e.Eval(ctx) {
return false
}
}
return true
}

func (e ExprAnd) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$and": []Expr(e),
})
}

type ExprEq struct {
Op1 Value
Op2 Value
}

func (o *ExprEq) Eval(ctx EvalContext) bool {
return reflect.DeepEqual(o.Op1.eval(ctx), o.Op2.eval(ctx))
}

func (e ExprEq) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$eq": []interface{}{e.Op1, e.Op2},
})
}

type ExprGt struct {
Op1 Value
Op2 Value
}

func (o *ExprGt) Eval(ctx EvalContext) bool {
return o.Op1.eval(ctx).(*MonetaryInt).Gt(o.Op2.eval(ctx).(*MonetaryInt))
}

func (e ExprGt) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$gt": []interface{}{e.Op1, e.Op2},
})
}

type ExprLt struct {
Op1 Value
Op2 Value
}

func (o *ExprLt) Eval(ctx EvalContext) bool {
return o.Op1.eval(ctx).(*MonetaryInt).Lt(o.Op2.eval(ctx).(*MonetaryInt))
}

func (e ExprLt) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$lt": []interface{}{e.Op1, e.Op2},
})
}

type ExprGte struct {
Op1 Value
Op2 Value
}

func (o *ExprGte) Eval(ctx EvalContext) bool {
return o.Op1.eval(ctx).(*MonetaryInt).Gte(o.Op2.eval(ctx).(*MonetaryInt))
}

func (e ExprGte) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$gte": []interface{}{e.Op1, e.Op2},
})
}

type ExprLte struct {
Op1 Value
Op2 Value
}

func (o *ExprLte) Eval(ctx EvalContext) bool {
return o.Op1.eval(ctx).(*MonetaryInt).Lte(o.Op2.eval(ctx).(*MonetaryInt))
}

func (e ExprLte) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$lte": []interface{}{e.Op1, e.Op2},
})
}

type ConstantExpr struct {
Value interface{}
}

func (e ConstantExpr) eval(ctx EvalContext) interface{} {
return e.Value
}

func (e ConstantExpr) MarshalJSON() ([]byte, error) {
return json.Marshal(e.Value)
}

type VariableExpr struct {
Name string
}

func (e VariableExpr) eval(ctx EvalContext) interface{} {
return ctx.Variables[e.Name]
}

func (e VariableExpr) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"$%s"`, e.Name)), nil
}

type MetaExpr struct {
Name string
}

func (e MetaExpr) eval(ctx EvalContext) interface{} {
return ctx.Metadata[e.Name]
}

func (e MetaExpr) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"$meta": e.Name,
})
}

func parse(v interface{}) (expr interface{}, err error) {
switch vv := v.(type) {
case map[string]interface{}:
if len(vv) != 1 {
return nil, errors.New("malformed expression")
}
for key, vvv := range vv {
switch {
case strings.HasPrefix(key, "$"):
switch key {
case "$meta":
value, ok := vvv.(string)
if !ok {
return nil, errors.New("$meta operator invalid")
}
return &MetaExpr{Name: value}, nil
case "$or", "$and":
slice, ok := vvv.([]interface{})
if !ok {
return nil, errors.New("Expected slice for operator " + key)
}
exprs := make([]Expr, 0)
for _, item := range slice {
r, err := parse(item)
if err != nil {
return nil, err
}
expr, ok := r.(Expr)
if !ok {
return nil, errors.New("unexpected value when parsing " + key)
}
exprs = append(exprs, expr)
}
switch key {
case "$and":
expr = ExprAnd(exprs)
case "$or":
expr = ExprOr(exprs)
}
case "$eq", "$gt", "$gte", "$lt", "$lte":
vv, ok := vvv.([]interface{})
if !ok {
return nil, errors.New("expected array when using $eq")
}
if len(vv) != 2 {
return nil, errors.New("expected 2 items when using $eq")
}
op1, err := parse(vv[0])
if err != nil {
return nil, err
}
op1Value, ok := op1.(Value)
if !ok {
return nil, errors.New("op1 must be valuable")
}
op2, err := parse(vv[1])
if err != nil {
return nil, err
}
op2Value, ok := op2.(Value)
if !ok {
return nil, errors.New("op2 must be valuable")
}
switch key {
case "$eq":
expr = &ExprEq{
Op1: op1Value,
Op2: op2Value,
}
case "$gt":
expr = &ExprGt{
Op1: op1Value,
Op2: op2Value,
}
case "$gte":
expr = &ExprGte{
Op1: op1Value,
Op2: op2Value,
}
case "$lt":
expr = &ExprLt{
Op1: op1Value,
Op2: op2Value,
}
case "$lte":
expr = &ExprLte{
Op1: op1Value,
Op2: op2Value,
}
}
default:
return nil, errors.New("unknown operator '" + key + "'")
}
}
}
case string:
if !strings.HasPrefix(vv, "$") {
return ConstantExpr{v}, nil
}
return VariableExpr{vv[1:]}, nil
case float64:
if math.Round(vv) != vv {
return nil, errors.New("only integer supported")
}
return ConstantExpr{NewMonetaryInt(int64(vv))}, nil
default:
return ConstantExpr{v}, nil
}

return expr, nil
}

func ParseRuleExpr(v map[string]interface{}) (Expr, error) {
ret, err := parse(v)
if err != nil {
return nil, err
}
return ret.(Expr), nil
}
Loading

0 comments on commit 74ee50b

Please sign in to comment.