chore: Update the lazy config

This commit is contained in:
BluJ
2023-05-01 13:04:48 -06:00
parent cb89f3a65f
commit a30ed1f0ab
19 changed files with 505 additions and 667 deletions

View File

@@ -1,37 +1,38 @@
import { Parser } from "ts-matches"
import { Config } from "../config/builder" import { Config } from "../config/builder"
import { ActionMetaData, ActionResult, Effects, ExportedAction } from "../types" import { ActionMetaData, ActionResult, Effects, ExportedAction } from "../types"
import { Utils, once, utils } from "../util" import { Utils, utils } from "../util"
import { TypeFromProps } from "../util/propertiesMatcher"
import { InputSpec } from "../config/configTypes"
export class CreatedAction<WrapperData, Input extends Config<InputSpec>> { export class CreatedAction<WrapperData, Type extends Record<string, any>> {
private constructor( private constructor(
private myMetaData: Omit<ActionMetaData, "input"> & { input: Input }, private myMetaData: Omit<ActionMetaData, "input"> & {
input: Config<Type, WrapperData, never>
},
readonly fn: (options: { readonly fn: (options: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<WrapperData>
input: TypeFromProps<Input> input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) {} ) {}
private validator = this.myMetaData.input.validator() as Parser< private validator = this.myMetaData.input.validator
unknown,
TypeFromProps<Input>
>
metaData = {
...this.myMetaData,
input: this.myMetaData.input.build(),
}
static of<WrapperData, Input extends Config<InputSpec>>( static of<
metaData: Omit<ActionMetaData, "input"> & { input: Input }, WrapperData,
Input extends Config<Type, WrapperData, never>,
Type extends Record<string, any> = (Input extends Config<any, infer B, any>
? B
: never) &
Record<string, any>,
>(
metaData: Omit<ActionMetaData, "input"> & {
input: Config<Type, WrapperData, never>
},
fn: (options: { fn: (options: {
effects: Effects effects: Effects
utils: Utils<WrapperData> utils: Utils<WrapperData>
input: TypeFromProps<Input> input: Type
}) => Promise<ActionResult>, }) => Promise<ActionResult>,
) { ) {
return new CreatedAction<WrapperData, Input>(metaData, fn) return new CreatedAction<WrapperData, Type>(metaData, fn)
} }
exportedAction: ExportedAction = ({ effects, input }) => { exportedAction: ExportedAction = ({ effects, input }) => {
@@ -43,7 +44,16 @@ export class CreatedAction<WrapperData, Input extends Config<InputSpec>> {
} }
async exportAction(effects: Effects) { async exportAction(effects: Effects) {
await effects.exportAction(this.metaData) const myUtils = utils<WrapperData>(effects)
const metaData = {
...this.myMetaData,
input: await this.myMetaData.input.build({
effects,
utils: myUtils,
config: null,
}),
}
await effects.exportAction(metaData)
} }
} }

View File

@@ -1,10 +0,0 @@
import { _ } from "../../util"
export class IBuilder<A> {
protected constructor(readonly a: A) {}
public build(): A {
return this.a
}
}
export type BuilderExtract<A> = A extends IBuilder<infer B> ? B : never

View File

@@ -1,9 +1,20 @@
import { InputSpec, ValueSpec } from "../configTypes" import { ValueSpec } from "../configTypes"
import { typeFromProps } from "../../util" import { Utils } from "../../util"
import { BuilderExtract, IBuilder } from "./builder"
import { Value } from "./value" import { Value } from "./value"
import { _ } from "../../util" import { _ } from "../../util"
import { Effects } from "../../types"
import { Parser, object } from "ts-matches"
export type LazyBuildOptions<Manifest, ConfigType> = {
effects: Effects
utils: Utils<Manifest>
config: ConfigType | null
}
export type LazyBuild<Manifest, ConfigType, ExpectedOut> = (
options: LazyBuildOptions<Manifest, ConfigType>,
) => Promise<ExpectedOut> | ExpectedOut
export type MaybeLazyValues<A> = LazyBuild<any, any, A> | A
/** /**
* Configs are the specs that are used by the os configuration form for this service. * Configs are the specs that are used by the os configuration form for this service.
* Here is an example of a simple configuration * Here is an example of a simple configuration
@@ -60,44 +71,33 @@ export const addNodesSpec = Config.of({ hostname: hostname, port: port });
``` ```
*/ */
export class Config<A extends InputSpec> extends IBuilder<A> { export class Config<Type extends Record<string, any>, WD, ConfigType> {
static empty() { private constructor(
return new Config({}) private readonly spec: {
} [K in keyof Type]: Value<Type[K], WD, ConfigType>
static withValue<K extends string, B extends ValueSpec>( },
key: K, public validator: Parser<unknown, Type>,
value: Value<B>, ) {}
) { async build(options: LazyBuildOptions<WD, ConfigType>) {
return Config.empty().withValue(key, value) const answer = {} as {
} [K in keyof Type]: ValueSpec
static addValue<K extends string, B extends ValueSpec>(
key: K,
value: Value<B>,
) {
return Config.empty().withValue(key, value)
}
static of<B extends { [key: string]: Value<ValueSpec> }>(spec: B) {
const answer: { [K in keyof B]: BuilderExtract<B[K]> } = {} as any
for (const key in spec) {
answer[key] = spec[key].build() as any
} }
return new Config(answer) for (const k in this.spec) {
} answer[k] = await this.spec[k].build(options)
withValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) { }
return new Config({ return answer
...this.a,
[key]: value.build(),
} as A & { [key in K]: B })
}
addValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
return new Config({
...this.a,
[key]: value.build(),
} as A & { [key in K]: B })
} }
public validator() { static of<Type extends Record<string, any>, Manifest, ConfigType>(spec: {
return typeFromProps(this.a) [K in keyof Type]: Value<Type[K], Manifest, ConfigType>
}) {
const validatorObj = {} as {
[K in keyof Type]: Parser<unknown, Type[K]>
}
for (const key in spec) {
validatorObj[key] = spec[key].validator
}
const validator = object(validatorObj)
return new Config<Type, Manifest, ConfigType>(spec, validator)
} }
} }

View File

@@ -1,13 +1,11 @@
import { BuilderExtract, IBuilder } from "./builder" import { Config, LazyBuild } from "./config"
import { Config } from "./config"
import { import {
InputSpec,
ListValueSpecText, ListValueSpecText,
Pattern, Pattern,
UniqueBy, UniqueBy,
ValueSpecList, ValueSpecList,
} from "../configTypes" } from "../configTypes"
import { guardAll } from "../../util" import { Parser, arrayOf, number, string } from "ts-matches"
/** /**
* Used as a subtype of Value.list * Used as a subtype of Value.list
```ts ```ts
@@ -21,8 +19,12 @@ export const authorizationList = List.string({
export const auth = Value.list(authorizationList); export const auth = Value.list(authorizationList);
``` ```
*/ */
export class List<A extends ValueSpecList> extends IBuilder<A> { export class List<Type, WD, ConfigType> {
static text( private constructor(
public build: LazyBuild<WD, ConfigType, ValueSpecList>,
public validator: Parser<unknown, Type>,
) {}
static text<WD, CT>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
@@ -43,27 +45,29 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
inputmode?: ListValueSpecText["inputmode"] inputmode?: ListValueSpecText["inputmode"]
}, },
) { ) {
const spec = { return new List<string[], WD, CT>(() => {
type: "text" as const, const spec = {
placeholder: null, type: "text" as const,
minLength: null, placeholder: null,
maxLength: null, minLength: null,
masked: false, maxLength: null,
inputmode: "text" as const, masked: false,
...aSpec, inputmode: "text" as const,
} ...aSpec,
return new List({ }
description: null, return {
warning: null, description: null,
default: [], warning: null,
type: "list" as const, default: [],
minLength: null, type: "list" as const,
maxLength: null, minLength: null,
...a, maxLength: null,
spec, ...a,
}) spec,
}
}, arrayOf(string))
} }
static number( static number<WD, CT>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
@@ -82,27 +86,29 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
placeholder?: string | null placeholder?: string | null
}, },
) { ) {
const spec = { return new List<number[], WD, CT>(() => {
type: "number" as const, const spec = {
placeholder: null, type: "number" as const,
min: null, placeholder: null,
max: null, min: null,
step: null, max: null,
units: null, step: null,
...aSpec, units: null,
} ...aSpec,
return new List({ }
description: null, return {
warning: null, description: null,
minLength: null, warning: null,
maxLength: null, minLength: null,
default: [], maxLength: null,
type: "list" as const, default: [],
...a, type: "list" as const,
spec, ...a,
}) spec,
}
}, arrayOf(number))
} }
static obj<Spec extends Config<InputSpec>>( static obj<Type extends Record<string, any>, WrapperData, ConfigType>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
@@ -113,36 +119,34 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
maxLength?: number | null maxLength?: number | null
}, },
aSpec: { aSpec: {
spec: Spec spec: Config<Type, WrapperData, ConfigType>
displayAs?: null | string displayAs?: null | string
uniqueBy?: null | UniqueBy uniqueBy?: null | UniqueBy
}, },
) { ) {
const { spec: previousSpecSpec, ...restSpec } = aSpec return new List<Type[], WrapperData, ConfigType>(async (options) => {
const specSpec = previousSpecSpec.build() as BuilderExtract<Spec> const { spec: previousSpecSpec, ...restSpec } = aSpec
const spec = { const specSpec = await previousSpecSpec.build(options)
type: "object" as const, const spec = {
displayAs: null, type: "object" as const,
uniqueBy: null, displayAs: null,
...restSpec, uniqueBy: null,
spec: specSpec, ...restSpec,
} spec: specSpec,
const value = { }
spec, const value = {
default: [], spec,
...a, default: [],
} ...a,
return new List({ }
description: null, return {
warning: null, description: null,
minLength: null, warning: null,
maxLength: null, minLength: null,
type: "list" as const, maxLength: null,
...value, type: "list" as const,
}) ...value,
} }
}, arrayOf(aSpec.spec.validator))
public validator() {
return guardAll(this.a)
} }
} }

