Skip to content
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

Closed
wants to merge 46 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
93d2931
Save
thsig Nov 21, 2023
595e652
test: add test and first take at implementation for simple, 1 ref
stefreak Nov 21, 2023
9d9ecf0
test: add simple test for ensuring we get leafs
stefreak Nov 21, 2023
e2ebf08
fix: resolve references only once. Uncomment the throw.
stefreak Nov 21, 2023
3c06ecd
fix: build
stefreak Nov 21, 2023
73dcad9
test: records template references (array, 1 reference)
stefreak Nov 22, 2023
c125de4
fix: path accounting
stefreak Nov 22, 2023
00d8a82
fix: add reference target logic
thsig Nov 22, 2023
4bff56a
Save
thsig Nov 22, 2023
24ea2e1
test: more test cases
stefreak Nov 22, 2023
fcc37d4
wip
stefreak Nov 24, 2023
d47d5e1
wip (does not compile)
stefreak Nov 24, 2023
c80a2cf
wip (compiles now)
stefreak Nov 24, 2023
3f9fa88
wip: rename the types in inputs.ts
stefreak Nov 24, 2023
49ca406
wip: temporary hack to avoid changing the parser right now
stefreak Nov 27, 2023
1f7ee2a
wip: remove recorder. compiles, but all tests fail.
stefreak Nov 27, 2023
1484e9d
wip: fix some tests
stefreak Nov 27, 2023
5504c56
wip: start implementing template string AST
stefreak Nov 29, 2023
3e72e3d
wip: all template tests pass, except for blocks and partial
stefreak Nov 30, 2023
e99008c
ast: all template string tests pass
stefreak Dec 1, 2023
cac9d94
undo accidental change
stefreak Dec 4, 2023
a9ef07e
wip
stefreak Dec 4, 2023
a1b5405
progress: lazy evaluation
stefreak Dec 5, 2023
81f5dea
wip: lazy evaluation
stefreak Dec 5, 2023
f507d35
wip: fix tests
stefreak Dec 6, 2023
6db6747
lazy: all resolveTemplateString and input tracking tests pass with full
stefreak Dec 6, 2023
6a5504b
proxy: wip
stefreak Dec 7, 2023
88e67b2
wip: working proxy and more tests
stefreak Dec 7, 2023
5320506
add test for partial proxy
stefreak Dec 7, 2023
70670d9
partial proxy & logical operators
stefreak Dec 7, 2023
30eece3
wip
stefreak Dec 8, 2023
dd634d4
wip: mutable proxy overlay
stefreak Dec 8, 2023
7a3f170
wip: make proxy read-only and record changes during validation / refi…
stefreak Dec 11, 2023
f342808
test(validation): add failing test for withContext and schema override
stefreak Dec 11, 2023
04adf6b
fix: ensure Object.keys works correctly on array
TimBeyer Dec 11, 2023
e944ea2
chore: remove confusing lazy class
stefreak Dec 11, 2023
dfbee86
chore: add some docs for diffing logic
TimBeyer Dec 11, 2023
f8fc3f4
refactor: do overlays on the config object
TimBeyer Dec 11, 2023
3960f9a
wip
stefreak Dec 11, 2023
58e4297
allow refining multiple times
stefreak Dec 11, 2023
1ad69f9
wip: add joi refine method
stefreak Dec 12, 2023
edab666
wip: integrate 1. next: pickEnvironment
stefreak Dec 12, 2023
7fb1f04
wip: provider initialization
stefreak Dec 13, 2023
da81199
wip: scanAndAddConfigs
stefreak Dec 13, 2023
44a0a5e
wip. next: renderConfigTemplate
stefreak Dec 14, 2023
9038449
wip: renderModules and renderConfigTemplate
stefreak Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip: add joi refine method
Co-authored-by: Tim Beyer <[email protected]>
Co-authored-by: Thorarinn Sigurdsson <[email protected]>
3 people committed Dec 12, 2023
commit 1ad69f9014ebb2c563a0dde54921f31017c88ba8
91 changes: 62 additions & 29 deletions core/src/template-string/validation.ts
Original file line number Diff line number Diff line change
@@ -6,13 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { z, infer as inferZodType, ZodIntersection, ZodObject } from "zod"
import { z, infer as inferZodType } from "zod"
import { ConfigContext, ContextResolveOpts } from "../config/template-contexts/base.js"
import { Collection, CollectionOrValue, isArray, isPlainObject } from "../util/objects.js"
import { TemplateLeaf, TemplatePrimitive, TemplateValue, templatePrimitiveDeepMap } from "./inputs.js"
import { TemplatePrimitive, TemplateValue } from "./inputs.js"
import { getLazyConfigProxy } from "./proxy.js"
import { PartialDeep } from "type-fest"
import { OverrideKeyPathLazily } from "./lazy.js"
import Joi from "@hapi/joi"

type Change = { path: (string | number)[]; value: CollectionOrValue<TemplatePrimitive> }

