Skip to content

Commit

Permalink
feat(config): support simple OR statements in template strings
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Mar 22, 2019
1 parent 08556ac commit 312e90b
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 51 deletions.
24 changes: 18 additions & 6 deletions docs/reference/template-strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ lookups of keys. However, it is possible to do nested templating. For a somewhat
There the name of the module is pulled from the project/environment configuration, and used to find the
appropriate key under the `modules` configuration context.

You can also do simple OR statements:


```yaml
# ...
env:
PROFILE_NAME: ${local.env.PROFILE || "default"}
LOG_LEVEL: ${local.env.LOG_LEVEL || variables.default-log-level}
```

This allows you to easily set default values when certain template keys are not available.

## Reference

### Project configuration context
Expand All @@ -51,7 +63,7 @@ The following keys are available in template strings under the `project` key in

```yaml
local:
local:
# A map of all local environment variables (see
# https://nodejs.org/api/process.html#process_process_env).
env: {}
Expand All @@ -70,7 +82,7 @@ The following keys are available in template strings under the `module` key in

```yaml
local:
local:
# A map of all local environment variables (see
# https://nodejs.org/api/process.html#process_process_env).
env: {}
Expand All @@ -83,7 +95,7 @@ local:
platform:
# Information about the environment that Garden is running against.
environment:
environment:
# The name of the environment Garden is running against.
#
# Example: "local"
Expand All @@ -97,7 +109,7 @@ environment:
# path: /home/me/code/my-project/my-module
# version: v17ad4cb3fd
#
modules:
modules:
{}
# A map of all configured plugins/providers for this environment and their configuration.
Expand All @@ -107,7 +119,7 @@ modules:
# name: local-kubernetes
# context: my-kube-context
#
providers:
providers:
{}
# A map of all variables defined in the project configuration.
Expand All @@ -116,6 +128,6 @@ providers:
# team-name: bananaramallama
# some-service-endpoint: 'https://someservice.com/api/v2'
#
variables:
variables:
{}
```
4 changes: 4 additions & 0 deletions garden-service/gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ module.exports = (gulp) => {
.pipe(gulp.dest(destDir)),
)

gulp.task("pegjs-watch", () =>
gulp.watch(pegjsSources, gulp.series(["pegjs"])),
)

gulp.task("tsc", () =>
tsProject.src()
.pipe(sourcemaps.init())
Expand Down
41 changes: 25 additions & 16 deletions garden-service/src/config/config-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ import { ModuleVersion } from "../vcs/base"

export type ContextKey = string[]

export interface ContextResolveOpts {
allowUndefined?: boolean
// a list of previously resolved paths, used to detect circular references
stack?: string[]
}

export interface ContextResolveParams {
key: ContextKey
nodePath: ContextKey
// a list of previously resolved paths, used to detect circular references
stack?: string[]
opts: ContextResolveOpts
}

export function schema(joiSchema: Joi.Schema) {
Expand All @@ -46,7 +51,7 @@ export abstract class ConfigContext {
return Joi.object().keys(schemas).required()
}

async resolve({ key, nodePath, stack }: ContextResolveParams): Promise<Primitive> {
async resolve({ key, nodePath, opts }: ContextResolveParams): Promise<Primitive | undefined> {
const path = key.join(".")
const fullPath = nodePath.concat(key).join(".")

Expand All @@ -57,15 +62,15 @@ export abstract class ConfigContext {
return resolved
}

stack = [...stack || []]
opts.stack = [...opts.stack || []]

if (stack.includes(fullPath)) {
if (opts.stack.includes(fullPath)) {
throw new ConfigurationError(
`Circular reference detected when resolving key ${path} (${stack.join(" -> ")})`,
`Circular reference detected when resolving key ${path} (${opts.stack.join(" -> ")})`,
{
nodePath,
fullPath,
stack,
opts,
},
)
}
Expand Down Expand Up @@ -94,15 +99,15 @@ export abstract class ConfigContext {
// handle nested contexts
if (value instanceof ConfigContext) {
const nestedKey = remainder
stack.push(stackEntry)
value = await value.resolve({ key: nestedKey, nodePath: nestedNodePath, stack })
opts.stack.push(stackEntry)
value = await value.resolve({ key: nestedKey, nodePath: nestedNodePath, opts })
break
}

// handle templated strings in context variables
if (isString(value)) {
stack.push(stackEntry)
value = await resolveTemplateString(value, this._rootContext, stack)
opts.stack.push(stackEntry)
value = await resolveTemplateString(value, this._rootContext, opts)
}

if (value === undefined) {
Expand All @@ -111,11 +116,15 @@ export abstract class ConfigContext {
}

if (value === undefined) {
throw new ConfigurationError(`Could not find key: ${path}`, {
nodePath,
fullPath,
stack,
})
if (opts.allowUndefined) {
return
} else {
throw new ConfigurationError(`Could not find key: ${path}`, {
nodePath,
fullPath,
opts,
})
}
}

if (!isPrimitive(value)) {
Expand Down
98 changes: 90 additions & 8 deletions garden-service/src/template-string-parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,121 @@ TemplateString
/ $(.*) { return [text()] }

NestedTemplateString
= a:(FormatString)+ b:NestedTemplateString? { return [...a, ...(b || [])] }
/ a:Prefix b:(FormatString)+ c:NestedTemplateString? { return [a, ...b, ...(c || [])] }
= a:(FormatString)+ b:NestedTemplateString? {
return [...a, ...(b || [])]
}
/ a:Prefix b:(FormatString)+ c:NestedTemplateString? {
return [a, ...b, ...(c || [])]
}
/ InvalidFormatString
/ Suffixreturn [text()] }

FormatString
= FormatStart head:Identifier tail:(KeySeparator Identifier)* FormatEnd {
const parts = [["", head]].concat(tail).map(p => p[1])
return options.getKey(parts)
= FormatStart key:Key FormatEnd {
return options.getKey(key)
}
/ FormatStart a:Key Or b:Key FormatEnd {
return options.resolve(a, { allowUndefined: true })
.then(result => {
return result || options.resolve(b, { allowUndefined: false })
})
}
/ FormatStart a:Key Or b:StringLiteral FormatEnd {
return options.resolve(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
}
/ FormatStart a:StringLiteral FormatEnd {
return a
}
/ FormatStart s:NestedTemplateString FormatEnd {
return options.resolve(s)
}

InvalidFormatString
= Prefix? FormatStart .* {
throw new options.TemplateStringError("Invalid template string: ..." + text())
throw new options.TemplateStringError("Invalid template string: " + text())
}

FormatStart
= "${"
= ws "${" ws

FormatEnd
= "}"
= ws "}" ws

Identifier
= [a-zA-Z][a-zA-Z0-9_\-]* { return text() }

KeySeparator
= "."

Key
= head:Identifier tail:(KeySeparator Identifier)* {
return [["", head]].concat(tail).map(p => p[1])
}

Or
= ws "||" ws

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

StringLiteral
= ws '"' chars:DoubleQuotedChar* '"' ws { return chars.join(""); }
/ ws "'" chars:SingleQuotedChar* "'" ws { return chars.join(""); }

Escape
= "\\"

DoubleQuotedChar
= [^\0-\x1F\x22\x5C]
/ Escape
sequence:(
'"'
/ "\\"
/ "/"
/ "b" { return "\b"; }
/ "f" { return "\f"; }
/ "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }
/ "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG) {
return String.fromCharCode(parseInt(digits, 16));
}
)
{ return sequence; }

SingleQuotedChar
= [^\0-\x1F\x27\x5C]
/ Escape
sequence:(
"'"
/ "\\"
/ "/"
/ "b" { return "\b"; }
/ "f" { return "\f"; }
/ "n" { return "\n"; }
/ "r" { return "\r"; }
/ "t" { return "\t"; }
/ "u" digits:$(HEXDIG HEXDIG HEXDIG HEXDIG) {
return String.fromCharCode(parseInt(digits, 16));
}
)
{ 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).
DIGIT = [0-9]
HEXDIG = [0-9a-f]i
10 changes: 5 additions & 5 deletions garden-service/src/template-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { resolve } from "path"
import Bluebird = require("bluebird")
import { asyncDeepMap } from "./util/util"
import { GardenBaseError } from "./exceptions"
import { ConfigContext } from "./config/config-context"
import { ConfigContext, ContextResolveOpts } from "./config/config-context"

export type StringOrStringPromise = Promise<string> | string

Expand Down Expand Up @@ -45,14 +45,14 @@ 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, stack?: string[]) {
export async function resolveTemplateString(string: string, context: ConfigContext, opts: ContextResolveOpts = {}) {
const parser = await getParser()
const parsed = parser.parse(string, {
getKey: async (key: string[]) => context.resolve({ key, nodePath: [], stack }),
getKey: async (key: string[]) => context.resolve({ key, nodePath: [], opts }),
// need this to allow nested template strings
resolve: async (parts: StringOrStringPromise[]) => {
resolve: async (parts: StringOrStringPromise[], resolveOpts?: ContextResolveOpts) => {
const s = (await Bluebird.all(parts)).join("")
return resolveTemplateString(`\$\{${s}\}`, context, stack)
return resolveTemplateString(`\$\{${s}\}`, context, { ...opts, ...resolveOpts || {} })
},
TemplateStringError,
})
Expand Down
Loading

0 comments on commit 312e90b

Please sign in to comment.