chore: migrate from ts-matches to zod across all TypeScript packages

This commit is contained in:
Aiden McClelland
2026-02-20 16:24:35 -07:00
parent c7a4f0f9cb
commit 31352a72c3
40 changed files with 963 additions and 1891 deletions

View File

@@ -19,7 +19,6 @@
"lodash.merge": "^4.6.2",
"mime": "^4.0.7",
"node-fetch": "^3.1.0",
"ts-matches": "^6.3.2",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"yaml": "^2.3.1"
@@ -38,7 +37,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.50",
"version": "0.4.0-beta.51",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",
@@ -49,8 +48,8 @@
"ini": "^5.0.0",
"isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7",
"ts-matches": "^6.3.2",
"yaml": "^2.7.1"
"yaml": "^2.7.1",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/jest": "^29.4.0",
@@ -6494,12 +6493,6 @@
}
}
},
"node_modules/ts-matches": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.3.2.tgz",
"integrity": "sha512-UhSgJymF8cLd4y0vV29qlKVCkQpUtekAaujXbQVc729FezS8HwqzepqvtjzQ3HboatIqN/Idor85O2RMwT7lIQ==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",

View File

@@ -28,7 +28,6 @@
"lodash.merge": "^4.6.2",
"mime": "^4.0.7",
"node-fetch": "^3.1.0",
"ts-matches": "^6.3.2",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"yaml": "^2.3.1"

View File