View File

@@ -1,22 +1,28 @@
import { BuilderExtract, IBuilder } from "./builder" import { Config, LazyBuild } from "./config"
import { Config } from "./config"
import { List } from "./list" import { List } from "./list"
import { Variants } from "./variants" import { Variants } from "./variants"
import { import {
InputSpec,
Pattern, Pattern,
ValueSpec, ValueSpec,
ValueSpecColor,
ValueSpecDatetime, ValueSpecDatetime,
ValueSpecList,
ValueSpecNumber,
ValueSpecSelect,
ValueSpecText, ValueSpecText,
ValueSpecTextarea, ValueSpecTextarea,
} from "../configTypes" } from "../configTypes"
import { guardAll } from "../../util" import { once } from "../../util"
import { DefaultString } from "../configTypes" import { DefaultString } from "../configTypes"
import { _ } from "../../util" import { _ } from "../../util"
import {
Parser,
anyOf,
arrayOf,
boolean,
literal,
literals,
number,
object,
string,
unknown,
} from "ts-matches"
type RequiredDefault<A> = type RequiredDefault<A> =
| false | false
@@ -40,6 +46,30 @@ function requiredLikeToAbove<Input extends RequiredDefault<A>, A>(
) )
}; };
} }
type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
| { default: unknown }
| never
? Type
: Type | null | undefined
type InputAsRequired<A, Type> = A extends
| { required: { default: any } | never }
| never
? Type
: Type | null | undefined
const testForAsRequiredParser = once(
() => object({ required: object({ default: unknown }) }).test,
)
function asRequiredParser<
Type,
Input,
Return extends
| Parser<unknown, Type>
| Parser<unknown, Type | null | undefined>,
>(parser: Parser<unknown, Type>, input: Input): Return {
if (testForAsRequiredParser()(input)) return parser as any
return parser.optional() as any
}
/** /**
* A value is going to be part of the form in the FE of the OS. * A value is going to be part of the form in the FE of the OS.
* Something like a boolean, a string, a number, etc. * Something like a boolean, a string, a number, etc.
@@ -62,22 +92,29 @@ const username = Value.string({
}); });
``` ```
*/ */
export class Value<A extends ValueSpec> extends IBuilder<A> { export class Value<Type, WD, ConfigType> {
static toggle(a: { private constructor(
public build: LazyBuild<WD, ConfigType, ValueSpec>,
public validator: Parser<unknown, Type>,
) {}
static toggle<WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
default?: boolean | null default?: boolean | null
}) { }) {
return new Value({ return new Value<boolean, WD, CT>(
description: null, async () => ({
warning: null, description: null,
default: null, warning: null,
type: "toggle" as const, default: null,
...a, type: "toggle" as const,
}) ...a,
}),
boolean,
)
} }
static text<Required extends RequiredDefault<DefaultString>>(a: { static text<Required extends RequiredDefault<DefaultString>, WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
@@ -92,21 +129,24 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
/** Default = 'text' */ /** Default = 'text' */
inputmode?: ValueSpecText["inputmode"] inputmode?: ValueSpecText["inputmode"]
}) { }) {
return new Value({ return new Value<AsRequired<string, Required>, WD, CT>(
type: "text" as const, async () => ({
description: null, type: "text" as const,
warning: null, description: null,
masked: false, warning: null,
placeholder: null, masked: false,
minLength: null, placeholder: null,
maxLength: null, minLength: null,
patterns: [], maxLength: null,
inputmode: "text", patterns: [],
...a, inputmode: "text",
...requiredLikeToAbove(a.required), ...a,
}) ...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
} }
static textarea(a: { static textarea<WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
@@ -115,17 +155,21 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
maxLength?: number | null maxLength?: number | null
placeholder?: string | null placeholder?: string | null
}) { }) {
return new Value({ return new Value<string, WD, CT>(
description: null, async () =>
warning: null, ({
minLength: null, description: null,
maxLength: null, warning: null,
placeholder: null, minLength: null,
type: "textarea" as const, maxLength: null,
...a, placeholder: null,
} as ValueSpecTextarea) type: "textarea" as const,
...a,
} satisfies ValueSpecTextarea),
string,
)
} }
static number<Required extends RequiredDefault<number>>(a: { static number<Required extends RequiredDefault<number>, WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
@@ -138,34 +182,41 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
units?: string | null units?: string | null
placeholder?: string | null placeholder?: string | null
}) { }) {
return new Value({ return new Value<AsRequired<number, Required>, WD, CT>(
type: "number" as const, () => ({
description: null, type: "number" as const,
warning: null, description: null,
min: null, warning: null,
max: null, min: null,
step: null, max: null,
units: null, step: null,
placeholder: null, units: null,
...a, placeholder: null,
...requiredLikeToAbove(a.required), ...a,
}) ...requiredLikeToAbove(a.required),
}),
asRequiredParser(number, a),
)
} }
static color<Required extends RequiredDefault<string>>(a: { static color<Required extends RequiredDefault<string>, WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
required: Required required: Required
}) { }) {
return new Value({ return new Value<AsRequired<string, Required>, WD, CT>(
type: "color" as const, () => ({
description: null, type: "color" as const,
warning: null, description: null,
...a, warning: null,
...requiredLikeToAbove(a.required), ...a,
}) ...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
} }
static datetime<Required extends RequiredDefault<string>>(a: { static datetime<Required extends RequiredDefault<string>, WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
@@ -176,21 +227,26 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
max?: string | null max?: string | null
step?: string | null step?: string | null
}) { }) {
return new Value({ return new Value<AsRequired<string, Required>, WD, CT>(
type: "datetime" as const, () => ({
description: null, type: "datetime" as const,
warning: null, description: null,
inputmode: "datetime-local", warning: null,
min: null, inputmode: "datetime-local",
max: null, min: null,
step: null, max: null,
...a, step: null,
...requiredLikeToAbove(a.required), ...a,
}) ...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
} }
static select< static select<
Required extends RequiredDefault<string>, Required extends RequiredDefault<string>,
B extends Record<string, string>, B extends Record<string, string>,
WD,
CT,
>(a: { >(a: {
name: string name: string
description?: string | null description?: string | null
@@ -198,15 +254,23 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
required: Required required: Required
values: B values: B
}) { }) {
return new Value({ return new Value<AsRequired<keyof B, Required>, WD, CT>(
description: null, () => ({
warning: null, description: null,
type: "select" as const, warning: null,
...a, type: "select" as const,
...requiredLikeToAbove(a.required), ...a,
}) ...requiredLikeToAbove(a.required),
}),
asRequiredParser(
anyOf(
...Object.keys(a.values).map((x: keyof B & string) => literal(x)),
),
a,
) as any,
)
} }
static multiselect<Values extends Record<string, string>>(a: { static multiselect<Values extends Record<string, string>, WD, CT>(a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
@@ -215,35 +279,44 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
minLength?: number | null minLength?: number | null
maxLength?: number | null maxLength?: number | null
}) { }) {
return new Value({ return new Value<(keyof Values)[], WD, CT>(
type: "multiselect" as const, () => ({
minLength: null, type: "multiselect" as const,
maxLength: null, minLength: null,
warning: null, maxLength: null,
description: null, warning: null,
...a, description: null,
}) ...a,
}),
arrayOf(
literals(...(Object.keys(a.values) as any as [keyof Values & string])),
),
)
} }
static object<Spec extends Config<InputSpec>>( static object<Type extends Record<string, any>, WrapperData, ConfigType>(
a: { a: {
name: string name: string
description?: string | null description?: string | null
warning?: string | null warning?: string | null
}, },
previousSpec: Spec, previousSpec: Config<Type, WrapperData, ConfigType>,
) { ) {
const spec = previousSpec.build() as BuilderExtract<Spec> return new Value<Type, WrapperData, ConfigType>(async (options) => {
return new Value({ const spec = await previousSpec.build(options as any)
type: "object" as const, return {
description: null, type: "object" as const,
warning: null, description: null,
...a, warning: null,
spec, ...a,
}) spec,
}
}, previousSpec.validator)
} }
static union< static union<
Required extends RequiredDefault<string>, Required extends RequiredDefault<string>,
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>, Type,
WrapperData,
ConfigType,
>( >(
a: { a: {
name: string name: string
@@ -252,23 +325,28 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
required: Required required: Required
default?: string | null default?: string | null
}, },
aVariants: V, aVariants: Variants<Type, WrapperData, ConfigType>,
) { ) {
const variants = aVariants.build() as BuilderExtract<V> return new Value<AsRequired<Type, Required>, WrapperData, ConfigType>(
return new Value({ async (options) => ({
type: "union" as const, type: "union" as const,
description: null, description: null,
warning: null, warning: null,
...a, ...a,
variants, variants: await aVariants.build(options as any),
...requiredLikeToAbove(a.required), ...requiredLikeToAbove(a.required),
}) }),
asRequiredParser(aVariants.validator, a),
)
} }
static list<A extends ValueSpecList>(a: List<A>) { static list<Type, WrapperData, ConfigType>(
return new Value(a.build()) a: List<Type, WrapperData, ConfigType>,
} ) {
public validator() { /// TODO
return guardAll(this.a) return new Value<Type, WrapperData, ConfigType>(
(options) => a.build(options),
a.validator,
)
} }
} }

