chore: Update the lazy config

This commit is contained in:
BluJ
2023-05-01 13:04:48 -06:00
parent cb89f3a65f
commit a30ed1f0ab
19 changed files with 505 additions and 667 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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,
)
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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>
},

View File

@@ -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}`
}

View File

@@ -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)
})

View File

@@ -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")) {

View File

@@ -423,6 +423,7 @@ oldSpecToBuilder(
},
{
// convert this to `start-sdk/lib` for conversions
startSdk: "..",
startSdk: "../..",
wrapperData: "./output.wrapperData",
},
)

View File

@@ -0,0 +1 @@
export type WrapperData = {}

View File

@@ -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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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
}