merge 036, everything broken

This commit is contained in:
Matt Hill
2024-03-20 13:32:57 -06:00
parent f4fadd366e
commit 5e6a7e134f
429 changed files with 42285 additions and 27221 deletions

View File

@@ -0,0 +1,137 @@
import { ValueSpec } from "../configTypes"
import { Value } from "./value"
import { _ } from "../../util"
import { Effects } from "../../types"
import { Parser, object } from "ts-matches"
export type LazyBuildOptions<Store> = {
effects: Effects
}
export type LazyBuild<Store, ExpectedOut> = (
options: LazyBuildOptions<Store>,
) => Promise<ExpectedOut> | ExpectedOut
// prettier-ignore
export type ExtractConfigType<A extends Record<string, any> | Config<Record<string, any>, any> | Config<Record<string, any>, never>> =
A extends Config<infer B, any> | Config<infer B, never> ? B :
A
export type ConfigSpecOf<A extends Record<string, any>, Store = never> = {
[K in keyof A]: Value<A[K], Store>
}
export type MaybeLazyValues<A> = LazyBuild<any, A> | A
/**
* Configs are the specs that are used by the os configuration form for this service.
* Here is an example of a simple configuration
```ts
const smallConfig = Config.of({
test: Value.boolean({
name: "Test",
description: "This is the description for the test",
warning: null,
default: false,
}),
});
```
The idea of a config is that now the form is going to ask for
Test: [ ] and the value is going to be checked as a boolean.
There are more complex values like selects, lists, and objects. See {@link Value}
Also, there is the ability to get a validator/parser from this config spec.
```ts
const matchSmallConfig = smallConfig.validator();
type SmallConfig = typeof matchSmallConfig._TYPE;
```
Here is an example of a more complex configuration which came from a configuration for a service
that works with bitcoin, like c-lightning.
```ts
export const hostname = Value.string({
name: "Hostname",
default: null,
description: "Domain or IP address of bitcoin peer",
warning: null,
required: true,
masked: false,
placeholder: null,
pattern:
"(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))",
patternDescription:
"Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
});
export const port = Value.number({
name: "Port",
default: null,
description: "Port that peer is listening on for inbound p2p connections",
warning: null,
required: false,
range: "[0,65535]",
integral: true,
units: null,
placeholder: null,
});
export const addNodesSpec = Config.of({ hostname: hostname, port: port });
```
*/
export class Config<Type extends Record<string, any>, Store = never> {
private constructor(
private readonly spec: {
[K in keyof Type]: Value<Type[K], Store> | Value<Type[K], never>
},
public validator: Parser<unknown, Type>,
) {}
async build(options: LazyBuildOptions<Store>) {
const answer = {} as {
[K in keyof Type]: ValueSpec
}
for (const k in this.spec) {
answer[k] = await this.spec[k].build(options as any)
}
return answer
}
static of<
Spec extends Record<string, Value<any, Store> | Value<any, never>>,
Store = never,
>(spec: Spec) {
const validatorObj = {} as {
[K in keyof Spec]: Parser<unknown, any>
}
for (const key in spec) {
validatorObj[key] = spec[key].validator
}
const validator = object(validatorObj)
return new Config<
{
[K in keyof Spec]: Spec[K] extends
| Value<infer T, Store>
| Value<infer T, never>
? T
: never
},
Store
>(spec, validator as any)
}
/**
* 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<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Config<Type, NewStore>
}
}

View File

@@ -0,0 +1,4 @@
import "./config"
import "./list"
import "./value"
import "./variants"

View File

@@ -0,0 +1,279 @@
import { Config, LazyBuild } from "./config"
import {
ListValueSpecText,
Pattern,
RandomString,
UniqueBy,
ValueSpecList,
ValueSpecListOf,
ValueSpecText,
} from "../configTypes"
import { Parser, arrayOf, number, string } from "ts-matches"
/**
* Used as a subtype of Value.list
```ts
export const authorizationList = List.string({
"name": "Authorization",
"range": "[0,*)",
"default": [],
"description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
"warning": null
}, {"masked":false,"placeholder":null,"pattern":"^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$","patternDescription":"Each item must be of the form \"<USERNAME>:<SALT>$<HASH>\"."});
export const auth = Value.list(authorizationList);
```
*/
export class List<Type, Store> {
private constructor(
public build: LazyBuild<Store, ValueSpecList>,
public validator: Parser<unknown, Type>,
) {}
static text(
a: {
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
},
aSpec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
inputmode?: ListValueSpecText["inputmode"]
generate?: null | RandomString
},
) {
return new List<string[], never>(() => {
const spec = {
type: "text" as const,
placeholder: null,
minLength: null,
maxLength: null,
masked: false,
inputmode: "text" as const,
generate: null,
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
description: null,
warning: null,
default: [],
type: "list" as const,
minLength: null,
maxLength: null,
disabled: false,
...a,
spec,
}
return built
}, arrayOf(string))
}
static dynamicText<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
generate?: null | RandomString
spec: {
/** Default = false */
masked?: boolean
placeholder?: string | null
minLength?: number | null
maxLength?: number | null
patterns: Pattern[]
/** Default = "text" */
inputmode?: ListValueSpecText["inputmode"]
}
}
>,
) {
return new List<string[], Store>(async (options) => {
const { spec: aSpec, ...a } = await getA(options)
const spec = {
type: "text" as const,
placeholder: null,
minLength: null,
maxLength: null,
masked: false,
inputmode: "text" as const,
generate: null,
...aSpec,
}
const built: ValueSpecListOf<"text"> = {
description: null,
warning: null,
default: [],
type: "list" as const,
minLength: null,
maxLength: null,
disabled: false,
...a,
spec,
}
return built
}, arrayOf(string))
}
static number(
a: {
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
},
aSpec: {
integer: boolean
min?: number | null
max?: number | null
step?: number | null
units?: string | null
placeholder?: string | null
},
) {
return new List<number[], never>(() => {
const spec = {
type: "number" as const,
placeholder: null,
min: null,
max: null,
step: null,
units: null,
...aSpec,
}
const built: ValueSpecListOf<"number"> = {
description: null,
warning: null,
minLength: null,
maxLength: null,
default: [],
type: "list" as const,
disabled: false,
...a,
spec,
}
return built
}, arrayOf(number))
}
static dynamicNumber<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
/** Default = [] */
default?: string[]
minLength?: number | null
maxLength?: number | null
disabled?: false | string
spec: {
integer: boolean
min?: number | null
max?: number | null
step?: number | null
units?: string | null
placeholder?: string | null
}
}
>,
) {
return new List<number[], Store>(async (options) => {
const { spec: aSpec, ...a } = await getA(options)
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,
disabled: false,
...a,
spec,
}
}, arrayOf(number))
}
static obj<Type extends Record<string, any>, Store>(
a: {
name: string
description?: string | null
warning?: string | null
/** Default [] */
default?: []
minLength?: number | null
maxLength?: number | null
},
aSpec: {
spec: Config<Type, Store>
displayAs?: null | string
uniqueBy?: null | UniqueBy
},
) {
return new List<Type[], Store>(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,
disabled: false,
...value,
}
}, arrayOf(aSpec.spec.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<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as List<Type, NewStore>
}
}