View File

@@ -1,6 +1,7 @@
import { InputSpec } from "../configTypes" import { InputSpec, ValueSpecUnion } from "../configTypes"
import { BuilderExtract, IBuilder } from "./builder"
import { Config } from "." import { Config } from "."
import { LazyBuild } from "./config"
import { Parser, anyOf, literals, object } from "ts-matches"
/** /**
* Used in the the Value.select { @link './value.ts' } * Used in the the Value.select { @link './value.ts' }
@@ -51,46 +52,55 @@ export const pruning = Value.union(
); );
``` ```
*/ */
export class Variants< export class Variants<Type, WD, ConfigType> {
A extends { private constructor(
[key: string]: { public build: LazyBuild<WD, ConfigType, ValueSpecUnion["variants"]>,
name: string public validator: Parser<unknown, Type>,
spec: InputSpec ) {}
} // A extends {
}, // [key: string]: {
> extends IBuilder<A> { // name: string
// spec: InputSpec
// }
// },
static of< static of<
A extends { TypeMap extends Record<string, Record<string, any>>,
[key: string]: { name: string; spec: Config<InputSpec> } WrapperData,
}, ConfigType,
>(a: A) { >(a: {
const variants: { [K in keyof TypeMap]: {
[K in keyof A]: { name: string; spec: BuilderExtract<A[K]["spec"]> } name: string
} = {} as any spec: Config<TypeMap[K], WrapperData, ConfigType>
for (const key in a) {
const value = a[key]
variants[key] = {
name: value.name,
spec: value.spec.build() as any,
}
} }
return new Variants(variants) }) {
} type TypeOut = {
[K in keyof TypeMap & string]: {
unionSelectKey: K
unionValueKey: TypeMap[K]
}
}[keyof TypeMap & string]
static empty() { const validator = anyOf(
return Variants.of({}) ...Object.entries(a).map(([name, { spec }]) =>
} object({
static withVariant<K extends string, B extends InputSpec>( unionSelectKey: literals(name),
key: K, unionValueKey: spec.validator,
value: Config<B>, }),
) { ),
return Variants.empty().withVariant(key, value) ) as Parser<unknown, TypeOut>
}
withVariant<K extends string, B extends InputSpec>(key: K, value: Config<B>) { return new Variants<TypeOut, WrapperData, ConfigType>(async (options) => {
return new Variants({ const variants = {} as {
...this.a, [K in keyof TypeMap]: { name: string; spec: InputSpec }
[key]: value.build(), }
} as A & { [key in K]: B }) for (const key in a) {
const value = a[key]
variants[key] = {
name: value.name,
spec: await value.spec.build(options),
}
}
return variants
}, validator)
} }
} }

