-
Notifications
You must be signed in to change notification settings - Fork 273
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
WIP: input tracking #5496
base: main
Are you sure you want to change the base?
WIP: input tracking #5496
Changes from 28 commits
93d2931
595e652
9d9ecf0
e2ebf08
3c06ecd
73dcad9
c125de4
00d8a82
4bff56a
24ea2e1
fcc37d4
d47d5e1
c80a2cf
3f9fa88
49ca406
1f7ee2a
1484e9d
5504c56
3e72e3d
e99008c
cac9d94
a9ef07e
a1b5405
81f5dea
f507d35
6db6747
6a5504b
88e67b2
5320506
70670d9
30eece3
dd634d4
7a3f170
f342808
04adf6b
e944ea2
dfbee86
f8fc3f4
3960f9a
58e4297
1ad69f9
edab666
7fb1f04
da81199
44a0a5e
9038449
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,23 +8,27 @@ | |
|
||
import type Joi from "@hapi/joi" | ||
import { isString } from "lodash-es" | ||
import { ConfigurationError } from "../../exceptions.js" | ||
import { | ||
resolveTemplateString, | ||
TemplateStringMissingKeyException, | ||
TemplateStringPassthroughException, | ||
} from "../../template-string/template-string.js" | ||
import { ConfigurationError, NotFoundError } from "../../exceptions.js" | ||
import { resolveTemplateString } from "../../template-string/template-string.js" | ||
import type { CustomObjectSchema } from "../common.js" | ||
import { isPrimitive, joi, joiIdentifier } from "../common.js" | ||
import { KeyedSet } from "../../util/keyed-set.js" | ||
import { naturalList } from "../../util/string.js" | ||
import { styles } from "../../logger/styles.js" | ||
import type { TemplateValue } from "../../template-string/inputs.js" | ||
import { TemplateLeaf, isTemplateLeafValue, isTemplateLeaf } from "../../template-string/inputs.js" | ||
import type { CollectionOrValue } from "../../util/objects.js" | ||
import { deepMap } from "../../util/objects.js" | ||
import { LazyValue } from "../../template-string/lazy.js" | ||
|
||
export type ContextKeySegment = string | number | ||
export type ContextKey = ContextKeySegment[] | ||
|
||
export type ObjectPath = (string | number)[] | ||
|
||
export interface ContextResolveOpts { | ||
// Allow templates to be partially resolved (used to defer runtime template resolution, for example) | ||
// TODO: rename to optional | ||
allowPartial?: boolean | ||
// a list of previously resolved paths, used to detect circular references | ||
stack?: string[] | ||
|
@@ -41,7 +45,10 @@ export interface ContextResolveParams { | |
export interface ContextResolveOutput { | ||
message?: string | ||
partial?: boolean | ||
resolved: any | ||
result: CollectionOrValue<TemplateValue> | ||
cached: boolean | ||
// for input tracking | ||
// ResolvedResult: ResolvedResult | ||
} | ||
|
||
export function schema(joiSchema: Joi.Schema) { | ||
|
@@ -56,9 +63,10 @@ export interface ConfigContextType { | |
} | ||
|
||
// Note: we're using classes here to be able to use decorators to describe each context node and key | ||
// TODO-steffen&thor: Make all instance variables of all config context classes read-only. | ||
export abstract class ConfigContext { | ||
private readonly _rootContext: ConfigContext | ||
private readonly _resolvedValues: { [path: string]: any } | ||
private readonly _resolvedValues: { [path: string]: CollectionOrValue<TemplateValue> } | ||
|
||
// This is used for special-casing e.g. runtime.* resolution | ||
protected _alwaysAllowPartial: boolean | ||
|
@@ -87,10 +95,10 @@ export abstract class ConfigContext { | |
const fullPath = renderKeyPath(nodePath.concat(key)) | ||
|
||
// if the key has previously been resolved, return it directly | ||
const resolved = this._resolvedValues[path] | ||
const cachedResult = this._resolvedValues[path] | ||
|
||
if (resolved) { | ||
return { resolved } | ||
if (cachedResult) { | ||
return { cached: true, result: cachedResult } | ||
} | ||
|
||
opts.stack = [...(opts.stack || [])] | ||
|
@@ -150,9 +158,14 @@ export abstract class ConfigContext { | |
if (remainder.length > 0) { | ||
opts.stack.push(stackEntry) | ||
const res = value.resolve({ key: remainder, nodePath: nestedNodePath, opts }) | ||
value = res.resolved | ||
value = res.result | ||
message = res.message | ||
partial = !!res.partial | ||
} else { | ||
// TODO: improve error message | ||
throw new ConfigurationError({ | ||
message: `Resolving to a context is not allowed.`, | ||
}) | ||
} | ||
break | ||
} | ||
|
@@ -163,6 +176,10 @@ export abstract class ConfigContext { | |
value = resolveTemplateString({ string: value, context: this._rootContext, contextOpts: opts }) | ||
} | ||
|
||
if (isTemplateLeaf(value) || value instanceof LazyValue) { | ||
break | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Break is not the right thing to do here for LazyValue; We need to unwrap the lazy value and continue diving into it, actually. |
||
} | ||
|
||
if (value === undefined) { | ||
break | ||
} | ||
|
@@ -186,31 +203,55 @@ export abstract class ConfigContext { | |
} | ||
} | ||
|
||
// If we're allowing partial strings, we throw the error immediately to end the resolution flow. The error | ||
// is caught in the surrounding template resolution code. | ||
if (this._alwaysAllowPartial) { | ||
// We use a separate exception type when contexts are specifically indicating that unresolvable keys should | ||
// be passed through. This is caught in the template parser code. | ||
throw new TemplateStringPassthroughException({ | ||
message, | ||
}) | ||
} else if (opts.allowPartial) { | ||
throw new TemplateStringMissingKeyException({ | ||
message, | ||
}) | ||
if (!opts.allowPartial && !this._alwaysAllowPartial) { | ||
throw new NotFoundError({ message }) | ||
} else { | ||
// Otherwise we return the undefined value, so that any logical expressions can be evaluated appropriately. | ||
// The template resolver will throw the error later if appropriate. | ||
return { resolved: undefined, message } | ||
return { | ||
message, | ||
cached: false, | ||
result: new TemplateLeaf({ | ||
expr: undefined, | ||
value: undefined, | ||
inputs: {}, | ||
}), | ||
} | ||
} | ||
} | ||
|
||
let result: CollectionOrValue<TemplateValue> | ||
|
||
if (value instanceof LazyValue) { | ||
result = value | ||
} else if (isTemplateLeaf(value)) { | ||
result = value | ||
} | ||
// Wrap normal data using deepMap | ||
else if (isTemplateLeafValue(value)) { | ||
result = new TemplateLeaf({ | ||
expr: undefined, | ||
value, | ||
inputs: {}, | ||
}) | ||
} else { | ||
// value is a collection | ||
result = deepMap(value, (v) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be |
||
if (isTemplateLeaf(v) || v instanceof LazyValue) { | ||
return v | ||
} | ||
return new TemplateLeaf({ | ||
expr: undefined, | ||
value: v, | ||
inputs: {}, | ||
}) | ||
}) | ||
} | ||
|
||
// Cache result, unless it is a partial resolution | ||
if (!partial) { | ||
this._resolvedValues[path] = value | ||
this._resolvedValues[path] = result | ||
} | ||
|
||
return { resolved: value } | ||
return { cached: false, result } | ||
} | ||
} | ||
|
||
|
@@ -242,7 +283,11 @@ export class ScanContext extends ConfigContext { | |
override resolve({ key, nodePath }: ContextResolveParams) { | ||
const fullKey = nodePath.concat(key) | ||
this.foundKeys.add(fullKey) | ||
return { resolved: renderTemplateString(fullKey), partial: true } | ||
return { | ||
partial: true, | ||
cached: false, | ||
result: new TemplateLeaf({ value: renderTemplateString(fullKey), expr: undefined, inputs: {} }), | ||
} | ||
} | ||
} | ||
|
||
|
@@ -319,7 +364,7 @@ function renderTemplateString(key: ContextKeySegment[]) { | |
/** | ||
* Given all the segments of a template string, return a string path for the key. | ||
*/ | ||
function renderKeyPath(key: ContextKeySegment[]): string { | ||
export function renderKeyPath(key: ContextKeySegment[]): string { | ||
// Note: We don't support bracket notation for the first part in a template string | ||
if (key.length === 0) { | ||
return "" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of error, it should just result in a plain object with all the values the context contains.