Skip to content

Commit

Permalink
improvement(config): allow non-string values to be output directly
Browse files Browse the repository at this point in the history
This makes it possible to use template strings to assign numeric, boolean
and null values to config keys, instead of always being cast to strings.
  • Loading branch information
edvald committed Jun 22, 2019
1 parent cf34cd3 commit 52ad5fa
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 57 deletions.
43 changes: 43 additions & 0 deletions docs/using-garden/configuration-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,49 @@ You can do conditional statements in template strings, using the `||` operator.
This allows you to easily set default values when certain template keys are not available, and to configure your
project based on varying context.

### Numbers, booleans and null values

When a template string key resolves to a number, boolean or null, the output is handled in two different ways,
depending on whether the template string is part of surrounding string or not.

If the template string is the whole string being interpolated, we assign the number, boolean or null directly to the
key:

```yaml
kind: Project
...
variables:
global-memory-limit: 100
---
kind: Module
...
services:
- name: my-service
...
limits:
memory: ${var.global-memory-limit} # <- resolves to a number, as opposed to "100" as a string
```

If however the template string is part of a surrounding string, the value is formatted into the string, as you would
expect:

```yaml
kind: Project
...
variables:
project-id: 123
some-key: null
---
kind: Module
...
services:
- name: my-service
...
env:
CONTEXT: project-${project-id} # <- resolves to "project-123"
SOME_VAR: foo-${var.some-key} # <- resolves to "foo-null"
```

## Next steps

We highly recommend browsing through the [Example projects](../examples/README.md) to see different examples of how projects and modules can be configured.
Expand Down
89 changes: 64 additions & 25 deletions garden-service/src/template-string-parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ TemplateString
= a:(FormatString)+ b:TemplateString? { return [...a, ...(b || [])] }
/ a:Prefix b:(FormatString)+ c:TemplateString? { return [a, ...b, ...(c || [])] }
/ InvalidFormatString
/ $(.*) { return [text()] }
/ $(.*) { return text() === "" ? [] : [text()] }

