Skip to content

Commit

Permalink
feat(template): support if-blocks for multi-line conditionals
Browse files Browse the repository at this point in the history
From the added docs:

In addition to the conditionals described above, you can use if/else
blocks. These are particularly handy when templating multi-line strings
and generated files in [module templates](./module-templates.md).

The syntax is
`${if <expression>}<content>[${else}]<alternative content>${endif}`,
where `<expression>` is any expression you'd put in a normal template
string.

Here's a basic example:

```yaml
variables:
  some-script: |
    #!/bin/sh
    echo "Hello, I'm a bash script!"

    ${if environment.name == "dev"}
    echo "-> debug mode"
    DEBUG=true
    ${else}
    DEBUG=false
    ${endif}
    ...
```

You can also nest if-blocks, should you need to.
  • Loading branch information
edvald committed Dec 15, 2020
1 parent fa5df97 commit 884fe32
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 34 deletions.
36 changes: 34 additions & 2 deletions core/src/template-string-parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
getKey,
getValue,
isArray,
isPlainObject,
isPrimitive,
optionalSuffix,
missingKeyExceptionType,
Expand All @@ -29,7 +30,10 @@ TemplateString
/ $(.*) { return text() === "" ? [] : [{ resolved: text() }] }

FormatString
= FormatStart e:Expression end:FormatEnd {
= FormatStart op:BlockOperator FormatEnd {
return { block: op }
}
/ FormatStart blockOperator:(ExpressionBlockOperator __)* e:Expression end:FormatEndWithOptional {
// Any unexpected error is returned immediately. Certain exceptions have special semantics that are caught below.
if (e && e._error && e._error.type !== missingKeyExceptionType && e._error.type !== passthroughExceptionType) {
return e
Expand All @@ -38,6 +42,21 @@ FormatString
// Need to provide the optional suffix as a variable because of a parsing bug in pegjs
const allowUndefined = end[1] === optionalSuffix

if (!isPlainObject(e)) {
e = { resolved: e }
}

if (e && blockOperator[0] && blockOperator[0][0]) {
e.block = blockOperator[0][0]
}

if (e && e.block && allowUndefined) {
const _error = new TemplateStringError("Cannot specify optional suffix in if-block.", {
text: text(),
})
return { _error }
}

if (getValue(e) === undefined) {
if (e && e._error && e._error.type === passthroughExceptionType) {
// We allow certain configuration contexts (e.g. placeholders for runtime.*) to indicate that a template
Expand Down Expand Up @@ -72,8 +91,21 @@ FormatStart
= "${" __

FormatEnd
= __ "}"

OptionalFormatEnd
= __ "}?"
/ __ "}"

FormatEndWithOptional
= OptionalFormatEnd
/ FormatEnd

BlockOperator
= "else"
/ "endif"

ExpressionBlockOperator
= "if"

Prefix
= !FormatStart (. ! FormatStart)* . { return text() }
Expand Down
87 changes: 78 additions & 9 deletions core/src/template-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { difference, flatten, uniq, isPlainObject, isNumber } from "lodash"
import { Primitive, StringMap, isPrimitive, objectSpreadKey } from "./config/common"
import { profile } from "./util/profiling"
import { dedent, deline } from "./util/string"
import { dedent, deline, truncate } from "./util/string"
import { isArray } from "util"
import { ObjectWithName } from "./util/util"

Expand Down Expand Up @@ -49,7 +49,17 @@ function getParser() {
return _parser
}

type ResolvedClause = ContextResolveOutput | { resolved: undefined; _error: Error }
interface ResolvedClause extends ContextResolveOutput {
block?: "if" | "else" | "else if" | "endif"
_error?: Error
}

interface ConditionalTree {
type: "root" | "if" | "if" | "else" | "value"
value?: any
children: ConditionalTree[]
parent?: ConditionalTree
}

function getValue(v: Primitive | undefined | ResolvedClause) {
return isPlainObject(v) ? (<ResolvedClause>v).resolved : v
Expand Down Expand Up @@ -85,6 +95,7 @@ export function resolveTemplateString(string: string, context: ConfigContext, op
passthroughExceptionType,
allowPartial: !!opts.allowPartial,
optionalSuffix: "}?",
isPlainObject,
isPrimitive,
})

Expand All @@ -103,17 +114,75 @@ export function resolveTemplateString(string: string, context: ConfigContext, op
let resolved: any = outputs[0]?.resolved

if (outputs.length > 1) {
resolved = outputs
.map((output) => {
const v = getValue(output)
return v === null ? "null" : v
})
.join("")
// Assemble the parts into a conditional tree
const tree: ConditionalTree = {
type: "root",
children: [],
}
let currentNode = tree

for (const part of outputs) {
if (part.block === "if") {
const node: ConditionalTree = {
type: "if",
value: !!part.resolved,
children: [],
parent: currentNode,
}
currentNode.children.push(node)
currentNode = node
} else if (part.block === "else") {
if (currentNode.type !== "if") {
throw new TemplateStringError("Found ${else} block without a preceding ${if...} block.", {})
}
const node: ConditionalTree = {
type: "else",
value: !currentNode.value,
children: [],
parent: currentNode.parent,
}
currentNode.parent!.children.push(node)
currentNode = node
} else if (part.block === "endif") {
if (currentNode.type === "if" || currentNode.type === "else") {
currentNode = currentNode.parent!
} else {
throw new TemplateStringError("Found ${endif} block without a preceding ${if...} block.", {})
}
} else {
const v = getValue(part)

currentNode.children.push({
type: "value",
value: v === null ? "null" : v,
children: [],
})
}
}

if (currentNode.type === "if" || currentNode.type === "else") {
throw new TemplateStringError("Missing ${endif} after ${if ...} block.", {})
}

// Walk down tree and resolve the output string
resolved = ""

function resolveTree(node: ConditionalTree) {
if (node.type === "value" && node.value !== undefined) {
resolved += node.value
} else if (node.type === "root" || ((node.type === "if" || node.type === "else") && !!node.value)) {
for (const child of node.children) {
resolveTree(child)
}
}
}

resolveTree(tree)
}

return resolved
} catch (err) {
const prefix = `Invalid template string ${string}: `
const prefix = `Invalid template string (${truncate(string, 35).replace(/\n/g, "\\n")}): `
const message = err.message.startsWith(prefix) ? err.message : prefix + err.message

throw new TemplateStringError(message, {
Expand Down
4 changes: 2 additions & 2 deletions core/test/unit/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1610,7 +1610,7 @@ describe("ActionRouter", () => {
}),
(err) =>
expect(stripAnsi(err.message)).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: Could not find key service-b under runtime.services."
"Invalid template string (${runtime.services.service-b.outpu…): Could not find key service-b under runtime.services."
)
)
})
Expand Down Expand Up @@ -1785,7 +1785,7 @@ describe("ActionRouter", () => {
}),
(err) =>
expect(stripAnsi(err.message)).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: Could not find key service-b under runtime.services."
"Invalid template string (${runtime.services.service-b.outpu…): Could not find key service-b under runtime.services."
)
)
})
Expand Down
2 changes: 1 addition & 1 deletion core/test/unit/src/config/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ describe("ConfigContext", () => {
() => resolveKey(c, ["nested", "key"]),
(err) =>
expect(err.message).to.equal(
"Invalid template string ${'${nested.key}'}: Invalid template string ${nested.key}: Circular reference detected when resolving key nested.key (nested -> nested.key)"
"Invalid template string (${'${nested.key}'}): Invalid template string (${nested.key}): Circular reference detected when resolving key nested.key (nested -> nested.key)"
)
)
})
Expand Down
4 changes: 2 additions & 2 deletions core/test/unit/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@ describe("Garden", () => {
(err) => {
expect(err.message).to.equal("Failed resolving one or more providers:\n" + "- test")
expect(stripAnsi(err.detail.messages[0])).to.equal(
"- test: Invalid template string ${bla.ble}: Could not find key bla. Available keys: environment, git, local, project, providers, secrets, var and variables."
"- test: Invalid template string (${bla.ble}): Could not find key bla. Available keys: environment, git, local, project, providers, secrets, var and variables."
)
}
)
Expand Down Expand Up @@ -2438,7 +2438,7 @@ describe("Garden", () => {
expect(stripAnsi(err.message)).to.equal(dedent`
Failed resolving one or more modules:
module-a: Invalid template string ${key}: Module module-a cannot reference itself.
module-a: Invalid template string (${key}): Module module-a cannot reference itself.
`)
)
})
Expand Down
Loading

0 comments on commit 884fe32

Please sign in to comment.