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

Export LiquidExpression class or evalExpression to evaluate single expressions #527

Closed
Prinzhorn opened this issue Aug 7, 2022 · 8 comments

Comments

@Prinzhorn
Copy link

Prinzhorn commented Aug 7, 2022

First of all thanks for creating and maintaining LiquidJS. I needed a white space sensitive generic templating language that is not tied to HTML and LiquidJS does exactly that.

Now I have a requirement that I think this library could very easily fulfill as well. You are probably aware how GitHub actions allows using templating and conditions inside YAML. E.g.

if: github.repository == 'octo-org/octo-repo-prod'

or

if: ${{ github.event_name == 'pull_request' && github.event.action == 'unassigned' }}

I have a very similar requirement (not sure yet if it will use YAML as well). Since I am already using LiquidJS I would love to use the same semantics here as well. But Instead of rendering a template and getting a string as a result I would need LiquidJS to evaluate a single expression and return the value as is (boolean, number, etc.). It would look like this:

import { LiquidExpression } from 'liquidjs';

const expressionEngine = new LiquidExpression();
const expression = expressionEngine.parse('a > b');
const result = expressionEngine.evaluate(expression, {a: 1, b: 2});

// In this specific case the expression yielded a boolean, but it could be any primitive.
assert(result === false);

I've been poking around the docs and code and I think what I want is a more ergonomic way of doing this?

const ctx = new Context({})
const trie = createTrie(defaultOperators)
const create = (str: string) => new Tokenizer(str, trie).readExpression()

Maybe add evalExpression analogous to evalValue?

@Prinzhorn Prinzhorn changed the title Export LiquidExpression class to evaluate single expressions Export LiquidExpression class or evalExpression to evaluate single expressions Aug 7, 2022
@harttle
Copy link
Owner

harttle commented Aug 14, 2022

Thank you for digging into the codebase and provide the snippet!

There was a keepOutputType option to allow .render() to return a primitive value when the input contains only one expression like {{ a < b }}. But it's still Liquid syntax not a pure Expression you described above.

Actually Expression is already exported, but you need more to do it properly. I exported some other utilities, and added one test case you may find useful:

liquidjs/test/e2e/issues.ts

Lines 263 to 268 in e874b40

it('#527 export Liquid Expression', () => {
const tokenizer = new Tokenizer('a > b', defaultOptions.operatorsTrie)
const expression = tokenizer.readExpression()
const result = toValueSync(expression.evaluate(new Context({ a: 1, b: 2 })))
expect(result).to.equal(false)
})

github-actions bot pushed a commit that referenced this issue Aug 14, 2022
# [9.40.0](v9.39.2...v9.40.0) (2022-08-14)

### Bug Fixes

* target ES6 for ESM bundles, fixes [#526](#526) ([905a6dd](905a6dd))

### Features

* export toValueSync & defaultOptions to evaluate expression, see [#527](#527) ([e874b40](e874b40))
@Prinzhorn
Copy link
Author

Thanks! Do you think exposing this as a simple evalExpression and evalExpressionSync would be a good idea? So that you don't have to worry about the internals. From what I can tell everything evalValue does could also be done manually, but it's just a nicer API.

@harttle
Copy link
Owner

harttle commented Aug 23, 2022

Makes sense! But evalValue and evalExpression can be confusing, we need to change the API name or maybe we can implement evalExpression functionality in evalValue. At least expression sounds like a special type of value.

@Prinzhorn
Copy link
Author

It looks like you've been unhappy with the name for at least four years (#58), but it's up to you if you want to deprecate evalValue.

maybe we can implement evalExpression functionality in evalValue

That does sound even more confusing if we add a flag or something to evalValue.

Maybe evalOutput and evalExpression would fly. "Outputs" is also the first thing in the tutorial, so the terminology is settled I guess. And "Expression" is generic enough that you would expect any type (bool, string, number, ...) from it.

@harttle
Copy link
Owner

harttle commented Aug 23, 2022

I agree with you that adding a flag to evalValue is not appropriate. What I am thinking is evalValue can support an expression apart from a variable.

evalOutput is useless if input is like {{val}}, because parseAndRender does exactly the same thing.

Currently evalValue accepts input like obj.prop and optionally with filters like obj.prop | capitalize, and already returns any type (bool, string, number, ...) without converting to string.

The concept of Output({{foo}}) is on the same level of Tag({%foo%}). And Value(foo) is just on the right level to represent expressions.

@Prinzhorn
Copy link
Author

I see, sounds good. I clearly don't know enough about the internals 😄

github-actions bot pushed a commit that referenced this issue Aug 24, 2022
# [9.41.0](v9.40.0...v9.41.0) (2022-08-24)

### Features

* use evalValue to parse & render expression, [#527](#527) ([071368a](071368a))
@harttle
Copy link
Owner

harttle commented Aug 24, 2022

I find evalValue actually supports expressions like a > b, but the second parameter is Context instead of plain object.

evalValue is no longer used by liquidjs itself so I guess there's no reason to continue using a internal Context class as argument. On v9.41.0 plain object is supported. Please refer to this test case:

liquidjs/test/e2e/issues.ts

Lines 269 to 278 in 071368a

it('#527 export Liquid Expression (evalValue)', async () => {
const liquid = new Liquid()
const result = await liquid.evalValue('a > b', { a: 1, b: 2 })
expect(result).to.equal(false)
})
it('#527 export Liquid Expression (evalValueSync)', async () => {
const liquid = new Liquid()
const result = liquid.evalValueSync('a > b', { a: 1, b: 2 })
expect(result).to.equal(false)
})

@Prinzhorn
Copy link
Author

That's amazing! Was playing with it to see if it fully solves my use-case and opened #533 for that

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

No branches or pull requests

2 participants