View File

@@ -0,0 +1,783 @@
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<A> =
| false
| {
default: A | null
}
function requiredLikeToAbove<Input extends RequiredDefault<A>, 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<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.
* 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<Type, Store> {
protected constructor(
public build: LazyBuild<Store, ValueSpec>,
public validator: Parser<unknown, Type>,
) {}
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<boolean, never>(
async () => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: a.immutable ?? false,
...a,
}),
boolean,
)
}
static dynamicToggle<Store = never>(
a: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: boolean
disabled?: false | string
}
>,
) {
return new Value<boolean, Store>(
async (options) => ({
description: null,
warning: null,
type: "toggle" as const,
disabled: false,
immutable: false,
...(await a(options)),
}),
boolean,
)
}
static text<Required extends RequiredDefault<DefaultString>>(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<AsRequired<string, Required>, 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<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<DefaultString>
/** 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<string | null | undefined, Store>(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<string, never>(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<Store = never>(
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<string, Store>(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<Required extends RequiredDefault<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<AsRequired<number, Required>, 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<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<number>
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<number | null | undefined, Store>(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<Required extends RequiredDefault<string>>(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<AsRequired<string, Required>, never>(
() => ({
type: "color" as const,
description: null,
warning: null,
disabled: false,
immutable: a.immutable ?? false,
...a,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(string, a),
)
}
static dynamicColor<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
disabled?: false | string
}
>,
) {
return new Value<string | null | undefined, Store>(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<Required extends RequiredDefault<string>>(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<AsRequired<string, Required>, 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<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
/** Default = 'datetime-local' */
inputmode?: ValueSpecDatetime["inputmode"]
min?: string | null
max?: string | null
disabled?: false | string
}
>,
) {
return new Value<string | null | undefined, Store>(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<string>,
B extends Record<string, string>,
>(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<AsRequired<keyof B, Required>, 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<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
required: RequiredDefault<string>
values: Record<string, string>
/**
* 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<string | null | undefined, Store>(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<Values extends Record<string, string>>(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<Store = never>(
getA: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
default: string[]
values: Record<string, string>
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<string[], Store>(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<Type extends Record<string, any>, Store>(
a: {
name: string
description?: string | null
warning?: string | null
},
spec: Config<Type, Store>,
) {
return new Value<Type, Store>(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<Required extends RequiredDefault<string>, 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<AsRequired<FilePath, Required>, Store>(
() => ({
...buildValue,
...requiredLikeToAbove(a.required),
}),
asRequiredParser(object({ filePath: string }), a),
)
}
static dynamicFile<Required extends boolean, Store>(
a: LazyBuild<
Store,
{
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}
>,
) {
return new Value<string | null | undefined, Store>(
async (options) => ({
type: "file" as const,
description: null,
warning: null,
...(await a(options)),
}),
string.optional(),
)
}
static union<Required extends RequiredDefault<string>, 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<Type, Store>,
) {
return new Value<AsRequired<Type, Required>, 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<string>,
Type extends Record<string, any>,
Store = never,
>(
getDisabledFn: LazyBuild<Store, string[] | false | string>,
a: {
name: string
description?: string | null
warning?: string | null
required: Required
},
aVariants: Variants<Type, Store> | Variants<Type, never>,
) {
return new Value<AsRequired<Type, Required>, 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<string>,
Type extends Record<string, any>,
Store = never,
>(
getA: LazyBuild<
Store,
{
disabled: string[] | false | string
name: string
description?: string | null
warning?: string | null
required: Required
}
>,
aVariants: Variants<Type, Store> | Variants<Type, never>,
) {
return new Value<Type | null | undefined, Store>(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<Type, Store>(a: List<Type, Store>) {
return new Value<Type, Store>((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<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Value<Type, NewStore>
}
}

View File

@@ -0,0 +1,120 @@
import { InputSpec, ValueSpecUnion } from "../configTypes"
import { LazyBuild, Config } from "./config"
import { Parser, anyOf, literals, object } from "ts-matches"
/**
* Used in the the Value.select { @link './value.ts' }
* to indicate the type of select variants that are available. The key for the record passed in will be the
* key to the tag.id in the Value.select
```ts
export const disabled = Config.of({});
export const size = Value.number({
name: "Max Chain Size",
default: 550,
description: "Limit of blockchain size on disk.",
warning: "Increasing this value will require re-syncing your node.",
required: true,
range: "[550,1000000)",
integral: true,
units: "MiB",
placeholder: null,
});
export const automatic = Config.of({ size: size });
export const size1 = Value.number({
name: "Failsafe Chain Size",
default: 65536,
description: "Prune blockchain if size expands beyond this.",
warning: null,
required: true,
range: "[550,1000000)",
integral: true,
units: "MiB",
placeholder: null,
});
export const manual = Config.of({ size: size1 });
export const pruningSettingsVariants = Variants.of({
disabled: { name: "Disabled", spec: disabled },
automatic: { name: "Automatic", spec: automatic },
manual: { name: "Manual", spec: manual },
});
export const pruning = Value.union(
{
name: "Pruning Settings",
description:
'- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the "pruneblockchain" RPC\n',
warning: null,
required: true,
default: "disabled",
},
pruningSettingsVariants
);
```
*/
export class Variants<Type, Store> {
static text: any
private constructor(
public build: LazyBuild<Store, ValueSpecUnion["variants"]>,
public validator: Parser<unknown, Type>,
) {}
static of<
VariantValues extends {
[K in string]: {
name: string
spec: Config<any, Store> | Config<any, never>
}
},
Store = never,
>(a: VariantValues) {
const validator = anyOf(
...Object.entries(a).map(([name, { spec }]) =>
object({
unionSelectKey: literals(name),
unionValueKey: spec.validator,
}),
),
) as Parser<unknown, any>
return new Variants<
{
[K in keyof VariantValues]: {
unionSelectKey: K
// prettier-ignore
unionValueKey:
VariantValues[K]["spec"] extends (Config<infer B, Store> | Config<infer B, never>) ? B :
never
}
}[keyof VariantValues],
Store
>(async (options) => {
const variants = {} as {
[K in keyof VariantValues]: { name: string; spec: InputSpec }
}
for (const key in a) {
const value = a[key]
variants[key] = {
name: value.name,
spec: await value.spec.build(options as any),
}
}
return variants
}, 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<Store>()({
myValue: a.withStore(),
})
```
*/
withStore<NewStore extends Store extends never ? any : Store>() {
return this as any as Variants<Type, NewStore>
}
}