Feature/more dynamic unions (#2972)

* with validators

* more dynamic unions

* fixes from v31

* better constructor for dynamic unions

* version bump

* fix build
This commit is contained in:
Aiden McClelland
2025-07-01 17:40:39 -06:00
committed by GitHub
parent 35d2ec8a44
commit 340775a593
21 changed files with 863 additions and 611 deletions

View File

@@ -46,7 +46,7 @@ export const runAction = async <
}
}
type GetActionInputType<A extends ActionInfo<T.ActionId, any>> =
A extends Action<T.ActionId, infer I> ? ExtractInputSpecType<I> : never
A extends Action<T.ActionId, infer I> ? I : never
type TaskBase = {
reason?: string

View File

@@ -13,13 +13,17 @@ export type LazyBuild<ExpectedOut> = (
) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore
export type ExtractInputSpecType<A extends Record<string, any> | InputSpec<Record<string, any>>> =
A extends InputSpec<infer B> ? B :
A
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
A extends InputSpec<infer B, any> ? B :
never
export type ExtractPartialInputSpecType<
A extends Record<string, any> | InputSpec<Record<string, any>>,
> = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
export type ExtractInputSpecStaticValidatedAs<
A extends InputSpec<any, Record<string, any>>,
> = A extends InputSpec<any, infer B> ? B : never
// export type ExtractPartialInputSpecType<
// A extends Record<string, any> | InputSpec<Record<string, any>>,
// > = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
export type InputSpecOf<A extends Record<string, any>> = {
[K in keyof A]: Value<A[K]>
@@ -82,35 +86,54 @@ export const addNodesSpec = InputSpec.of({ hostname: hostname, port: port });
```
*/
export class InputSpec<Type extends Record<string, any>> {
export class InputSpec<
Type extends StaticValidatedAs,
StaticValidatedAs extends Record<string, any> = Type,
> {
private constructor(
private readonly spec: {
[K in keyof Type]: Value<Type[K]>
},
public validator: Parser<unknown, Type>,
public readonly validator: Parser<unknown, StaticValidatedAs>,
) {}
public _TYPE: Type = null as any as Type
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
async build(options: LazyBuildOptions) {
async build(options: LazyBuildOptions): Promise<{
spec: {
[K in keyof Type]: ValueSpec
}
validator: Parser<unknown, Type>
}> {
const answer = {} as {
[K in keyof Type]: ValueSpec
}
for (const k in this.spec) {
answer[k] = await this.spec[k].build(options as any)
const validator = {} as {
[K in keyof Type]: Parser<unknown, any>
}
for (const k in this.spec) {
const built = await this.spec[k].build(options as any)
answer[k] = built.spec
validator[k] = built.validator
}
return {
spec: answer,
validator: object(validator) as any,
}
return answer
}
static of<Spec extends Record<string, Value<any>>>(spec: Spec) {
const validatorObj = {} as {
[K in keyof Spec]: Parser<unknown, any>
}
for (const key in spec) {
validatorObj[key] = spec[key].validator
}
const validator = object(validatorObj)
return new InputSpec<{
[K in keyof Spec]: Spec[K] extends Value<infer T> ? T : never
}>(spec, validator as any)
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
const validator = object(
Object.fromEntries(
Object.entries(spec).map(([k, v]) => [k, v.validator]),
),
)
return new InputSpec<
{
[K in keyof Spec]: Spec[K] extends Value<infer T, any> ? T : never
},
{
[K in keyof Spec]: Spec[K] extends Value<any, infer T> ? T : never
}
>(spec, validator as any)
}
}

View File

@@ -9,11 +9,15 @@ import {
} from "../inputSpecTypes"
import { Parser, arrayOf, string } from "ts-matches"
export class List<Type> {
export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
private constructor(
public build: LazyBuild<ValueSpecList>,
public validator: Parser<unknown, Type>,
public build: LazyBuild<{
spec: ValueSpecList
validator: Parser<unknown, Type>
}>,
public readonly validator: Parser<unknown, StaticValidatedAs>,
) {}
readonly _TYPE: Type = null as any
static text(
a: {
@@ -58,6 +62,7 @@ export class List<Type> {
generate?: null | RandomString
},
) {
const validator = arrayOf(string)
return new List<string[]>(() => {
const spec = {
type: "text" as const,
@@ -81,8 +86,8 @@ export class List<Type> {
...a,
spec,
}
return built
}, arrayOf(string))
return { spec: built, validator }
}, validator)
}
static dynamicText(
@@ -105,6 +110,7 @@ export class List<Type> {
}
}>,
) {
const validator = arrayOf(string)
return new List<string[]>(async (options) => {
const { spec: aSpec, ...a } = await getA(options)
const spec = {
@@ -129,11 +135,15 @@ export class List<Type> {
...a,
spec,
}
return built
}, arrayOf(string))
return { spec: built, validator }
}, validator)
}
static obj<Type extends Record<string, any>>(
static obj<
Type extends StaticValidatedAs,
StaticValidatedAs extends Record<string, any>,
>(
a: {
name: string
description?: string | null
@@ -143,20 +153,20 @@ export class List<Type> {
maxLength?: number | null
},
aSpec: {
spec: InputSpec<Type>
spec: InputSpec<Type, StaticValidatedAs>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
) {
return new List<Type[]>(async (options) => {
return new List<Type[], StaticValidatedAs[]>(async (options) => {
const { spec: previousSpecSpec, ...restSpec } = aSpec
const specSpec = await previousSpecSpec.build(options)
const built = await previousSpecSpec.build(options)
const spec = {
type: "object" as const,
displayAs: null,
uniqueBy: null,
...restSpec,
spec: specSpec,
spec: built.spec,
}
const value = {
spec,
@@ -164,13 +174,16 @@ export class List<Type> {
...a,
}
return {
description: null,
warning: null,
minLength: null,
maxLength: null,
type: "list" as const,
disabled: false,
...value,
spec: {
description: null,
warning: null,
minLength: null,
maxLength: null,
type: "list" as const,
disabled: false,
...value,
},
validator: arrayOf(built.validator),
}
}, arrayOf(aSpec.spec.validator))
}

View File

@@ -1,6 +1,6 @@
import { InputSpec, LazyBuild } from "./inputSpec"
import { ExtractInputSpecType, InputSpec, LazyBuild } from "./inputSpec"
import { List } from "./list"
import { Variants } from "./variants"
import { UnionRes, UnionResStaticValidatedAs, Variants } from "./variants"
import {
FilePath,
Pattern,
@@ -34,19 +34,21 @@ type AsRequired<T, Required extends boolean> = Required extends true
const testForAsRequiredParser = once(
() => object({ required: literal(true) }).test,
)
function asRequiredParser<
Type,
Input,
Return extends Parser<unknown, Type> | Parser<unknown, Type | null>,
>(parser: Parser<unknown, Type>, input: Input): Return {
function asRequiredParser<Type, Input extends { required: boolean }>(
parser: Parser<unknown, Type>,
input: Input,
): Parser<unknown, AsRequired<Type, Input["required"]>> {
if (testForAsRequiredParser()(input)) return parser as any
return parser.nullable() as any
}
export class Value<Type> {
export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
protected constructor(
public build: LazyBuild<ValueSpec>,
public validator: Parser<unknown, Type>,
public build: LazyBuild<{
spec: ValueSpec
validator: Parser<unknown, Type>
}>,
public readonly validator: Parser<unknown, StaticValidatedAs>,
) {}
public _TYPE: Type = null as any as Type
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
@@ -79,16 +81,20 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
const validator = boolean
return new Value<boolean>(
async () => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
boolean,
validator,
)
}
static dynamicToggle(
@@ -100,16 +106,20 @@ export class Value<Type> {
disabled?: false | string
}>,
) {
const validator = boolean
return new Value<boolean>(
async (options) => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: false,
...(await a(options)),
spec: {
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: false,
...(await a(options)),
},
validator,
}),
boolean,
validator,
)
}
/**
@@ -187,32 +197,36 @@ export class Value<Type> {
*/
generate?: RandomString | null
}) {
const validator = asRequiredParser(string, a)
return new Value<AsRequired<string, Required>>(
async () => ({
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: a.immutable ?? false,
generate: a.generate ?? null,
...a,
spec: {
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: a.immutable ?? false,
generate: a.generate ?? null,
...a,
},
validator,
}),
asRequiredParser(string, a),
validator,
)
}
static dynamicText(
static dynamicText<Required extends boolean>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: DefaultString | null
required: boolean
required: Required
masked?: boolean
placeholder?: string | null
minLength?: number | null
@@ -223,24 +237,30 @@ export class Value<Type> {
generate?: null | RandomString
}>,
) {
return new Value<string | null>(async (options) => {
const a = await getA(options)
return {
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: false,
generate: a.generate ?? null,
...a,
}
}, string.nullable())
return new Value<AsRequired<string, Required>, string | null>(
async (options) => {
const a = await getA(options)
return {
spec: {
type: "text" as const,
description: null,
warning: null,
masked: false,
placeholder: null,
minLength: null,
maxLength: null,
patterns: [],
inputmode: "text",
disabled: false,
immutable: false,
generate: a.generate ?? null,
...a,
},
validator: asRequiredParser(string, a),
}
},
string.nullable(),
)
}
/**
* @description Displays a large textarea field for long form entry.
@@ -278,40 +298,9 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
return new Value<AsRequired<string, Required>>(
async () => {
const built: ValueSpecTextarea = {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}
return built
},
asRequiredParser(string, a),
)
}
static dynamicTextarea(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string | null
required: boolean
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
disabled?: false | string
}>,
) {
return new Value<string | null>(async (options) => {
const a = await getA(options)
return {
const validator = asRequiredParser(string, a)
return new Value<AsRequired<string, Required>>(async () => {
const built: ValueSpecTextarea = {
description: null,
warning: null,
minLength: null,
@@ -319,10 +308,45 @@ export class Value<Type> {
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: false,
immutable: a.immutable ?? false,
...a,
}
}, string.nullable())
return { spec: built, validator }
}, validator)
}
static dynamicTextarea<Required extends boolean>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string | null
required: Required
minLength?: number | null
maxLength?: number | null
placeholder?: string | null
disabled?: false | string
}>,
) {
return new Value<AsRequired<string, Required>, string | null>(
async (options) => {
const a = await getA(options)
return {
spec: {
description: null,
warning: null,
minLength: null,
maxLength: null,
placeholder: null,
type: "textarea" as const,
disabled: false,
immutable: false,
...a,
},
validator: asRequiredParser(string, a),
}
},
string.nullable(),
)
}
/**
* @description Displays a number input field
@@ -382,30 +406,34 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
const validator = asRequiredParser(number, a)
return new Value<AsRequired<number, Required>>(
() => ({
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
asRequiredParser(number, a),
validator,
)
}
static dynamicNumber(
static dynamicNumber<Required extends boolean>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: number | null
required: boolean
required: Required
min?: number | null
max?: number | null
step?: number | null
@@ -415,22 +443,28 @@ export class Value<Type> {
disabled?: false | string
}>,
) {
return new Value<number | null>(async (options) => {
const a = await getA(options)
return {
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false,
immutable: false,
...a,
}
}, number.nullable())
return new Value<AsRequired<number, Required>, number | null>(
async (options) => {
const a = await getA(options)
return {
spec: {
type: "number" as const,
description: null,
warning: null,
min: null,
max: null,
step: null,
units: null,
placeholder: null,
disabled: false as const,
immutable: false,
...a,
},
validator: asRequiredParser(number, a),
}
},
number.nullable(),
)
}
/**
* @description Displays a browser-native color selector.
@@ -468,40 +502,50 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
const validator = asRequiredParser(string, a)
return new Value<AsRequired<string, Required>>(
() => ({
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
asRequiredParser(string, a),
validator,
)
}
static dynamicColor(
static dynamicColor<Required extends boolean>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string | null
required: boolean
required: Required
disabled?: false | string
}>,
) {
return new Value<string | null>(async (options) => {
const a = await getA(options)
return {
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: false,
...a,
}
}, string.nullable())
return new Value<AsRequired<string, Required>, string | null>(
async (options) => {
const a = await getA(options)
return {
spec: {
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: false,
...a,
},
validator: asRequiredParser(string, a),
}
},
string.nullable(),
)
}
/**
* @description Displays a browser-native date/time selector.
@@ -549,49 +593,59 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
const validator = asRequiredParser(string, a)
return new Value<AsRequired<string, Required>>(
() => ({
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
step: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
step: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
asRequiredParser(string, a),
validator,
)
}
static dynamicDatetime(
static dynamicDatetime<Required extends boolean>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string | null
required: boolean
required: Required
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
disabled?: false | string
}>,
) {
return new Value<string | null>(async (options) => {
const a = await getA(options)
return {
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
disabled: false,
immutable: false,
...a,
}
}, string.nullable())
return new Value<AsRequired<string, Required>, string | null>(
async (options) => {
const a = await getA(options)
return {
spec: {
type: "datetime" as const,
description: null,
warning: null,
inputmode: "datetime-local",
min: null,
max: null,
disabled: false,
immutable: false,
...a,
},
validator: asRequiredParser(string, a),
}
},
string.nullable(),
)
}
/**
* @description Displays a select modal with radio buttons, allowing for a single selection.
@@ -644,39 +698,50 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
const validator = anyOf(
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
)
return new Value<keyof Values & string>(
() => ({
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
anyOf(
...Object.keys(a.values).map((x: keyof Values & string) => literal(x)),
),
validator,
)
}
static dynamicSelect(
static dynamicSelect<Values extends Record<string, string>>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string
values: Record<string, string>
values: Values
disabled?: false | string | string[]
}>,
) {
return new Value<string>(async (options) => {
return new Value<keyof Values & string, string>(async (options) => {
const a = await getA(options)
return {
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: false,
...a,
spec: {
description: null,
warning: null,
type: "select" as const,
disabled: false,
immutable: false,
...a,
},
validator: anyOf(
...Object.keys(a.values).map((x: keyof Values & string) =>
literal(x),
),
),
}
}, string)
}
@@ -732,45 +797,56 @@ export class Value<Type> {
*/
immutable?: boolean
}) {
return new Value<(keyof Values)[]>(
const validator = arrayOf(
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
)
return new Value<(keyof Values & string)[]>(
() => ({
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
spec: {
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
},
validator,
}),
arrayOf(
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
),
validator,
)
}
static dynamicMultiselect(
static dynamicMultiselect<Values extends Record<string, string>>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Record<string, string>
values: Values
minLength?: number | null
maxLength?: number | null
disabled?: false | string | string[]
}>,
) {
return new Value<string[]>(async (options) => {
return new Value<(keyof Values & string)[], string[]>(async (options) => {
const a = await getA(options)
return {
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: false,
...a,
spec: {
type: "multiselect" as const,
minLength: null,
maxLength: null,
warning: null,
description: null,
disabled: false,
immutable: false,
...a,
},
validator: arrayOf(
literals(
...(Object.keys(a.values) as any as [keyof Values & string]),
),
),
}
}, arrayOf(string))
}
@@ -791,21 +867,27 @@ export class Value<Type> {
),
* ```
*/
static object<Type extends Record<string, any>>(
static object<
Type extends StaticValidatedAs,
StaticValidatedAs extends Record<string, any>,
>(
a: {
name: string
description?: string | null
},
spec: InputSpec<Type>,
spec: InputSpec<Type, StaticValidatedAs>,
) {
return new Value<Type>(async (options) => {
return new Value<Type, StaticValidatedAs>(async (options) => {
const built = await spec.build(options as any)
return {
type: "object" as const,
description: null,
warning: null,
...a,
spec: built,
spec: {
type: "object" as const,
description: null,
warning: null,
...a,
spec: built.spec,
},
validator: built.validator,
}
}, spec.validator)
}
@@ -886,38 +968,42 @@ export class Value<Type> {
spec: InputSpec<any>
}
},
>(
a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
/**
* @description Provide a default value from the list of variants.
* @type { string }
* @example default: 'variant1'
*/
default: keyof VariantValues & string
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
},
aVariants: Variants<VariantValues>,
) {
return new Value<typeof aVariants.validator._TYPE>(
async (options) => ({
type: "union" as const,
description: null,
warning: null,
disabled: false,
...a,
variants: await aVariants.build(options as any),
immutable: a.immutable ?? false,
}),
aVariants.validator,
)
>(a: {
name: string
description?: string | null
/** Presents a warning prompt before permitting the value to change. */
warning?: string | null
variants: Variants<VariantValues>
/**
* @description Provide a default value from the list of variants.
* @type { string }
* @example default: 'variant1'
*/
default: keyof VariantValues & string
/**
* @description Once set, the value can never be changed.
* @default false
*/
immutable?: boolean
}) {
return new Value<
typeof a.variants._TYPE,
typeof a.variants.validator._TYPE
>(async (options) => {
const built = await a.variants.build(options as any)
return {
spec: {
type: "union" as const,
description: null,
warning: null,
disabled: false,
...a,
variants: built.spec,
immutable: a.immutable ?? false,
},
validator: built.validator,
}
}, a.variants.validator)
}
static dynamicUnion<
VariantValues extends {
@@ -931,22 +1017,69 @@ export class Value<Type> {
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
aVariants: Variants<VariantValues>,
) {
return new Value<typeof aVariants.validator._TYPE>(async (options) => {
const newValues = await getA(options)
return {
type: "union" as const,
description: null,
warning: null,
...newValues,
variants: await aVariants.build(options as any),
immutable: false,
): Value<UnionRes<VariantValues>, unknown>
static dynamicUnion<
VariantValues extends StaticVariantValues,
StaticVariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any, any>
}
}, aVariants.validator)
},
>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
validator: Parser<unknown, UnionResStaticValidatedAs<StaticVariantValues>>,
): Value<
UnionRes<VariantValues>,
UnionResStaticValidatedAs<StaticVariantValues>
>
static dynamicUnion<
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any>
}
},
>(
getA: LazyBuild<{
name: string
description?: string | null
warning?: string | null
variants: Variants<VariantValues>
default: keyof VariantValues & string
disabled: string[] | false | string
}>,
validator: Parser<unknown, unknown> = any,
) {
return new Value<UnionRes<VariantValues>, typeof validator._TYPE>(
async (options) => {
const newValues = await getA(options)
const built = await newValues.variants.build(options as any)
return {
spec: {
type: "union" as const,
description: null,
warning: null,
...newValues,
variants: built.spec,
immutable: false,
},
validator: built.validator,
}
},
validator,
)
}
/**
* @description Presents an interface to add/remove/edit items in a list.
@@ -1022,16 +1155,45 @@ export class Value<Type> {
hiddenExample: Value.hidden(),
* ```
*/
static hidden<T>(): Value<T, unknown>
static hidden<T>(parser: Parser<unknown, T>): Value<T>
static hidden<T>(parser: Parser<unknown, T> = any) {
return new Value<T>(async () => {
const built: ValueSpecHidden = {
type: "hidden" as const,
return new Value<T, typeof parser._TYPE>(async () => {
return {
spec: {
type: "hidden" as const,
} as ValueSpecHidden,
validator: parser,
}
return built
}, parser)
}
map<U>(fn: (value: Type) => U): Value<U> {
return new Value(this.build, this.validator.map(fn))
/**
* @description Provides a way to define a hidden field with a static value. Useful for tracking
* @example
* ```
hiddenExample: Value.hidden(),
* ```
*/
static dynamicHidden<T>(getParser: LazyBuild<Parser<unknown, T>>) {
return new Value<T, unknown>(async (options) => {
const validator = await getParser(options)
return {
spec: {
type: "hidden" as const,
} as ValueSpecHidden,
validator,
}
}, any)
}
map<U>(fn: (value: StaticValidatedAs) => U): Value<U> {
return new Value(async (effects) => {
const built = await this.build(effects)
return {
spec: built.spec,
validator: built.validator.map(fn),
}
}, this.validator.map(fn))
}
}

View File

@@ -4,9 +4,9 @@ import {
LazyBuild,
InputSpec,
ExtractInputSpecType,
ExtractPartialInputSpecType,
ExtractInputSpecStaticValidatedAs,
} from "./inputSpec"
import { Parser, anyOf, literal, object } from "ts-matches"
import { Parser, any, anyOf, literal, object } from "ts-matches"
export type UnionRes<
VariantValues extends {
@@ -28,6 +28,26 @@ export type UnionRes<
}
}[K]
export type UnionResStaticValidatedAs<
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any>
}
},
K extends keyof VariantValues & string = keyof VariantValues & string,
> = {
[key in keyof VariantValues]: {
selection: key
value: ExtractInputSpecStaticValidatedAs<VariantValues[key]["spec"]>
other?: {
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
ExtractInputSpecStaticValidatedAs<VariantValues[key2]["spec"]>
>
}
}
}[K]
/**
* Used in the the Value.select { @link './value.ts' }
* to indicate the type of select variants that are available. The key for the record passed in will be the
@@ -80,14 +100,21 @@ export class Variants<
VariantValues extends {
[K in string]: {
name: string
spec: InputSpec<any>
spec: InputSpec<any, any>
}
},
> {
private constructor(
public build: LazyBuild<ValueSpecUnion["variants"]>,
public validator: Parser<unknown, UnionRes<VariantValues>>,
public build: LazyBuild<{
spec: ValueSpecUnion["variants"]
validator: Parser<unknown, UnionRes<VariantValues>>
}>,
public readonly validator: Parser<
unknown,
UnionResStaticValidatedAs<VariantValues>
>,
) {}
readonly _TYPE: UnionRes<VariantValues> = null as any
static of<
VariantValues extends {
[K in string]: {
@@ -96,30 +123,71 @@ export class Variants<
}
},
>(a: VariantValues) {
const validator = anyOf(
...Object.entries(a).map(([id, { spec }]) =>
object({
selection: literal(id),
value: spec.validator,
}),
const staticValidators = {} as {
[K in keyof VariantValues]: Parser<
unknown,
ExtractInputSpecStaticValidatedAs<VariantValues[K]["spec"]>
>
}
for (const key in a) {
const value = a[key]
staticValidators[key] = value.spec.validator
}
const other = object(
Object.fromEntries(
Object.entries(staticValidators).map(([k, v]) => [k, any.optional()]),
),
) as Parser<unknown, any>
return new Variants<VariantValues>(async (options) => {
const variants = {} as {
[K in keyof VariantValues]: {
name: string
spec: Record<string, ValueSpec>
).optional()
return new Variants<VariantValues>(
async (options) => {
const validators = {} as {
[K in keyof VariantValues]: Parser<
unknown,
ExtractInputSpecType<VariantValues[K]["spec"]>
>
}
}
for (const key in a) {
const value = a[key]
variants[key] = {
name: value.name,
spec: await value.spec.build(options as any),
const variants = {} as {
[K in keyof VariantValues]: {
name: string
spec: Record<string, ValueSpec>
}
}
}
return variants
}, validator)
for (const key in a) {
const value = a[key]
const built = await value.spec.build(options as any)
variants[key] = {
name: value.name,
spec: built.spec,
}
validators[key] = built.validator
}
const other = object(
Object.fromEntries(
Object.entries(validators).map(([k, v]) => [k, any.optional()]),
),
).optional()
return {
spec: variants,
validator: anyOf(
...Object.entries(validators).map(([k, v]) =>
object({
selection: literal(k),
value: v,
other,
}),
),
) as any,
}
},
anyOf(
...Object.entries(staticValidators).map(([k, v]) =>
object({
selection: literal(k),
value: v,
other,
}),
),
) as any,
)
}
}

View File

@@ -7,7 +7,9 @@ import { Variants } from "./builder/variants"
/**
* Base SMTP settings, to be used by StartOS for system wide SMTP
*/
export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>>({
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
InputSpecOf<SmtpValue>
>({
server: Value.text({
name: "SMTP Server",
required: true,
@@ -42,40 +44,39 @@ export const customSmtp = InputSpec.of<InputSpecOf<SmtpValue>>({
}),
})
const smtpVariants = Variants.of({
disabled: { name: "Disabled", spec: InputSpec.of({}) },
system: {
name: "System Credentials",
spec: InputSpec.of({
customFrom: Value.text({
name: "Custom From Address",
description:
"A custom from address for this service. If not provided, the system from address will be used.",
required: false,
default: null,
placeholder: "<name>test@example.com",
inputmode: "email",
patterns: [Patterns.email],
}),
}),
},
custom: {
name: "Custom Credentials",
spec: customSmtp,
},
})
/**
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
*/
export const smtpInputSpec = Value.dynamicUnion(
async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()
const disabled = smtp ? [] : ["system"]
return {
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
default: "disabled",
disabled,
}
},
Variants.of({
disabled: { name: "Disabled", spec: InputSpec.of({}) },
system: {
name: "System Credentials",
spec: InputSpec.of({
customFrom: Value.text({
name: "Custom From Address",
description:
"A custom from address for this service. If not provided, the system from address will be used.",
required: false,
default: null,
placeholder: "<name>test@example.com",
inputmode: "email",
patterns: [Patterns.email],
}),
}),
},
custom: {
name: "Custom Credentials",
spec: customSmtp,
},
}),
)
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
const smtp = await new GetSystemSmtp(effects).once()
const disabled = smtp ? [] : ["system"]
return {
name: "SMTP",
description: "Optionally provide an SMTP server for sending emails",
default: "disabled",
disabled,
variants: smtpVariants,
}
}, smtpVariants.validator)

View File

@@ -1,23 +1,17 @@
import { InputSpec } from "./input/builder"
import {
ExtractInputSpecType,
ExtractPartialInputSpecType,
} from "./input/builder/inputSpec"
import { ExtractInputSpecType } from "./input/builder/inputSpec"
import * as T from "../types"
import { once } from "../util"
import { InitScript } from "../inits"
import { Parser } from "ts-matches"
export type Run<
A extends Record<string, any> | InputSpec<Record<string, any>>,
> = (options: {
export type Run<A extends Record<string, any>> = (options: {
effects: T.Effects
input: ExtractInputSpecType<A>
input: A
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
export type GetInput<
A extends Record<string, any> | InputSpec<Record<string, any>>,
> = (options: {
export type GetInput<A extends Record<string, any>> = (options: {
effects: T.Effects
}) => Promise<null | void | undefined | ExtractPartialInputSpecType<A>>
}) => Promise<null | void | undefined | T.DeepPartial<A>>
export type MaybeFn<T> = T | ((options: { effects: T.Effects }) => Promise<T>)
function callMaybeFn<T>(
@@ -43,39 +37,38 @@ function mapMaybeFn<T, U>(
export interface ActionInfo<
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
Type extends Record<string, any>,
> {
readonly id: Id
readonly _INPUT: InputSpecType
readonly _INPUT: Type
}
export class Action<
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
> implements ActionInfo<Id, InputSpecType>
export class Action<Id extends T.ActionId, Type extends Record<string, any>>
implements ActionInfo<Id, Type>
{
readonly _INPUT: InputSpecType = null as any as InputSpecType
readonly _INPUT: Type = null as any as Type
private cachedParser?: Parser<unknown, Type>
private constructor(
readonly id: Id,
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
private readonly inputSpec: InputSpecType,
private readonly getInputFn: GetInput<InputSpecType>,
private readonly runFn: Run<InputSpecType>,
private readonly inputSpec: InputSpec<Type>,
private readonly getInputFn: GetInput<Type>,
private readonly runFn: Run<Type>,
) {}
static withInput<
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
InputSpecType extends InputSpec<Record<string, any>>,
>(
id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
inputSpec: InputSpecType,
getInput: GetInput<InputSpecType>,
run: Run<InputSpecType>,
): Action<Id, InputSpecType> {
return new Action(
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
run: Run<ExtractInputSpecType<InputSpecType>>,
): Action<Id, ExtractInputSpecType<InputSpecType>> {
return new Action<Id, ExtractInputSpecType<InputSpecType>>(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: true })),
inputSpec,
inputSpec as any,
getInput,
run,
)
@@ -88,7 +81,7 @@ export class Action<
return new Action(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
{},
InputSpec.of({}),
async () => null,
run,
)
@@ -107,16 +100,27 @@ export class Action<
return metadata
}
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
const built = await this.inputSpec.build(options)
this.cachedParser = built.validator
return {
spec: await this.inputSpec.build(options),
spec: built.spec,
value: (await this.getInputFn(options)) || null,
}
}
async run(options: {
effects: T.Effects
input: ExtractInputSpecType<InputSpecType>
input: Type
}): Promise<T.ActionResult | null> {
return (await this.runFn(options)) || null
const parser =
this.cachedParser ?? (await this.inputSpec.build(options)).validator
return (
(await this.runFn({
effects: options.effects,
input: this.cachedParser
? this.cachedParser.unsafeCast(options.input)
: options.input,
})) || null
)
}
}

View File

@@ -15,10 +15,10 @@ describe("InputSpec Types", () => {
{ spec: InputSpec.of({}) } as any,
) as any
const someList = await Value.list(test).build({} as any)
if (isValueSpecListOf(someList, "text")) {
someList.spec satisfies ListValueSpecOf<"text">
} else if (isValueSpecListOf(someList, "object")) {
someList.spec satisfies ListValueSpecOf<"object">
if (isValueSpecListOf(someList.spec, "text")) {
someList.spec.spec satisfies ListValueSpecOf<"text">
} else if (isValueSpecListOf(someList.spec, "object")) {
someList.spec.spec satisfies ListValueSpecOf<"object">
} else {
throw new Error(
"Failed to figure out the type: " + JSON.stringify(someList),

View File

@@ -333,16 +333,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
)
* ```
*/
withInput: <
Id extends T.ActionId,
InputSpecType extends Record<string, any> | InputSpec<any>,
>(
id: Id,
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
inputSpec: InputSpecType,
getInput: GetInput<InputSpecType>,
run: Run<InputSpecType>,
) => Action.withInput(id, metadata, inputSpec, getInput, run),
withInput: Action.withInput,
/**
* @description Use this function to create an action that does not accept form input
* @param id - a unique ID for this action

View File

@@ -18,7 +18,9 @@ describe("builder tests", () => {
required: true,
default: null,
}),
}).build({} as any)
})
.build({} as any)
.then((a) => a.spec)
expect(bitcoinPropertiesBuilt).toMatchObject({
"peer-tor-address": {
type: "text",
@@ -41,66 +43,66 @@ describe("builder tests", () => {
describe("values", () => {
test("toggle", async () => {
const value = Value.toggle({
const value = await Value.toggle({
name: "Testing",
description: null,
warning: null,
default: false,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast(false)
testOutput<typeof validator._TYPE, boolean>()(null)
})
test("text", async () => {
const value = Value.text({
const value = await Value.text({
name: "Testing",
required: true,
default: null,
})
}).build({} as any)
const validator = value.validator
const rawIs = await value.build({} as any)
const rawIs = value.spec
validator.unsafeCast("test text")
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null)
})
test("text with default", async () => {
const value = Value.text({
const value = await Value.text({
name: "Testing",
required: true,
default: "this is a default value",
})
}).build({} as any)
const validator = value.validator
const rawIs = await value.build({} as any)
const rawIs = value.spec
validator.unsafeCast("test text")
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null)
})
test("optional text", async () => {
const value = Value.text({
const value = await Value.text({
name: "Testing",
required: false,
default: null,
})
}).build({} as any)
const validator = value.validator
const rawIs = await value.build({} as any)
const rawIs = value.spec
validator.unsafeCast("test text")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
})
test("color", async () => {
const value = Value.color({
const value = await Value.color({
name: "Testing",
required: false,
default: null,
description: null,
warning: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("#000000")
testOutput<typeof validator._TYPE, string | null>()(null)
})
test("datetime", async () => {
const value = Value.datetime({
const value = await Value.datetime({
name: "Testing",
required: true,
default: null,
@@ -109,13 +111,13 @@ describe("values", () => {
inputmode: "date",
min: null,
max: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("2021-01-01")
testOutput<typeof validator._TYPE, string>()(null)
})
test("optional datetime", async () => {
const value = Value.datetime({
const value = await Value.datetime({
name: "Testing",
required: false,
default: null,
@@ -124,13 +126,13 @@ describe("values", () => {
inputmode: "date",
min: null,
max: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("2021-01-01")
testOutput<typeof validator._TYPE, string | null>()(null)
})
test("textarea", async () => {
const value = Value.textarea({
const value = await Value.textarea({
name: "Testing",
required: false,
default: null,
@@ -139,13 +141,13 @@ describe("values", () => {
minLength: null,
maxLength: null,
placeholder: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("test text")
testOutput<typeof validator._TYPE, string | null>()(null)
})
test("number", async () => {
const value = Value.number({
const value = await Value.number({
name: "Testing",
required: true,
default: null,
@@ -157,13 +159,13 @@ describe("values", () => {
step: null,
units: null,
placeholder: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number>()(null)
})
test("optional number", async () => {
const value = Value.number({
const value = await Value.number({
name: "Testing",
required: false,
default: null,
@@ -175,13 +177,13 @@ describe("values", () => {
step: null,
units: null,
placeholder: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number | null>()(null)
})
test("select", async () => {
const value = Value.select({
const value = await Value.select({
name: "Testing",
default: "a",
values: {
@@ -190,7 +192,7 @@ describe("values", () => {
},
description: null,
warning: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("a")
validator.unsafeCast("b")
@@ -198,7 +200,7 @@ describe("values", () => {
testOutput<typeof validator._TYPE, "a" | "b">()(null)
})
test("nullable select", async () => {
const value = Value.select({
const value = await Value.select({
name: "Testing",
default: "a",
values: {
@@ -207,14 +209,14 @@ describe("values", () => {
},
description: null,
warning: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("a")
validator.unsafeCast("b")
testOutput<typeof validator._TYPE, "a" | "b">()(null)
})
test("multiselect", async () => {
const value = Value.multiselect({
const value = await Value.multiselect({
name: "Testing",
values: {
a: "A",
@@ -225,7 +227,7 @@ describe("values", () => {
warning: null,
minLength: null,
maxLength: null,
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast([])
validator.unsafeCast(["a", "b"])
@@ -235,7 +237,7 @@ describe("values", () => {
testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null)
})
test("object", async () => {
const value = Value.object(
const value = await Value.object(
{
name: "Testing",
description: null,
@@ -248,20 +250,18 @@ describe("values", () => {
default: false,
}),
}),
)
).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: true })
testOutput<typeof validator._TYPE, { a: boolean }>()(null)
})
test("union", async () => {
const value = Value.union(
{
name: "Testing",
default: "a",
description: null,
warning: null,
},
Variants.of({
const value = await Value.union({
name: "Testing",
default: "a",
description: null,
warning: null,
variants: Variants.of({
a: {
name: "a",
spec: InputSpec.of({
@@ -274,7 +274,7 @@ describe("values", () => {
}),
},
}),
)
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: "a", value: { b: false } })
type Test = typeof validator._TYPE
@@ -297,17 +297,17 @@ describe("values", () => {
utils: "utils",
} as any
test("toggle", async () => {
const value = Value.dynamicToggle(async () => ({
const value = await Value.dynamicToggle(async () => ({
name: "Testing",
description: null,
warning: null,
default: false,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast(false)
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, boolean>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
description: null,
warning: null,
@@ -315,68 +315,68 @@ describe("values", () => {
})
})
test("text", async () => {
const value = Value.dynamicText(async () => ({
const value = await Value.dynamicText(async () => ({
name: "Testing",
required: false,
default: null,
}))
})).build({} as any)
const validator = value.validator
const rawIs = await value.build({} as any)
const rawIs = value.spec
validator.unsafeCast("test text")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: false,
default: null,
})
})
test("text with default", async () => {
const value = Value.dynamicText(async () => ({
const value = await Value.dynamicText(async () => ({
name: "Testing",
required: false,
default: "this is a default value",
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast("test text")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: false,
default: "this is a default value",
})
})
test("optional text", async () => {
const value = Value.dynamicText(async () => ({
const value = await Value.dynamicText(async () => ({
name: "Testing",
required: false,
default: null,
}))
})).build({} as any)
const validator = value.validator
const rawIs = await value.build({} as any)
const rawIs = value.spec
validator.unsafeCast("test text")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: false,
default: null,
})
})
test("color", async () => {
const value = Value.dynamicColor(async () => ({
const value = await Value.dynamicColor(async () => ({
name: "Testing",
required: false,
default: null,
description: null,
warning: null,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast("#000000")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: false,
default: null,
@@ -423,21 +423,21 @@ describe("values", () => {
)
.build(true)
const value = Value.dynamicDatetime(async ({ effects }) => {
const value = await Value.dynamicDatetime(async ({ effects }) => {
return {
name: "Testing",
required: true,
required: false,
default: null,
inputmode: "date",
}
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast("2021-01-01")
validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: true,
required: false,
default: null,
description: null,
warning: null,
@@ -445,7 +445,7 @@ describe("values", () => {
})
})
test("textarea", async () => {
const value = Value.dynamicTextarea(async () => ({
const value = await Value.dynamicTextarea(async () => ({
name: "Testing",
required: false,
default: null,
@@ -454,19 +454,19 @@ describe("values", () => {
minLength: null,
maxLength: null,
placeholder: null,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast("test text")
testOutput<typeof validator._TYPE, string | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: false,
})
})
test("number", async () => {
const value = Value.dynamicNumber(() => ({
const value = await Value.dynamicNumber(() => ({
name: "Testing",
required: true,
required: false,
default: null,
integer: false,
description: null,
@@ -476,19 +476,19 @@ describe("values", () => {
step: null,
units: null,
placeholder: null,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast(2)
validator.unsafeCast(null)
expect(() => validator.unsafeCast("null")).toThrowError()
testOutput<typeof validator._TYPE, number | null>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
expect(value.spec).toMatchObject({
name: "Testing",
required: true,
required: false,
})
})
test("select", async () => {
const value = Value.dynamicSelect(() => ({
const value = await Value.dynamicSelect(() => ({
name: "Testing",
default: "a",
values: {
@@ -497,18 +497,17 @@ describe("values", () => {
},
description: null,
warning: null,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast("a")
validator.unsafeCast("b")
validator.unsafeCast("c")
testOutput<typeof validator._TYPE, string>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
testOutput<typeof validator._TYPE, "a" | "b">()(null)
expect(value.spec).toMatchObject({
name: "Testing",
})
})
test("multiselect", async () => {
const value = Value.dynamicMultiselect(() => ({
const value = await Value.dynamicMultiselect(() => ({
name: "Testing",
values: {
a: "A",
@@ -519,16 +518,15 @@ describe("values", () => {
warning: null,
minLength: null,
maxLength: null,
}))
})).build({} as any)
const validator = value.validator
validator.unsafeCast([])
validator.unsafeCast(["a", "b"])
validator.unsafeCast(["c"])
expect(() => validator.unsafeCast([4])).toThrowError()
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, Array<string>>()(null)
expect(await value.build(fakeOptions)).toMatchObject({
testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null)
expect(value.spec).toMatchObject({
name: "Testing",
default: [],
})
@@ -536,15 +534,13 @@ describe("values", () => {
})
describe("filtering", () => {
test("union", async () => {
const value = Value.dynamicUnion(
() => ({
name: "Testing",
default: "a",
description: null,
warning: null,
disabled: ["a", "c"],
}),
Variants.of({
const value = await Value.dynamicUnion(() => ({
name: "Testing",
default: "a",
description: null,
warning: null,
disabled: ["a", "c"],
variants: Variants.of({
a: {
name: "a",
spec: InputSpec.of({
@@ -568,7 +564,7 @@ describe("values", () => {
}),
},
}),
)
})).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: "a", value: { b: false } })
type Test = typeof validator._TYPE
@@ -598,7 +594,7 @@ describe("values", () => {
}
>()(null)
const built = await value.build({} as any)
const built = value.spec
expect(built).toMatchObject({
name: "Testing",
variants: {
@@ -623,15 +619,13 @@ describe("values", () => {
})
})
test("dynamic union", async () => {
const value = Value.dynamicUnion(
() => ({
disabled: ["a", "c"],
name: "Testing",
default: "b",
description: null,
warning: null,
}),
Variants.of({
const value = await Value.dynamicUnion(() => ({
disabled: ["a", "c"],
name: "Testing",
default: "b",
description: null,
warning: null,
variants: Variants.of({
a: {
name: "a",
spec: InputSpec.of({
@@ -655,7 +649,7 @@ describe("values", () => {
}),
},
}),
)
})).build({} as any)
const validator = value.validator
validator.unsafeCast({ selection: "a", value: { b: false } })
type Test = typeof validator._TYPE
@@ -685,7 +679,7 @@ describe("values", () => {
}
>()(null)
const built = await value.build({} as any)
const built = value.spec
expect(built).toMatchObject({
name: "Testing",
variants: {
@@ -712,7 +706,7 @@ describe("values", () => {
describe("Builder List", () => {
test("obj", async () => {
const value = Value.list(
const value = await Value.list(
List.obj(
{
name: "test",
@@ -728,13 +722,13 @@ describe("Builder List", () => {
}),
},
),
)
).build({} as any)
const validator = value.validator
validator.unsafeCast([{ test: true }])
testOutput<typeof validator._TYPE, { test: boolean }[]>()(null)
})
test("text", async () => {
const value = Value.list(
const value = await Value.list(
List.text(
{
name: "test",
@@ -743,25 +737,25 @@ describe("Builder List", () => {
patterns: [],
},
),
)
).build({} as any)
const validator = value.validator
validator.unsafeCast(["test", "text"])
testOutput<typeof validator._TYPE, string[]>()(null)
})
describe("dynamic", () => {
test("text", async () => {
const value = Value.list(
const value = await Value.list(
List.dynamicText(() => ({
name: "test",
spec: { patterns: [] },
})),
)
).build({} as any)
const validator = value.validator
validator.unsafeCast(["test", "text"])
expect(() => validator.unsafeCast([3, 4])).toThrowError()
expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string[]>()(null)
expect(await value.build({} as any)).toMatchObject({
expect(value.spec).toMatchObject({
name: "test",
spec: { patterns: [] },
})
@@ -771,7 +765,7 @@ describe("Builder List", () => {
describe("Nested nullable values", () => {
test("Testing text", async () => {
const value = InputSpec.of({
const value = await InputSpec.of({
a: Value.text({
name: "Temp Name",
description:
@@ -779,7 +773,7 @@ describe("Nested nullable values", () => {
required: false,
default: null,
}),
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "test" })
@@ -787,7 +781,7 @@ describe("Nested nullable values", () => {
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
})
test("Testing number", async () => {
const value = InputSpec.of({
const value = await InputSpec.of({
a: Value.number({
name: "Temp Name",
description:
@@ -802,7 +796,7 @@ describe("Nested nullable values", () => {
step: null,
units: null,
}),
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: 5 })
@@ -810,7 +804,7 @@ describe("Nested nullable values", () => {
testOutput<typeof validator._TYPE, { a: number | null }>()(null)
})
test("Testing color", async () => {
const value = InputSpec.of({
const value = await InputSpec.of({
a: Value.color({
name: "Temp Name",
description:
@@ -819,7 +813,7 @@ describe("Nested nullable values", () => {
default: null,
warning: null,
}),
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "5" })
@@ -827,7 +821,7 @@ describe("Nested nullable values", () => {
testOutput<typeof validator._TYPE, { a: string | null }>()(null)
})
test("Testing select", async () => {
const value = InputSpec.of({
const value = await InputSpec.of({
a: Value.select({
name: "Temp Name",
description:
@@ -838,7 +832,7 @@ describe("Nested nullable values", () => {
a: "A",
},
}),
})
}).build({} as any)
const higher = await Value.select({
name: "Temp Name",
description:
@@ -856,7 +850,7 @@ describe("Nested nullable values", () => {
testOutput<typeof validator._TYPE, { a: "a" }>()(null)
})
test("Testing multiselect", async () => {
const value = InputSpec.of({
const value = await InputSpec.of({
a: Value.multiselect({
name: "Temp Name",
description:
@@ -870,7 +864,7 @@ describe("Nested nullable values", () => {
minLength: null,
maxLength: null,
}),
})
}).build({} as any)
const validator = value.validator
validator.unsafeCast({ a: [] })
validator.unsafeCast({ a: ["a"] })

View File

@@ -1,4 +1,4 @@
import { InputSpecSpec, matchInputSpecSpec } from "./output"
import { inputSpecSpec, InputSpecSpec } from "./output"
import * as _I from "../index"
import { camelCase } from "../../scripts/oldSpecToBuilder"
import { deepMerge } from "../../../base/lib/util"
@@ -97,25 +97,27 @@ describe("Inputs", () => {
},
}
test("test valid input", () => {
const output = matchInputSpecSpec.unsafeCast(validInput)
test("test valid input", async () => {
const { validator } = await inputSpecSpec.build({} as any)
const output = validator.unsafeCast(validInput)
expect(output).toEqual(validInput)
})
test("test no longer care about the conversion of min/max and validating", () => {
matchInputSpecSpec.unsafeCast(
test("test no longer care about the conversion of min/max and validating", async () => {
const { validator } = await inputSpecSpec.build({} as any)
validator.unsafeCast(
deepMerge({}, validInput, { rpc: { advanced: { threads: 0 } } }),
)
})
test("test errors should throw for number in string", () => {
test("test errors should throw for number in string", async () => {
const { validator } = await inputSpecSpec.build({} as any)
expect(() =>
matchInputSpecSpec.unsafeCast(
deepMerge({}, validInput, { rpc: { enable: 2 } }),
),
validator.unsafeCast(deepMerge({}, validInput, { rpc: { enable: 2 } })),
).toThrowError()
})
test("Test that we set serialversion to something not segwit or non-segwit", () => {
test("Test that we set serialversion to something not segwit or non-segwit", async () => {
const { validator } = await inputSpecSpec.build({} as any)
expect(() =>
matchInputSpecSpec.unsafeCast(
validator.unsafeCast(
deepMerge({}, validInput, {
rpc: { advanced: { serialversion: "testing" } },
}),

View File

@@ -134,19 +134,15 @@ export class VersionGraph<CurrentVersion extends string>
for (let rangeStr in version.options.migrations.other) {
const range = VersionRange.parse(rangeStr)
const vRange = graph.addVertex(range, [], [])
graph.addEdge(
version.options.migrations.other[rangeStr],
vRange,
vertex,
)
const migration = version.options.migrations.other[rangeStr]
if (migration.up) graph.addEdge(migration.up, vRange, vertex)
if (migration.down) graph.addEdge(migration.down, vertex, vRange)
for (let matching of graph.findVertex(
(v) => isExver(v.metadata) && v.metadata.satisfies(range),
)) {
graph.addEdge(
version.options.migrations.other[rangeStr],
matching,
vertex,
)
if (migration.up) graph.addEdge(migration.up, matching, vertex)
if (migration.down)
graph.addEdge(migration.down, vertex, matching)
}
}
}

View File

@@ -23,7 +23,13 @@ export type VersionOptions<Version extends string> = {
/**
* Additional migrations, such as fast-forward migrations, or migrations from other flavors.
*/
other?: Record<string, (opts: { effects: T.Effects }) => Promise<void>>
other?: Record<
string,
{
up?: (opts: { effects: T.Effects }) => Promise<void>
down?: (opts: { effects: T.Effects }) => Promise<void>
}
>
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.30",
"version": "0.4.0-beta.32",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.30",
"version": "0.4.0-beta.32",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.30",
"version": "0.4.0-beta.32",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",

View File

@@ -39,13 +39,7 @@ const {InputSpec, List, Value, Variants} = sdk
const namedConsts = new Set(["InputSpec", "Value", "List"])
const inputSpecName = newConst("inputSpecSpec", convertInputSpec(data))
const inputSpecMatcherName = newConst(
"matchInputSpecSpec",
`${inputSpecName}.validator`,
)
outputLines.push(
`export type InputSpecSpec = typeof ${inputSpecMatcherName}._TYPE;`,
)
outputLines.push(`export type InputSpecSpec = typeof ${inputSpecName}._TYPE;`)
return outputLines.join("\n")
@@ -195,7 +189,8 @@ const {InputSpec, List, Value, Variants} = sdk
description: ${JSON.stringify(value.tag.description || null)},
warning: ${JSON.stringify(value.tag.warning || null)},
default: ${JSON.stringify(value.default)},
}, ${variants})`
variants: ${variants},
})`
}
case "list": {
if (value.subtype === "enum") {
@@ -322,7 +317,8 @@ const {InputSpec, List, Value, Variants} = sdk
)},
warning: ${JSON.stringify(value?.spec?.tag?.warning || null)},
default: ${JSON.stringify(value?.spec?.default || null)},
}, ${variants})
variants: ${variants},
})
`,
)
const listInputSpec = maybeNewConst(