@@ -3,33 +3,39 @@ import {
types as T,
utils,
VersionRange,
z,
} from "@start9labs/start-sdk"
import * as net from "net"
import { object, string, number, literals, some, unknown } from "ts-matches"
import { Effects } from "../Models/Effects"
import { CallbackHolder } from "../Models/CallbackHolder"
import { asError } from "@start9labs/start-sdk/base/lib/util"
const matchRpcError = object({
error: object({
code: number,
message: string,
data: some(
string,
object({
details: string,
debug: string.nullable().optional(),
}),
)
const matchRpcError = z.object({
error: z.object({
code: z.number(),
message: z.string(),
data: z
.union([
z.string(),
z.object({
details: z.string(),
debug: z.string().nullable().optional(),
}),
])
.nullable()
.optional(),
}),
})
const testRpcError = matchRpcError.test
const testRpcResult = object({
result: unknown,
}).test
type RpcError = typeof matchRpcError._TYPE
function testRpcError(v: unknown): v is RpcError {
return matchRpcError.safeParse(v).success
}
const matchRpcResult = z.object({
result: z.unknown(),
})
function testRpcResult(v: unknown): v is z.infer<typeof matchRpcResult> {
return matchRpcResult.safeParse(v).success
}
type RpcError = z.infer<typeof matchRpcError>
const SOCKET_PATH = "/media/startos/rpc/host.sock"
let hostSystemId = 0
@@ -71,7 +77,7 @@ const rpcRoundFor =
"Error in host RPC:",
utils.asError({ method, params, error: res.error }),
)
if (string.test(res.error.data)) {
if (typeof res.error.data === "string") {
message += ": " + res.error.data
console.error(`Details: ${res.error.data}`)
} else {

View File

@@ -1,25 +1,13 @@
// @ts-check
import * as net from "net"
import {
object,
some,
string,
literal,
array,
number,
matches,
any,
shape,
anyOf,
literals,
} from "ts-matches"
import {
ExtendedVersion,
types as T,
utils,
VersionRange,
z,
} from "@start9labs/start-sdk"
import * as fs from "fs"
@@ -29,89 +17,92 @@ import { jsonPath, unNestPath } from "../Models/JsonPath"
import { System } from "../Interfaces/System"
import { makeEffects } from "./EffectCreator"
type MaybePromise<T> = T | Promise<T>
export const matchRpcResult = anyOf(
object({ result: any }),
object({
error: object({
code: number,
message: string,
data: object({
details: string.optional(),
debug: any.optional(),
})
export const matchRpcResult = z.union([
z.object({ result: z.any() }),
z.object({
error: z.object({
code: z.number(),
message: z.string(),
data: z
.object({
details: z.string().optional(),
debug: z.any().optional(),
})
.nullable()
.optional(),
}),
}),
)
])
export type RpcResult = typeof matchRpcResult._TYPE
export type RpcResult = z.infer<typeof matchRpcResult>
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
const SOCKET_PARENT = "/media/startos/rpc"
const SOCKET_PATH = "/media/startos/rpc/service.sock"
const jsonrpc = "2.0" as const
const isResult = object({ result: any }).test
const isResultSchema = z.object({ result: z.any() })
const isResult = (v: unknown): v is z.infer<typeof isResultSchema> =>
isResultSchema.safeParse(v).success
const idType = some(string, number, literal(null))
const idType = z.union([z.string(), z.number(), z.literal(null)])
type IdType = null | string | number | undefined
const runType = object({
const runType = z.object({
id: idType.optional(),
method: literal("execute"),
params: object({
id: string,
procedure: string,
input: any,
timeout: number.nullable().optional(),
method: z.literal("execute"),
params: z.object({
id: z.string(),
procedure: z.string(),
input: z.any(),
timeout: z.number().nullable().optional(),
}),
})
const sandboxRunType = object({
const sandboxRunType = z.object({
id: idType.optional(),
method: literal("sandbox"),
params: object({
id: string,
procedure: string,
input: any,
timeout: number.nullable().optional(),
method: z.literal("sandbox"),
params: z.object({
id: z.string(),
procedure: z.string(),
input: z.any(),
timeout: z.number().nullable().optional(),
}),
})
const callbackType = object({
method: literal("callback"),
params: object({
id: number,
args: array,
const callbackType = z.object({
method: z.literal("callback"),
params: z.object({
id: z.number(),
args: z.array(z.unknown()),
}),
})
const initType = object({
const initType = z.object({
id: idType.optional(),
method: literal("init"),
params: object({
id: string,
kind: literals("install", "update", "restore").nullable(),
method: z.literal("init"),
params: z.object({
id: z.string(),
kind: z.enum(["install", "update", "restore"]).nullable(),
}),
})
const startType = object({
const startType = z.object({
id: idType.optional(),
method: literal("start"),
method: z.literal("start"),
})
const stopType = object({
const stopType = z.object({
id: idType.optional(),
method: literal("stop"),
method: z.literal("stop"),
})
const exitType = object({
const exitType = z.object({
id: idType.optional(),
method: literal("exit"),
params: object({
id: string,
target: string.nullable(),
method: z.literal("exit"),
params: z.object({
id: z.string(),
target: z.string().nullable(),
}),
})
const evalType = object({
const evalType = z.object({
id: idType.optional(),
method: literal("eval"),
params: object({
script: string,
method: z.literal("eval"),
params: z.object({
script: z.string(),
}),
})
@@ -144,7 +135,9 @@ const handleRpc = (id: IdType, result: Promise<RpcResult>) =>
},
}))
const hasId = object({ id: idType }).test
const hasIdSchema = z.object({ id: idType })
const hasId = (v: unknown): v is z.infer<typeof hasIdSchema> =>
hasIdSchema.safeParse(v).success
export class RpcListener {
shouldExit = false
unixSocketServer = net.createServer(async (server) => {})
@@ -246,40 +239,52 @@ export class RpcListener {
}
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
return matches(input)
.when(runType, async ({ id, params }) => {
const parsed = z.object({ method: z.string() }).safeParse(input)
if (!parsed.success) {
console.warn(
`Couldn't parse the following input ${JSON.stringify(input)}`,
)
return {
jsonrpc,
id: (input as any)?.id,
error: {
code: -32602,
message: "invalid params",
data: {
details: JSON.stringify(input),
},
},
}
}
switch (parsed.data.method) {
case "execute": {
const { id, params } = runType.parse(input)
const system = this.system
const procedure = jsonPath.unsafeCast(params.procedure)
const { input, timeout, id: eventId } = params
const result = this.getResult(
procedure,
system,
eventId,
timeout,
input,
)
const procedure = jsonPath.parse(params.procedure)
const { input: inp, timeout, id: eventId } = params
const result = this.getResult(procedure, system, eventId, timeout, inp)
return handleRpc(id, result)
})
.when(sandboxRunType, async ({ id, params }) => {
}
case "sandbox": {
const { id, params } = sandboxRunType.parse(input)
const system = this.system
const procedure = jsonPath.unsafeCast(params.procedure)
const { input, timeout, id: eventId } = params
const result = this.getResult(
procedure,
system,
eventId,
timeout,
input,
)
const procedure = jsonPath.parse(params.procedure)
const { input: inp, timeout, id: eventId } = params
const result = this.getResult(procedure, system, eventId, timeout, inp)
return handleRpc(id, result)
})
.when(callbackType, async ({ params: { id, args } }) => {
}
case "callback": {
const {
params: { id, args },
} = callbackType.parse(input)
this.callCallback(id, args)
return null
})
.when(startType, async ({ id }) => {
}
case "start": {
const { id } = startType.parse(input)
const callbacks =
this.callbacks?.getChild("main") || this.callbacks?.child("main")
const effects = makeEffects({
@@ -290,8 +295,9 @@ export class RpcListener {
id,
this.system.start(effects).then((result) => ({ result })),
)
})
.when(stopType, async ({ id }) => {
}
case "stop": {
const { id } = stopType.parse(input)
return handleRpc(
id,
this.system.stop().then((result) => {
@@ -300,8 +306,9 @@ export class RpcListener {
return { result }
}),
)
})
.when(exitType, async ({ id, params }) => {
}
case "exit": {
const { id, params } = exitType.parse(input)
return handleRpc(
id,
(async () => {
@@ -323,8 +330,9 @@ export class RpcListener {
}
})().then((result) => ({ result })),
)
})
.when(initType, async ({ id, params }) => {
}
case "init": {
const { id, params } = initType.parse(input)
return handleRpc(
id,
(async () => {
@@ -349,8 +357,9 @@ export class RpcListener {
}
})().then((result) => ({ result })),
)
})
.when(evalType, async ({ id, params }) => {
}
case "eval": {
const { id, params } = evalType.parse(input)
return handleRpc(
id,
(async () => {
@@ -375,41 +384,28 @@ export class RpcListener {
}
})(),
)
})
.when(
shape({ id: idType.optional(), method: string }),
({ id, method }) => ({
}
default: {
const { id, method } = z
.object({ id: idType.optional(), method: z.string() })
.passthrough()
.parse(input)
return {
jsonrpc,
id,
error: {
code: -32601,
message: `Method not found`,
message: "Method not found",
data: {
details: method,
},
},
}),
)
.defaultToLazy(() => {
console.warn(
`Couldn't parse the following input ${JSON.stringify(input)}`,
)
return {
jsonrpc,
id: (input as any)?.id,
error: {
code: -32602,
message: "invalid params",
data: {
details: JSON.stringify(input),
},
},
}
})
}
}
}
private getResult(
procedure: typeof jsonPath._TYPE,
procedure: z.infer<typeof jsonPath>,
system: System,
eventId: string,
timeout: number | null | undefined,
@@ -449,26 +445,18 @@ export class RpcListener {
)
}
}
})().then(ensureResultTypeShape, (error) =>
matches(error)
.when(
object({
error: string,
code: number.defaultTo(0),
}),
(error) => ({
error: {
code: error.code,
message: error.error,
},
}),
)
.defaultToLazy(() => ({
error: {
code: 0,
message: String(error),
},
})),
)
})().then(ensureResultTypeShape, (error) => {
const errorSchema = z.object({
error: z.string(),
code: z.number().default(0),
})
const parsed = errorSchema.safeParse(error)
if (parsed.success) {
return {
error: { code: parsed.data.code, message: parsed.data.error },
}
}
return { error: { code: 0, message: String(error) } }
})
}
}

View File

@@ -2,7 +2,7 @@ import * as fs from "fs/promises"
import * as cp from "child_process"
import { SubContainer, types as T } from "@start9labs/start-sdk"
import { promisify } from "util"
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
import { DockerProcedure } from "../../../Models/DockerProcedure"
import { Volume } from "./matchVolume"
import {
CommandOptions,
@@ -28,7 +28,7 @@ export class DockerProcedureContainer extends Drop {
effects: T.Effects,
packageId: string,
data: DockerProcedure,
volumes: { [id: VolumeId]: Volume },
volumes: { [id: string]: Volume },
name: string,
options: { subcontainer?: SubContainer<SDKManifest> } = {},
) {
@@ -47,7 +47,7 @@ export class DockerProcedureContainer extends Drop {
effects: T.Effects,
packageId: string,
data: DockerProcedure,
volumes: { [id: VolumeId]: Volume },
volumes: { [id: string]: Volume },
name: string,
) {
const subcontainer = await SubContainerOwned.of(
@@ -64,7 +64,7 @@ export class DockerProcedureContainer extends Drop {
? `${subcontainer.rootfs}${mounts[mount]}`
: `${subcontainer.rootfs}/${mounts[mount]}`
await fs.mkdir(path, { recursive: true })
const volumeMount = volumes[mount]
const volumeMount: Volume = volumes[mount]
if (volumeMount.type === "data") {
await subcontainer.mount(
Mounts.of().mountVolume({

View File

@@ -15,26 +15,11 @@ import { System } from "../../../Interfaces/System"
import { matchManifest, Manifest } from "./matchManifest"
import * as childProcess from "node:child_process"
import { DockerProcedureContainer } from "./DockerProcedureContainer"
import { DockerProcedure } from "../../../Models/DockerProcedure"
import { promisify } from "node:util"
import * as U from "./oldEmbassyTypes"
import { MainLoop } from "./MainLoop"
import {
matches,
boolean,
dictionary,
literal,
literals,
object,
string,
unknown,
any,
tuple,
number,
anyOf,
deferred,
Parser,
array,
} from "ts-matches"
import { z } from "@start9labs/start-sdk"
import { AddSslOptions } from "@start9labs/start-sdk/base/lib/osBindings"
import {
BindOptionsByProtocol,
@@ -57,6 +42,15 @@ function todo(): never {
throw new Error("Not implemented")
}
/**
* Local type for procedure values from the manifest.
* The manifest's zod schemas use ZodTypeAny casts that produce `unknown` in zod v4.
* This type restores the expected shape for type-safe property access.
*/
type Procedure =
| (DockerProcedure & { type: "docker" })
| { type: "script"; args: unknown[] | null }
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
@@ -65,26 +59,24 @@ const configFile = FileHelper.json(
base: new Volume("embassy"),
subpath: "config.json",
},
matches.any,
z.any(),
)
const dependsOnFile = FileHelper.json(
{
base: new Volume("embassy"),
subpath: "dependsOn.json",
},
dictionary([string, array(string)]),
z.record(z.string(), z.array(z.string())),
)
const matchResult = object({
result: any,
const matchResult = z.object({
result: z.any(),
})
const matchError = object({
error: string,
const matchError = z.object({
error: z.string(),
})
const matchErrorCode = object<{
"error-code": [number, string] | readonly [number, string]
}>({
"error-code": tuple(number, string),
const matchErrorCode = z.object({
"error-code": z.tuple([z.number(), z.string()]),
})
const assertNever = (
@@ -96,29 +88,34 @@ const assertNever = (
/**
Should be changing the type for specific properties, and this is mostly a transformation for the old return types to the newer one.
*/
function isMatchResult(a: unknown): a is z.infer<typeof matchResult> {
return matchResult.safeParse(a).success
}
function isMatchError(a: unknown): a is z.infer<typeof matchError> {
return matchError.safeParse(a).success
}
function isMatchErrorCode(a: unknown): a is z.infer<typeof matchErrorCode> {
return matchErrorCode.safeParse(a).success
}
const fromReturnType = <A>(a: U.ResultType<A>): A => {
if (matchResult.test(a)) {
if (isMatchResult(a)) {
return a.result
}
if (matchError.test(a)) {
if (isMatchError(a)) {
console.info({ passedErrorStack: new Error().stack, error: a.error })
throw { error: a.error }
}
if (matchErrorCode.test(a)) {
if (isMatchErrorCode(a)) {
const [code, message] = a["error-code"]
throw { error: message, code }
}
return assertNever(a)
return assertNever(a as never)
}
const matchSetResult = object({
"depends-on": dictionary([string, array(string)])
.nullable()
.optional(),
dependsOn: dictionary([string, array(string)])
.nullable()
.optional(),
signal: literals(
const matchSetResult = z.object({
"depends-on": z.record(z.string(), z.array(z.string())).nullable().optional(),
dependsOn: z.record(z.string(), z.array(z.string())).nullable().optional(),
signal: z.enum([
"SIGTERM",
"SIGHUP",
"SIGINT",
@@ -151,7 +148,7 @@ const matchSetResult = object({
"SIGPWR",
"SIGSYS",
"SIGINFO",
),
]),
})
type OldGetConfigRes = {
@@ -233,33 +230,29 @@ const asProperty = (x: PackagePropertiesV2): PropertiesReturn =>
Object.fromEntries(
Object.entries(x).map(([key, value]) => [key, asProperty_(value)]),
)
const [matchPackageProperties, setMatchPackageProperties] =
deferred<PackagePropertiesV2>()
const matchPackagePropertyObject: Parser<unknown, PackagePropertyObject> =
object({
value: matchPackageProperties,
type: literal("object"),
description: string,
})
const matchPackagePropertyObject: z.ZodType<PackagePropertyObject> = z.object({
value: z.lazy(() => matchPackageProperties),
type: z.literal("object"),
description: z.string(),
})
const matchPackagePropertyString: Parser<unknown, PackagePropertyString> =
object({
type: literal("string"),
description: string.nullable().optional(),
value: string,
copyable: boolean.nullable().optional(),
qr: boolean.nullable().optional(),
masked: boolean.nullable().optional(),
})
setMatchPackageProperties(
dictionary([
string,
anyOf(matchPackagePropertyObject, matchPackagePropertyString),
]),
const matchPackagePropertyString: z.ZodType<PackagePropertyString> = z.object({
type: z.literal("string"),
description: z.string().nullable().optional(),
value: z.string(),
copyable: z.boolean().nullable().optional(),
qr: z.boolean().nullable().optional(),
masked: z.boolean().nullable().optional(),
})
const matchPackageProperties: z.ZodType<PackagePropertiesV2> = z.lazy(() =>
z.record(
z.string(),
z.union([matchPackagePropertyObject, matchPackagePropertyString]),
),
)
const matchProperties = object({
version: literal(2),
const matchProperties = z.object({
version: z.literal(2),
data: matchPackageProperties,
})
@@ -303,7 +296,7 @@ export class SystemForEmbassy implements System {
})
const manifestData = await fs.readFile(manifestLocation, "utf-8")
return new SystemForEmbassy(
matchManifest.unsafeCast(JSON.parse(manifestData)),
matchManifest.parse(JSON.parse(manifestData)),
moduleCode,
)
}
@@ -389,7 +382,9 @@ export class SystemForEmbassy implements System {
delete this.currentRunning
if (currentRunning) {
await currentRunning.clean({
timeout: fromDuration(this.manifest.main["sigterm-timeout"] || "30s"),
timeout: fromDuration(
(this.manifest.main["sigterm-timeout"] as any) || "30s",
),
})
}
}
@@ -623,7 +618,7 @@ export class SystemForEmbassy implements System {
effects: Effects,
timeoutMs: number | null,
): Promise<void> {
const backup = this.manifest.backup.create
const backup = this.manifest.backup.create as Procedure
if (backup.type === "docker") {
const commands = [backup.entrypoint, ...backup.args]
const container = await DockerProcedureContainer.of(
@@ -656,7 +651,7 @@ export class SystemForEmbassy implements System {
encoding: "utf-8",
})
.catch((_) => null)
const restoreBackup = this.manifest.backup.restore
const restoreBackup = this.manifest.backup.restore as Procedure
if (restoreBackup.type === "docker") {
const commands = [restoreBackup.entrypoint, ...restoreBackup.args]
const container = await DockerProcedureContainer.of(
@@ -689,7 +684,7 @@ export class SystemForEmbassy implements System {
effects: Effects,
timeoutMs: number | null,
): Promise<OldGetConfigRes> {
const config = this.manifest.config?.get
const config = this.manifest.config?.get as Procedure | undefined
if (!config) return { spec: {} }
if (config.type === "docker") {
const commands = [config.entrypoint, ...config.args]
@@ -731,7 +726,7 @@ export class SystemForEmbassy implements System {
)
await updateConfig(effects, this.manifest, spec, newConfig)
await configFile.write(effects, newConfig)
const setConfigValue = this.manifest.config?.set
const setConfigValue = this.manifest.config?.set as Procedure | undefined
if (!setConfigValue) return
if (setConfigValue.type === "docker") {
const commands = [
@@ -746,7 +741,7 @@ export class SystemForEmbassy implements System {
this.manifest.volumes,
`Set Config - ${commands.join(" ")}`,
)
const answer = matchSetResult.unsafeCast(
const answer = matchSetResult.parse(
JSON.parse(
(await container.execFail(commands, timeoutMs)).stdout.toString(),
),
@@ -759,7 +754,7 @@ export class SystemForEmbassy implements System {
const method = moduleCode.setConfig
if (!method) throw new Error("Expecting that the method setConfig exists")
const answer = matchSetResult.unsafeCast(
const answer = matchSetResult.parse(
await method(
polyfillEffects(effects, this.manifest),
newConfig as U.Config,
@@ -788,7 +783,11 @@ export class SystemForEmbassy implements System {
const requiredDeps = {
...Object.fromEntries(
Object.entries(this.manifest.dependencies ?? {})
.filter(([k, v]) => v?.requirement.type === "required")
.filter(
([k, v]) =>
(v?.requirement as { type: string } | undefined)?.type ===
"required",
)
.map((x) => [x[0], []]) || [],
),
}
@@ -856,7 +855,7 @@ export class SystemForEmbassy implements System {
}
if (migration) {
const [_, procedure] = migration
const [_, procedure] = migration as readonly [unknown, Procedure]
if (procedure.type === "docker") {
const commands = [procedure.entrypoint, ...procedure.args]
const container = await DockerProcedureContainer.of(
@@ -894,7 +893,10 @@ export class SystemForEmbassy implements System {
effects: Effects,
timeoutMs: number | null,
): Promise<PropertiesReturn> {
const setConfigValue = this.manifest.properties
const setConfigValue = this.manifest.properties as
| Procedure
| null
| undefined
if (!setConfigValue) throw new Error("There is no properties")
if (setConfigValue.type === "docker") {
const commands = [setConfigValue.entrypoint, ...setConfigValue.args]
@@ -905,7 +907,7 @@ export class SystemForEmbassy implements System {
this.manifest.volumes,
`Properties - ${commands.join(" ")}`,
)
const properties = matchProperties.unsafeCast(
const properties = matchProperties.parse(
JSON.parse(
(await container.execFail(commands, timeoutMs)).stdout.toString(),
),
@@ -916,7 +918,7 @@ export class SystemForEmbassy implements System {
const method = moduleCode.properties
if (!method)
throw new Error("Expecting that the method properties exists")
const properties = matchProperties.unsafeCast(
const properties = matchProperties.parse(
await method(polyfillEffects(effects, this.manifest)).then(
fromReturnType,
),
@@ -931,7 +933,8 @@ export class SystemForEmbassy implements System {
formData: unknown,
timeoutMs: number | null,
): Promise<T.ActionResult> {
const actionProcedure = this.manifest.actions?.[actionId]?.implementation
const actionProcedure = this.manifest.actions?.[actionId]
?.implementation as Procedure | undefined
const toActionResult = ({
message,
value,
@@ -998,7 +1001,9 @@ export class SystemForEmbassy implements System {
oldConfig: unknown,
timeoutMs: number | null,
): Promise<object> {
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check
const actionProcedure = this.manifest.dependencies?.[id]?.config?.check as
| Procedure
| undefined
if (!actionProcedure) return { message: "Action not found", value: null }
if (actionProcedure.type === "docker") {
const commands = [
@@ -1090,40 +1095,50 @@ export class SystemForEmbassy implements System {
}
}
const matchPointer = object({
type: literal("pointer"),
const matchPointer = z.object({
type: z.literal("pointer"),
})
const matchPointerPackage = object({
subtype: literal("package"),
target: literals("tor-key", "tor-address", "lan-address"),
"package-id": string,
interface: string,
const matchPointerPackage = z.object({
subtype: z.literal("package"),
target: z.enum(["tor-key", "tor-address", "lan-address"]),
"package-id": z.string(),
interface: z.string(),
})
const matchPointerConfig = object({
subtype: literal("package"),
target: literals("config"),
"package-id": string,
selector: string,
multi: boolean,
const matchPointerConfig = z.object({
subtype: z.literal("package"),
target: z.enum(["config"]),
"package-id": z.string(),
selector: z.string(),
multi: z.boolean(),
})
const matchSpec = object({
spec: object,
const matchSpec = z.object({
spec: z.record(z.string(), z.unknown()),
})
const matchVariants = object({ variants: dictionary([string, unknown]) })
const matchVariants = z.object({ variants: z.record(z.string(), z.unknown()) })
function isMatchPointer(v: unknown): v is z.infer<typeof matchPointer> {
return matchPointer.safeParse(v).success
}
function isMatchSpec(v: unknown): v is z.infer<typeof matchSpec> {
return matchSpec.safeParse(v).success
}
function isMatchVariants(v: unknown): v is z.infer<typeof matchVariants> {
return matchVariants.safeParse(v).success
}
function cleanSpecOfPointers<T>(mutSpec: T): T {
if (!object.test(mutSpec)) return mutSpec
if (typeof mutSpec !== "object" || mutSpec === null) return mutSpec
for (const key in mutSpec) {
const value = mutSpec[key]
if (matchSpec.test(value)) value.spec = cleanSpecOfPointers(value.spec)
if (matchVariants.test(value))
if (isMatchSpec(value))
value.spec = cleanSpecOfPointers(value.spec) as Record<string, unknown>
if (isMatchVariants(value))
value.variants = Object.fromEntries(
Object.entries(value.variants).map(([key, value]) => [
key,
cleanSpecOfPointers(value),
]),
)
if (!matchPointer.test(value)) continue
if (!isMatchPointer(value)) continue
delete mutSpec[key]
// // if (value.target === )
}
@@ -1269,7 +1284,7 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) {
}
async function convertToNewConfig(value: OldGetConfigRes) {
try {
const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec)
const valueSpec: OldConfigSpec = matchOldConfigSpec.parse(value.spec)
const spec = transformConfigSpec(valueSpec)
if (!value.config) return { spec, config: null }
const config = transformOldConfigToNew(valueSpec, value.config) ?? null

View File

@@ -4,9 +4,9 @@ import synapseManifest from "./__fixtures__/synapseManifest"
describe("matchManifest", () => {
test("gittea", () => {
matchManifest.unsafeCast(giteaManifest)
matchManifest.parse(giteaManifest)
})
test("synapse", () => {
matchManifest.unsafeCast(synapseManifest)
matchManifest.parse(synapseManifest)
})
})

View File

@@ -1,126 +1,121 @@
import {
object,
literal,
string,
array,
boolean,
dictionary,
literals,
number,
unknown,
some,
every,
} from "ts-matches"
import { z } from "@start9labs/start-sdk"
import { matchVolume } from "./matchVolume"
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
const matchJsProcedure = object({
type: literal("script"),
args: array(unknown).nullable().optional().defaultTo([]),
const matchJsProcedure = z.object({
type: z.literal("script"),
args: z.array(z.unknown()).nullable().optional().default([]),
})
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
export type Procedure = typeof matchProcedure._TYPE
const matchProcedure = z.union([matchDockerProcedure, matchJsProcedure])
export type Procedure = z.infer<typeof matchProcedure>
const matchAction = object({
name: string,
description: string,
warning: string.nullable().optional(),
const matchAction = z.object({
name: z.string(),
description: z.string(),
warning: z.string().nullable().optional(),
implementation: matchProcedure,
"allowed-statuses": array(literals("running", "stopped")),
"input-spec": unknown.nullable().optional(),
"allowed-statuses": z.array(z.enum(["running", "stopped"])),
"input-spec": z.unknown().nullable().optional(),
})
export const matchManifest = object({
id: string,
title: string,
version: string,
export const matchManifest = z.object({
id: z.string(),
title: z.string(),
version: z.string(),
main: matchDockerProcedure,
assets: object({
assets: string.nullable().optional(),
scripts: string.nullable().optional(),
})
assets: z
.object({
assets: z.string().nullable().optional(),
scripts: z.string().nullable().optional(),
})
.nullable()
.optional(),
"health-checks": dictionary([
string,
every(
"health-checks": z.record(
z.string(),
z.intersection(
matchProcedure,
object({
name: string,
["success-message"]: string.nullable().optional(),
z.object({
name: z.string(),
"success-message": z.string().nullable().optional(),
}),
),
]),
config: object({
get: matchProcedure,
set: matchProcedure,
})
),
config: z
.object({
get: matchProcedure,
set: matchProcedure,
})
.nullable()
.optional(),
properties: matchProcedure.nullable().optional(),
volumes: dictionary([string, matchVolume]),
interfaces: dictionary([
string,
object({
name: string,
description: string,
"tor-config": object({
"port-mapping": dictionary([string, string]),
})
volumes: z.record(z.string(), matchVolume),
interfaces: z.record(
z.string(),
z.object({
name: z.string(),
description: z.string(),
"tor-config": z
.object({
"port-mapping": z.record(z.string(), z.string()),
})
.nullable()
.optional(),
"lan-config": dictionary([
string,
object({
ssl: boolean,
internal: number,
}),
])
"lan-config": z
.record(
z.string(),
z.object({
ssl: z.boolean(),
internal: z.number(),
}),
)
.nullable()
.optional(),
ui: boolean,
protocols: array(string),
ui: z.boolean(),
protocols: z.array(z.string()),
}),
]),
backup: object({
),
backup: z.object({
create: matchProcedure,
restore: matchProcedure,
}),
migrations: object({
to: dictionary([string, matchProcedure]),
from: dictionary([string, matchProcedure]),
})
migrations: z
.object({
to: z.record(z.string(), matchProcedure),
from: z.record(z.string(), matchProcedure),
})
.nullable()
.optional(),
dependencies: dictionary([
string,
object({
version: string,
requirement: some(
object({
type: literal("opt-in"),
how: string,
}),
object({
type: literal("opt-out"),
how: string,
}),
object({
type: literal("required"),
}),
),
description: string.nullable().optional(),
config: object({
check: matchProcedure,
"auto-configure": matchProcedure,
dependencies: z.record(
z.string(),
z
.object({
version: z.string(),
requirement: z.union([
z.object({
type: z.literal("opt-in"),
how: z.string(),
}),
z.object({
type: z.literal("opt-out"),
how: z.string(),
}),
z.object({
type: z.literal("required"),
}),
]),
description: z.string().nullable().optional(),
config: z
.object({
check: matchProcedure,
"auto-configure": matchProcedure,
})
.nullable()
.optional(),
})
.nullable()
.optional(),
})
.nullable()
.optional(),
]),
),
actions: dictionary([string, matchAction]),
actions: z.record(z.string(), matchAction),
})
export type Manifest = typeof matchManifest._TYPE
export type Manifest = z.infer<typeof matchManifest>

View File

@@ -1,32 +1,32 @@
import { object, literal, string, boolean, some } from "ts-matches"
import { z } from "@start9labs/start-sdk"
const matchDataVolume = object({
type: literal("data"),
readonly: boolean.optional(),
const matchDataVolume = z.object({
type: z.literal("data"),
readonly: z.boolean().optional(),
})
const matchAssetVolume = object({
type: literal("assets"),
const matchAssetVolume = z.object({
type: z.literal("assets"),
})
const matchPointerVolume = object({
type: literal("pointer"),
"package-id": string,
"volume-id": string,
path: string,
readonly: boolean,
const matchPointerVolume = z.object({
type: z.literal("pointer"),
"package-id": z.string(),
"volume-id": z.string(),
path: z.string(),
readonly: z.boolean(),
})
const matchCertificateVolume = object({
type: literal("certificate"),
"interface-id": string,
const matchCertificateVolume = z.object({
type: z.literal("certificate"),
"interface-id": z.string(),
})
const matchBackupVolume = object({
type: literal("backup"),
readonly: boolean,
const matchBackupVolume = z.object({
type: z.literal("backup"),
readonly: z.boolean(),
})
export const matchVolume = some(
export const matchVolume = z.union([
matchDataVolume,
matchAssetVolume,
matchPointerVolume,
matchCertificateVolume,
matchBackupVolume,
)
export type Volume = typeof matchVolume._TYPE
])
export type Volume = z.infer<typeof matchVolume>

View File

@@ -12,43 +12,43 @@ import nostrConfig2 from "./__fixtures__/nostrConfig2"
describe("transformConfigSpec", () => {
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
matchOldConfigSpec.unsafeCast(
matchOldConfigSpec.parse(
fixtureEmbassyPagesConfig.homepage.variants["web-page"],
)
})
test("matchOldConfigSpec(embassyPages)", () => {
matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
matchOldConfigSpec.parse(fixtureEmbassyPagesConfig)
})
test("transformConfigSpec(embassyPages)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
const spec = matchOldConfigSpec.parse(fixtureEmbassyPagesConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("matchOldConfigSpec(RTL.nodes)", () => {
matchOldValueSpecList.unsafeCast(fixtureRTLConfig.nodes)
matchOldValueSpecList.parse(fixtureRTLConfig.nodes)
})
test("matchOldConfigSpec(RTL)", () => {
matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
matchOldConfigSpec.parse(fixtureRTLConfig)
})
test("transformConfigSpec(RTL)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
const spec = matchOldConfigSpec.parse(fixtureRTLConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("transformConfigSpec(searNXG)", () => {
const spec = matchOldConfigSpec.unsafeCast(searNXG)
const spec = matchOldConfigSpec.parse(searNXG)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("transformConfigSpec(bitcoind)", () => {
const spec = matchOldConfigSpec.unsafeCast(bitcoind)
const spec = matchOldConfigSpec.parse(bitcoind)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("transformConfigSpec(nostr)", () => {
const spec = matchOldConfigSpec.unsafeCast(nostr)
const spec = matchOldConfigSpec.parse(nostr)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("transformConfigSpec(nostr2)", () => {
const spec = matchOldConfigSpec.unsafeCast(nostrConfig2)
const spec = matchOldConfigSpec.parse(nostrConfig2)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
})

View File

@@ -1,19 +1,4 @@
import { IST } from "@start9labs/start-sdk"
import {
dictionary,
object,
anyOf,
string,
literals,
array,
number,
boolean,
Parser,
deferred,
every,
nill,
literal,
} from "ts-matches"
import { IST, z } from "@start9labs/start-sdk"
export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
@@ -82,7 +67,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
name: oldVal.name,
description: oldVal.description || null,
warning: oldVal.warning || null,
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)),
spec: transformConfigSpec(matchOldConfigSpec.parse(oldVal.spec)),
}
} else if (oldVal.type === "string") {
newVal = {
@@ -121,7 +106,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
...obj,
[id]: {
name: oldVal.tag["variant-names"][id] || id,
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)),
spec: transformConfigSpec(matchOldConfigSpec.parse(spec)),
},
}),
{} as Record<string, { name: string; spec: IST.InputSpec }>,
@@ -153,7 +138,7 @@ export function transformOldConfigToNew(
if (isObject(val)) {
newVal = transformOldConfigToNew(
matchOldConfigSpec.unsafeCast(val.spec),
matchOldConfigSpec.parse(val.spec),
config[key],
)
}
@@ -172,7 +157,7 @@ export function transformOldConfigToNew(
newVal = {
selection,
value: transformOldConfigToNew(
matchOldConfigSpec.unsafeCast(val.variants[selection]),
matchOldConfigSpec.parse(val.variants[selection]),
config[key],
),
}
@@ -183,10 +168,7 @@ export function transformOldConfigToNew(
if (isObjectList(val)) {
newVal = (config[key] as object[]).map((obj) =>
transformOldConfigToNew(
matchOldConfigSpec.unsafeCast(val.spec.spec),
obj,
),
transformOldConfigToNew(matchOldConfigSpec.parse(val.spec.spec), obj),
)
} else if (isUnionList(val)) return obj
}
@@ -212,7 +194,7 @@ export function transformNewConfigToOld(
if (isObject(val)) {
newVal = transformNewConfigToOld(
matchOldConfigSpec.unsafeCast(val.spec),
matchOldConfigSpec.parse(val.spec),
config[key],
)
}
@@ -221,7 +203,7 @@ export function transformNewConfigToOld(
newVal = {
[val.tag.id]: config[key].selection,
...transformNewConfigToOld(
matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]),
matchOldConfigSpec.parse(val.variants[config[key].selection]),
config[key].value,
),
}
@@ -230,10 +212,7 @@ export function transformNewConfigToOld(
if (isList(val)) {
if (isObjectList(val)) {
newVal = (config[key] as object[]).map((obj) =>
transformNewConfigToOld(
matchOldConfigSpec.unsafeCast(val.spec.spec),
obj,
),
transformNewConfigToOld(matchOldConfigSpec.parse(val.spec.spec), obj),
)
} else if (isUnionList(val)) return obj
}
@@ -337,9 +316,7 @@ function getListSpec(
default: oldVal.default as Record<string, unknown>[],
spec: {
type: "object",
spec: transformConfigSpec(
matchOldConfigSpec.unsafeCast(oldVal.spec.spec),
),
spec: transformConfigSpec(matchOldConfigSpec.parse(oldVal.spec.spec)),
uniqueBy: oldVal.spec["unique-by"] || null,
displayAs: oldVal.spec["display-as"] || null,
},
@@ -393,211 +370,281 @@ function isUnionList(
}
export type OldConfigSpec = Record<string, OldValueSpec>
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
unknown,
OldConfigSpec
>
export const matchOldDefaultString = anyOf(
string,
object({ charset: string, len: number }),
export const matchOldConfigSpec: z.ZodType<OldConfigSpec> = z.lazy(() =>
z.record(z.string(), matchOldValueSpec),
)
type OldDefaultString = typeof matchOldDefaultString._TYPE
export const matchOldDefaultString = z.union([
z.string(),
z.object({ charset: z.string(), len: z.number() }),
])
type OldDefaultString = z.infer<typeof matchOldDefaultString>
export const matchOldValueSpecString = object({
type: literals("string"),
name: string,
masked: boolean.nullable().optional(),
copyable: boolean.nullable().optional(),
nullable: boolean.nullable().optional(),
placeholder: string.nullable().optional(),
pattern: string.nullable().optional(),
"pattern-description": string.nullable().optional(),
export const matchOldValueSpecString = z.object({
type: z.enum(["string"]),
name: z.string(),
masked: z.boolean().nullable().optional(),
copyable: z.boolean().nullable().optional(),
nullable: z.boolean().nullable().optional(),
placeholder: z.string().nullable().optional(),
pattern: z.string().nullable().optional(),
"pattern-description": z.string().nullable().optional(),
default: matchOldDefaultString.nullable().optional(),
textarea: boolean.nullable().optional(),
description: string.nullable().optional(),
warning: string.nullable().optional(),
textarea: z.boolean().nullable().optional(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
})
export const matchOldValueSpecNumber = object({
type: literals("number"),
nullable: boolean,
name: string,
range: string,
integral: boolean,
default: number.nullable().optional(),
description: string.nullable().optional(),
warning: string.nullable().optional(),
units: string.nullable().optional(),
placeholder: anyOf(number, string).nullable().optional(),
export const matchOldValueSpecNumber = z.object({
type: z.enum(["number"]),
nullable: z.boolean(),
name: z.string(),
range: z.string(),
integral: z.boolean(),
default: z.number().nullable().optional(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
units: z.string().nullable().optional(),
placeholder: z.union([z.number(), z.string()]).nullable().optional(),
})
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
type OldValueSpecNumber = z.infer<typeof matchOldValueSpecNumber>
export const matchOldValueSpecBoolean = object({
type: literals("boolean"),
default: boolean,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
export const matchOldValueSpecBoolean = z.object({
type: z.enum(["boolean"]),
default: z.boolean(),
name: z.string(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
})
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
type OldValueSpecBoolean = z.infer<typeof matchOldValueSpecBoolean>
const matchOldValueSpecObject = object({
type: literals("object"),
spec: _matchOldConfigSpec,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
type OldValueSpecObject = {
type: "object"
spec: OldConfigSpec
name: string
description?: string | null
warning?: string | null
}
const matchOldValueSpecObject: z.ZodType<OldValueSpecObject> = z.object({
type: z.enum(["object"]),
spec: z.lazy(() => matchOldConfigSpec),
name: z.string(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
})
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
const matchOldValueSpecEnum = object({
values: array(string),
"value-names": dictionary([string, string]),
type: literals("enum"),
default: string,
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
const matchOldValueSpecEnum = z.object({
values: z.array(z.string()),
"value-names": z.record(z.string(), z.string()),
type: z.enum(["enum"]),
default: z.string(),
name: z.string(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
})
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
type OldValueSpecEnum = z.infer<typeof matchOldValueSpecEnum>
const matchOldUnionTagSpec = object({
id: string, // The name of the field containing one of the union variants
"variant-names": dictionary([string, string]), // The name of each variant
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
const matchOldUnionTagSpec = z.object({
id: z.string(), // The name of the field containing one of the union variants
"variant-names": z.record(z.string(), z.string()), // The name of each variant
name: z.string(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
})
const matchOldValueSpecUnion = object({
type: literals("union"),
type OldValueSpecUnion = {
type: "union"
tag: z.infer<typeof matchOldUnionTagSpec>
variants: Record<string, OldConfigSpec>
default: string
}
const matchOldValueSpecUnion: z.ZodType<OldValueSpecUnion> = z.object({
type: z.enum(["union"]),
tag: matchOldUnionTagSpec,
variants: dictionary([string, _matchOldConfigSpec]),
default: string,
variants: z.record(
z.string(),
z.lazy(() => matchOldConfigSpec),
),
default: z.string(),
})
type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE
const [matchOldUniqueBy, setOldUniqueBy] = deferred<OldUniqueBy>()
type OldUniqueBy =
| null
| string
| { any: OldUniqueBy[] }
| { all: OldUniqueBy[] }
setOldUniqueBy(
anyOf(
nill,
string,
object({ any: array(matchOldUniqueBy) }),
object({ all: array(matchOldUniqueBy) }),
),
const matchOldUniqueBy: z.ZodType<OldUniqueBy> = z.lazy(() =>
z.union([
z.null(),
z.string(),
z.object({ any: z.array(matchOldUniqueBy) }),
z.object({ all: z.array(matchOldUniqueBy) }),
]),
)
const matchOldListValueSpecObject = object({
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
"display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
})
const matchOldListValueSpecUnion = object({
type OldListValueSpecObject = {
spec: OldConfigSpec
"unique-by"?: OldUniqueBy | null
"display-as"?: string | null
}
const matchOldListValueSpecObject: z.ZodType<OldListValueSpecObject> = z.object(
{
spec: z.lazy(() => matchOldConfigSpec), // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
"display-as": z.string().nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
},
)
type OldListValueSpecUnion = {
"unique-by"?: OldUniqueBy | null
"display-as"?: string | null
tag: z.infer<typeof matchOldUnionTagSpec>
variants: Record<string, OldConfigSpec>
}
const matchOldListValueSpecUnion: z.ZodType<OldListValueSpecUnion> = z.object({
"unique-by": matchOldUniqueBy.nullable().optional(),
"display-as": string.nullable().optional(),
"display-as": z.string().nullable().optional(),
tag: matchOldUnionTagSpec,
variants: dictionary([string, _matchOldConfigSpec]),
variants: z.record(
z.string(),
z.lazy(() => matchOldConfigSpec),
),
})
const matchOldListValueSpecString = object({
masked: boolean.nullable().optional(),
copyable: boolean.nullable().optional(),
pattern: string.nullable().optional(),
"pattern-description": string.nullable().optional(),
placeholder: string.nullable().optional(),
const matchOldListValueSpecString = z.object({
masked: z.boolean().nullable().optional(),
copyable: z.boolean().nullable().optional(),
pattern: z.string().nullable().optional(),
"pattern-description": z.string().nullable().optional(),
placeholder: z.string().nullable().optional(),
})
const matchOldListValueSpecEnum = object({
values: array(string),
"value-names": dictionary([string, string]),
const matchOldListValueSpecEnum = z.object({
values: z.array(z.string()),
"value-names": z.record(z.string(), z.string()),
})
const matchOldListValueSpecNumber = object({
range: string,
integral: boolean,
units: string.nullable().optional(),
placeholder: anyOf(number, string).nullable().optional(),
const matchOldListValueSpecNumber = z.object({
range: z.string(),
integral: z.boolean(),
units: z.string().nullable().optional(),
placeholder: z.union([z.number(), z.string()]).nullable().optional(),
})
type OldValueSpecListBase = {
type: "list"
range: string
default: string[] | number[] | OldDefaultString[] | Record<string, unknown>[]
name: string
description?: string | null
warning?: string | null
}
type OldValueSpecList = OldValueSpecListBase &
(
| { subtype: "string"; spec: z.infer<typeof matchOldListValueSpecString> }
| { subtype: "enum"; spec: z.infer<typeof matchOldListValueSpecEnum> }
| { subtype: "object"; spec: OldListValueSpecObject }
| { subtype: "number"; spec: z.infer<typeof matchOldListValueSpecNumber> }
| { subtype: "union"; spec: OldListValueSpecUnion }
)
// represents a spec for a list
export const matchOldValueSpecList = every(
object({
type: literals("list"),
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: anyOf(
array(string),
array(number),
array(matchOldDefaultString),
array(object),
),
name: string,
description: string.nullable().optional(),
warning: string.nullable().optional(),
}),
anyOf(
object({
subtype: literals("string"),
spec: matchOldListValueSpecString,
export const matchOldValueSpecList: z.ZodType<OldValueSpecList> =
z.intersection(
z.object({
type: z.enum(["list"]),
range: z.string(), // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
default: z.union([
z.array(z.string()),
z.array(z.number()),
z.array(matchOldDefaultString),
z.array(z.object({}).passthrough()),
]),
name: z.string(),
description: z.string().nullable().optional(),
warning: z.string().nullable().optional(),
}),
object({
subtype: literals("enum"),
spec: matchOldListValueSpecEnum,
}),
object({
subtype: literals("object"),
spec: matchOldListValueSpecObject,
}),
object({
subtype: literals("number"),
spec: matchOldListValueSpecNumber,
}),
object({
subtype: literals("union"),
spec: matchOldListValueSpecUnion,
}),
),
)
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
z.union([
z.object({
subtype: z.enum(["string"]),
spec: matchOldListValueSpecString,
}),
z.object({
subtype: z.enum(["enum"]),
spec: matchOldListValueSpecEnum,
}),
z.object({
subtype: z.enum(["object"]),
spec: matchOldListValueSpecObject,
}),
z.object({
subtype: z.enum(["number"]),
spec: matchOldListValueSpecNumber,
}),
z.object({
subtype: z.enum(["union"]),
spec: matchOldListValueSpecUnion,
}),
]),
) as unknown as z.ZodType<OldValueSpecList>
const matchOldValueSpecPointer = every(
object({
type: literal("pointer"),
}),
anyOf(
object({
subtype: literal("package"),
target: literals("tor-key", "tor-address", "lan-address"),
"package-id": string,
interface: string,
}),
object({
subtype: literal("package"),
target: literals("config"),
"package-id": string,
selector: string,
multi: boolean,
}),
),
type OldValueSpecPointer = {
type: "pointer"
} & (
| {
subtype: "package"
target: "tor-key" | "tor-address" | "lan-address"
"package-id": string
interface: string
}
| {
subtype: "package"
target: "config"
"package-id": string
selector: string
multi: boolean
}
)
type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE
const matchOldValueSpecPointer: z.ZodType<OldValueSpecPointer> = z.intersection(
z.object({
type: z.literal("pointer"),
}),
z.union([
z.object({
subtype: z.literal("package"),
target: z.enum(["tor-key", "tor-address", "lan-address"]),
"package-id": z.string(),
interface: z.string(),
}),
z.object({
subtype: z.literal("package"),
target: z.enum(["config"]),
"package-id": z.string(),
selector: z.string(),
multi: z.boolean(),
}),
]),
) as unknown as z.ZodType<OldValueSpecPointer>
export const matchOldValueSpec = anyOf(
type OldValueSpecString = z.infer<typeof matchOldValueSpecString>
type OldValueSpec =
| OldValueSpecString
| OldValueSpecNumber
| OldValueSpecBoolean
| OldValueSpecObject
| OldValueSpecEnum
| OldValueSpecList
| OldValueSpecUnion
| OldValueSpecPointer
export const matchOldValueSpec: z.ZodType<OldValueSpec> = z.union([
matchOldValueSpecString,
matchOldValueSpecNumber,
matchOldValueSpecBoolean,
matchOldValueSpecObject,
matchOldValueSpecObject as z.ZodType<OldValueSpecObject>,
matchOldValueSpecEnum,
matchOldValueSpecList,
matchOldValueSpecUnion,
matchOldValueSpecPointer,
)
type OldValueSpec = typeof matchOldValueSpec._TYPE
setMatchOldConfigSpec(dictionary([string, matchOldValueSpec]))
matchOldValueSpecList as z.ZodType<OldValueSpecList>,
matchOldValueSpecUnion as z.ZodType<OldValueSpecUnion>,
matchOldValueSpecPointer as z.ZodType<OldValueSpecPointer>,
])
export class Range {
min?: number

View File

@@ -1,41 +1,19 @@
import {
object,
literal,
string,
boolean,
array,
dictionary,
literals,
number,
Parser,
some,
} from "ts-matches"
import { z } from "@start9labs/start-sdk"
import { matchDuration } from "./Duration"
const VolumeId = string
const Path = string
export type VolumeId = string
export type Path = string
export const matchDockerProcedure = object({
type: literal("docker"),
image: string,
system: boolean.optional(),
entrypoint: string,
args: array(string).defaultTo([]),
mounts: dictionary([VolumeId, Path]).optional(),
"io-format": literals(
"json",
"json-pretty",
"yaml",
"cbor",
"toml",
"toml-pretty",
)
export const matchDockerProcedure = z.object({
type: z.literal("docker"),
image: z.string(),
system: z.boolean().optional(),
entrypoint: z.string(),
args: z.array(z.string()).default([]),
mounts: z.record(z.string(), z.string()).optional(),
"io-format": z
.enum(["json", "json-pretty", "yaml", "cbor", "toml", "toml-pretty"])
.nullable()
.optional(),
"sigterm-timeout": some(number, matchDuration).onMismatch(30),
inject: boolean.defaultTo(false),
"sigterm-timeout": z.union([z.number(), matchDuration]).catch(30),
inject: z.boolean().default(false),
})
export type DockerProcedure = typeof matchDockerProcedure._TYPE
export type DockerProcedure = z.infer<typeof matchDockerProcedure>

View File

@@ -1,11 +1,11 @@
import { string } from "ts-matches"
import { z } from "@start9labs/start-sdk"
export type TimeUnit = "d" | "h" | "s" | "ms" | "m" | "µs" | "ns"
export type Duration = `${number}${TimeUnit}`
const durationRegex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
export const matchDuration = string.refine(isDuration)
export const matchDuration = z.string().refine(isDuration)
export function isDuration(value: string): value is Duration {
return durationRegex.test(value)
}

View File

@@ -1,10 +1,10 @@
import { literals, some, string } from "ts-matches"
import { z } from "@start9labs/start-sdk"
type NestedPath<A extends string, B extends string> = `/${A}/${string}/${B}`
type NestedPaths = NestedPath<"actions", "run" | "getInput">
// prettier-ignore
type UnNestPaths<A> =
A extends `${infer A}/${infer B}` ? [...UnNestPaths<A>, ... UnNestPaths<B>] :
type UnNestPaths<A> =
A extends `${infer A}/${infer B}` ? [...UnNestPaths<A>, ... UnNestPaths<B>] :
[A]
export function unNestPath<A extends string>(a: A): UnNestPaths<A> {
@@ -17,14 +17,14 @@ function isNestedPath(path: string): path is NestedPaths {
return true
return false
}
export const jsonPath = some(
literals(
export const jsonPath = z.union([
z.enum([
"/packageInit",
"/packageUninit",
"/backup/create",
"/backup/restore",
),
string.refine(isNestedPath, "isNestedPath"),
)
]),
z.string().refine(isNestedPath),
])
export type JsonPath = typeof jsonPath._TYPE
export type JsonPath = z.infer<typeof jsonPath>