From a30ed1f0ab2f379ef864deab2a4ce9023fe0bbdf Mon Sep 17 00:00:00 2001 From: BluJ Date: Mon, 1 May 2023 13:04:48 -0600 Subject: [PATCH] chore: Update the lazy config --- lib/actions/createAction.ts | 50 +++-- lib/config/builder/builder.ts | 10 - lib/config/builder/config.ts | 78 +++---- lib/config/builder/list.ts | 150 +++++++------ lib/config/builder/value.ts | 310 ++++++++++++++++---------- lib/config/builder/variants.ts | 88 ++++---- lib/config/configTypes.ts | 4 +- lib/config/setupConfig.ts | 31 +-- lib/mainFn/NetworkInterfaceBuilder.ts | 2 +- lib/mainFn/Origin.ts | 4 +- lib/test/configBuilder.test.ts | 133 ++++++----- lib/test/configTypes.test.ts | 9 +- lib/test/makeOutput.ts | 3 +- lib/test/output.wrapperData.ts | 1 + lib/types.ts | 7 +- lib/util/index.ts | 1 - lib/util/nullIfEmpty.ts | 4 +- lib/util/propertiesMatcher.ts | 264 ---------------------- scripts/oldSpecToBuilder.ts | 23 +- 19 files changed, 505 insertions(+), 667 deletions(-) delete mode 100644 lib/config/builder/builder.ts create mode 100644 lib/test/output.wrapperData.ts delete mode 100644 lib/util/propertiesMatcher.ts diff --git a/lib/actions/createAction.ts b/lib/actions/createAction.ts index fc64eef..d4e5e8c 100644 --- a/lib/actions/createAction.ts +++ b/lib/actions/createAction.ts @@ -1,37 +1,38 @@ -import { Parser } from "ts-matches" import { Config } from "../config/builder" import { ActionMetaData, ActionResult, Effects, ExportedAction } from "../types" -import { Utils, once, utils } from "../util" -import { TypeFromProps } from "../util/propertiesMatcher" -import { InputSpec } from "../config/configTypes" +import { Utils, utils } from "../util" -export class CreatedAction> { +export class CreatedAction> { private constructor( - private myMetaData: Omit & { input: Input }, + private myMetaData: Omit & { + input: Config + }, readonly fn: (options: { effects: Effects utils: Utils - input: TypeFromProps + input: Type }) => Promise, ) {} - private validator = this.myMetaData.input.validator() as Parser< - unknown, - TypeFromProps - > - metaData = { - ...this.myMetaData, - input: this.myMetaData.input.build(), - } + private validator = this.myMetaData.input.validator - static of>( - metaData: Omit & { input: Input }, + static of< + WrapperData, + Input extends Config, + Type extends Record = (Input extends Config + ? B + : never) & + Record, + >( + metaData: Omit & { + input: Config + }, fn: (options: { effects: Effects utils: Utils - input: TypeFromProps + input: Type }) => Promise, ) { - return new CreatedAction(metaData, fn) + return new CreatedAction(metaData, fn) } exportedAction: ExportedAction = ({ effects, input }) => { @@ -43,7 +44,16 @@ export class CreatedAction> { } async exportAction(effects: Effects) { - await effects.exportAction(this.metaData) + const myUtils = utils(effects) + const metaData = { + ...this.myMetaData, + input: await this.myMetaData.input.build({ + effects, + utils: myUtils, + config: null, + }), + } + await effects.exportAction(metaData) } } diff --git a/lib/config/builder/builder.ts b/lib/config/builder/builder.ts deleted file mode 100644 index 33805de..0000000 --- a/lib/config/builder/builder.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { _ } from "../../util" -export class IBuilder { - protected constructor(readonly a: A) {} - - public build(): A { - return this.a - } -} - -export type BuilderExtract = A extends IBuilder ? B : never diff --git a/lib/config/builder/config.ts b/lib/config/builder/config.ts index 2f2bfb3..110903f 100644 --- a/lib/config/builder/config.ts +++ b/lib/config/builder/config.ts @@ -1,9 +1,20 @@ -import { InputSpec, ValueSpec } from "../configTypes" -import { typeFromProps } from "../../util" -import { BuilderExtract, IBuilder } from "./builder" +import { ValueSpec } from "../configTypes" +import { Utils } from "../../util" import { Value } from "./value" import { _ } from "../../util" +import { Effects } from "../../types" +import { Parser, object } from "ts-matches" +export type LazyBuildOptions = { + effects: Effects + utils: Utils + config: ConfigType | null +} +export type LazyBuild = ( + options: LazyBuildOptions, +) => Promise | ExpectedOut + +export type MaybeLazyValues = LazyBuild | A /** * Configs are the specs that are used by the os configuration form for this service. * Here is an example of a simple configuration @@ -60,44 +71,33 @@ export const addNodesSpec = Config.of({ hostname: hostname, port: port }); ``` */ -export class Config extends IBuilder { - static empty() { - return new Config({}) - } - static withValue( - key: K, - value: Value, - ) { - return Config.empty().withValue(key, value) - } - static addValue( - key: K, - value: Value, - ) { - return Config.empty().withValue(key, value) - } - - static of }>(spec: B) { - const answer: { [K in keyof B]: BuilderExtract } = {} as any - for (const key in spec) { - answer[key] = spec[key].build() as any +export class Config, WD, ConfigType> { + private constructor( + private readonly spec: { + [K in keyof Type]: Value + }, + public validator: Parser, + ) {} + async build(options: LazyBuildOptions) { + const answer = {} as { + [K in keyof Type]: ValueSpec } - return new Config(answer) - } - withValue(key: K, value: Value) { - return new Config({ - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }) - } - addValue(key: K, value: Value) { - return new Config({ - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }) + for (const k in this.spec) { + answer[k] = await this.spec[k].build(options) + } + return answer } - public validator() { - return typeFromProps(this.a) + static of, Manifest, ConfigType>(spec: { + [K in keyof Type]: Value + }) { + const validatorObj = {} as { + [K in keyof Type]: Parser + } + for (const key in spec) { + validatorObj[key] = spec[key].validator + } + const validator = object(validatorObj) + return new Config(spec, validator) } } diff --git a/lib/config/builder/list.ts b/lib/config/builder/list.ts index 2e4ee8f..1cf09aa 100644 --- a/lib/config/builder/list.ts +++ b/lib/config/builder/list.ts @@ -1,13 +1,11 @@ -import { BuilderExtract, IBuilder } from "./builder" -import { Config } from "./config" +import { Config, LazyBuild } from "./config" import { - InputSpec, ListValueSpecText, Pattern, UniqueBy, ValueSpecList, } from "../configTypes" -import { guardAll } from "../../util" +import { Parser, arrayOf, number, string } from "ts-matches" /** * Used as a subtype of Value.list ```ts @@ -21,8 +19,12 @@ export const authorizationList = List.string({ export const auth = Value.list(authorizationList); ``` */ -export class List extends IBuilder { - static text( +export class List { + private constructor( + public build: LazyBuild, + public validator: Parser, + ) {} + static text( a: { name: string description?: string | null @@ -43,27 +45,29 @@ export class List extends IBuilder { inputmode?: ListValueSpecText["inputmode"] }, ) { - const spec = { - type: "text" as const, - placeholder: null, - minLength: null, - maxLength: null, - masked: false, - inputmode: "text" as const, - ...aSpec, - } - return new List({ - description: null, - warning: null, - default: [], - type: "list" as const, - minLength: null, - maxLength: null, - ...a, - spec, - }) + return new List(() => { + const spec = { + type: "text" as const, + placeholder: null, + minLength: null, + maxLength: null, + masked: false, + inputmode: "text" as const, + ...aSpec, + } + return { + description: null, + warning: null, + default: [], + type: "list" as const, + minLength: null, + maxLength: null, + ...a, + spec, + } + }, arrayOf(string)) } - static number( + static number( a: { name: string description?: string | null @@ -82,27 +86,29 @@ export class List extends IBuilder { placeholder?: string | null }, ) { - const spec = { - type: "number" as const, - placeholder: null, - min: null, - max: null, - step: null, - units: null, - ...aSpec, - } - return new List({ - description: null, - warning: null, - minLength: null, - maxLength: null, - default: [], - type: "list" as const, - ...a, - spec, - }) + return new List(() => { + const spec = { + type: "number" as const, + placeholder: null, + min: null, + max: null, + step: null, + units: null, + ...aSpec, + } + return { + description: null, + warning: null, + minLength: null, + maxLength: null, + default: [], + type: "list" as const, + ...a, + spec, + } + }, arrayOf(number)) } - static obj>( + static obj, WrapperData, ConfigType>( a: { name: string description?: string | null @@ -113,36 +119,34 @@ export class List extends IBuilder { maxLength?: number | null }, aSpec: { - spec: Spec + spec: Config displayAs?: null | string uniqueBy?: null | UniqueBy }, ) { - const { spec: previousSpecSpec, ...restSpec } = aSpec - const specSpec = previousSpecSpec.build() as BuilderExtract - const spec = { - type: "object" as const, - displayAs: null, - uniqueBy: null, - ...restSpec, - spec: specSpec, - } - const value = { - spec, - default: [], - ...a, - } - return new List({ - description: null, - warning: null, - minLength: null, - maxLength: null, - type: "list" as const, - ...value, - }) - } - - public validator() { - return guardAll(this.a) + return new List(async (options) => { + const { spec: previousSpecSpec, ...restSpec } = aSpec + const specSpec = await previousSpecSpec.build(options) + const spec = { + type: "object" as const, + displayAs: null, + uniqueBy: null, + ...restSpec, + spec: specSpec, + } + const value = { + spec, + default: [], + ...a, + } + return { + description: null, + warning: null, + minLength: null, + maxLength: null, + type: "list" as const, + ...value, + } + }, arrayOf(aSpec.spec.validator)) } } diff --git a/lib/config/builder/value.ts b/lib/config/builder/value.ts index d1dd1c6..a7f9e82 100644 --- a/lib/config/builder/value.ts +++ b/lib/config/builder/value.ts @@ -1,22 +1,28 @@ -import { BuilderExtract, IBuilder } from "./builder" -import { Config } from "./config" +import { Config, LazyBuild } from "./config" import { List } from "./list" import { Variants } from "./variants" import { - InputSpec, Pattern, ValueSpec, - ValueSpecColor, ValueSpecDatetime, - ValueSpecList, - ValueSpecNumber, - ValueSpecSelect, ValueSpecText, ValueSpecTextarea, } from "../configTypes" -import { guardAll } from "../../util" +import { once } from "../../util" import { DefaultString } from "../configTypes" import { _ } from "../../util" +import { + Parser, + anyOf, + arrayOf, + boolean, + literal, + literals, + number, + object, + string, + unknown, +} from "ts-matches" type RequiredDefault = | false @@ -40,6 +46,30 @@ function requiredLikeToAbove, A>( ) }; } +type AsRequired = MaybeRequiredType extends + | { default: unknown } + | never + ? Type + : Type | null | undefined + +type InputAsRequired = 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 + | Parser, +>(parser: Parser, 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. * Something like a boolean, a string, a number, etc. @@ -62,22 +92,29 @@ const username = Value.string({ }); ``` */ -export class Value extends IBuilder { - static toggle(a: { +export class Value { + private constructor( + public build: LazyBuild, + public validator: Parser, + ) {} + static toggle(a: { name: string description?: string | null warning?: string | null default?: boolean | null }) { - return new Value({ - description: null, - warning: null, - default: null, - type: "toggle" as const, - ...a, - }) + return new Value( + async () => ({ + description: null, + warning: null, + default: null, + type: "toggle" as const, + ...a, + }), + boolean, + ) } - static text>(a: { + static text, WD, CT>(a: { name: string description?: string | null warning?: string | null @@ -92,21 +129,24 @@ export class Value extends IBuilder { /** Default = 'text' */ inputmode?: ValueSpecText["inputmode"] }) { - return new Value({ - type: "text" as const, - description: null, - warning: null, - masked: false, - placeholder: null, - minLength: null, - maxLength: null, - patterns: [], - inputmode: "text", - ...a, - ...requiredLikeToAbove(a.required), - }) + return new Value, WD, CT>( + async () => ({ + type: "text" as const, + description: null, + warning: null, + masked: false, + placeholder: null, + minLength: null, + maxLength: null, + patterns: [], + inputmode: "text", + ...a, + ...requiredLikeToAbove(a.required), + }), + asRequiredParser(string, a), + ) } - static textarea(a: { + static textarea(a: { name: string description?: string | null warning?: string | null @@ -115,17 +155,21 @@ export class Value extends IBuilder { maxLength?: number | null placeholder?: string | null }) { - return new Value({ - description: null, - warning: null, - minLength: null, - maxLength: null, - placeholder: null, - type: "textarea" as const, - ...a, - } as ValueSpecTextarea) + return new Value( + async () => + ({ + description: null, + warning: null, + minLength: null, + maxLength: null, + placeholder: null, + type: "textarea" as const, + ...a, + } satisfies ValueSpecTextarea), + string, + ) } - static number>(a: { + static number, WD, CT>(a: { name: string description?: string | null warning?: string | null @@ -138,34 +182,41 @@ export class Value extends IBuilder { units?: string | null placeholder?: string | null }) { - return new Value({ - type: "number" as const, - description: null, - warning: null, - min: null, - max: null, - step: null, - units: null, - placeholder: null, - ...a, - ...requiredLikeToAbove(a.required), - }) + return new Value, WD, CT>( + () => ({ + type: "number" as const, + description: null, + warning: null, + min: null, + max: null, + step: null, + units: null, + placeholder: null, + ...a, + ...requiredLikeToAbove(a.required), + }), + asRequiredParser(number, a), + ) } - static color>(a: { + static color, WD, CT>(a: { name: string description?: string | null warning?: string | null required: Required }) { - return new Value({ - type: "color" as const, - description: null, - warning: null, - ...a, - ...requiredLikeToAbove(a.required), - }) + return new Value, WD, CT>( + () => ({ + type: "color" as const, + description: null, + warning: null, + ...a, + ...requiredLikeToAbove(a.required), + }), + + asRequiredParser(string, a), + ) } - static datetime>(a: { + static datetime, WD, CT>(a: { name: string description?: string | null warning?: string | null @@ -176,21 +227,26 @@ export class Value extends IBuilder { max?: string | null step?: string | null }) { - return new Value({ - type: "datetime" as const, - description: null, - warning: null, - inputmode: "datetime-local", - min: null, - max: null, - step: null, - ...a, - ...requiredLikeToAbove(a.required), - }) + return new Value, WD, CT>( + () => ({ + type: "datetime" as const, + description: null, + warning: null, + inputmode: "datetime-local", + min: null, + max: null, + step: null, + ...a, + ...requiredLikeToAbove(a.required), + }), + asRequiredParser(string, a), + ) } static select< Required extends RequiredDefault, B extends Record, + WD, + CT, >(a: { name: string description?: string | null @@ -198,15 +254,23 @@ export class Value extends IBuilder { required: Required values: B }) { - return new Value({ - description: null, - warning: null, - type: "select" as const, - ...a, - ...requiredLikeToAbove(a.required), - }) + return new Value, WD, CT>( + () => ({ + description: null, + warning: null, + type: "select" as const, + ...a, + ...requiredLikeToAbove(a.required), + }), + asRequiredParser( + anyOf( + ...Object.keys(a.values).map((x: keyof B & string) => literal(x)), + ), + a, + ) as any, + ) } - static multiselect>(a: { + static multiselect, WD, CT>(a: { name: string description?: string | null warning?: string | null @@ -215,35 +279,44 @@ export class Value extends IBuilder { minLength?: number | null maxLength?: number | null }) { - return new Value({ - type: "multiselect" as const, - minLength: null, - maxLength: null, - warning: null, - description: null, - ...a, - }) + return new Value<(keyof Values)[], WD, CT>( + () => ({ + type: "multiselect" as const, + minLength: null, + maxLength: null, + warning: null, + description: null, + ...a, + }), + arrayOf( + literals(...(Object.keys(a.values) as any as [keyof Values & string])), + ), + ) } - static object>( + static object, WrapperData, ConfigType>( a: { name: string description?: string | null warning?: string | null }, - previousSpec: Spec, + previousSpec: Config, ) { - const spec = previousSpec.build() as BuilderExtract - return new Value({ - type: "object" as const, - description: null, - warning: null, - ...a, - spec, - }) + return new Value(async (options) => { + const spec = await previousSpec.build(options as any) + return { + type: "object" as const, + description: null, + warning: null, + ...a, + spec, + } + }, previousSpec.validator) } static union< Required extends RequiredDefault, - V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>, + Type, + WrapperData, + ConfigType, >( a: { name: string @@ -252,23 +325,28 @@ export class Value extends IBuilder { required: Required default?: string | null }, - aVariants: V, + aVariants: Variants, ) { - const variants = aVariants.build() as BuilderExtract - return new Value({ - type: "union" as const, - description: null, - warning: null, - ...a, - variants, - ...requiredLikeToAbove(a.required), - }) + return new Value, WrapperData, ConfigType>( + async (options) => ({ + type: "union" as const, + description: null, + warning: null, + ...a, + variants: await aVariants.build(options as any), + ...requiredLikeToAbove(a.required), + }), + asRequiredParser(aVariants.validator, a), + ) } - static list(a: List) { - return new Value(a.build()) - } - public validator() { - return guardAll(this.a) + static list( + a: List, + ) { + /// TODO + return new Value( + (options) => a.build(options), + a.validator, + ) } } diff --git a/lib/config/builder/variants.ts b/lib/config/builder/variants.ts index 5d9c7c0..0aa39a5 100644 --- a/lib/config/builder/variants.ts +++ b/lib/config/builder/variants.ts @@ -1,6 +1,7 @@ -import { InputSpec } from "../configTypes" -import { BuilderExtract, IBuilder } from "./builder" +import { InputSpec, ValueSpecUnion } from "../configTypes" import { Config } from "." +import { LazyBuild } from "./config" +import { Parser, anyOf, literals, object } from "ts-matches" /** * Used in the the Value.select { @link './value.ts' } @@ -51,46 +52,55 @@ export const pruning = Value.union( ); ``` */ -export class Variants< - A extends { - [key: string]: { - name: string - spec: InputSpec - } - }, -> extends IBuilder { +export class Variants { + private constructor( + public build: LazyBuild, + public validator: Parser, + ) {} + // A extends { + // [key: string]: { + // name: string + // spec: InputSpec + // } + // }, static of< - A extends { - [key: string]: { name: string; spec: Config } - }, - >(a: A) { - const variants: { - [K in keyof A]: { name: string; spec: BuilderExtract } - } = {} as any - for (const key in a) { - const value = a[key] - variants[key] = { - name: value.name, - spec: value.spec.build() as any, - } + TypeMap extends Record>, + WrapperData, + ConfigType, + >(a: { + [K in keyof TypeMap]: { + name: string + spec: Config } - return new Variants(variants) - } + }) { + type TypeOut = { + [K in keyof TypeMap & string]: { + unionSelectKey: K + unionValueKey: TypeMap[K] + } + }[keyof TypeMap & string] - static empty() { - return Variants.of({}) - } - static withVariant( - key: K, - value: Config, - ) { - return Variants.empty().withVariant(key, value) - } + const validator = anyOf( + ...Object.entries(a).map(([name, { spec }]) => + object({ + unionSelectKey: literals(name), + unionValueKey: spec.validator, + }), + ), + ) as Parser - withVariant(key: K, value: Config) { - return new Variants({ - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }) + return new Variants(async (options) => { + const variants = {} as { + [K in keyof TypeMap]: { name: string; spec: InputSpec } + } + for (const key in a) { + const value = a[key] + variants[key] = { + name: value.name, + spec: await value.spec.build(options), + } + } + return variants + }, validator) } } diff --git a/lib/config/configTypes.ts b/lib/config/configTypes.ts index d9928e8..29a3899 100644 --- a/lib/config/configTypes.ts +++ b/lib/config/configTypes.ts @@ -189,10 +189,10 @@ export type DefaultString = // sometimes the type checker needs just a little bit of help export function isValueSpecListOf( - t: ValueSpecListOf, + t: ValueSpec, s: S, ): t is ValueSpecListOf & { spec: ListValueSpecOf } { - return t.spec.type === s + return "spec" in t && t.spec.type === s } export const unionSelectKey = "unionSelectKey" as const export type UnionSelectKey = typeof unionSelectKey diff --git a/lib/config/setupConfig.ts b/lib/config/setupConfig.ts index 892509a..42cf321 100644 --- a/lib/config/setupConfig.ts +++ b/lib/config/setupConfig.ts @@ -2,7 +2,6 @@ import { Config } from "./builder" import { DeepPartial, Dependencies, Effects, ExpectedExports } from "../types" import { InputSpec } from "./configTypes" import { Utils, nullIfEmpty, once, utils } from "../util" -import { TypeFromProps } from "../util/propertiesMatcher" import { GenericManifest } from "../manifest/ManifestTypes" import * as D from "./dependencies" @@ -30,18 +29,18 @@ export type Read = (options: { */ export function setupConfig< WD, - A extends Config, + Type extends Record, Manifest extends GenericManifest, >( - spec: A, - write: Save, Manifest>, - read: Read>, + spec: Config, + write: Save, + read: Read, ) { - const validator = once(() => spec.validator()) + const validator = spec.validator return { setConfig: (async ({ effects, input }) => { - if (!validator().test(input)) { - await effects.console.error(String(validator().errorMessage(input))) + if (!validator.test(input)) { + await effects.console.error(String(validator.errorMessage(input))) return { error: "Set config type error for config" } } await write({ @@ -51,12 +50,18 @@ export function setupConfig< dependencies: D.dependenciesSet(), }) }) as ExpectedExports.setConfig, - getConfig: (async ({ effects, config }) => { + getConfig: (async ({ effects }) => { + const myUtils = utils(effects) + const configValue = nullIfEmpty( + (await read({ effects, utils: myUtils })) || null, + ) return { - spec: spec.build(), - config: nullIfEmpty( - (await read({ effects, utils: utils(effects) })) || null, - ), + spec: await spec.build({ + effects, + utils: myUtils, + config: configValue, + }), + config: configValue, } }) as ExpectedExports.getConfig, } diff --git a/lib/mainFn/NetworkInterfaceBuilder.ts b/lib/mainFn/NetworkInterfaceBuilder.ts index b7cb50d..46f2e4f 100644 --- a/lib/mainFn/NetworkInterfaceBuilder.ts +++ b/lib/mainFn/NetworkInterfaceBuilder.ts @@ -10,7 +10,7 @@ export class NetworkInterfaceBuilder { id: string description: string ui: boolean - basic?: null | { password: string; username: string } + basic?: null | { password: null | string; username: string } path?: string search?: Record }, diff --git a/lib/mainFn/Origin.ts b/lib/mainFn/Origin.ts index 02b964c..fbe3295 100644 --- a/lib/mainFn/Origin.ts +++ b/lib/mainFn/Origin.ts @@ -4,14 +4,14 @@ export class Origin { withAuth( origin?: | { - password: string + password: null | string username: string } | null | undefined, ) { // 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}` } diff --git a/lib/test/configBuilder.test.ts b/lib/test/configBuilder.test.ts index c04f82a..39daf4a 100644 --- a/lib/test/configBuilder.test.ts +++ b/lib/test/configBuilder.test.ts @@ -3,22 +3,21 @@ import { Config } from "../config/builder/config" import { List } from "../config/builder/list" import { Value } from "../config/builder/value" import { Variants } from "../config/builder/variants" +import { ValueSpec } from "../config/configTypes" +import { Parser } from "ts-matches" +type test = unknown | { test: 5 } describe("builder tests", () => { - test("text", () => { + test("text", async () => { const bitcoinPropertiesBuilt: { - "peer-tor-address": { - name: string - description: string | null - type: "text" - } - } = Config.of({ + "peer-tor-address": ValueSpec + } = await Config.of({ "peer-tor-address": Value.text({ name: "Peer tor address", description: "The Tor address of the peer interface", required: { default: null }, }), - }).build() + }).build({} as any) expect(JSON.stringify(bitcoinPropertiesBuilt)).toEqual( /*json*/ `{ "peer-tor-address": { @@ -43,62 +42,62 @@ describe("builder tests", () => { }) describe("values", () => { - test("toggle", () => { + test("toggle", async () => { const value = Value.toggle({ name: "Testing", description: null, warning: null, default: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast(false) testOutput()(null) }) - test("text", () => { + test("text", async () => { const value = Value.text({ name: "Testing", required: { default: null }, }) - const validator = value.validator() - const rawIs = value.build() + const validator = value.validator + const rawIs = await value.build({} as any) validator.unsafeCast("test text") expect(() => validator.unsafeCast(null)).toThrowError() testOutput()(null) }) - test("text", () => { + test("text", async () => { const value = Value.text({ name: "Testing", required: { default: "null" }, }) - const validator = value.validator() - const rawIs = value.build() + const validator = value.validator + const rawIs = await value.build({} as any) validator.unsafeCast("test text") expect(() => validator.unsafeCast(null)).toThrowError() testOutput()(null) }) - test("optional text", () => { + test("optional text", async () => { const value = Value.text({ name: "Testing", required: false, }) - const validator = value.validator() - const rawIs = value.build() + const validator = value.validator + const rawIs = await value.build({} as any) validator.unsafeCast("test text") validator.unsafeCast(null) testOutput()(null) }) - test("color", () => { + test("color", async () => { const value = Value.color({ name: "Testing", required: false, description: null, warning: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("#000000") testOutput()(null) }) - test("datetime", () => { + test("datetime", async () => { const value = Value.datetime({ name: "Testing", required: { default: null }, @@ -109,11 +108,11 @@ describe("values", () => { max: null, step: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("2021-01-01") testOutput()(null) }) - test("optional datetime", () => { + test("optional datetime", async () => { const value = Value.datetime({ name: "Testing", required: false, @@ -124,11 +123,11 @@ describe("values", () => { max: null, step: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("2021-01-01") testOutput()(null) }) - test("textarea", () => { + test("textarea", async () => { const value = Value.textarea({ name: "Testing", required: false, @@ -138,11 +137,11 @@ describe("values", () => { maxLength: null, placeholder: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("test text") testOutput()(null) }) - test("number", () => { + test("number", async () => { const value = Value.number({ name: "Testing", required: { default: null }, @@ -155,11 +154,11 @@ describe("values", () => { units: null, placeholder: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast(2) testOutput()(null) }) - test("optional number", () => { + test("optional number", async () => { const value = Value.number({ name: "Testing", required: false, @@ -172,11 +171,11 @@ describe("values", () => { units: null, placeholder: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast(2) testOutput()(null) }) - test("select", () => { + test("select", async () => { const value = Value.select({ name: "Testing", required: { default: null }, @@ -187,13 +186,13 @@ describe("values", () => { description: null, warning: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("a") validator.unsafeCast("b") expect(() => validator.unsafeCast(null)).toThrowError() testOutput()(null) }) - test("nullable select", () => { + test("nullable select", async () => { const value = Value.select({ name: "Testing", required: false, @@ -204,13 +203,13 @@ describe("values", () => { description: null, warning: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast("a") validator.unsafeCast("b") validator.unsafeCast(null) testOutput()(null) }) - test("multiselect", () => { + test("multiselect", async () => { const value = Value.multiselect({ name: "Testing", values: { @@ -223,12 +222,15 @@ describe("values", () => { minLength: null, maxLength: null, }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast([]) validator.unsafeCast(["a", "b"]) + + expect(() => validator.unsafeCast(["e"])).toThrowError() + expect(() => validator.unsafeCast([4])).toThrowError() testOutput>()(null) }) - test("object", () => { + test("object", async () => { const value = Value.object( { name: "Testing", @@ -244,11 +246,11 @@ describe("values", () => { }), }), ) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: true }) testOutput()(null) }) - test("union", () => { + test("union", async () => { const value = Value.union( { name: "Testing", @@ -271,14 +273,14 @@ describe("values", () => { }, }), ) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } }) type Test = typeof validator._TYPE testOutput()( null, ) }) - test("list", () => { + test("list", async () => { const value = Value.list( List.number( { @@ -289,14 +291,14 @@ describe("values", () => { }, ), ) - const validator = value.validator() + const validator = value.validator validator.unsafeCast([1, 2, 3]) testOutput()(null) }) }) describe("Builder List", () => { - test("obj", () => { + test("obj", async () => { const value = Value.list( List.obj( { @@ -314,11 +316,11 @@ describe("Builder List", () => { }, ), ) - const validator = value.validator() + const validator = value.validator validator.unsafeCast([{ test: true }]) testOutput()(null) }) - test("text", () => { + test("text", async () => { const value = Value.list( List.text( { @@ -329,26 +331,14 @@ describe("Builder List", () => { }, ), ) - const validator = value.validator() + const validator = value.validator validator.unsafeCast(["test", "text"]) testOutput()(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", () => { - test("Testing text", () => { + test("Testing text", async () => { const value = Config.of({ a: Value.text({ name: "Temp Name", @@ -357,13 +347,13 @@ describe("Nested nullable values", () => { required: false, }), }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: null }) validator.unsafeCast({ a: "test" }) expect(() => validator.unsafeCast({ a: 4 })).toThrowError() testOutput()(null) }) - test("Testing number", () => { + test("Testing number", async () => { const value = Config.of({ a: Value.number({ name: "Temp Name", @@ -379,13 +369,13 @@ describe("Nested nullable values", () => { units: null, }), }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: null }) validator.unsafeCast({ a: 5 }) expect(() => validator.unsafeCast({ a: "4" })).toThrowError() testOutput()(null) }) - test("Testing color", () => { + test("Testing color", async () => { const value = Config.of({ a: Value.color({ name: "Temp Name", @@ -395,13 +385,13 @@ describe("Nested nullable values", () => { warning: null, }), }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: null }) validator.unsafeCast({ a: "5" }) expect(() => validator.unsafeCast({ a: 4 })).toThrowError() testOutput()(null) }) - test("Testing select", () => { + test("Testing select", async () => { const value = Config.of({ a: Value.select({ name: "Temp Name", @@ -414,7 +404,7 @@ describe("Nested nullable values", () => { }, }), }) - const higher = Value.select({ + const higher = await Value.select({ name: "Temp Name", description: "If no name is provided, the name from config will be used", required: false, @@ -422,15 +412,15 @@ describe("Nested nullable values", () => { values: { a: "A", }, - }).build() + }).build({} as any) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: null }) validator.unsafeCast({ a: "a" }) expect(() => validator.unsafeCast({ a: "4" })).toThrowError() testOutput()(null) }) - test("Testing multiselect", () => { + test("Testing multiselect", async () => { const value = Config.of({ a: Value.multiselect({ name: "Temp Name", @@ -446,9 +436,10 @@ describe("Nested nullable values", () => { maxLength: null, }), }) - const validator = value.validator() + const validator = value.validator validator.unsafeCast({ a: [] }) validator.unsafeCast({ a: ["a"] }) + expect(() => validator.unsafeCast({ a: ["4"] })).toThrowError() expect(() => validator.unsafeCast({ a: "4" })).toThrowError() testOutput()(null) }) diff --git a/lib/test/configTypes.test.ts b/lib/test/configTypes.test.ts index e535f07..a50ec6c 100644 --- a/lib/test/configTypes.test.ts +++ b/lib/test/configTypes.test.ts @@ -4,11 +4,14 @@ import { List } from "../config/builder/list" import { Value } from "../config/builder/value" describe("Config Types", () => { - test("isValueSpecListOf", () => { + test("isValueSpecListOf", async () => { const options = [List.obj, List.text, List.number] for (const option of options) { - const test = option({} as any, { spec: Config.of({}) } as any) as any - const someList = Value.list(test).build() + const test = (option as any)( + {} as any, + { spec: Config.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, "number")) { diff --git a/lib/test/makeOutput.ts b/lib/test/makeOutput.ts index 5d0143b..d4eb063 100644 --- a/lib/test/makeOutput.ts +++ b/lib/test/makeOutput.ts @@ -423,6 +423,7 @@ oldSpecToBuilder( }, { // convert this to `start-sdk/lib` for conversions - startSdk: "..", + startSdk: "../..", + wrapperData: "./output.wrapperData", }, ) diff --git a/lib/test/output.wrapperData.ts b/lib/test/output.wrapperData.ts new file mode 100644 index 0000000..74bb0bf --- /dev/null +++ b/lib/test/output.wrapperData.ts @@ -0,0 +1 @@ +export type WrapperData = {} diff --git a/lib/types.ts b/lib/types.ts index 1b36cb6..916bae6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -15,10 +15,7 @@ export namespace ExpectedExports { input: Record }) => Promise /** 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: { - effects: Effects - config: unknown - }) => Promise + export type getConfig = (options: { effects: Effects }) => Promise // /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ // export type dependencies = Dependencies; /** For backing up service data though the startOS UI */ @@ -407,7 +404,7 @@ never // prettier-ignore export type EnsureWrapperDataPath = _EnsureWrapperDataPath -/* rsync options: https://linux.die.net/man/1/rsync +/** rsync options: https://linux.die.net/man/1/rsync */ export type BackupOptions = { delete: boolean diff --git a/lib/util/index.ts b/lib/util/index.ts index c8418dc..9ffe355 100644 --- a/lib/util/index.ts +++ b/lib/util/index.ts @@ -11,7 +11,6 @@ import { import { LocalBinding, LocalPort, NetworkBuilder, TorHostname } from "../mainFn" import { ExtractWrapperData } from "../types" -export { guardAll, typeFromProps } from "./propertiesMatcher" export { default as nullIfEmpty } from "./nullIfEmpty" export { FileHelper } from "./fileHelper" export { getWrapperData } from "./getWrapperData" diff --git a/lib/util/nullIfEmpty.ts b/lib/util/nullIfEmpty.ts index 0777b45..337b909 100644 --- a/lib/util/nullIfEmpty.ts +++ b/lib/util/nullIfEmpty.ts @@ -4,7 +4,9 @@ * @param s * @returns */ -export default function nullIfEmpty(s: null | Record) { +export default function nullIfEmpty>( + s: null | A, +) { if (s === null) return null return Object.keys(s).length === 0 ? null : s } diff --git a/lib/util/propertiesMatcher.ts b/lib/util/propertiesMatcher.ts deleted file mode 100644 index df6987d..0000000 --- a/lib/util/propertiesMatcher.ts +++ /dev/null @@ -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 extends { required: false; default: null | undefined | never } ? Type | undefined | null: - Type - -// prettier-ignore -type GuardNumber = - A extends { type: TypeNumber } ? GuardDefaultRequired : - unknown -// prettier-ignore -type GuardText = - A extends { type: TypeText } ? GuardDefaultRequired : - unknown -// prettier-ignore -type GuardTextarea = - A extends { type: TypeTextarea } ? GuardDefaultRequired : - unknown -// prettier-ignore -type GuardToggle = - A extends { type: TypeToggle } ? GuardDefaultRequired : - unknown - -type TrueKeyOf = _ extends Record ? keyof T : never -// prettier-ignore -type GuardObject = - A extends { type: TypeObject, spec: infer B } ? ( - { [K in TrueKeyOf & string]: _> } - ) : - unknown -// prettier-ignore -export type GuardList = - A extends { type: TypeList, spec?: { type: infer B, spec?: infer C } } ? Array & ({ type: B, spec: C })>> : - A extends { type: TypeList, spec?: { type: infer B } } ? Array & ({ type: B })>> : - unknown -// prettier-ignore -type GuardSelect = - A extends { type: TypeSelect, values: infer B } ? ( - GuardDefaultRequired> - ) : - unknown -// prettier-ignore -type GuardMultiselect = - A extends { type: TypeMultiselect, values: infer B} ?(keyof B)[] : -unknown -// prettier-ignore -type GuardColor = - A extends { type: TypeColor } ? GuardDefaultRequired : - unknown -// prettier-ignore -type GuardDatetime = -A extends { type: TypeDatetime } ? GuardDefaultRequired : -unknown -type AsString = A extends - | string - | number - | bigint - | boolean - | null - | undefined - ? `${A}` - : "UnknownValue" -// prettier-ignore -type VariantValue = - A extends { name: string, spec: infer B } ? TypeFromProps<_> : - `neverVariantValue${AsString}` -// prettier-ignore -type GuardUnion = - A extends { type: TypeUnion, variants: infer Variants & Record } ? ( - _<{[key in keyof Variants]: {[k in UnionSelectKey]: key} & {[k in UnionValueKey]: VariantValue}}[keyof Variants]> - ) : - unknown - -export type GuardAll = GuardNumber & - GuardText & - GuardTextarea & - GuardToggle & - GuardObject & - GuardList & - GuardUnion & - GuardSelect & - GuardMultiselect & - GuardColor & - GuardDatetime -// prettier-ignore -export type TypeFromProps = - A extends Config ? TypeFromProps : - A extends Record ? { [K in keyof A & string]: _> } : - 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, value: unknown) { - if (matchInteger.test(value)) { - return parser.validate(Number.isInteger, "isIntegral") - } - return parser -} -function requiredParser(parser: Parser, 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( - value: A, -): Parser> { - 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( - valueDictionary: A, -): Parser> { - if (!recordString.test(valueDictionary)) return unknown as any - return object( - Object.fromEntries( - Object.entries(valueDictionary).map(([key, value]) => [ - key, - guardAll(value), - ]), - ), - ) as any -} diff --git a/scripts/oldSpecToBuilder.ts b/scripts/oldSpecToBuilder.ts index ecbdf0a..0a8e1be 100644 --- a/scripts/oldSpecToBuilder.ts +++ b/scripts/oldSpecToBuilder.ts @@ -29,29 +29,40 @@ function isString(x: unknown): x is string { export default async function makeFileContentFromOld( inputData: Promise | any, - { startSdk = "start-sdk", nested = true } = {}, + { + startSdk = "start-sdk", + nested = true, + wrapperData = "../../wrapperData", + } = {}, ) { const outputLines: string[] = [] 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 namedConsts = new Set(["Config", "Value", "List"]) - const configName = newConst("ConfigSpec", convertInputSpec(data)) + const configNameRaw = newConst("configSpecRaw", convertInputSpec(data)) const configMatcherName = newConst( "matchConfigSpec", - `${configName}.validator()`, + `${configNameRaw}.validator`, ) outputLines.push( `export type ConfigSpec = typeof ${configMatcherName}._TYPE;`, ) + const configName = newConst( + "ConfigSpec", + `${configNameRaw} as Config`, + ) return outputLines.join("\n") - function newConst(key: string, data: string) { + function newConst(key: string, data: string, type?: string) { const variableName = getNextConstName(camelCase(key)) - outputLines.push(`export const ${variableName} = ${data};`) + outputLines.push( + `export const ${variableName}${!type ? "" : `: ${type}`} = ${data};`, + ) return variableName } function maybeNewConst(key: string, data: string) {