import { Config, LazyBuild, LazyBuildOptions } from "./config" import { List } from "./list" import { Variants } from "./variants" import { FilePath, Pattern, RandomString, ValueSpec, ValueSpecDatetime, ValueSpecText, ValueSpecTextarea, } from "../configTypes" import { DefaultString } from "../configTypes" import { _ } from "../../util" import { Parser, anyOf, arrayOf, boolean, literal, literals, number, object, string, unknown, } from "ts-matches" import { once } from "../../util/once" export type RequiredDefault = | false | { default: A | null } function requiredLikeToAbove, A>( requiredLike: Input, ) { // prettier-ignore return { required: (typeof requiredLike === 'object' ? true : requiredLike) as ( Input extends { default: unknown} ? true: Input extends true ? true : false ), default:(typeof requiredLike === 'object' ? requiredLike.default : null) as ( Input extends { default: infer Default } ? Default : null ) }; } 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. * in the fe it will ask for the name of value, and use the rest of the value to determine how to render it. * While writing with a value, you will start with `Value.` then let the IDE suggest the rest. * for things like string, the options are going to be in {}. * Keep an eye out for another config builder types as params. * Note, usually this is going to be used in a `Config` {@link Config} builder. ```ts const username = Value.string({ name: "Username", default: "bitcoin", description: "The username for connecting to Bitcoin over RPC.", warning: null, required: true, masked: true, placeholder: null, pattern: "^[a-zA-Z0-9_]+$", patternDescription: "Must be alphanumeric (can contain underscore).", }); ``` */ export class Value { protected constructor( public build: LazyBuild, public validator: Parser, ) {} static toggle(a: { name: string description?: string | null warning?: string | null default: boolean /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value( async () => ({ description: null, warning: null, type: "toggle" as const, disabled: false, immutable: a.immutable ?? false, ...a, }), boolean, ) } static dynamicToggle( a: LazyBuild< Store, { name: string description?: string | null warning?: string | null default: boolean disabled?: false | string } >, ) { return new Value( async (options) => ({ description: null, warning: null, type: "toggle" as const, disabled: false, immutable: false, ...(await a(options)), }), boolean, ) } static text>(a: { name: string description?: string | null warning?: string | null required: Required /** Default = false */ masked?: boolean placeholder?: string | null minLength?: number | null maxLength?: number | null patterns?: Pattern[] /** Default = 'text' */ inputmode?: ValueSpecText["inputmode"] /** Immutable means it can only be configured at the first config then never again * Default is false */ immutable?: boolean generate?: null | RandomString }) { return new Value, never>( async () => ({ type: "text" as const, description: null, warning: null, masked: false, placeholder: null, minLength: null, maxLength: null, patterns: [], inputmode: "text", disabled: false, immutable: a.immutable ?? false, generate: a.generate ?? null, ...a, ...requiredLikeToAbove(a.required), }), asRequiredParser(string, a), ) } static dynamicText( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault /** Default = false */ masked?: boolean placeholder?: string | null minLength?: number | null maxLength?: number | null patterns?: Pattern[] /** Default = 'text' */ inputmode?: ValueSpecText["inputmode"] disabled?: string | false /** Immutable means it can only be configured at the first config then never again * Default is false */ generate?: null | RandomString } >, ) { return new Value(async (options) => { const a = await getA(options) return { type: "text" as const, description: null, warning: null, masked: false, placeholder: null, minLength: null, maxLength: null, patterns: [], inputmode: "text", disabled: false, immutable: false, generate: a.generate ?? null, ...a, ...requiredLikeToAbove(a.required), } }, string.optional()) } static textarea(a: { name: string description?: string | null warning?: string | null required: boolean minLength?: number | null maxLength?: number | null placeholder?: string | null /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value(async () => { const built: ValueSpecTextarea = { description: null, warning: null, minLength: null, maxLength: null, placeholder: null, type: "textarea" as const, disabled: false, immutable: a.immutable ?? false, ...a, } return built }, string) } static dynamicTextarea( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: boolean minLength?: number | null maxLength?: number | null placeholder?: string | null disabled?: false | string } >, ) { return new Value(async (options) => { const a = await getA(options) return { description: null, warning: null, minLength: null, maxLength: null, placeholder: null, type: "textarea" as const, disabled: false, immutable: false, ...a, } }, string) } static number>(a: { name: string description?: string | null warning?: string | null required: Required min?: number | null max?: number | null /** Default = '1' */ step?: number | null integer: boolean units?: string | null placeholder?: string | null /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value, never>( () => ({ type: "number" as const, description: null, warning: null, min: null, max: null, step: null, units: null, placeholder: null, disabled: false, immutable: a.immutable ?? false, ...a, ...requiredLikeToAbove(a.required), }), asRequiredParser(number, a), ) } static dynamicNumber( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault min?: number | null max?: number | null /** Default = '1' */ step?: number | null integer: boolean units?: string | null placeholder?: string | null disabled?: false | string } >, ) { return new Value(async (options) => { const a = await getA(options) return { type: "number" as const, description: null, warning: null, min: null, max: null, step: null, units: null, placeholder: null, disabled: false, immutable: false, ...a, ...requiredLikeToAbove(a.required), } }, number.optional()) } static color>(a: { name: string description?: string | null warning?: string | null required: Required /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value, never>( () => ({ type: "color" as const, description: null, warning: null, disabled: false, immutable: a.immutable ?? false, ...a, ...requiredLikeToAbove(a.required), }), asRequiredParser(string, a), ) } static dynamicColor( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault disabled?: false | string } >, ) { return new Value(async (options) => { const a = await getA(options) return { type: "color" as const, description: null, warning: null, disabled: false, immutable: false, ...a, ...requiredLikeToAbove(a.required), } }, string.optional()) } static datetime>(a: { name: string description?: string | null warning?: string | null required: Required /** Default = 'datetime-local' */ inputmode?: ValueSpecDatetime["inputmode"] min?: string | null max?: string | null /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value, never>( () => ({ type: "datetime" as const, description: null, warning: null, inputmode: "datetime-local", min: null, max: null, step: null, disabled: false, immutable: a.immutable ?? false, ...a, ...requiredLikeToAbove(a.required), }), asRequiredParser(string, a), ) } static dynamicDatetime( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault /** Default = 'datetime-local' */ inputmode?: ValueSpecDatetime["inputmode"] min?: string | null max?: string | null disabled?: false | string } >, ) { return new Value(async (options) => { const a = await getA(options) return { type: "datetime" as const, description: null, warning: null, inputmode: "datetime-local", min: null, max: null, disabled: false, immutable: false, ...a, ...requiredLikeToAbove(a.required), } }, string.optional()) } static select< Required extends RequiredDefault, B extends Record, >(a: { name: string description?: string | null warning?: string | null required: Required values: B /** * Disabled: false means that there is nothing disabled, good to modify * string means that this is the message displayed and the whole thing is disabled * string[] means that the options are disabled */ disabled?: false | string | (string & keyof B)[] /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean }) { return new Value, never>( () => ({ description: null, warning: null, type: "select" as const, disabled: false, immutable: a.immutable ?? false, ...a, ...requiredLikeToAbove(a.required), }), asRequiredParser( anyOf( ...Object.keys(a.values).map((x: keyof B & string) => literal(x)), ), a, ) as any, ) } static dynamicSelect( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null required: RequiredDefault values: Record /** * Disabled: false means that there is nothing disabled, good to modify * string means that this is the message displayed and the whole thing is disabled * string[] means that the options are disabled */ disabled?: false | string | string[] } >, ) { return new Value(async (options) => { const a = await getA(options) return { description: null, warning: null, type: "select" as const, disabled: false, immutable: false, ...a, ...requiredLikeToAbove(a.required), } }, string.optional()) } static multiselect>(a: { name: string description?: string | null warning?: string | null default: string[] values: Values minLength?: number | null maxLength?: number | null /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean /** * Disabled: false means that there is nothing disabled, good to modify * string means that this is the message displayed and the whole thing is disabled * string[] means that the options are disabled */ disabled?: false | string | (string & keyof Values)[] }) { return new Value<(keyof Values)[], never>( () => ({ type: "multiselect" as const, minLength: null, maxLength: null, warning: null, description: null, disabled: false, immutable: a.immutable ?? false, ...a, }), arrayOf( literals(...(Object.keys(a.values) as any as [keyof Values & string])), ), ) } static dynamicMultiselect( getA: LazyBuild< Store, { name: string description?: string | null warning?: string | null default: string[] values: Record minLength?: number | null maxLength?: number | null /** * Disabled: false means that there is nothing disabled, good to modify * string means that this is the message displayed and the whole thing is disabled * string[] means that the options are disabled */ disabled?: false | string | string[] } >, ) { return new Value(async (options) => { const a = await getA(options) return { type: "multiselect" as const, minLength: null, maxLength: null, warning: null, description: null, disabled: false, immutable: false, ...a, } }, arrayOf(string)) } static object, Store>( a: { name: string description?: string | null warning?: string | null }, spec: Config, ) { return new Value(async (options) => { const built = await spec.build(options as any) return { type: "object" as const, description: null, warning: null, ...a, spec: built, } }, spec.validator) } static file, Store>(a: { name: string description?: string | null warning?: string | null extensions: string[] required: Required }) { const buildValue = { type: "file" as const, description: null, warning: null, ...a, } return new Value, Store>( () => ({ ...buildValue, ...requiredLikeToAbove(a.required), }), asRequiredParser(object({ filePath: string }), a), ) } static dynamicFile( a: LazyBuild< Store, { name: string description?: string | null warning?: string | null extensions: string[] required: Required } >, ) { return new Value( async (options) => ({ type: "file" as const, description: null, warning: null, ...(await a(options)), }), string.optional(), ) } static union, Type, Store>( a: { name: string description?: string | null warning?: string | null required: Required /** Immutable means it can only be configed at the first config then never again Default is false */ immutable?: boolean /** * Disabled: false means that there is nothing disabled, good to modify * string means that this is the message displayed and the whole thing is disabled * string[] means that the options are disabled */ disabled?: false | string | string[] }, aVariants: Variants, ) { return new Value, Store>( async (options) => ({ type: "union" as const, description: null, warning: null, disabled: false, ...a, variants: await aVariants.build(options as any), ...requiredLikeToAbove(a.required), immutable: a.immutable ?? false, }), asRequiredParser(aVariants.validator, a), ) } static filteredUnion< Required extends RequiredDefault, Type extends Record, Store = never, >( getDisabledFn: LazyBuild, a: { name: string description?: string | null warning?: string | null required: Required }, aVariants: Variants | Variants, ) { return new Value, Store>( async (options) => ({ type: "union" as const, description: null, warning: null, ...a, variants: await aVariants.build(options as any), ...requiredLikeToAbove(a.required), disabled: (await getDisabledFn(options)) || false, immutable: false, }), asRequiredParser(aVariants.validator, a), ) } static dynamicUnion< Required extends RequiredDefault, Type extends Record, Store = never, >( getA: LazyBuild< Store, { disabled: string[] | false | string name: string description?: string | null warning?: string | null required: Required } >, aVariants: Variants | Variants, ) { return new Value(async (options) => { const newValues = await getA(options) return { type: "union" as const, description: null, warning: null, ...newValues, variants: await aVariants.build(options as any), ...requiredLikeToAbove(newValues.required), immutable: false, } }, aVariants.validator.optional()) } static list(a: List) { return new Value((options) => a.build(options), a.validator) } /** * Use this during the times that the input needs a more specific type. * Used in types that the value/ variant/ list/ config is constructed somewhere else. ```ts const a = Config.text({ name: "a", required: false, }) return Config.of()({ myValue: a.withStore(), }) ``` */ withStore() { return this as any as Value } }