@@ -23,9 +22,9 @@ type Change = { path: (string | number)[]; value: CollectionOrValue<TemplatePrim
// We also know that the object now has been validated so we know that the object will
// afterwards be conforming to the type given during validation, deriving from the base object.
// Thus we only need to track additions or changes, never deletions.
function getChangeset<T extends CollectionOrValue<TemplatePrimitive>>(
base: PartialDeep<T>,
compare: T,
function getChangeset(
base: CollectionOrValue<TemplatePrimitive>,
compare: CollectionOrValue<TemplatePrimitive>,
path: (string | number)[] = [],
changeset: Change[] = []
): Change[] {
@@ -88,67 +87,101 @@ function getOverlayProxy(targetObject: Collection<TemplatePrimitive>, changes: C
return proxy
}

export type GardenConfigParams<ZodShape extends z.ZodRawShape> = {
export type GardenConfigParams = {
parsedConfig: CollectionOrValue<TemplateValue>
context: ConfigContext
opts: ContextResolveOpts
overlays?: Change[]
validator?: z.ZodObject<ZodShape>
}

export class GardenConfig<ZodShape extends z.ZodRawShape = {}> {
type TypeAssertion<T> = (object: any) => object is T
export class GardenConfig<ConfigType extends Collection<TemplatePrimitive> = Collection<TemplatePrimitive>> {
private parsedConfig: CollectionOrValue<TemplateValue>
private context: ConfigContext
private opts: ContextResolveOpts
private validator: z.ZodObject<ZodShape>
private overlays: Change[]

constructor({ parsedConfig, context, opts, validator = z.object({}) as z.ZodObject<ZodShape>, overlays = [] }: GardenConfigParams<ZodShape>) {
constructor({ parsedConfig, context, opts, overlays = [] }: GardenConfigParams) {
this.parsedConfig = parsedConfig
this.context = context
this.opts = opts
this.validator = validator
this.overlays = overlays
}

public withContext(context: ConfigContext): GardenConfig<{}> {
public withContext(context: ConfigContext): GardenConfig {
// we wipe the types, because a new context can result in different results when evaluating template strings
return new GardenConfig({
parsedConfig: this.parsedConfig,
context,
opts: this.opts,
overlays: [],
validator: z.object({}),
})
}

public refine<Augmentation extends z.ZodRawShape>(incoming: Augmentation): GardenConfig<z.objectUtil.extendShape<ZodShape, Augmentation>> {
public assertType<Type extends CollectionOrValue<TemplatePrimitive>>(assertion: TypeAssertion<Type>): GardenConfig<ConfigType & Type> {
const rawConfig = this.getConfig()
const configIsOfType = assertion(rawConfig)

if (configIsOfType) {
return new GardenConfig<ConfigType & Type>({
parsedConfig: this.parsedConfig,
context: this.context,
opts: this.opts,
overlays: this.overlays,
})
} else {
// TODO: Write a better error message
throw new Error("Config is not of the expected type")
}
}

public refineWithZod<Validator extends z.AnyZodObject>(validator: Validator): GardenConfig<ConfigType & inferZodType<Validator>> {
// merge the schemas
const newValidator = this.validator.extend(incoming)

// instantiate proxy without overlays
const rawConfig = this.getProxy([])
const rawConfig = this.getConfig([])

// validate config and extract changes
const validated = validator.parse(rawConfig)
const changes = getChangeset(rawConfig, validated)

return new GardenConfig({
parsedConfig: this.parsedConfig,
context: this.context,
opts: this.opts,
overlays: [...changes],
})
}

// With joi we can't infer the type from the schema
public refineWithJoi<JoiType extends Collection<TemplatePrimitive>>(validator: Joi.SchemaLike): GardenConfig<ConfigType & JoiType> {
// instantiate proxy without overlays
const rawConfig = this.getConfig([])

// validate config and extract changes
const validated = newValidator.parse(rawConfig)
const validated = Joi.attempt(rawConfig, validator)
const changes = getChangeset(rawConfig as any, validated)

return new GardenConfig({
parsedConfig: this.parsedConfig,
context: this.context,
opts: this.opts,
overlays: [...changes],
validator: newValidator,
})
}

public getProxy(overlays?: Change[]): inferZodType<z.ZodObject<ZodShape>> {
return getOverlayProxy(
getLazyConfigProxy({
parsedConfig: this.parsedConfig,
context: this.context,
opts: this.opts,
}) as inferZodType<z.ZodObject<ZodShape>>,
overlays || this.overlays
) as inferZodType<z.ZodObject<ZodShape>>
public getConfig(overlays?: Change[]): ConfigType {
const configProxy = getLazyConfigProxy({
parsedConfig: this.parsedConfig,
context: this.context,
opts: this.opts,
}) as ConfigType

const changes = overlays || this.overlays
if (changes.length > 0) {
return getOverlayProxy(configProxy, changes) as ConfigType
}

return configProxy
}
}
142 changes: 110 additions & 32 deletions core/test/unit/src/template-string/validation.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import { parseTemplateCollection } from "../../../../src/template-string/templat
import { expect } from "chai"
import { GenericContext } from "../../../../src/config/template-contexts/base.js"
import { GardenConfig } from "../../../../src/template-string/validation.js"
import Joi from "@hapi/joi"

// In the future we might
// const varsFromFirstConfig = firstConfig.atPath("var") // can contain lazy values
@@ -35,15 +36,17 @@ describe("GardenConfig", () => {
opts: {},
})

const config = unrefinedConfig.refine({
kind: z.literal("Deployment"),
type: z.literal("kubernetes"),
spec: z.object({
files: z.array(z.string()),
}),
})
const config = unrefinedConfig.refineWithZod(
z.object({
kind: z.literal("Deployment"),
type: z.literal("kubernetes"),
spec: z.object({
files: z.array(z.string()),
}),
})
)

const proxy = config.getProxy()
const proxy = config.getConfig()

// proxy has type hints, no need to use bracket notation
expect(proxy.spec.files[0]).to.equal("manifests/deployment.yaml")
@@ -68,17 +71,19 @@ describe("GardenConfig", () => {
opts: {},
})

const config = unrefinedConfig.refine({
kind: z.literal("Deployment"),
type: z.literal("kubernetes"),
spec: z.object({
// replicas defaults to 1
replicas: z.number().default(1),
files: z.array(z.string()),
}),
})
const config = unrefinedConfig.refineWithZod(
z.object({
kind: z.literal("Deployment"),
type: z.literal("kubernetes"),
spec: z.object({
// replicas defaults to 1
replicas: z.number().default(1),
files: z.array(z.string()),
}),
})
)

const proxy = config.getProxy()
const proxy = config.getConfig()

// const spec = proxy.spec

@@ -98,7 +103,7 @@ describe("GardenConfig", () => {
},
})

const unrefinedProxy = unrefinedConfig.getProxy()
const unrefinedProxy = unrefinedConfig.getConfig()

// the unrefined config has not been mutated
expect(unrefinedProxy).to.deep.equal({
@@ -124,31 +129,104 @@ describe("GardenConfig", () => {
opts: {
allowPartial: true,
},
}).refine({
// if replicas is not specified, it defaults to 1
replicas: z.number().default(1),
})
}).refineWithZod(
z.object({
// if replicas is not specified, it defaults to 1
replicas: z.number().default(1),
})
)

const proxy1 = config1.getProxy()
const proxy1 = config1.getConfig()

// replicas is specified, but it's using a variable that's not defined yet and the proxy is in `allowPartial` mode
expect(proxy1.replicas).to.equal(1)

// Now var.replicas is defined and the default from spec.replicas should not be used anymore.
const config2 = config1
.withContext(new GenericContext({ var: { replicas: 7 } }))
.refine({
// if replicas is not specified, it defaults to 1
replicas: z.number().default(1),
})
.refineWithZod(
z.object({
// if replicas is not specified, it defaults to 1
replicas: z.number().default(1),
})
)

// You can even refine multiple times, and the types will be merged together.
.refine({
foobar: z.string().default("foobar"),
})
.refineWithZod(
z.object({
foobar: z.string().default("foobar"),
})
)

const proxy2 = config2.getConfig()

proxy2 satisfies { replicas: number; foobar: string }

const proxy2 = config2.getProxy()
expect(proxy2.replicas).to.equal(7)
expect(proxy2.foobar).to.equal("foobar")
})

it("can be used with any type assertion", () => {
const context = new GenericContext({ var: { fruits: ["apple", "banana"] } })

const isFruits = (value: any): value is { fruits: string[] } => {
if (Array.isArray(value.fruits)) {
return value.fruits.every((item) => {
return typeof item === "string"
})
}
return false
}

const parsedConfig = parseTemplateCollection({
value: {
fruits: "${var.fruits}",
},
source: { source: undefined },
})

const config = new GardenConfig({
parsedConfig,
context,
opts: {
allowPartial: true,
},
}).assertType(isFruits)

const proxy = config.getConfig()

proxy satisfies { fruits: string[] }

expect(proxy.fruits).to.deep.equal(["apple", "banana"])
})

it("can be used with joi validators", () => {
const fruitsSchema = Joi.object({ fruits: Joi.array().items(Joi.string()) })
type Fruits = {
fruits: string[]
}

const context = new GenericContext({ var: { fruits: ["apple", "banana"] } })

const parsedConfig = parseTemplateCollection({
value: {
fruits: "${var.fruits}",
},
source: { source: undefined },
})

const config = new GardenConfig({
parsedConfig,
context,
opts: {
allowPartial: true,
},
}).refineWithJoi<Fruits>(fruitsSchema)

const proxy = config.getConfig()

proxy satisfies Fruits

expect(proxy.fruits).to.deep.equal(["apple", "banana"])
})
})