FormatString
= FormatStart key:Key FormatEnd {
return options.getKey(key)
= FormatStart v:Literal FormatEnd {
return v
}
/ FormatStart head:Key tail:(Or (Key / StringLiteral))+ FormatEnd {
/ FormatStart v:Key FormatEnd {
return options.getKey(v)
}
/ FormatStart head:LiteralOrKey tail:(Or LiteralOrKey)* FormatEnd {
const keys = [head, ...tail.map(t => t[1])]

// Resolve all the keys
return Promise.all(keys.map(key =>
typeof key === "string" ? key : options.getKey(key, { allowUndefined: true })
options.lodash.isArray(key) ? options.getKey(key, { allowUndefined: true }) : key,
))
.then(candidates => {
// Return the first non-undefined value
Expand All @@ -34,19 +37,10 @@ FormatString
throw new options.ConfigurationError("None of the keys could be resolved in the conditional: " + text())
})
}
/ FormatStart a:Key Or b:StringLiteral FormatEnd {
return options.getKey(a, { allowUndefined: true })
.then(result => {
return result || b
})
}
// These would be odd in configuration, but there's no reason to throw if it comes up.
/ FormatStart a:StringLiteral Or b:StringLiteral FormatEnd {
return a || b
}
/ FormatStart a:StringLiteral FormatEnd {
return a
}

LiteralOrKey
= Literal
/ Key

InvalidFormatString
= Prefix? FormatStart .* {
Expand All @@ -73,9 +67,60 @@ Key
Or
= ws "||" ws

// Some of the below is based on https://github.com/pegjs/pegjs/blob/master/examples/json.pegjs
Prefix
= !FormatStart (. ! FormatStart)* . { return text() }

Suffix
= !FormatEnd (. ! FormatEnd)* . { return text() }

// Much of the below is based on https://github.com/pegjs/pegjs/blob/master/examples/json.pegjs
ws "whitespace" = [ \t\n\r]*

// ----- Literals -----

Literal
= BooleanLiteral
/ NullLiteral
/ NumberLiteral
/ StringLiteral

BooleanLiteral
= ws "true" ws { return true }
/ ws "false" ws { return false }

NullLiteral
= ws "null" ws { return null }

NumberLiteral
= ws Minus? Int Frac? Exp? ws { return parseFloat(text()); }

DecimalPoint
= "."

Digit1_9
= [1-9]

E
= [eE]

Exp
= E (Minus / Plus)? DIGIT+

Frac
= DecimalPoint DIGIT+

Int
= Zero / (Digit1_9 DIGIT*)

Minus
= "-"

Plus
= "+"

Zero
= "0"

StringLiteral
= ws '"' chars:DoubleQuotedChar* '"' ws { return chars.join(""); }
/ ws "'" chars:SingleQuotedChar* "'" ws { return chars.join(""); }
Expand Down Expand Up @@ -119,12 +164,6 @@ SingleQuotedChar
)
{ return sequence; }

Prefix
= !FormatStart (. ! FormatStart)* . { return text() }

Suffix
= !FormatEnd (. ! FormatEnd)* . { return text() }

// ----- Core ABNF Rules -----

// See RFC 4234, Appendix B (http://tools.ietf.org/html/rfc4234).
Expand Down
19 changes: 16 additions & 3 deletions garden-service/src/template-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import lodash = require("lodash")
import Bluebird = require("bluebird")
import { asyncDeepMap } from "./util/util"
import { GardenBaseError, ConfigurationError } from "./exceptions"
import { ConfigContext, ContextResolveOpts, ContextResolveParams } from "./config/config-context"
import { KeyedSet } from "./util/keyed-set"
import { uniq } from "lodash"
import { Primitive } from "./config/common"

export type StringOrStringPromise = Promise<string> | string

Expand All @@ -37,7 +39,9 @@ async function getParser() {
* The context should be a ConfigContext instance. The optional `stack` parameter is used to detect circular
* dependencies when resolving context variables.
*/
export async function resolveTemplateString(string: string, context: ConfigContext, opts: ContextResolveOpts = {}) {
export async function resolveTemplateString(
string: string, context: ConfigContext, opts: ContextResolveOpts = {},
): Promise<Primitive | undefined> {
const parser = await getParser()
const parsed = parser.parse(string, {
getKey: async (key: string[], resolveOpts?: ContextResolveOpts) => {
Expand All @@ -48,12 +52,21 @@ export async function resolveTemplateString(string: string, context: ConfigConte
const s = (await Bluebird.all(parts)).join("")
return resolveTemplateString(`\$\{${s}\}`, context, { ...opts, ...resolveOpts || {} })
},
// Some utilities to pass to the parser
lodash,
ConfigurationError,
TemplateStringError,
})

const resolved = await Bluebird.all(parsed)
return resolved.join("")
const resolved: (Primitive | undefined)[] = await Bluebird.all(parsed)

const result = resolved.length === 1
// Return value directly if there is only one value in the output
? resolved[0]
// Else join together all the parts as a string. Output null as a literal string and not an empty string.
: resolved.map(v => v === null ? "null" : v).join("")

return <Primitive | undefined>result
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
project:
name: non-string-template-values
environments:
- name: local
providers:
- name: test-plugin
variables:
allow-publish: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Module
name: module-a
type: test
allowPublish: ${var.allow-publish}
10 changes: 10 additions & 0 deletions garden-service/test/unit/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,16 @@ describe("Garden", () => {
it.skip("should set default values properly", async () => {
throw new Error("TODO")
})

it("should handle template variables for non-string fields", async () => {
const projectRoot = getDataDir("test-projects", "non-string-template-values")
const garden = await makeTestGarden(projectRoot)

const module = await garden.resolveModuleConfig("module-a")

// We template in the value for the module's allowPublish field to test this
expect(module.allowPublish).to.equal(false)
})
})

describe("resolveVersion", () => {
Expand Down
Loading

0 comments on commit 52ad5fa

Please sign in to comment.