Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

getting list of parsed variables before expression is evaluated for validation #75

Open
aldas opened this issue Feb 8, 2022 · 6 comments

Comments

@aldas
Copy link

aldas commented Feb 8, 2022

I have expression vtg_ground_speed_unit > 0 ? vtg_ground_speed_knots : vtg_ground_speed_kph.

These variables (vtg_ground_speed_unit, vtg_ground_speed_knots, vtg_ground_speed_kph) can only be from fixed list (coming from configuration file). So it would be nice if would be a way to get all variables from parsed expression so they could be validated against current configuration - to see if they exist or not.

So far I have tried to create selector that captures all seen variables in "test evaluation".

Something like that:

type variableCaptor struct {
	variables []string
}

func (c *variableCaptor) SelectGVal(_ context.Context, variable string) (interface{}, error) {
	c.captureVariable(variable)
	return 1.0, nil
}

func (c *variableCaptor) captureVariable(variable string) {
	for _, v := range c.variables {
		if variable == v {
			return
		}
	}
	c.variables = append(c.variables, variable)
}

func TestCaptureVariables(t *testing.T) {
	var testCases = []struct {
		name       string
		expression string
		expect     []string
	}{
		{
			name:       "capture 3 variables from IF",
			expression: "vtg_ground_speed_unit > 0 ? vtg_ground_speed_knots : vtg_ground_speed_kph",
			expect:     []string{"vtg_ground_speed_unit", "vtg_ground_speed_knots", "vtg_ground_speed_kph"},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			eval, err := compile(tc.expression, gval.Full())
			if !assert.NoError(t, err) {
				return
			}
			captor := &variableCaptor{variables: make([]string, 0)}

			_, err = eval.EvalBool(context.Background(), captor)
			if !assert.NoError(t, err) {
				return
			}
			assert.Equal(t, tc.expect, captor.variables)
		})
	}
}

but this has limitation as vtg_ground_speed_kph will not get captured as executing all paths from expression involves knowing how to provides values for getting true/false sides.

Is there a way to traverse parsed tree without evaluation and pick all parts that are used as "variable"?

@aldas
Copy link
Author

aldas commented Feb 8, 2022

So far what I have come up is to have 2 extra functions in DSL that must be used for those variables in expression that come from configuration.

  • v(<timeserieCode>) is equivalent just plain old parameter getter
  • n(<timeserieCode>) is for cases when you need to use is as argument for some other function

and then we can do quite primitive regexp capture to get list of all referenced configuration variables.

this does not feel eloquent but seems to work

var timeserieRegexp = regexp.MustCompile(`(?:^|\W)[vn]\(([a-zA-Z0-9-_]{0,100})\)`)

func TestVariableSelector(t *testing.T) {
	expression := `avg(n(me1_rpm)) + v(me2_rpm) + 0.5`

	// ------------------ related timeserie capture start ---------------------
	result := make([]string, 0)
	matches := timeserieRegexp.FindAllStringSubmatch(expression, -1)
	for _, match := range matches {
		tmp := match[1]
		if !containsString(result, tmp) {
			result = append(result, tmp)
		}
	}
	assert.Equal(t, []string{"me1_rpm", "me2_rpm"}, result)
	// ------------------ related timeserie capture end ---------------------

	value, err := gval.Evaluate(expression,
		nil,
		gval.VariableSelector(func(path gval.Evaluables) gval.Evaluable {
			return func(c context.Context, v interface{}) (interface{}, error) {
				keys, err := path.EvalStrings(c, v)
				if err != nil {
					return nil, err
				}
				return keys[0], nil
			}
		}),
		// `avg(<timeserie>)` calculates average for timeserie from cache/database
		gval.Function("avg", func(timeserieCode string) (interface{}, error) {
			if timeserieCode == "me1_rpm" { // just for testing
				return 5.0, nil
			}
			return 0.0, nil
		}),
		// `v(<timeserie>)` helps to identify all related timeseries used by their value
		gval.Function("v", func(timeserieCode string) (interface{}, error) {
			return 1.0, nil
		}),
		// `n(<timeserie>)` helps to identify all related timeseries used by their code
		gval.Function("n", func(timeserieCode string) (interface{}, error) {
			return timeserieCode, nil
		}),
	)
	assert.NoError(t, err)
	assert.Equal(t, 6.5, value)
}

