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

Feature: Support builder pattern in template engine #10

Merged
merged 12 commits into from
Jul 1, 2022

Conversation

oscar60310
Copy link
Contributor

@oscar60310 oscar60310 commented May 25, 2022

  • 01687ba Support query builder creation and execution.
    {% req user %}
    select * from users;
    {% endreq %}
    select * from group where userId = '{{ user.value()[0].id }}'
    We'll traverse all nodes and find the .value() function call, and replace then with filters.
    select * from group where userId = '{{ (user | execute)[0].id }}'
  • 581cee4 5e0be39 Support main builder
    {% req user main %}
    select * from users;
    {% endreq %}
  • a9bf698 8ca2533 Migrate packages to use builders: render() -> execute()
  • 1cda034 4c4d038 Refactor: modulize extensions
    • Extensions now have two kinds: runtime and compileTime extensions.
    • Put the extensions with the same feature togeter instead of grouping by
      types.
    • We only test with a full extension now, instead of test by files.
    • Metadata objects became a key-value mapping with extension name
      {
        "error.vulcan.com": {
          "errorCodes": [
            { "code": "QAQ", "locations": [{ "lineNo": 2, "columnNo": 9 }] }
          ]
        },
        "builder.vulcan.com": { "finalBuilderName": "FINAL_BUILDER" },
        "parameter.vulcan.com": [
          { "name": "users", "locations": [{ "lineNo": 1, "columnNo": 38 }] }
        ],
        "filter.vulcan.com": []
      }
  • 719d8a9 throw error if different builders use same name

Besides some mock interfaces and functions, almost all code are covered by unit testing :D
image
image

@oscar60310 oscar60310 force-pushed the feature/builder-pattern-te branch 3 times, most recently from d9e8b9a to 66f161f Compare June 2, 2022 04:05
@oscar60310 oscar60310 force-pushed the feature/builder-pattern-te branch 7 times, most recently from 993c66d to 719d8a9 Compare June 9, 2022 07:54
@oscar60310 oscar60310 changed the title [WIP] Feature: Support builder pattern in template engine Feature: Support builder pattern in template engine Jun 9, 2022
@oscar60310 oscar60310 requested review from kokokuo and wwwy3y3 June 9, 2022 08:13
@oscar60310 oscar60310 marked this pull request as ready for review June 9, 2022 08:13
Copy link
Contributor

@kokokuo kokokuo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent , beside some question and suggestion, other is LGTM 👍

this.compileTimeEnv.extensionsList,
{}
);
this.traverseAST(ast);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could let AST to Ast for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, fixed.