View File

@@ -189,10 +189,10 @@ export type DefaultString =
// sometimes the type checker needs just a little bit of help // sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType>( export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpecListOf<ListValueSpecType>, t: ValueSpec,
s: S, s: S,
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } { ): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
return t.spec.type === s return "spec" in t && t.spec.type === s
} }
export const unionSelectKey = "unionSelectKey" as const export const unionSelectKey = "unionSelectKey" as const
export type UnionSelectKey = typeof unionSelectKey export type UnionSelectKey = typeof unionSelectKey

View File

@@ -2,7 +2,6 @@ import { Config } from "./builder"
import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types" import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types"
import { InputSpec } from "./configTypes" import { InputSpec } from "./configTypes"
import { Utils, nullIfEmpty, once, utils } from "../util" import { Utils, nullIfEmpty, once, utils } from "../util"
import { TypeFromProps } from "../util/propertiesMatcher"
import { GenericManifest } from "../manifest/ManifestTypes" import { GenericManifest } from "../manifest/ManifestTypes"
import * as D from "./dependencies" import * as D from "./dependencies"
@@ -30,18 +29,18 @@ export type Read<WD, A> = (options: {
*/ */
export function setupConfig< export function setupConfig<
WD, WD,
A extends Config<InputSpec>, Type extends Record<string, any>,
Manifest extends GenericManifest, Manifest extends GenericManifest,
>( >(
spec: A, spec: Config<Type, WD, Type>,
write: Save<WD, TypeFromProps<A>, Manifest>, write: Save<WD, Type, Manifest>,
read: Read<WD, TypeFromProps<A>>, read: Read<WD, Type>,
) { ) {
const validator = once(() => spec.validator()) const validator = spec.validator
return { return {
setConfig: (async ({ effects, input }) => { setConfig: (async ({ effects, input }) => {
if (!validator().test(input)) { if (!validator.test(input)) {
await effects.console.error(String(validator().errorMessage(input))) await effects.console.error(String(validator.errorMessage(input)))
return { error: "Set config type error for config" } return { error: "Set config type error for config" }
} }
await write({ await write({
@@ -51,12 +50,18 @@ export function setupConfig<
dependencies: D.dependenciesSet<Manifest>(), dependencies: D.dependenciesSet<Manifest>(),
}) })
}) as ExpectedExports.setConfig, }) as ExpectedExports.setConfig,
getConfig: (async ({ effects, config }) => { getConfig: (async ({ effects }) => {
const myUtils = utils<WD>(effects)
const configValue = nullIfEmpty(
(await read({ effects, utils: myUtils })) || null,
)
return { return {
spec: spec.build(), spec: await spec.build({
config: nullIfEmpty( effects,
(await read({ effects, utils: utils<WD>(effects) })) || null, utils: myUtils,
), config: configValue,
}),
config: configValue,
} }
}) as ExpectedExports.getConfig, }) as ExpectedExports.getConfig,
} }

View File

@@ -10,7 +10,7 @@ export class NetworkInterfaceBuilder {
id: string id: string
description: string description: string
ui: boolean ui: boolean
basic?: null | { password: string; username: string } basic?: null | { password: null | string; username: string }
path?: string path?: string
search?: Record<string, string> search?: Record<string, string>
}, },

View File

@@ -4,14 +4,14 @@ export class Origin {
withAuth( withAuth(
origin?: origin?:
| { | {
password: string password: null | string
username: string username: string
} }
| null | null
| undefined, | undefined,
) { ) {
// prettier-ignore // prettier-ignore
const urlAuth = !!(origin) ? `${origin.username}:${origin.password}@` : const urlAuth = !!(origin) ? `${origin.username}${origin.password != null ?`:${origin.password}`:''}@` :
''; '';
return `${this.protocol}://${urlAuth}${this.host}` return `${this.protocol}://${urlAuth}${this.host}`
} }

View File

@@ -3,22 +3,21 @@ import { Config } from "../config/builder/config"
import { List } from "../config/builder/list" import { List } from "../config/builder/list"
import { Value } from "../config/builder/value" import { Value } from "../config/builder/value"
import { Variants } from "../config/builder/variants" import { Variants } from "../config/builder/variants"
import { ValueSpec } from "../config/configTypes"
import { Parser } from "ts-matches"
type test = unknown | { test: 5 }
describe("builder tests", () => { describe("builder tests", () => {
test("text", () => { test("text", async () => {
const bitcoinPropertiesBuilt: { const bitcoinPropertiesBuilt: {
"peer-tor-address": { "peer-tor-address": ValueSpec
name: string } = await Config.of({
description: string | null
type: "text"
}
} = Config.of({
"peer-tor-address": Value.text({ "peer-tor-address": Value.text({
name: "Peer tor address", name: "Peer tor address",
description: "The Tor address of the peer interface", description: "The Tor address of the peer interface",
required: { default: null }, required: { default: null },
}), }),
}).build() }).build({} as any)
expect(JSON.stringify(bitcoinPropertiesBuilt)).toEqual( expect(JSON.stringify(bitcoinPropertiesBuilt)).toEqual(
/*json*/ `{ /*json*/ `{
"peer-tor-address": { "peer-tor-address": {
@@ -43,62 +42,62 @@ describe("builder tests", () => {
}) })
describe("values", () => { describe("values", () => {
test("toggle", () => { test("toggle", async () => {
const value = Value.toggle({ const value = Value.toggle({
name: "Testing", name: "Testing",
description: null, description: null,
warning: null, warning: null,
default: null, default: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast(false) validator.unsafeCast(false)
testOutput<typeof validator._TYPE, boolean>()(null) testOutput<typeof validator._TYPE, boolean>()(null)
}) })
test("text", () => { test("text", async () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: { default: null }, required: { default: null },
}) })
const validator = value.validator() const validator = value.validator
const rawIs = value.build() const rawIs = await value.build({} as any)
validator.unsafeCast("test text") validator.unsafeCast("test text")
expect(() => validator.unsafeCast(null)).toThrowError() expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null) testOutput<typeof validator._TYPE, string>()(null)
}) })
test("text", () => { test("text", async () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: { default: "null" }, required: { default: "null" },
}) })
const validator = value.validator() const validator = value.validator
const rawIs = value.build() const rawIs = await value.build({} as any)
validator.unsafeCast("test text") validator.unsafeCast("test text")
expect(() => validator.unsafeCast(null)).toThrowError() expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, string>()(null) testOutput<typeof validator._TYPE, string>()(null)
}) })
test("optional text", () => { test("optional text", async () => {
const value = Value.text({ const value = Value.text({
name: "Testing", name: "Testing",
required: false, required: false,
}) })
const validator = value.validator() const validator = value.validator
const rawIs = value.build() const rawIs = await value.build({} as any)
validator.unsafeCast("test text") validator.unsafeCast("test text")
validator.unsafeCast(null) validator.unsafeCast(null)
testOutput<typeof validator._TYPE, string | null | undefined>()(null) testOutput<typeof validator._TYPE, string | null | undefined>()(null)
}) })
test("color", () => { test("color", async () => {
const value = Value.color({ const value = Value.color({
name: "Testing", name: "Testing",
required: false, required: false,
description: null, description: null,
warning: null, warning: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("#000000") validator.unsafeCast("#000000")
testOutput<typeof validator._TYPE, string | null | undefined>()(null) testOutput<typeof validator._TYPE, string | null | undefined>()(null)
}) })
test("datetime", () => { test("datetime", async () => {
const value = Value.datetime({ const value = Value.datetime({
name: "Testing", name: "Testing",
required: { default: null }, required: { default: null },
@@ -109,11 +108,11 @@ describe("values", () => {
max: null, max: null,
step: null, step: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("2021-01-01") validator.unsafeCast("2021-01-01")
testOutput<typeof validator._TYPE, string>()(null) testOutput<typeof validator._TYPE, string>()(null)
}) })
test("optional datetime", () => { test("optional datetime", async () => {
const value = Value.datetime({ const value = Value.datetime({
name: "Testing", name: "Testing",
required: false, required: false,
@@ -124,11 +123,11 @@ describe("values", () => {
max: null, max: null,
step: null, step: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("2021-01-01") validator.unsafeCast("2021-01-01")
testOutput<typeof validator._TYPE, string | null | undefined>()(null) testOutput<typeof validator._TYPE, string | null | undefined>()(null)
}) })
test("textarea", () => { test("textarea", async () => {
const value = Value.textarea({ const value = Value.textarea({
name: "Testing", name: "Testing",
required: false, required: false,
@@ -138,11 +137,11 @@ describe("values", () => {
maxLength: null, maxLength: null,
placeholder: null, placeholder: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("test text") validator.unsafeCast("test text")
testOutput<typeof validator._TYPE, string>()(null) testOutput<typeof validator._TYPE, string>()(null)
}) })
test("number", () => { test("number", async () => {
const value = Value.number({ const value = Value.number({
name: "Testing", name: "Testing",
required: { default: null }, required: { default: null },
@@ -155,11 +154,11 @@ describe("values", () => {
units: null, units: null,
placeholder: null, placeholder: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast(2) validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number>()(null) testOutput<typeof validator._TYPE, number>()(null)
}) })
test("optional number", () => { test("optional number", async () => {
const value = Value.number({ const value = Value.number({
name: "Testing", name: "Testing",
required: false, required: false,
@@ -172,11 +171,11 @@ describe("values", () => {
units: null, units: null,
placeholder: null, placeholder: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast(2) validator.unsafeCast(2)
testOutput<typeof validator._TYPE, number | null | undefined>()(null) testOutput<typeof validator._TYPE, number | null | undefined>()(null)
}) })
test("select", () => { test("select", async () => {
const value = Value.select({ const value = Value.select({
name: "Testing", name: "Testing",
required: { default: null }, required: { default: null },
@@ -187,13 +186,13 @@ describe("values", () => {
description: null, description: null,
warning: null, warning: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("a") validator.unsafeCast("a")
validator.unsafeCast("b") validator.unsafeCast("b")
expect(() => validator.unsafeCast(null)).toThrowError() expect(() => validator.unsafeCast(null)).toThrowError()
testOutput<typeof validator._TYPE, "a" | "b">()(null) testOutput<typeof validator._TYPE, "a" | "b">()(null)
}) })
test("nullable select", () => { test("nullable select", async () => {
const value = Value.select({ const value = Value.select({
name: "Testing", name: "Testing",
required: false, required: false,
@@ -204,13 +203,13 @@ describe("values", () => {
description: null, description: null,
warning: null, warning: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast("a") validator.unsafeCast("a")
validator.unsafeCast("b") validator.unsafeCast("b")
validator.unsafeCast(null) validator.unsafeCast(null)
testOutput<typeof validator._TYPE, "a" | "b" | null | undefined>()(null) testOutput<typeof validator._TYPE, "a" | "b" | null | undefined>()(null)
}) })
test("multiselect", () => { test("multiselect", async () => {
const value = Value.multiselect({ const value = Value.multiselect({
name: "Testing", name: "Testing",
values: { values: {
@@ -223,12 +222,15 @@ describe("values", () => {
minLength: null, minLength: null,
maxLength: null, maxLength: null,
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast([]) validator.unsafeCast([])
validator.unsafeCast(["a", "b"]) validator.unsafeCast(["a", "b"])
expect(() => validator.unsafeCast(["e"])).toThrowError()
expect(() => validator.unsafeCast([4])).toThrowError()
testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null) testOutput<typeof validator._TYPE, Array<"a" | "b">>()(null)
}) })
test("object", () => { test("object", async () => {
const value = Value.object( const value = Value.object(
{ {
name: "Testing", name: "Testing",
@@ -244,11 +246,11 @@ describe("values", () => {
}), }),
}), }),
) )
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: true }) validator.unsafeCast({ a: true })
testOutput<typeof validator._TYPE, { a: boolean }>()(null) testOutput<typeof validator._TYPE, { a: boolean }>()(null)
}) })
test("union", () => { test("union", async () => {
const value = Value.union( const value = Value.union(
{ {
name: "Testing", name: "Testing",
@@ -271,14 +273,14 @@ describe("values", () => {
}, },
}), }),
) )
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } }) validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } })
type Test = typeof validator._TYPE type Test = typeof validator._TYPE
testOutput<Test, { unionSelectKey: "a"; unionValueKey: { b: boolean } }>()( testOutput<Test, { unionSelectKey: "a"; unionValueKey: { b: boolean } }>()(
null, null,
) )
}) })
test("list", () => { test("list", async () => {
const value = Value.list( const value = Value.list(
List.number( List.number(
{ {
@@ -289,14 +291,14 @@ describe("values", () => {
}, },
), ),
) )
const validator = value.validator() const validator = value.validator
validator.unsafeCast([1, 2, 3]) validator.unsafeCast([1, 2, 3])
testOutput<typeof validator._TYPE, number[]>()(null) testOutput<typeof validator._TYPE, number[]>()(null)
}) })
}) })
describe("Builder List", () => { describe("Builder List", () => {
test("obj", () => { test("obj", async () => {
const value = Value.list( const value = Value.list(
List.obj( List.obj(
{ {
@@ -314,11 +316,11 @@ describe("Builder List", () => {
}, },
), ),
) )
const validator = value.validator() const validator = value.validator
validator.unsafeCast([{ test: true }]) validator.unsafeCast([{ test: true }])
testOutput<typeof validator._TYPE, { test: boolean }[]>()(null) testOutput<typeof validator._TYPE, { test: boolean }[]>()(null)
}) })
test("text", () => { test("text", async () => {
const value = Value.list( const value = Value.list(
List.text( List.text(
{ {
@@ -329,26 +331,14 @@ describe("Builder List", () => {
}, },
), ),
) )
const validator = value.validator() const validator = value.validator
validator.unsafeCast(["test", "text"]) validator.unsafeCast(["test", "text"])
testOutput<typeof validator._TYPE, string[]>()(null) testOutput<typeof validator._TYPE, string[]>()(null)
}) })
Value.multiselect({
name: "Media Sources",
minLength: null,
maxLength: null,
default: ["nextcloud"],
description: "List of Media Sources to use with Jellyfin",
warning: null,
values: {
nextcloud: "NextCloud",
filebrowser: "File Browser",
},
})
}) })
describe("Nested nullable values", () => { describe("Nested nullable values", () => {
test("Testing text", () => { test("Testing text", async () => {
const value = Config.of({ const value = Config.of({
a: Value.text({ a: Value.text({
name: "Temp Name", name: "Temp Name",
@@ -357,13 +347,13 @@ describe("Nested nullable values", () => {
required: false, required: false,
}), }),
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: null }) validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "test" }) validator.unsafeCast({ a: "test" })
expect(() => validator.unsafeCast({ a: 4 })).toThrowError() expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
testOutput<typeof validator._TYPE, { a: string | null | undefined }>()(null) testOutput<typeof validator._TYPE, { a: string | null | undefined }>()(null)
}) })
test("Testing number", () => { test("Testing number", async () => {
const value = Config.of({ const value = Config.of({
a: Value.number({ a: Value.number({
name: "Temp Name", name: "Temp Name",
@@ -379,13 +369,13 @@ describe("Nested nullable values", () => {
units: null, units: null,
}), }),
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: null }) validator.unsafeCast({ a: null })
validator.unsafeCast({ a: 5 }) validator.unsafeCast({ a: 5 })
expect(() => validator.unsafeCast({ a: "4" })).toThrowError() expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
testOutput<typeof validator._TYPE, { a: number | null | undefined }>()(null) testOutput<typeof validator._TYPE, { a: number | null | undefined }>()(null)
}) })
test("Testing color", () => { test("Testing color", async () => {
const value = Config.of({ const value = Config.of({
a: Value.color({ a: Value.color({
name: "Temp Name", name: "Temp Name",
@@ -395,13 +385,13 @@ describe("Nested nullable values", () => {
warning: null, warning: null,
}), }),
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: null }) validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "5" }) validator.unsafeCast({ a: "5" })
expect(() => validator.unsafeCast({ a: 4 })).toThrowError() expect(() => validator.unsafeCast({ a: 4 })).toThrowError()
testOutput<typeof validator._TYPE, { a: string | null | undefined }>()(null) testOutput<typeof validator._TYPE, { a: string | null | undefined }>()(null)
}) })
test("Testing select", () => { test("Testing select", async () => {
const value = Config.of({ const value = Config.of({
a: Value.select({ a: Value.select({
name: "Temp Name", name: "Temp Name",
@@ -414,7 +404,7 @@ describe("Nested nullable values", () => {
}, },
}), }),
}) })
const higher = Value.select({ const higher = await Value.select({
name: "Temp Name", name: "Temp Name",
description: "If no name is provided, the name from config will be used", description: "If no name is provided, the name from config will be used",
required: false, required: false,
@@ -422,15 +412,15 @@ describe("Nested nullable values", () => {
values: { values: {
a: "A", a: "A",
}, },
}).build() }).build({} as any)
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: null }) validator.unsafeCast({ a: null })
validator.unsafeCast({ a: "a" }) validator.unsafeCast({ a: "a" })
expect(() => validator.unsafeCast({ a: "4" })).toThrowError() expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
testOutput<typeof validator._TYPE, { a: "a" | null | undefined }>()(null) testOutput<typeof validator._TYPE, { a: "a" | null | undefined }>()(null)
}) })
test("Testing multiselect", () => { test("Testing multiselect", async () => {
const value = Config.of({ const value = Config.of({
a: Value.multiselect({ a: Value.multiselect({
name: "Temp Name", name: "Temp Name",
@@ -446,9 +436,10 @@ describe("Nested nullable values", () => {
maxLength: null, maxLength: null,
}), }),
}) })
const validator = value.validator() const validator = value.validator
validator.unsafeCast({ a: [] }) validator.unsafeCast({ a: [] })
validator.unsafeCast({ a: ["a"] }) validator.unsafeCast({ a: ["a"] })
expect(() => validator.unsafeCast({ a: ["4"] })).toThrowError()
expect(() => validator.unsafeCast({ a: "4" })).toThrowError() expect(() => validator.unsafeCast({ a: "4" })).toThrowError()
testOutput<typeof validator._TYPE, { a: "a"[] }>()(null) testOutput<typeof validator._TYPE, { a: "a"[] }>()(null)
}) })

View File

@@ -4,11 +4,14 @@ import { List } from "../config/builder/list"
import { Value } from "../config/builder/value" import { Value } from "../config/builder/value"
describe("Config Types", () => { describe("Config Types", () => {
test("isValueSpecListOf", () => { test("isValueSpecListOf", async () => {
const options = [List.obj, List.text, List.number] const options = [List.obj, List.text, List.number]
for (const option of options) { for (const option of options) {
const test = option({} as any, { spec: Config.of({}) } as any) as any const test = (option as any)(
const someList = Value.list(test).build() {} as any,
{ spec: Config.of({}) } as any,
) as any
const someList = await Value.list(test).build({} as any)
if (isValueSpecListOf(someList, "text")) { if (isValueSpecListOf(someList, "text")) {
someList.spec satisfies ListValueSpecOf<"text"> someList.spec satisfies ListValueSpecOf<"text">
} else if (isValueSpecListOf(someList, "number")) { } else if (isValueSpecListOf(someList, "number")) {

View File

@@ -423,6 +423,7 @@ oldSpecToBuilder(
}, },
{ {
// convert this to `start-sdk/lib` for conversions // convert this to `start-sdk/lib` for conversions
startSdk: "..", startSdk: "../..",
wrapperData: "./output.wrapperData",
}, },
) )

View File

@@ -0,0 +1 @@
export type WrapperData = {}

View File

@@ -15,10 +15,7 @@ export namespace ExpectedExports {
input: Record<string, unknown> input: Record<string, unknown>
}) => Promise<void> }) => Promise<void>
/** Get configuration returns a shape that describes the format that the start9 ui will generate, and later send to the set config */ /** Get configuration returns a shape that describes the format that the start9 ui will generate, and later send to the set config */
export type getConfig = (options: { export type getConfig = (options: { effects: Effects }) => Promise<ConfigRes>
effects: Effects
config: unknown
}) => Promise<ConfigRes>
// /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ // /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
// export type dependencies = Dependencies; // export type dependencies = Dependencies;
/** For backing up service data though the startOS UI */ /** For backing up service data though the startOS UI */
@@ -407,7 +404,7 @@ never
// prettier-ignore // prettier-ignore
export type EnsureWrapperDataPath<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path> export type EnsureWrapperDataPath<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path>
/* rsync options: https://linux.die.net/man/1/rsync /** rsync options: https://linux.die.net/man/1/rsync
*/ */
export type BackupOptions = { export type BackupOptions = {
delete: boolean delete: boolean

View File

@@ -11,7 +11,6 @@ import {
import { LocalBinding, LocalPort, NetworkBuilder, TorHostname } from "../mainFn" import { LocalBinding, LocalPort, NetworkBuilder, TorHostname } from "../mainFn"
import { ExtractWrapperData } from "../types" import { ExtractWrapperData } from "../types"
export { guardAll, typeFromProps } from "./propertiesMatcher"
export { default as nullIfEmpty } from "./nullIfEmpty" export { default as nullIfEmpty } from "./nullIfEmpty"
export { FileHelper } from "./fileHelper" export { FileHelper } from "./fileHelper"
export { getWrapperData } from "./getWrapperData" export { getWrapperData } from "./getWrapperData"

View File

@@ -4,7 +4,9 @@
* @param s * @param s
* @returns * @returns
*/ */
export default function nullIfEmpty(s: null | Record<string, unknown>) { export default function nullIfEmpty<A extends Record<string, any>>(
s: null | A,
) {
if (s === null) return null if (s === null) return null
return Object.keys(s).length === 0 ? null : s return Object.keys(s).length === 0 ? null : s
} }

View File

@@ -1,264 +0,0 @@
import * as matches from "ts-matches"
import { Parser, Validator } from "ts-matches"
import {
UnionSelectKey,
UnionValueKey,
ValueSpec as ValueSpecAny,
InputSpec,
} from "../config/configTypes"
import { Config } from "../config/builder/config"
import { _ } from "../util"
const {
string,
some,
arrayOf,
object,
dictionary,
unknown,
number,
literals,
boolean,
nill,
} = matches
type TypeToggle = "toggle"
type TypeText = "text"
type TypeTextarea = "textarea"
type TypeNumber = "number"
type TypeObject = "object"
type TypeList = "list"
type TypeSelect = "select"
type TypeMultiselect = "multiselect"
type TypeColor = "color"
type TypeDatetime = "datetime"
type TypeUnion = "union"
// prettier-ignore
type GuardDefaultRequired<A, Type> =
A extends { required: false; default: null | undefined | never } ? Type | undefined | null:
Type
// prettier-ignore
type GuardNumber<A> =
A extends { type: TypeNumber } ? GuardDefaultRequired<A, number> :
unknown
// prettier-ignore
type GuardText<A> =
A extends { type: TypeText } ? GuardDefaultRequired<A, string> :
unknown
// prettier-ignore
type GuardTextarea<A> =
A extends { type: TypeTextarea } ? GuardDefaultRequired<A, string> :
unknown
// prettier-ignore
type GuardToggle<A> =
A extends { type: TypeToggle } ? GuardDefaultRequired<A, boolean> :
unknown
type TrueKeyOf<T> = _<T> extends Record<string, unknown> ? keyof T : never
// prettier-ignore
type GuardObject<A> =
A extends { type: TypeObject, spec: infer B } ? (
{ [K in TrueKeyOf<B> & string]: _<GuardAll<B[K]>> }
) :
unknown
// prettier-ignore
export type GuardList<A> =
A extends { type: TypeList, spec?: { type: infer B, spec?: infer C } } ? Array<GuardAll<Omit<A, "type" | "spec"> & ({ type: B, spec: C })>> :
A extends { type: TypeList, spec?: { type: infer B } } ? Array<GuardAll<Omit<A, "type"> & ({ type: B })>> :
unknown
// prettier-ignore
type GuardSelect<A> =
A extends { type: TypeSelect, values: infer B } ? (
GuardDefaultRequired<A, TrueKeyOf<B>>
) :
unknown
// prettier-ignore
type GuardMultiselect<A> =
A extends { type: TypeMultiselect, values: infer B} ?(keyof B)[] :
unknown
// prettier-ignore
type GuardColor<A> =
A extends { type: TypeColor } ? GuardDefaultRequired<A, string> :
unknown
// prettier-ignore
type GuardDatetime<A> =
A extends { type: TypeDatetime } ? GuardDefaultRequired<A, string> :
unknown
type AsString<A> = A extends
| string
| number
| bigint
| boolean
| null
| undefined
? `${A}`
: "UnknownValue"
// prettier-ignore
type VariantValue<A> =
A extends { name: string, spec: infer B } ? TypeFromProps<_<B>> :
`neverVariantValue${AsString<A>}`
// prettier-ignore
type GuardUnion<A> =
A extends { type: TypeUnion, variants: infer Variants & Record<string, unknown> } ? (
_<{[key in keyof Variants]: {[k in UnionSelectKey]: key} & {[k in UnionValueKey]: VariantValue<Variants[key]>}}[keyof Variants]>
) :
unknown
export type GuardAll<A> = GuardNumber<A> &
GuardText<A> &
GuardTextarea<A> &
GuardToggle<A> &
GuardObject<A> &
GuardList<A> &
GuardUnion<A> &
GuardSelect<A> &
GuardMultiselect<A> &
GuardColor<A> &
GuardDatetime<A>
// prettier-ignore
export type TypeFromProps<A> =
A extends Config<infer B> ? TypeFromProps<B> :
A extends Record<string, unknown> ? { [K in keyof A & string]: _<GuardAll<A[K]>> } :
unknown;
const isType = object({ type: string })
const matchVariant = object({
name: string,
spec: unknown,
})
const recordString = dictionary([string, unknown])
const matchDefault = object({ default: unknown })
const matchRequired = object(
{
required: literals(false),
default: nill,
},
["default"],
)
const matchInteger = object({ integer: literals(true) })
const matchSpec = object({ spec: recordString })
const matchUnion = object({
variants: dictionary([string, matchVariant]),
})
const matchValues = object({
values: dictionary([string, string]),
})
function withInteger(parser: Parser<unknown, number>, value: unknown) {
if (matchInteger.test(value)) {
return parser.validate(Number.isInteger, "isIntegral")
}
return parser
}
function requiredParser<A>(parser: Parser<unknown, A>, value: unknown) {
if (matchRequired.test(value)) return parser.optional()
return parser
}
/**
* InputSpec: Tells the UI how to ask for information, verification, and will send the service a config in a shape via the spec.
* ValueSpecAny: This is any of the values in a config spec.
*
* Use this when we want to convert a value spec any into a parser for what a config will look like
* @param value
* @returns
*/
export function guardAll<A extends ValueSpecAny>(
value: A,
): Parser<unknown, GuardAll<A>> {
if (!isType.test(value)) {
return unknown as any
}
switch (value.type) {
case "toggle":
return requiredParser(boolean, value) as any
case "text":
return requiredParser(string, value) as any
case "textarea":
return requiredParser(string, value) as any
case "color":
return requiredParser(string, value) as any
case "datetime":
return requiredParser(string, value) as any
case "number":
return requiredParser(withInteger(number, value), value) as any
case "object":
if (matchSpec.test(value)) {
return requiredParser(typeFromProps(value.spec), value) as any
}
return unknown as any
case "list": {
const spec = (matchSpec.test(value) && value.spec) || {}
return requiredParser(
matches.arrayOf(guardAll(spec as any)),
value,
) as any
}
case "select":
if (matchValues.test(value)) {
const valueKeys = Object.keys(value.values)
return requiredParser(
literals(valueKeys[0], ...valueKeys),
value,
) as any
}
return unknown as any
case "multiselect":
if (matchValues.test(value)) {
const valueKeys = Object.keys(value.values)
return requiredParser(
arrayOf(literals(valueKeys[0], ...valueKeys)),
value,
) as any
}
return unknown as any
case "union":
if (matchUnion.test(value)) {
return some(
...Object.entries(value.variants)
.filter(([name]) => string.test(name))
.map(([name, { spec }]) =>
object({
unionSelectKey: literals(name),
unionValueKey: typeFromProps(spec),
}),
),
) as any
}
return unknown as any
}
return unknown as any
}
/**
* InputSpec: Tells the UI how to ask for information, verification, and will send the service a config in a shape via the spec.
* ValueSpecAny: This is any of the values in a config spec.
*
* Use this when we want to convert a config spec into a parser for what a config will look like
* @param valueDictionary
* @returns
*/
export function typeFromProps<A extends InputSpec>(
valueDictionary: A,
): Parser<unknown, TypeFromProps<A>> {
if (!recordString.test(valueDictionary)) return unknown as any
return object(
Object.fromEntries(
Object.entries(valueDictionary).map(([key, value]) => [
key,
guardAll(value),
]),
),
) as any
}

View File

@@ -29,29 +29,40 @@ function isString(x: unknown): x is string {
export default async function makeFileContentFromOld( export default async function makeFileContentFromOld(
inputData: Promise<any> | any, inputData: Promise<any> | any,
{ startSdk = "start-sdk", nested = true } = {}, {
startSdk = "start-sdk",
nested = true,
wrapperData = "../../wrapperData",
} = {},
) { ) {
const outputLines: string[] = [] const outputLines: string[] = []
outputLines.push(` outputLines.push(`
import {Config, Value, List, Variants} from '${startSdk}/config/builder' import {Config, Value, List, Variants} from '${startSdk}/lib/config/builder'
import {WrapperData} from '${wrapperData}'
`) `)
const data = await inputData const data = await inputData
const namedConsts = new Set(["Config", "Value", "List"]) const namedConsts = new Set(["Config", "Value", "List"])
const configName = newConst("ConfigSpec", convertInputSpec(data)) const configNameRaw = newConst("configSpecRaw", convertInputSpec(data))
const configMatcherName = newConst( const configMatcherName = newConst(
"matchConfigSpec", "matchConfigSpec",
`${configName}.validator()`, `${configNameRaw}.validator`,
) )
outputLines.push( outputLines.push(
`export type ConfigSpec = typeof ${configMatcherName}._TYPE;`, `export type ConfigSpec = typeof ${configMatcherName}._TYPE;`,
) )
const configName = newConst(
"ConfigSpec",
`${configNameRaw} as Config<ConfigSpec, WrapperData, ConfigSpec>`,
)
return outputLines.join("\n") return outputLines.join("\n")
function newConst(key: string, data: string) { function newConst(key: string, data: string, type?: string) {
const variableName = getNextConstName(camelCase(key)) const variableName = getNextConstName(camelCase(key))
outputLines.push(`export const ${variableName} = ${data};`) outputLines.push(
`export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`,
)
return variableName return variableName
} }
function maybeNewConst(key: string, data: string) { function maybeNewConst(key: string, data: string) {