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

Add support for conditions and loops to AbstractCodeWriter #1144

Merged
merged 8 commits into from
Mar 23, 2022

Conversation

mtdowling
Copy link
Member

@mtdowling mtdowling commented Mar 21, 2022

AbstractCodeWriter now supports Mustache-like behavior by allowing conditions and loops to be evaluated in templates. Note that this does disallow using '/', '?', '^', and '#' as formatters.

Conditions are defined using ${?foo} where "foo" is the writer named parameter to evaluate. ? is used to check if a value is truthy.

The following template with a "foo" value set to "Noid":

${?foo}
Hello, ${foo:L}!
${/foo}

Evaluates to:

Hello, Noid!

^ can be used to check if a value is falsey. The following template with no set "baz" value:

${?baz}
  Yes baz
${/baz}
${^baz}
  No baz
${/baz}

Evaluates to:

  No baz

A variable is falsey if it is Boolean false, an empty Iterable, an empty Optional, an empty Map, or an empty String. A variable is truthy if it is not falsey.

If a conditional expression or loop expression is the non-whitespace text on a line and is immediately followed by a \r or \n, then the entire line is omitted from the output.

Loops can be created using ${#foo}. Each value contained in the referenced named parameter is sent through the contained template, one after the other.

The following template with a "foo" value of ["a", "b"]:

${#foo}
- ${currentKey:L}: ${currentValue:L}
${/foo}

Evaluates to:

- 0: a
- 1: b

Each iteration of the loop creates a new state in the writer that sets a named parameter named key that contains the current index of the loop or the current key of a map entry; value containing the current value of the iterable or current value of a map entry; key.first is set to true when the first value is being evaluated, and key.last is set to true when the last value is being evaluated.

SimpleCodeWriter writer = new SimpleCodeWriter()
        .trimTrailingSpaces(false)
        .insertTrailingNewline(false);
writer.putContext("foo", Arrays.asList("a", "b", "c"));
writer.write("""
        ${#foo}
        ${?key.first}
        [
        ${/key.first}
            ${value:L}${^key.last},${/key.last}
        ${?key.last}
        ]
        ${/key.last}
        ${/foo}""");

assertThat(writer.toString(), equalTo("""
        [
            a,
            b,
            c
        ]
        """);

A custom variable name can be used in loops. For example:

${#foo as key1, value1}
    - ${key1:L}: ${value1:L} (first: ${key1.first:L}, last: ${key.last:L}
${/foo}

Whitespace that comes before an expression removed by putting ~ at the beginning of an expression.

Assuming that the first positional argument is "hi":

Greeting:
    ${~L}

Expands to:

Greeting:hi

Whitespace that comes after an expression can be removed by adding ~ to the end of the expression:

${L~}

.    

Expands to:

hi.

Leading whitespace cannot be removed when using inline block alignment ('|'). The following is invalid:

    ${~C|}

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

CodeWriter now supports Mustache-like behavior by allowing conditions
and loops to be evaluated in templates. Note that this does disallow
using '/', '?', '^', and '#' as formatters.

Conditions are defined using `${?foo}` where "foo" is the writer named
parameter to evaluate. `?` is used to check if a value is truthy.

The following template with a "foo" value set to "Noid":

```
${?foo}
Hello, ${foo:L}!
${/foo}
```

Evaluates to:

```
Hello, Noid!
```

`^` can be used to check if a value is falsey. The following template
with no set "baz" value:

```
${baz}
  Yes baz
${/baz}
${^baz}
  No baz
${/baz}
```

Evaluates to:

```
  No baz
```

A variable is falsey if it is Boolean false, an empty Iterable, an empty
Optional, an empty Map, or an empty String. A variable is truthy if it
is not falsey.

If a conditional expression or loop expression is the non-whitespace
text on a line and is immediately followed by a `\r` or `\n`, then the
entire line is omitted from the output.

Loops can be created using `${#foo}`. Each value contained in the
referenced named parameter is sent through the contained templated one
after the other.

The following template with a "foo" value of ["a", "b"]:

```
${#foo}
- ${currentKey:L}: ${currentValue:L}
${/foo}
```

Evaluates to:

```
- 0: a
- 1: b
```

Each iteration of the loop creates a new state in the writer that sets a
named parameter named `currentKey` that contains the current index of
the loop or the current key of a map entry; `currentValue` containing
the current value of the iterable or current value of a map entry;
`currentIsFirst` is set to true when the first value is being evaluated,
and `currentIsLast` is set to true when the last value is being evaluated.

```
SimpleCodeWriter writer = new SimpleCodeWriter()
        .trimTrailingSpaces(false)
        .insertTrailingNewline(false);
writer.putContext("foo", Arrays.asList("a", "b", "c"));
writer.write("""
        ${#foo}
        ${?currentIsFirst}
        [
        ${/currentIsFirst}
            ${currentValue:L}${^currentIsLast},${/currentIsLast}
        ${?currentIsLast}
        ]
        ${/currentIsLast}
        ${/foo}""");

assertThat(writer.toString(), equalTo("""
        [
            a,
            b,
            c
        ]
        """);
```
@mtdowling mtdowling requested a review from a team as a code owner March 21, 2022 18:23
@adamthom-amzn
Copy link
Contributor

I wonder if you could trim down some of the bloat from currentIsFirst and currentKey, etc. Maybe something overly complicated and bad like

SimpleCodeWriter writer = new SimpleCodeWriter()
        .trimTrailingSpaces(false)
        .insertTrailingNewline(false);
writer.putContext("foo", Arrays.asList("a", "b", "c"));
writer.write("""
        ${#foo|k,v}
        ${?kIsFirst}
        [
        ${/kIsFirst}
            ${v:L}${^kIsLast},${/kIsLast}
        ${?kIsLast}
        ]
        ${/kIsLast}
        ${/foo}""");

assertThat(writer.toString(), equalTo("""
        [
            a,
            b,
            c
        ]
        """);

?, #, /, and ^ are used for conditions and loops. Reserving
~ for potentially controlling whitespace in the future.
@mtdowling
Copy link
Member Author

I pushed an update that allows loops to define a custom key and value variable name. It now defaults to key, key.first, key.last, and value.

This commit adds documentation for templating controls and updates the
existing documentation to no longer reference the deprecated CodeWriter.
@mtdowling mtdowling dismissed JordonPhillips’s stale review March 23, 2022 18:33

Feedback was addressed. The UI isn't showing any new requested changes.

@mtdowling mtdowling merged commit 80e54b8 into main Mar 23, 2022
@mtdowling mtdowling deleted the conditional-codewriter branch April 8, 2022 05:31
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.

3 participants