Skip to content

Commit

Permalink
feat(template): add optional suffix, to allow undefined values
Browse files Browse the repository at this point in the history
In some cases, you may want to provide configuration values only for
certain cases, e.g. only for specific environments. By default, an
error is thrown when a template string resolves to an undefined value,
but you can explicitly allow that by adding a `?` after the template.

Example:

```yaml
kind: Project
...
providers:
  - name: kubernetes
    kubeconfig: ${var.kubeconfig}?
  ...
```

This is useful when you don't want to provide _any_ value unless one is
explicitly set, effectively falling back to whichever the default is
for the field in question.
  • Loading branch information
edvald committed Feb 14, 2020
1 parent fca3bd8 commit 1eb0a92
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 24 deletions.
17 changes: 17 additions & 0 deletions docs/guides/variables-and-templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ services:
...
```

### Optional values

In some cases, you may want to provide configuration values only for certain cases, e.g. only for specific environments. By default, an error is thrown when a template string resolves to an undefined value, but you can explicitly allow that by adding a `?` after the template.

Example:

```yaml
kind: Project
...
providers:
- name: kubernetes
kubeconfig: ${var.kubeconfig}?
...
```

This is useful when you don't want to provide _any_ value unless one is explicitly set, effectively falling back to whichever the default is for the field in question.

## Project variables

A common use case for templating is to define variables in the project/environment configuration, and to use template strings to propagate values to modules in the project.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/template-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Template Strings Reference

# Template string reference

Below you'll find the schema of the keys available when interpolating template strings (see our [Configuration Files](../guides/configuration-files.md) guide for more information and usage examples).
Below you'll find the schema of the keys available when interpolating template strings (see our [Variables and Templating](../guides/variables-and-templating.md) guide for more information and usage examples).

Note that there are four sections below, since different configuration sections have different keys available to them. Please make sure to refer to the correct section.

Expand Down
2 changes: 1 addition & 1 deletion garden-service/src/docs/templates/template-strings.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ title: Template Strings Reference

# Template string reference

Below you'll find the schema of the keys available when interpolating template strings (see our [Configuration Files](../guides/configuration-files.md) guide for more information and usage examples).
Below you'll find the schema of the keys available when interpolating template strings (see our [Variables and Templating](../guides/variables-and-templating.md) guide for more information and usage examples).

Note that there are four sections below, since different configuration sections have different keys available to them. Please make sure to refer to the correct section.

Expand Down
10 changes: 7 additions & 3 deletions garden-service/src/template-string-parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ TemplateString
/ $(.*) { return text() === "" ? [] : [{ resolved: text() }] }

FormatString
= FormatStart e:Expression FormatEnd {
= FormatStart e:Expression end:FormatEnd {
return Promise.resolve(e)
.then(v => {
if (v && v._error) {
return v
}

if (v === undefined && !options.allowUndefined) {
// Need to provide the optional suffix as a variable because of a parsing bug in pegjs
const allowUndefined = options.allowUndefined || end[1] === options.optionalSuffix

if (options.getValue(v) === undefined && !allowUndefined) {
const _error = new options.TemplateStringError(v.message || "Unable to resolve one or more keys.", {
text: text(),
})
Expand All @@ -42,7 +45,8 @@ FormatStart
= "${" __

FormatEnd
= __ "}"
= __ "}?"
/ __ "}"

KeySeparator
= "."
Expand Down
8 changes: 2 additions & 6 deletions garden-service/src/template-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function resolveTemplateString(
ConfigurationError,
TemplateStringError,
allowUndefined: opts.allowUndefined,
optionalSuffix: "}?",
})

const outputs: ResolvedClause[] = await Bluebird.map(parsed, async (p: any) => {
Expand All @@ -93,12 +94,7 @@ export async function resolveTemplateString(
.join("")
}

if (resolved === undefined && !opts.allowUndefined) {
throw new ConfigurationError(`Template string resolves to undefined value.`, {
string,
resolved,
})
} else if (resolved !== undefined && !isPrimitive(resolved)) {
if (resolved !== undefined && !isPrimitive(resolved)) {
throw new ConfigurationError(
`Template string doesn't resolve to a primitive (string, number, boolean or null).`,
{
Expand Down
10 changes: 4 additions & 6 deletions garden-service/test/unit/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1460,9 +1460,8 @@ describe("ActionRouter", () => {
},
}),
(err) =>
expect(err.message).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: " +
"Template string resolves to undefined value."
expect(stripAnsi(err.message)).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: Could not find key runtime."
)
)
})
Expand Down Expand Up @@ -1636,9 +1635,8 @@ describe("ActionRouter", () => {
},
}),
(err) =>
expect(err.message).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: " +
"Template string resolves to undefined value."
expect(stripAnsi(err.message)).to.equal(
"Invalid template string ${runtime.services.service-b.outputs.foo}: Could not find key runtime."
)
)
})
Expand Down
4 changes: 2 additions & 2 deletions garden-service/test/unit/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1370,8 +1370,8 @@ describe("Garden", () => {
() => garden.resolveProviders(),
(err) => {
expect(err.message).to.equal("Failed resolving one or more providers:\n" + "- test")
expect(err.detail.messages[0]).to.equal(
"- test: Invalid template string ${bla.ble}: Template string resolves to undefined value."
expect(stripAnsi(err.detail.messages[0])).to.equal(
"- test: Invalid template string ${bla.ble}: Could not find key bla."
)
}
)
Expand Down
24 changes: 19 additions & 5 deletions garden-service/test/unit/src/template-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ describe("resolveTemplateString", async () => {
expect(res).to.equal(undefined)
})

it("should allow undefined values if ? suffix is present", async () => {
const res = await resolveTemplateString("${foo}?", new TestContext({}))
expect(res).to.equal(undefined)
})

it("should interpolate a format string with a prefix", async () => {
const res = await resolveTemplateString("prefix-${some}", new TestContext({ some: "value" }))
expect(res).to.equal("prefix-value")
Expand All @@ -52,6 +57,16 @@ describe("resolveTemplateString", async () => {
expect(res).to.equal("value-suffix")
})

it("should interpolate a format string with a prefix and a suffix", async () => {
const res = await resolveTemplateString("prefix-${some}-suffix", new TestContext({ some: "value" }))
expect(res).to.equal("prefix-value-suffix")
})

it("should interpolate an optional format string with a prefix and a suffix", async () => {
const res = await resolveTemplateString("prefix-${some}?-suffix", new TestContext({}))
expect(res).to.equal("prefix--suffix")
})

it("should interpolate a format string with a prefix with whitespace", async () => {
const res = await resolveTemplateString("prefix ${some}", new TestContext({ some: "value" }))
expect(res).to.equal("prefix value")
Expand Down Expand Up @@ -86,7 +101,7 @@ describe("resolveTemplateString", async () => {
try {
await resolveTemplateString("${some}", new TestContext({}))
} catch (err) {
expect(err.message).to.equal("Invalid template string ${some}: Template string resolves to undefined value.")
expect(stripAnsi(err.message)).to.equal("Invalid template string ${some}: Could not find key some.")
return
}

Expand All @@ -97,8 +112,8 @@ describe("resolveTemplateString", async () => {
try {
await resolveTemplateString("${some.other}", new TestContext({ some: {} }))
} catch (err) {
expect(err.message).to.equal(
"Invalid template string ${some.other}: Template string resolves to undefined value."
expect(stripAnsi(err.message)).to.equal(
"Invalid template string ${some.other}: Could not find key other under some."
)
return
}
Expand Down Expand Up @@ -259,8 +274,7 @@ describe("resolveTemplateString", async () => {
it("should throw if neither key in logical OR is valid", async () => {
return expectError(
() => resolveTemplateString("${a || b}", new TestContext({})),
(err) =>
expect(err.message).to.equal("Invalid template string ${a || b}: Template string resolves to undefined value.")
(err) => expect(stripAnsi(err.message)).to.equal("Invalid template string ${a || b}: Could not find key b.")
)
})

Expand Down

0 comments on commit 1eb0a92

Please sign in to comment.