mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-31 04:33:40 +00:00
178 lines
6.8 KiB
JavaScript
178 lines
6.8 KiB
JavaScript
import { matches } from "../dependencies.js";
|
|
const isType = matches.shape({ type: matches.string });
|
|
const recordString = matches.dictionary([matches.string, matches.unknown]);
|
|
const matchDefault = matches.shape({ default: matches.unknown });
|
|
const matchNullable = matches.shape({ nullable: matches.literal(true) });
|
|
const matchPattern = matches.shape({ pattern: matches.string });
|
|
const rangeRegex = /(\[|\()(\*|(\d|\.)+),(\*|(\d|\.)+)(\]|\))/;
|
|
const matchRange = matches.shape({ range: matches.regex(rangeRegex) });
|
|
const matchIntegral = matches.shape({ integral: matches.literal(true) });
|
|
const matchSpec = matches.shape({ spec: recordString });
|
|
const matchSubType = matches.shape({ subtype: matches.string });
|
|
const matchUnion = matches.shape({
|
|
tag: matches.shape({ id: matches.string }),
|
|
variants: recordString,
|
|
});
|
|
const matchValues = matches.shape({
|
|
values: matches.arrayOf(matches.string),
|
|
});
|
|
function charRange(value = "") {
|
|
const split = value
|
|
.split("-")
|
|
.filter(Boolean)
|
|
.map((x) => x.charCodeAt(0));
|
|
if (split.length < 1)
|
|
return null;
|
|
if (split.length === 1)
|
|
return [split[0], split[0]];
|
|
return [split[0], split[1]];
|
|
}
|
|
/**
|
|
* @param generate.charset Pattern like "a-z" or "a-z,1-5"
|
|
* @param generate.len Length to make random variable
|
|
* @param param1
|
|
* @returns
|
|
*/
|
|
export function generateDefault(generate, { random = () => Math.random() } = {}) {
|
|
const validCharSets = generate.charset.split(",").map(charRange)
|
|
.filter(Array.isArray);
|
|
if (validCharSets.length === 0) {
|
|
throw new Error("Expecing that we have a valid charset");
|
|
}
|
|
const max = validCharSets.reduce((acc, x) => x.reduce((x, y) => Math.max(x, y), acc), 0);
|
|
let i = 0;
|
|
const answer = Array(generate.len);
|
|
while (i < generate.len) {
|
|
const nextValue = Math.round(random() * max);
|
|
const inRange = validCharSets.reduce((acc, [lower, upper]) => acc || (nextValue >= lower && nextValue <= upper), false);
|
|
if (!inRange)
|
|
continue;
|
|
answer[i] = String.fromCharCode(nextValue);
|
|
i++;
|
|
}
|
|
return answer.join("");
|
|
}
|
|
function withPattern(value) {
|
|
if (matchPattern.test(value))
|
|
return matches.regex(RegExp(value.pattern));
|
|
return matches.string;
|
|
}
|
|
export function matchNumberWithRange(range) {
|
|
const matched = rangeRegex.exec(range);
|
|
if (!matched)
|
|
return matches.number;
|
|
const [, left, leftValue, , rightValue, , right] = matched;
|
|
return matches.number
|
|
.validate(leftValue === "*"
|
|
? (_) => true
|
|
: left === "["
|
|
? (x) => x >= Number(leftValue)
|
|
: (x) => x > Number(leftValue), leftValue === "*"
|
|
? "any"
|
|
: left === "["
|
|
? `greaterThanOrEqualTo${leftValue}`
|
|
: `greaterThan${leftValue}`)
|
|
.validate(rightValue === "*"
|
|
? (_) => true
|
|
: right === "]"
|
|
? (x) => x <= Number(rightValue)
|
|
: (x) => x < Number(rightValue), rightValue === "*"
|
|
? "any"
|
|
: right === "]"
|
|
? `lessThanOrEqualTo${rightValue}`
|
|
: `lessThan${rightValue}`);
|
|
}
|
|
function withIntegral(parser, value) {
|
|
if (matchIntegral.test(value)) {
|
|
return parser.validate(Number.isInteger, "isIntegral");
|
|
}
|
|
return parser;
|
|
}
|
|
function withRange(value) {
|
|
if (matchRange.test(value)) {
|
|
return matchNumberWithRange(value.range);
|
|
}
|
|
return matches.number;
|
|
}
|
|
const isGenerator = matches.shape({ charset: matches.string, len: matches.number }).test;
|
|
function defaultNullable(parser, value) {
|
|
if (matchDefault.test(value)) {
|
|
if (isGenerator(value.default)) {
|
|
return parser.defaultTo(parser.unsafeCast(generateDefault(value.default)));
|
|
}
|
|
return parser.defaultTo(value.default);
|
|
}
|
|
if (matchNullable.test(value))
|
|
return parser.optional();
|
|
return parser;
|
|
}
|
|
/**
|
|
* ConfigSpec: 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(value) {
|
|
if (!isType.test(value)) {
|
|
// deno-lint-ignore no-explicit-any
|
|
return matches.unknown;
|
|
}
|
|
switch (value.type) {
|
|
case "boolean":
|
|
// deno-lint-ignore no-explicit-any
|
|
return defaultNullable(matches.boolean, value);
|
|
case "string":
|
|
// deno-lint-ignore no-explicit-any
|
|
return defaultNullable(withPattern(value), value);
|
|
case "number":
|
|
return defaultNullable(withIntegral(withRange(value), value), value);
|
|
case "object":
|
|
if (matchSpec.test(value)) {
|
|
// deno-lint-ignore no-explicit-any
|
|
return defaultNullable(typeFromProps(value.spec), value);
|
|
}
|
|
// deno-lint-ignore no-explicit-any
|
|
return matches.unknown;
|
|
case "list": {
|
|
const spec = (matchSpec.test(value) && value.spec) || {};
|
|
const rangeValidate = (matchRange.test(value) && matchNumberWithRange(value.range).test) ||
|
|
(() => true);
|
|
const subtype = matchSubType.unsafeCast(value).subtype;
|
|
return defaultNullable(matches
|
|
// deno-lint-ignore no-explicit-any
|
|
.arrayOf(guardAll({ type: subtype, ...spec }))
|
|
.validate((x) => rangeValidate(x.length), "valid length"), value);
|
|
}
|
|
case "enum":
|
|
if (matchValues.test(value)) {
|
|
return defaultNullable(matches.literals(value.values[0], ...value.values), value);
|
|
}
|
|
// deno-lint-ignore no-explicit-any
|
|
return matches.unknown;
|
|
case "union":
|
|
if (matchUnion.test(value)) {
|
|
return matches.some(...Object.entries(value.variants).map(([variant, spec]) => matches.shape({ [value.tag.id]: matches.literal(variant) }).concat(typeFromProps(spec))));
|
|
}
|
|
// deno-lint-ignore no-explicit-any
|
|
return matches.unknown;
|
|
}
|
|
// deno-lint-ignore no-explicit-any
|
|
return matches.unknown;
|
|
}
|
|
/**
|
|
* ConfigSpec: 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(valueDictionary) {
|
|
// deno-lint-ignore no-explicit-any
|
|
if (!recordString.test(valueDictionary))
|
|
return matches.unknown;
|
|
return matches.shape(Object.fromEntries(Object.entries(valueDictionary).map(([key, value]) => [key, guardAll(value)])));
|
|
}
|