nextToken = parser.peekToken();
let mainBuilder = false;
if (nextToken.type !== lexer.TOKEN_BLOCK_END) {
if (nextToken.type !== lexer.TOKEN_SYMBOL || nextToken.value !== 'main') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is TOKEN_SYMBOL ( like % ) and why need to check is not TOKEN_SYMBOL ?
If it is a symbol but not main, you also make it a main builder default, too? ( If true, maybe we could give a comment for notice ? )

Copy link
Contributor Author

@oscar60310 oscar60310 Jun 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is TOKEN_SYMBOL ( like % ) and why need to check is not TOKEN_SYMBOL ?

A symbol token is something like main, roughly speaking it is some char wich isn't contain "()[]{}%*-+~/#,:|.<>=!", not start with int ..... You can check here to see how it works.
I checked it must be SYMBOL or I threw error here to prevent following cases:

{% req main %} -- correct

---- incorrect
{% req "main" %}
{% req { main } %}
....

If it is a symbol but not main, you also make it a main builder default, too? ( If true, maybe we could give a comment for notice ? )

No, I won't. parser.fail will throw an error and the code below won't be executed.
I'll update the return of this function to "never" to indicate that:

-      fail(message: string, lineno?: number, colno?: number): void;
+      fail(message: string, lineno?: number, colno?: number): never;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for replying to me and the logistic part I misunderstood, thanks !

private checkMainBuilder(node: nunjucks.nodes.CallExtensionAsync) {
const isMainBuilder =
node.extName === this.getName() &&
(node.args.children[1] as nunjucks.nodes.Literal).value === 'true';
Copy link
Contributor

@kokokuo kokokuo Jun 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just would like to check my comprehension, Is here you check the true in the literal nod which from here and here you added in the child ?

Copy link
Contributor Author

@oscar60310 oscar60310 Jun 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the first argument indicates the variable name, and the second one indicates whether it is the main builder.

this.hasMainBuilder = true;
}

private wrapOutputWithBuilder() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just would like to check my comprehsion. Is this function used to wrap the builder from raw sql and the builder default is the main builder, like the below sample?

{% req user %}
select * from users where id = '{{params.id}}'
{% endreq %}
select * from public.orders where userId = '{{ user.id.value() }}';

then I have a question when we face the below case, the not wrap req statement will still show the result? because since the main builder will output ?

{% req user main %}
select * from users where id = '{{params.id}}'
{% endreq %}

select * from public.orders where userId = '{{ user.id.value() }}';
----

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only check whether you have a main builder in your code to decide to send output or not, so:
1.

{% req user %}
select * from users where id = '{{params.id}}'
{% endreq %}
select * from public.orders where userId = '{{ user.id.value() }}';

This statement will be wrapped, the finial query will be select * from public.orders where userId = 'xxxx';

{% req user main %}
select * from users where id = '{{params.id}}'
{% endreq %}

select * from public.orders where userId = '{{ user.id.value() }}';

In this case, because we have defined main builder, the second query select * from public.orders where userId = '{{ user.id.value() }}'; won't be executed. But the first one select * from users where id = '{{params.id}}' will be executed twice (because .value() call)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for relying to me about the logistic question, thanks !

Comment on lines +63 to +68
const args = originalArgs
.slice(1, originalArgs.length - 1)
.filter((value) => typeof value !== 'function');
const contentArgs = originalArgs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could some comment to hint what is the difference between the args and contentArgs or give some sample to let us know because even saw the type in the TagRunnerOptions, still could not actually know what they are.

Copy link
Contributor Author

@oscar60310 oscar60310 Jul 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that there are some differences between these two arguments:

  1. args will be passed to function directly: Literal("main") => extension.__run(context, "main"), our options of the extension should be set on these values.
  2. contentArgs will be "rendered" and passed to function: Output('main') => t = ""; t += "main", the content we want the output should be set on these values.

I've updated the comments of the interface:

  protected createAsyncExtensionNode(
    /**
     * The arguments of this extension, they'll be rendered and passed to run function.
     * It usually contains the configuration of the extension, e.g. {% req variable %} The variable name of req extension.
     * Note that these arguments will be pass to run function directly: Literal('123') => "123", so adding Output nodes causes compiling issues. Output("123") => t += "123"
     */
    argsNodeList: nunjucks.nodes.NodeList,
    /** The content (usually the body) of this extension, they'll be passed to run function as render functions
     * It usually contains the Output of your extension, e.g. {% req variable %} select * from user {% endreq %}, the "select * from user" should be put in this field.
     * Note that these nodes will be rendered as the output of template: Output("123") => t = ""; t += "123", so adding nodes with no output like Symbol, Literal ... might cause compiling issues.  Literal('123') => t = ""; 123
     */
    contentNodes: nunjucks.nodes.Node[] = []
  )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it will be more clear and thanks for searching the different parts and adding the rich comment 👍

@oscar60310 oscar60310 force-pushed the feature/ioc-container branch from 01687ba to 1c5e76f Compare June 30, 2022 07:33
- Add builder value visitor to modify AST
- Add execute filter to call builder.execute()
- Support replace callback while visit nodes' children
add a new argument (the second one) of extension, its value should be "true" or "false", indicating
whether this builder is the final builder or not.
- Extensions now have two kinds: runtime and compileTime extensions.
- Put the extensions with same feature togeter instead of grouping by
types.
- We only test with a full extension now, instead of test by files.
@oscar60310 oscar60310 force-pushed the feature/builder-pattern-te branch from e878082 to 20a8dce Compare June 30, 2022 08:25
@oscar60310
Copy link
Contributor Author

Hi @kokokuo , all issues have been fixed.

@kokokuo
Copy link
Contributor

kokokuo commented Jul 1, 2022

It has been reviewed again, thanks for fixing and replying to me!

Base automatically changed from feature/ioc-container to develop July 1, 2022 10:29
@kokokuo kokokuo merged commit cda4f04 into develop Jul 1, 2022
@kokokuo kokokuo deleted the feature/builder-pattern-te branch July 1, 2022 10:29
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

Successfully merging this pull request may close these issues.

2 participants