mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
merge 036, everything broken
This commit is contained in:
137
sdk/lib/config/builder/config.ts
Normal file
137
sdk/lib/config/builder/config.ts
Normal 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>
|
||||
}
|
||||
}
|
||||
4
sdk/lib/config/builder/index.ts
Normal file
4
sdk/lib/config/builder/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import "./config"
|
||||
import "./list"
|
||||
import "./value"
|
||||
import "./variants"
|
||||
279
sdk/lib/config/builder/list.ts
Normal file
279
sdk/lib/config/builder/list.ts
Normal 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>
|
||||
}
|
||||
}
|
||||
783
sdk/lib/config/builder/value.ts
Normal file
783
sdk/lib/config/builder/value.ts
Normal 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>
|
||||
}
|
||||
}
|
||||
120
sdk/lib/config/builder/variants.ts
Normal file
120
sdk/lib/config/builder/variants.ts
Normal 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>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user