func containsString(haystack []string, needle string) bool {
	for _, v := range haystack {
		if v == needle {
			return true
		}
	}
	return false
}

@skyf0cker
Copy link

@aldas
Maybe you can use init language to do what you want to do
https://pkg.go.dev/github.com/PaesslerAG/gval#Init

@generikvault
Copy link
Collaborator

generikvault commented Feb 15, 2022 via email

@skyf0cker
Copy link

skyf0cker commented Feb 16, 2022

You would probably need to reimplement / copy the ident extension inside gval and pass the variable name through the context of the parser. Maybe by storing an AddVariable closure inside the context. vophan1ee @.> schrieb am Di., 15. Feb. 2022, 10:23:

@aldas https://github.com/aldas Maybe you can use init language to do what you want to do https://pkg.go.dev/github.com/PaesslerAG/gval#Init — Reply to this email directly, view it on GitHub <#75 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHS6ZHVATMDJ6JS3TVP2ZY3U3ILQLANCNFSM5N2BGHHQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub. You are receiving this because you are subscribed to this thread.Message ID: @.
>

From reply of @aldas , i think he just want to get all the tokens of his expression.
So can he just use init langueage and scan all tokens by parser like:

	gval.NewLanguage(
		gval.Full(),
		gval.Init(func(ctx context.Context, parser *gval.Parser) (gval.Evaluable, error) {
			var tokens []string
			for {
				switch parser.Scan() {
				case scanner.EOF:
					break
				default:
					token := parser.TokenText()
					tokens = append(tokens, token)
				}
			}

			// do some check jobs
		}),
	)

I'm not sure if this can solve his question....

@shivam-940
Copy link

shivam-940 commented Apr 28, 2022

You would probably need to reimplement / copy the ident extension inside gval and pass the variable name through the context of the parser. Maybe by storing an AddVariable closure inside the context. vophan1ee @.> schrieb am Di., 15. Feb. 2022, 10:23:

@aldas https://github.com/aldas Maybe you can use init language to do what you want to do https://pkg.go.dev/github.com/PaesslerAG/gval#Init — Reply to this email directly, view it on GitHub <#75 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHS6ZHVATMDJ6JS3TVP2ZY3U3ILQLANCNFSM5N2BGHHQ . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub. You are receiving this because you are subscribed to this thread.Message ID: _
@**
.**_>

From reply of @aldas , i think he just want to get all the tokens of his expression. So can he just use init langueage and scan all tokens by parser like:

	gval.NewLanguage(
		gval.Full(),
		gval.Init(func(ctx context.Context, parser *gval.Parser) (gval.Evaluable, error) {
			var tokens []string
			for {
				switch parser.Scan() {
				case scanner.EOF:
					break
				default:
					token := parser.TokenText()
					tokens = append(tokens, token)
				}
			}

			// do some check jobs
		}),
	)

I'm not sure if this can solve his question....

I have a similar use case to check if the expression entered is correct or not for validation purposes. For example, if expression contains some function which is not defined in my language, I have to raise an error saying expression is incorrect without even evaluating the expression and same for variables (for eg. foo.bar.random).
I am not able to properly use this method. Please can you help here. @skyf0cker
Example expr = lpadding(user.id, 10, '0') where lpadding is a function which takes 3 args and user.id is a variable.
If I get output telling lpadding is the function used and user.id is the variable used.....I might be able to validate it with my config which contains set of functions and variables that can only be used in expressions.

@annlumia
Copy link

Really hope there is a solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants