mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-26 10:21:55 +00:00
chore: Update the lazy config
This commit is contained in:
@@ -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<WrapperData, Input extends Config<InputSpec>> {
|
||||
export class CreatedAction<WrapperData, Type extends Record<string, any>> {
|
||||
private constructor(
|
||||
private myMetaData: Omit<ActionMetaData, "input"> & { input: Input },
|
||||
private myMetaData: Omit<ActionMetaData, "input"> & {
|
||||
input: Config<Type, WrapperData, never>
|
||||
},
|
||||
readonly fn: (options: {
|
||||
effects: Effects
|
||||
utils: Utils<WrapperData>
|
||||
input: TypeFromProps<Input>
|
||||
input: Type
|
||||
}) => Promise<ActionResult>,
|
||||
) {}
|
||||
private validator = this.myMetaData.input.validator() as Parser<
|
||||
unknown,
|
||||
TypeFromProps<Input>
|
||||
>
|
||||
metaData = {
|
||||
...this.myMetaData,
|
||||
input: this.myMetaData.input.build(),
|
||||
}
|
||||
private validator = this.myMetaData.input.validator
|
||||
|
||||
static of<WrapperData, Input extends Config<InputSpec>>(
|
||||
metaData: Omit<ActionMetaData, "input"> & { input: Input },
|
||||
static of<
|
||||
WrapperData,
|
||||
Input extends Config<Type, WrapperData, never>,
|
||||
Type extends Record<string, any> = (Input extends Config<any, infer B, any>
|
||||
? B
|
||||
: never) &
|
||||
Record<string, any>,
|
||||
>(
|
||||
metaData: Omit<ActionMetaData, "input"> & {
|
||||
input: Config<Type, WrapperData, never>
|
||||
},
|
||||
fn: (options: {
|
||||
effects: Effects
|
||||
utils: Utils<WrapperData>
|
||||
input: TypeFromProps<Input>
|
||||
input: Type
|
||||
}) => Promise<ActionResult>,
|
||||
) {
|
||||
return new CreatedAction<WrapperData, Input>(metaData, fn)
|
||||
return new CreatedAction<WrapperData, Type>(metaData, fn)
|
||||
}
|
||||
|
||||
exportedAction: ExportedAction = ({ effects, input }) => {
|
||||
@@ -43,7 +44,16 @@ export class CreatedAction<WrapperData, Input extends Config<InputSpec>> {
|
||||
}
|
||||
|
||||
async exportAction(effects: Effects) {
|
||||
await effects.exportAction(this.metaData)
|
||||
const myUtils = utils<WrapperData>(effects)
|
||||
const metaData = {
|
||||
...this.myMetaData,
|
||||
input: await this.myMetaData.input.build({
|
||||
effects,
|
||||
utils: myUtils,
|
||||
config: null,
|
||||
}),
|
||||
}
|
||||
await effects.exportAction(metaData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { _ } from "../../util"
|
||||
export class IBuilder<A> {
|
||||
protected constructor(readonly a: A) {}
|
||||
|
||||
public build(): A {
|
||||
return this.a
|
||||
}
|
||||
}
|
||||
|
||||
export type BuilderExtract<A> = A extends IBuilder<infer B> ? B : never
|
||||
@@ -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<Manifest, ConfigType> = {
|
||||
effects: Effects
|
||||
utils: Utils<Manifest>
|
||||
config: ConfigType | null
|
||||
}
|
||||
export type LazyBuild<Manifest, ConfigType, ExpectedOut> = (
|
||||
options: LazyBuildOptions<Manifest, ConfigType>,
|
||||
) => Promise<ExpectedOut> | ExpectedOut
|
||||
|
||||
export type MaybeLazyValues<A> = LazyBuild<any, any, A> | A
|
||||
/**
|
||||
* Configs are the specs that are used by the os configuration form for this service.
|
||||
* Here is an example of a simple configuration
|
||||
@@ -60,44 +71,33 @@ export const addNodesSpec = Config.of({ hostname: hostname, port: port });
|
||||
|
||||
```
|
||||
*/
|
||||
export class Config<A extends InputSpec> extends IBuilder<A> {
|
||||
static empty() {
|
||||
return new Config({})
|
||||
}
|
||||
static withValue<K extends string, B extends ValueSpec>(
|
||||
key: K,
|
||||
value: Value<B>,
|
||||
) {
|
||||
return Config.empty().withValue(key, value)
|
||||
}
|
||||
static addValue<K extends string, B extends ValueSpec>(
|
||||
key: K,
|
||||
value: Value<B>,
|
||||
) {
|
||||
return Config.empty().withValue(key, value)
|
||||
}
|
||||
|
||||
static of<B extends { [key: string]: Value<ValueSpec> }>(spec: B) {
|
||||
const answer: { [K in keyof B]: BuilderExtract<B[K]> } = {} as any
|
||||
for (const key in spec) {
|
||||
answer[key] = spec[key].build() as any
|
||||
export class Config<Type extends Record<string, any>, WD, ConfigType> {
|
||||
private constructor(
|
||||
private readonly spec: {
|
||||
[K in keyof Type]: Value<Type[K], WD, ConfigType>
|
||||
},
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
async build(options: LazyBuildOptions<WD, ConfigType>) {
|
||||
const answer = {} as {
|
||||
[K in keyof Type]: ValueSpec
|
||||
}
|
||||
return new Config(answer)
|
||||
}
|
||||
withValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
|
||||
return new Config({
|
||||
...this.a,
|
||||
[key]: value.build(),
|
||||
} as A & { [key in K]: B })
|
||||
}
|
||||
addValue<K extends string, B extends ValueSpec>(key: K, value: Value<B>) {
|
||||
return new Config({
|
||||
...this.a,
|
||||
[key]: value.build(),
|
||||
} as A & { [key in K]: B })
|
||||
for (const k in this.spec) {
|
||||
answer[k] = await this.spec[k].build(options)
|
||||
}
|
||||
return answer
|
||||
}
|
||||
|
||||
public validator() {
|
||||
return typeFromProps(this.a)
|
||||
static of<Type extends Record<string, any>, Manifest, ConfigType>(spec: {
|
||||
[K in keyof Type]: Value<Type[K], Manifest, ConfigType>
|
||||
}) {
|
||||
const validatorObj = {} as {
|
||||
[K in keyof Type]: Parser<unknown, Type[K]>
|
||||
}
|
||||
for (const key in spec) {
|
||||
validatorObj[key] = spec[key].validator
|
||||
}
|
||||
const validator = object(validatorObj)
|
||||
return new Config<Type, Manifest, ConfigType>(spec, validator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<A extends ValueSpecList> extends IBuilder<A> {
|
||||
static text(
|
||||
export class List<Type, WD, ConfigType> {
|
||||
private constructor(
|
||||
public build: LazyBuild<WD, ConfigType, ValueSpecList>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
static text<WD, CT>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -43,27 +45,29 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
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<string[], WD, CT>(() => {
|
||||
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<WD, CT>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -82,27 +86,29 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
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<number[], WD, CT>(() => {
|
||||
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<Spec extends Config<InputSpec>>(
|
||||
static obj<Type extends Record<string, any>, WrapperData, ConfigType>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -113,36 +119,34 @@ export class List<A extends ValueSpecList> extends IBuilder<A> {
|
||||
maxLength?: number | null
|
||||
},
|
||||
aSpec: {
|
||||
spec: Spec
|
||||
spec: Config<Type, WrapperData, ConfigType>
|
||||
displayAs?: null | string
|
||||
uniqueBy?: null | UniqueBy
|
||||
},
|
||||
) {
|
||||
const { spec: previousSpecSpec, ...restSpec } = aSpec
|
||||
const specSpec = previousSpecSpec.build() as BuilderExtract<Spec>
|
||||
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<Type[], WrapperData, ConfigType>(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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<A> =
|
||||
| false
|
||||
@@ -40,6 +46,30 @@ function requiredLikeToAbove<Input extends RequiredDefault<A>, A>(
|
||||
)
|
||||
};
|
||||
}
|
||||
type AsRequired<Type, MaybeRequiredType> = MaybeRequiredType extends
|
||||
| { default: unknown }
|
||||
| never
|
||||
? Type
|
||||
: Type | null | undefined
|
||||
|
||||
type InputAsRequired<A, Type> = A extends
|
||||
| { required: { default: any } | never }
|
||||
| never
|
||||
? Type
|
||||
: Type | null | undefined
|
||||
const testForAsRequiredParser = once(
|
||||
() => object({ required: object({ default: unknown }) }).test,
|
||||
)
|
||||
function asRequiredParser<
|
||||
Type,
|
||||
Input,
|
||||
Return extends
|
||||
| Parser<unknown, Type>
|
||||
| Parser<unknown, Type | null | undefined>,
|
||||
>(parser: Parser<unknown, Type>, input: Input): Return {
|
||||
if (testForAsRequiredParser()(input)) return parser as any
|
||||
return parser.optional() as any
|
||||
}
|
||||
/**
|
||||
* A value is going to be part of the form in the FE of the OS.
|
||||
* Something like a boolean, a string, a number, etc.
|
||||
@@ -62,22 +92,29 @@ const username = Value.string({
|
||||
});
|
||||
```
|
||||
*/
|
||||
export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
static toggle(a: {
|
||||
export class Value<Type, WD, ConfigType> {
|
||||
private constructor(
|
||||
public build: LazyBuild<WD, ConfigType, ValueSpec>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
static toggle<WD, CT>(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<boolean, WD, CT>(
|
||||
async () => ({
|
||||
description: null,
|
||||
warning: null,
|
||||
default: null,
|
||||
type: "toggle" as const,
|
||||
...a,
|
||||
}),
|
||||
boolean,
|
||||
)
|
||||
}
|
||||
static text<Required extends RequiredDefault<DefaultString>>(a: {
|
||||
static text<Required extends RequiredDefault<DefaultString>, WD, CT>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
@@ -92,21 +129,24 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
/** 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<AsRequired<string, Required>, 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<WD, CT>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
@@ -115,17 +155,21 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
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<string, WD, CT>(
|
||||
async () =>
|
||||
({
|
||||
description: null,
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
placeholder: null,
|
||||
type: "textarea" as const,
|
||||
...a,
|
||||
} satisfies ValueSpecTextarea),
|
||||
string,
|
||||
)
|
||||
}
|
||||
static number<Required extends RequiredDefault<number>>(a: {
|
||||
static number<Required extends RequiredDefault<number>, WD, CT>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
@@ -138,34 +182,41 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
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<AsRequired<number, Required>, 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<Required extends RequiredDefault<string>>(a: {
|
||||
static color<Required extends RequiredDefault<string>, 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<AsRequired<string, Required>, WD, CT>(
|
||||
() => ({
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
}),
|
||||
|
||||
asRequiredParser(string, a),
|
||||
)
|
||||
}
|
||||
static datetime<Required extends RequiredDefault<string>>(a: {
|
||||
static datetime<Required extends RequiredDefault<string>, WD, CT>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
@@ -176,21 +227,26 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
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<AsRequired<string, Required>, 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<string>,
|
||||
B extends Record<string, string>,
|
||||
WD,
|
||||
CT,
|
||||
>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -198,15 +254,23 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
required: Required
|
||||
values: B
|
||||
}) {
|
||||
return new Value({
|
||||
description: null,
|
||||
warning: null,
|
||||
type: "select" as const,
|
||||
...a,
|
||||
...requiredLikeToAbove(a.required),
|
||||
})
|
||||
return new Value<AsRequired<keyof B, Required>, 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<Values extends Record<string, string>>(a: {
|
||||
static multiselect<Values extends Record<string, string>, WD, CT>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
@@ -215,35 +279,44 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
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<Spec extends Config<InputSpec>>(
|
||||
static object<Type extends Record<string, any>, WrapperData, ConfigType>(
|
||||
a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
warning?: string | null
|
||||
},
|
||||
previousSpec: Spec,
|
||||
previousSpec: Config<Type, WrapperData, ConfigType>,
|
||||
) {
|
||||
const spec = previousSpec.build() as BuilderExtract<Spec>
|
||||
return new Value({
|
||||
type: "object" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
spec,
|
||||
})
|
||||
return new Value<Type, WrapperData, ConfigType>(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<string>,
|
||||
V extends Variants<{ [key: string]: { name: string; spec: InputSpec } }>,
|
||||
Type,
|
||||
WrapperData,
|
||||
ConfigType,
|
||||
>(
|
||||
a: {
|
||||
name: string
|
||||
@@ -252,23 +325,28 @@ export class Value<A extends ValueSpec> extends IBuilder<A> {
|
||||
required: Required
|
||||
default?: string | null
|
||||
},
|
||||
aVariants: V,
|
||||
aVariants: Variants<Type, WrapperData, ConfigType>,
|
||||
) {
|
||||
const variants = aVariants.build() as BuilderExtract<V>
|
||||
return new Value({
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
variants,
|
||||
...requiredLikeToAbove(a.required),
|
||||
})
|
||||
return new Value<AsRequired<Type, Required>, 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 extends ValueSpecList>(a: List<A>) {
|
||||
return new Value(a.build())
|
||||
}
|
||||
public validator() {
|
||||
return guardAll(this.a)
|
||||
static list<Type, WrapperData, ConfigType>(
|
||||
a: List<Type, WrapperData, ConfigType>,
|
||||
) {
|
||||
/// TODO
|
||||
return new Value<Type, WrapperData, ConfigType>(
|
||||
(options) => a.build(options),
|
||||
a.validator,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<A> {
|
||||
export class Variants<Type, WD, ConfigType> {
|
||||
private constructor(
|
||||
public build: LazyBuild<WD, ConfigType, ValueSpecUnion["variants"]>,
|
||||
public validator: Parser<unknown, Type>,
|
||||
) {}
|
||||
// A extends {
|
||||
// [key: string]: {
|
||||
// name: string
|
||||
// spec: InputSpec
|
||||
// }
|
||||
// },
|
||||
static of<
|
||||
A extends {
|
||||
[key: string]: { name: string; spec: Config<InputSpec> }
|
||||
},
|
||||
>(a: A) {
|
||||
const variants: {
|
||||
[K in keyof A]: { name: string; spec: BuilderExtract<A[K]["spec"]> }
|
||||
} = {} 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<string, Record<string, any>>,
|
||||
WrapperData,
|
||||
ConfigType,
|
||||
>(a: {
|
||||
[K in keyof TypeMap]: {
|
||||
name: string
|
||||
spec: Config<TypeMap[K], WrapperData, ConfigType>
|
||||
}
|
||||
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<K extends string, B extends InputSpec>(
|
||||
key: K,
|
||||
value: Config<B>,
|
||||
) {
|
||||
return Variants.empty().withVariant(key, value)
|
||||
}
|
||||
const validator = anyOf(
|
||||
...Object.entries(a).map(([name, { spec }]) =>
|
||||
object({
|
||||
unionSelectKey: literals(name),
|
||||
unionValueKey: spec.validator,
|
||||
}),
|
||||
),
|
||||
) as Parser<unknown, TypeOut>
|
||||
|
||||
withVariant<K extends string, B extends InputSpec>(key: K, value: Config<B>) {
|
||||
return new Variants({
|
||||
...this.a,
|
||||
[key]: value.build(),
|
||||
} as A & { [key in K]: B })
|
||||
return new Variants<TypeOut, WrapperData, ConfigType>(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +189,10 @@ export type DefaultString =
|
||||
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpecListOf<ListValueSpecType>,
|
||||
t: ValueSpec,
|
||||
s: S,
|
||||
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
|
||||
return t.spec.type === s
|
||||
return "spec" in t && t.spec.type === s
|
||||
}
|
||||
export const unionSelectKey = "unionSelectKey" as const
|
||||
export type UnionSelectKey = typeof unionSelectKey
|
||||
|
||||
@@ -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<WD, A> = (options: {
|
||||
*/
|
||||
export function setupConfig<
|
||||
WD,
|
||||
A extends Config<InputSpec>,
|
||||
Type extends Record<string, any>,
|
||||
Manifest extends GenericManifest,
|
||||
>(
|
||||
spec: A,
|
||||
write: Save<WD, TypeFromProps<A>, Manifest>,
|
||||
read: Read<WD, TypeFromProps<A>>,
|
||||
spec: Config<Type, WD, Type>,
|
||||
write: Save<WD, Type, Manifest>,
|
||||
read: Read<WD, Type>,
|
||||
) {
|
||||
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<Manifest>(),
|
||||
})
|
||||
}) as ExpectedExports.setConfig,
|
||||
getConfig: (async ({ effects, config }) => {
|
||||
getConfig: (async ({ effects }) => {
|
||||
const myUtils = utils<WD>(effects)
|
||||
const configValue = nullIfEmpty(
|
||||
(await read({ effects, utils: myUtils })) || null,
|
||||
)
|
||||
return {
|
||||
spec: spec.build(),
|
||||
config: nullIfEmpty(
|
||||
(await read({ effects, utils: utils<WD>(effects) })) || null,
|
||||
),
|
||||
spec: await spec.build({
|
||||
effects,
|
||||
utils: myUtils,
|
||||
config: configValue,
|
||||
}),
|
||||
config: configValue,
|
||||
}
|
||||
}) as ExpectedExports.getConfig,
|
||||
}
|
||||
|
||||
@@ -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<string, string>
|
||||
},
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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<typeof validator._TYPE, boolean>()(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<typeof validator._TYPE, string>()(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<typeof validator._TYPE, string>()(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<typeof validator._TYPE, string | null | undefined>()(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<typeof validator._TYPE, string | null | undefined>()(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<typeof validator._TYPE, string>()(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<typeof validator._TYPE, string | null | undefined>()(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<typeof validator._TYPE, string>()(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<typeof validator._TYPE, number>()(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<typeof validator._TYPE, number | null | undefined>()(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<typeof validator._TYPE, "a" | "b">()(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<typeof validator._TYPE, "a" | "b" | null | undefined>()(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<typeof validator._TYPE, Array<"a" | "b">>()(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<typeof validator._TYPE, { a: boolean }>()(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<Test, { unionSelectKey: "a"; unionValueKey: { b: boolean } }>()(
|
||||
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<typeof validator._TYPE, number[]>()(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<typeof validator._TYPE, { test: boolean }[]>()(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<typeof validator._TYPE, string[]>()(null)
|
||||
})
|
||||
Value.multiselect({
|
||||
name: "Media Sources",
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
default: ["nextcloud"],
|
||||
description: "List of Media Sources to use with Jellyfin",
|
||||
warning: null,
|
||||
values: {
|
||||
nextcloud: "NextCloud",
|
||||
filebrowser: "File Browser",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe("Nested nullable values", () => {
|
||||
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<typeof validator._TYPE, { a: string | null | undefined }>()(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<typeof validator._TYPE, { a: number | null | undefined }>()(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<typeof validator._TYPE, { a: string | null | undefined }>()(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<typeof validator._TYPE, { a: "a" | null | undefined }>()(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<typeof validator._TYPE, { a: "a"[] }>()(null)
|
||||
})
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -423,6 +423,7 @@ oldSpecToBuilder(
|
||||
},
|
||||
{
|
||||
// convert this to `start-sdk/lib` for conversions
|
||||
startSdk: "..",
|
||||
startSdk: "../..",
|
||||
wrapperData: "./output.wrapperData",
|
||||
},
|
||||
)
|
||||
|
||||
1
lib/test/output.wrapperData.ts
Normal file
1
lib/test/output.wrapperData.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type WrapperData = {}
|
||||
@@ -15,10 +15,7 @@ export namespace ExpectedExports {
|
||||
input: Record<string, unknown>
|
||||
}) => Promise<void>
|
||||
/** 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<ConfigRes>
|
||||
export type getConfig = (options: { effects: Effects }) => Promise<ConfigRes>
|
||||
// /** 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<WrapperData, Path extends string> = _EnsureWrapperDataPath<WrapperData, Path, Path>
|
||||
|
||||
/* rsync options: https://linux.die.net/man/1/rsync
|
||||
/** rsync options: https://linux.die.net/man/1/rsync
|
||||
*/
|
||||
export type BackupOptions = {
|
||||
delete: boolean
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* @param s
|
||||
* @returns
|
||||
*/
|
||||
export default function nullIfEmpty(s: null | Record<string, unknown>) {
|
||||
export default function nullIfEmpty<A extends Record<string, any>>(
|
||||
s: null | A,
|
||||
) {
|
||||
if (s === null) return null
|
||||
return Object.keys(s).length === 0 ? null : s
|
||||
}
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
import * as matches from "ts-matches"
|
||||
import { Parser, Validator } from "ts-matches"
|
||||
import {
|
||||
UnionSelectKey,
|
||||
UnionValueKey,
|
||||
ValueSpec as ValueSpecAny,
|
||||
InputSpec,
|
||||
} from "../config/configTypes"
|
||||
import { Config } from "../config/builder/config"
|
||||
import { _ } from "../util"
|
||||
|
||||
const {
|
||||
string,
|
||||
some,
|
||||
arrayOf,
|
||||
object,
|
||||
dictionary,
|
||||
unknown,
|
||||
number,
|
||||
literals,
|
||||
boolean,
|
||||
nill,
|
||||
} = matches
|
||||
|
||||
type TypeToggle = "toggle"
|
||||
type TypeText = "text"
|
||||
type TypeTextarea = "textarea"
|
||||
type TypeNumber = "number"
|
||||
type TypeObject = "object"
|
||||
type TypeList = "list"
|
||||
type TypeSelect = "select"
|
||||
type TypeMultiselect = "multiselect"
|
||||
type TypeColor = "color"
|
||||
type TypeDatetime = "datetime"
|
||||
type TypeUnion = "union"
|
||||
|
||||
// prettier-ignore
|
||||
type GuardDefaultRequired<A, Type> =
|
||||
A extends { required: false; default: null | undefined | never } ? Type | undefined | null:
|
||||
Type
|
||||
|
||||
// prettier-ignore
|
||||
type GuardNumber<A> =
|
||||
A extends { type: TypeNumber } ? GuardDefaultRequired<A, number> :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardText<A> =
|
||||
A extends { type: TypeText } ? GuardDefaultRequired<A, string> :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardTextarea<A> =
|
||||
A extends { type: TypeTextarea } ? GuardDefaultRequired<A, string> :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardToggle<A> =
|
||||
A extends { type: TypeToggle } ? GuardDefaultRequired<A, boolean> :
|
||||
unknown
|
||||
|
||||
type TrueKeyOf<T> = _<T> extends Record<string, unknown> ? keyof T : never
|
||||
// prettier-ignore
|
||||
type GuardObject<A> =
|
||||
A extends { type: TypeObject, spec: infer B } ? (
|
||||
{ [K in TrueKeyOf<B> & string]: _<GuardAll<B[K]>> }
|
||||
) :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
export type GuardList<A> =
|
||||
A extends { type: TypeList, spec?: { type: infer B, spec?: infer C } } ? Array<GuardAll<Omit<A, "type" | "spec"> & ({ type: B, spec: C })>> :
|
||||
A extends { type: TypeList, spec?: { type: infer B } } ? Array<GuardAll<Omit<A, "type"> & ({ type: B })>> :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardSelect<A> =
|
||||
A extends { type: TypeSelect, values: infer B } ? (
|
||||
GuardDefaultRequired<A, TrueKeyOf<B>>
|
||||
) :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardMultiselect<A> =
|
||||
A extends { type: TypeMultiselect, values: infer B} ?(keyof B)[] :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardColor<A> =
|
||||
A extends { type: TypeColor } ? GuardDefaultRequired<A, string> :
|
||||
unknown
|
||||
// prettier-ignore
|
||||
type GuardDatetime<A> =
|
||||
A extends { type: TypeDatetime } ? GuardDefaultRequired<A, string> :
|
||||
unknown
|
||||
type AsString<A> = A extends
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
? `${A}`
|
||||
: "UnknownValue"
|
||||
// prettier-ignore
|
||||
type VariantValue<A> =
|
||||
A extends { name: string, spec: infer B } ? TypeFromProps<_<B>> :
|
||||
`neverVariantValue${AsString<A>}`
|
||||
// prettier-ignore
|
||||
type GuardUnion<A> =
|
||||
A extends { type: TypeUnion, variants: infer Variants & Record<string, unknown> } ? (
|
||||
_<{[key in keyof Variants]: {[k in UnionSelectKey]: key} & {[k in UnionValueKey]: VariantValue<Variants[key]>}}[keyof Variants]>
|
||||
) :
|
||||
unknown
|
||||
|
||||
export type GuardAll<A> = GuardNumber<A> &
|
||||
GuardText<A> &
|
||||
GuardTextarea<A> &
|
||||
GuardToggle<A> &
|
||||
GuardObject<A> &
|
||||
GuardList<A> &
|
||||
GuardUnion<A> &
|
||||
GuardSelect<A> &
|
||||
GuardMultiselect<A> &
|
||||
GuardColor<A> &
|
||||
GuardDatetime<A>
|
||||
// prettier-ignore
|
||||
export type TypeFromProps<A> =
|
||||
A extends Config<infer B> ? TypeFromProps<B> :
|
||||
A extends Record<string, unknown> ? { [K in keyof A & string]: _<GuardAll<A[K]>> } :
|
||||
unknown;
|
||||
const isType = object({ type: string })
|
||||
const matchVariant = object({
|
||||
name: string,
|
||||
spec: unknown,
|
||||
})
|
||||
const recordString = dictionary([string, unknown])
|
||||
const matchDefault = object({ default: unknown })
|
||||
const matchRequired = object(
|
||||
{
|
||||
required: literals(false),
|
||||
default: nill,
|
||||
},
|
||||
["default"],
|
||||
)
|
||||
const matchInteger = object({ integer: literals(true) })
|
||||
const matchSpec = object({ spec: recordString })
|
||||
const matchUnion = object({
|
||||
variants: dictionary([string, matchVariant]),
|
||||
})
|
||||
const matchValues = object({
|
||||
values: dictionary([string, string]),
|
||||
})
|
||||
|
||||
function withInteger(parser: Parser<unknown, number>, value: unknown) {
|
||||
if (matchInteger.test(value)) {
|
||||
return parser.validate(Number.isInteger, "isIntegral")
|
||||
}
|
||||
return parser
|
||||
}
|
||||
function requiredParser<A>(parser: Parser<unknown, A>, value: unknown) {
|
||||
if (matchRequired.test(value)) return parser.optional()
|
||||
return parser
|
||||
}
|
||||
|
||||
/**
|
||||
* InputSpec: Tells the UI how to ask for information, verification, and will send the service a config in a shape via the spec.
|
||||
* ValueSpecAny: This is any of the values in a config spec.
|
||||
*
|
||||
* Use this when we want to convert a value spec any into a parser for what a config will look like
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
export function guardAll<A extends ValueSpecAny>(
|
||||
value: A,
|
||||
): Parser<unknown, GuardAll<A>> {
|
||||
if (!isType.test(value)) {
|
||||
return unknown as any
|
||||
}
|
||||
switch (value.type) {
|
||||
case "toggle":
|
||||
return requiredParser(boolean, value) as any
|
||||
|
||||
case "text":
|
||||
return requiredParser(string, value) as any
|
||||
|
||||
case "textarea":
|
||||
return requiredParser(string, value) as any
|
||||
|
||||
case "color":
|
||||
return requiredParser(string, value) as any
|
||||
|
||||
case "datetime":
|
||||
return requiredParser(string, value) as any
|
||||
|
||||
case "number":
|
||||
return requiredParser(withInteger(number, value), value) as any
|
||||
|
||||
case "object":
|
||||
if (matchSpec.test(value)) {
|
||||
return requiredParser(typeFromProps(value.spec), value) as any
|
||||
}
|
||||
return unknown as any
|
||||
|
||||
case "list": {
|
||||
const spec = (matchSpec.test(value) && value.spec) || {}
|
||||
|
||||
return requiredParser(
|
||||
matches.arrayOf(guardAll(spec as any)),
|
||||
value,
|
||||
) as any
|
||||
}
|
||||
case "select":
|
||||
if (matchValues.test(value)) {
|
||||
const valueKeys = Object.keys(value.values)
|
||||
return requiredParser(
|
||||
literals(valueKeys[0], ...valueKeys),
|
||||
value,
|
||||
) as any
|
||||
}
|
||||
return unknown as any
|
||||
|
||||
case "multiselect":
|
||||
if (matchValues.test(value)) {
|
||||
const valueKeys = Object.keys(value.values)
|
||||
return requiredParser(
|
||||
arrayOf(literals(valueKeys[0], ...valueKeys)),
|
||||
value,
|
||||
) as any
|
||||
}
|
||||
return unknown as any
|
||||
|
||||
case "union":
|
||||
if (matchUnion.test(value)) {
|
||||
return some(
|
||||
...Object.entries(value.variants)
|
||||
.filter(([name]) => string.test(name))
|
||||
.map(([name, { spec }]) =>
|
||||
object({
|
||||
unionSelectKey: literals(name),
|
||||
unionValueKey: typeFromProps(spec),
|
||||
}),
|
||||
),
|
||||
) as any
|
||||
}
|
||||
return unknown as any
|
||||
}
|
||||
|
||||
return unknown as any
|
||||
}
|
||||
/**
|
||||
* InputSpec: Tells the UI how to ask for information, verification, and will send the service a config in a shape via the spec.
|
||||
* ValueSpecAny: This is any of the values in a config spec.
|
||||
*
|
||||
* Use this when we want to convert a config spec into a parser for what a config will look like
|
||||
* @param valueDictionary
|
||||
* @returns
|
||||
*/
|
||||
export function typeFromProps<A extends InputSpec>(
|
||||
valueDictionary: A,
|
||||
): Parser<unknown, TypeFromProps<A>> {
|
||||
if (!recordString.test(valueDictionary)) return unknown as any
|
||||
return object(
|
||||
Object.fromEntries(
|
||||
Object.entries(valueDictionary).map(([key, value]) => [
|
||||
key,
|
||||
guardAll(value),
|
||||
]),
|
||||
),
|
||||
) as any
|
||||
}
|
||||
Reference in New Issue
Block a user