diff --git a/lib/scripts/oldSpecToBuilder.ts b/lib/scripts/oldSpecToBuilder.ts index f016cda..223f526 100644 --- a/lib/scripts/oldSpecToBuilder.ts +++ b/lib/scripts/oldSpecToBuilder.ts @@ -1,5 +1,6 @@ import camelCase from "lodash/camelCase"; import * as fs from "fs"; +import { string } from "ts-matches"; export async function writeConvertedFile( file: string, @@ -97,14 +98,22 @@ export default async function makeFileContent( )})`; } case "enum": { + const allValueNames = new Set( + ...value?.spec?.["values"] || [], + ...Object.keys(value?.spec?.["value-names"] || {}) + ); + const values = Object.fromEntries( + Array.from(allValueNames) + .filter(string.test) + .map(key => [key, value?.spec?.["value-names"]?.[key] || key]) + ) return `Value.select(${JSON.stringify( { name: value.name || null, description: value.description || null, warning: value.warning || null, default: value.default || null, - values: value.values || null, - valueNames: value["value-names"] || null, + values, }, null, 2 @@ -205,15 +214,23 @@ export default async function makeFileContent( )})`; } case "enum": { + const allValueNames = new Set( + ...value?.spec?.["values"] || [], + ...Object.keys(value?.spec?.["value-names"] || {}) + ); + const values = Object.fromEntries( + Array.from(allValueNames) + .filter(string.test) + .map(key => [key, value?.spec?.["value-names"]?.[key] || key]) + ) return `Value.multiselect(${JSON.stringify( { name: value.name || null, range: value.range || null, - values: value?.spec?.["values"] || null, - valueNames: value?.spec?.["value-names"] || {}, default: value.default || null, description: value.description || null, warning: value.warning || null, + values, }, null, 2 @@ -230,8 +247,8 @@ export default async function makeFileContent( spec: { spec: ${specName}, displayAs: ${JSON.stringify( - value?.spec?.["display-as"] || null - )}, + value?.spec?.["display-as"] || null + )}, uniqueBy: ${JSON.stringify(value?.spec?.["unique-by"] || null)}, }, default: ${JSON.stringify(value.default || null)}, @@ -249,28 +266,14 @@ export default async function makeFileContent( name:${JSON.stringify(value.name || null)}, range:${JSON.stringify(value.range || null)}, spec: { - tag: { - "id":${JSON.stringify(value?.spec?.tag?.["id"] || null)}, - "name": ${JSON.stringify( - value?.spec?.tag?.name || null - )}, - "description": ${JSON.stringify( - value?.spec?.tag?.description || null - )}, - "warning": ${JSON.stringify( - value?.spec?.tag?.warning || null - )}, - variantNames: ${JSON.stringify( - value?.spec?.tag?.["variant-names"] || {} - )}, - }, + variants: ${variants}, displayAs: ${JSON.stringify( - value?.spec?.["display-as"] || null - )}, + value?.spec?.["display-as"] || null + )}, uniqueBy: ${JSON.stringify( - value?.spec?.["unique-by"] || null - )}, + value?.spec?.["unique-by"] || null + )}, default: ${JSON.stringify(value?.spec?.default || null)}, }, default: ${JSON.stringify(value.default || null)}, diff --git a/lib/types/config-types.ts b/lib/types/config-types.ts index 6ac74dd..d7f02c0 100644 --- a/lib/types/config-types.ts +++ b/lib/types/config-types.ts @@ -49,6 +49,7 @@ export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone { export interface ValueSpecSelect extends SelectBase, WithStandalone { type: "select"; default: string; + } export interface ValueSpecMultiselect extends SelectBase, WithStandalone { @@ -114,14 +115,14 @@ export interface ValueSpecListOf spec: ListValueSpecOf; range: string; // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules default: - | string[] - | number[] - | DefaultString[] - | Record[] - | readonly string[] - | readonly number[] - | readonly DefaultString[] - | readonly Record[]; + | string[] + | number[] + | DefaultString[] + | Record[] + | readonly string[] + | readonly number[] + | readonly DefaultString[] + | readonly Record[]; } // sometimes the type checker needs just a little bit of help diff --git a/lib/util/artifacts/output.ts b/lib/util/artifacts/output.ts index 70d7434..48a6b33 100644 --- a/lib/util/artifacts/output.ts +++ b/lib/util/artifacts/output.ts @@ -1,5 +1,5 @@ - import {Config, Value, List, Variants} from '../../config/builder'; +import { Config, Value, List, Variants } from '../../config/builder'; export const enable = Value.boolean({ "name": "Enable", @@ -54,11 +54,7 @@ export const serialversion = Value.select({ "description": "Return raw transaction or block hex with Segwit or non-SegWit serialization.", "warning": null, "default": "segwit", - "values": [ - "non-segwit", - "segwit" - ], - "valueNames": {} + "values": {} }); export const servertimeout = Value.number({ "name": "Rpc Server Timeout", @@ -93,28 +89,28 @@ export const workqueue = Value.number({ "units": "requests", "placeholder": null }); -export const advancedSpec = Config.of({"auth": auth,"serialversion": serialversion,"servertimeout": servertimeout,"threads": threads,"workqueue": workqueue,});; +export const advancedSpec = Config.of({ "auth": auth, "serialversion": serialversion, "servertimeout": servertimeout, "threads": threads, "workqueue": workqueue, });; export const advanced = Value.object({ - name: "Advanced", - description: "Advanced RPC Settings", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: advancedSpec, - valueNames: {}, - }); -export const rpcSettingsSpec = Config.of({"enable": enable,"username": username,"password": password,"advanced": advanced,});; + name: "Advanced", + description: "Advanced RPC Settings", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: advancedSpec, + valueNames: {}, +}); +export const rpcSettingsSpec = Config.of({ "enable": enable, "username": username, "password": password, "advanced": advanced, });; export const rpc = Value.object({ - name: "RPC Settings", - description: "RPC configuration options.", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: rpcSettingsSpec, - valueNames: {}, - }); + name: "RPC Settings", + description: "RPC configuration options.", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: rpcSettingsSpec, + valueNames: {}, +}); export const zmqEnabled = Value.boolean({ "name": "ZeroMQ Enabled", "default": true, @@ -150,17 +146,17 @@ export const discardfee = Value.number({ "units": "BTC/kB", "placeholder": null }); -export const walletSpec = Config.of({"enable": enable1,"avoidpartialspends": avoidpartialspends,"discardfee": discardfee,});; +export const walletSpec = Config.of({ "enable": enable1, "avoidpartialspends": avoidpartialspends, "discardfee": discardfee, });; export const wallet = Value.object({ - name: "Wallet", - description: "Wallet Settings", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: walletSpec, - valueNames: {}, - }); + name: "Wallet", + description: "Wallet Settings", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: walletSpec, + valueNames: {}, +}); export const mempoolfullrbf = Value.boolean({ "name": "Enable Full RBF", "default": false, @@ -195,17 +191,17 @@ export const mempoolexpiry = Value.number({ "units": "Hr", "placeholder": null }); -export const mempoolSpec = Config.of({"mempoolfullrbf": mempoolfullrbf,"persistmempool": persistmempool,"maxmempool": maxmempool,"mempoolexpiry": mempoolexpiry,});; +export const mempoolSpec = Config.of({ "mempoolfullrbf": mempoolfullrbf, "persistmempool": persistmempool, "maxmempool": maxmempool, "mempoolexpiry": mempoolexpiry, });; export const mempool = Value.object({ - name: "Mempool", - description: "Mempool Settings", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: mempoolSpec, - valueNames: {}, - }); + name: "Mempool", + description: "Mempool Settings", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: mempoolSpec, + valueNames: {}, +}); export const listen = Value.boolean({ "name": "Make Public", "default": true, @@ -247,31 +243,31 @@ export const port = Value.number({ "units": null, "placeholder": null }); -export const addNodesSpec = Config.of({"hostname": hostname,"port": port,});; +export const addNodesSpec = Config.of({ "hostname": hostname, "port": port, });; export const addNodesList = List.obj({ - name: "Add Nodes", - range: "[0,*)", - spec: { - spec: addNodesSpec, - displayAs: null, - uniqueBy: null, - }, - default: [], - description: "Add addresses of nodes to connect to.", - warning: null, - }); + name: "Add Nodes", + range: "[0,*)", + spec: { + spec: addNodesSpec, + displayAs: null, + uniqueBy: null, + }, + default: [], + description: "Add addresses of nodes to connect to.", + warning: null, +}); export const addnode = Value.list(addNodesList); -export const peersSpec = Config.of({"listen": listen,"onlyconnect": onlyconnect,"onlyonion": onlyonion,"addnode": addnode,});; +export const peersSpec = Config.of({ "listen": listen, "onlyconnect": onlyconnect, "onlyonion": onlyonion, "addnode": addnode, });; export const peers = Value.object({ - name: "Peers", - description: "Peer Connection Settings", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: peersSpec, - valueNames: {}, - }); + name: "Peers", + description: "Peer Connection Settings", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: peersSpec, + valueNames: {}, +}); export const dbcache = Value.number({ "name": "Database Cache", "default": null, @@ -295,7 +291,7 @@ export const size = Value.number({ "units": "MiB", "placeholder": null }); -export const automatic = Config.of({"size": size,});; +export const automatic = Config.of({ "size": size, });; export const size1 = Value.number({ "name": "Failsafe Chain Size", "default": 65536, @@ -307,19 +303,19 @@ export const size1 = Value.number({ "units": "MiB", "placeholder": null }); -export const manual = Config.of({"size": size1,});; -export const pruningSettingsVariants = Variants.of({"disabled": disabled,"automatic": automatic,"manual": manual,}); +export const manual = Config.of({ "size": size1, });; +export const pruningSettingsVariants = Variants.of({ "disabled": disabled, "automatic": automatic, "manual": manual, }); export const pruning = Value.union({ - name: "Pruning Settings", - description: "Blockchain Pruning Options\nReduce the blockchain size on disk\n", - warning: "If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n", - default: "disabled", - variants: pruningSettingsVariants, - tag: {"id":"mode","name":"Pruning Mode","description":"- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the \"pruneblockchain\" RPC\n","warning":null,"variantNames":{"disabled":"Disabled","automatic":"Automatic","manual":"Manual"}}, - displayAs: null, - uniqueBy: null, - variantNames: null, - }); + name: "Pruning Settings", + description: "Blockchain Pruning Options\nReduce the blockchain size on disk\n", + warning: "If you set pruning to Manual and your disk is smaller than the total size of the blockchain, you MUST have something running that prunes these blocks or you may overfill your disk!\nDisabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days. Make sure you have enough free disk space or you may fill up your disk.\n", + default: "disabled", + variants: pruningSettingsVariants, + tag: { "id": "mode", "name": "Pruning Mode", "description": "- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n- Manual: Prune blockchain with the \"pruneblockchain\" RPC\n", "warning": null, "variantNames": { "disabled": "Disabled", "automatic": "Automatic", "manual": "Manual" } }, + displayAs: null, + uniqueBy: null, + variantNames: null, +}); export const blockfilterindex = Value.boolean({ "name": "Compute Compact Block Filters (BIP158)", "default": true, @@ -332,45 +328,45 @@ export const peerblockfilters = Value.boolean({ "description": "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", "warning": null }); -export const blockFiltersSpec = Config.of({"blockfilterindex": blockfilterindex,"peerblockfilters": peerblockfilters,});; +export const blockFiltersSpec = Config.of({ "blockfilterindex": blockfilterindex, "peerblockfilters": peerblockfilters, });; export const blockfilters = Value.object({ - name: "Block Filters", - description: "Settings for storing and serving compact block filters", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: blockFiltersSpec, - valueNames: {}, - }); + name: "Block Filters", + description: "Settings for storing and serving compact block filters", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: blockFiltersSpec, + valueNames: {}, +}); export const peerbloomfilters = Value.boolean({ "name": "Serve Bloom Filters to Peers", "default": false, "description": "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", "warning": "This is ONLY for use with Bisq integration, please use Block Filters for all other applications." }); -export const bloomFiltersBip37Spec = Config.of({"peerbloomfilters": peerbloomfilters,});; +export const bloomFiltersBip37Spec = Config.of({ "peerbloomfilters": peerbloomfilters, });; export const bloomfilters = Value.object({ - name: "Bloom Filters (BIP37)", - description: "Setting for serving Bloom Filters", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: bloomFiltersBip37Spec, - valueNames: {}, - }); -export const advancedSpec1 = Config.of({"mempool": mempool,"peers": peers,"dbcache": dbcache,"pruning": pruning,"blockfilters": blockfilters,"bloomfilters": bloomfilters,});; + name: "Bloom Filters (BIP37)", + description: "Setting for serving Bloom Filters", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: bloomFiltersBip37Spec, + valueNames: {}, +}); +export const advancedSpec1 = Config.of({ "mempool": mempool, "peers": peers, "dbcache": dbcache, "pruning": pruning, "blockfilters": blockfilters, "bloomfilters": bloomfilters, });; export const advanced1 = Value.object({ - name: "Advanced", - description: "Advanced Settings", - warning: null, - default: null, - displayAs: null, - uniqueBy: null, - spec: advancedSpec1, - valueNames: {}, - }); -export const inputSpec = Config.of({"rpc": rpc,"zmq-enabled": zmqEnabled,"txindex": txindex,"wallet": wallet,"advanced": advanced1,});; + name: "Advanced", + description: "Advanced Settings", + warning: null, + default: null, + displayAs: null, + uniqueBy: null, + spec: advancedSpec1, + valueNames: {}, +}); +export const inputSpec = Config.of({ "rpc": rpc, "zmq-enabled": zmqEnabled, "txindex": txindex, "wallet": wallet, "advanced": advanced1, });; export const matchInputSpec = inputSpec.validator(); export type InputSpec = typeof matchInputSpec._TYPE; \ No newline at end of file diff --git a/lib/util/propertiesMatcher.ts b/lib/util/propertiesMatcher.ts index 3cc3f00..d148943 100644 --- a/lib/util/propertiesMatcher.ts +++ b/lib/util/propertiesMatcher.ts @@ -1,6 +1,9 @@ import * as matches from "ts-matches"; +import { Parser } from "ts-matches"; import { InputSpec, ValueSpec as ValueSpecAny } from "../types/config-types"; +const { string, some, object, arrayOf, dictionary, unknown, number, literals, boolean, array } = matches + type TypeBoolean = "boolean"; type TypeString = "string"; type TypeNumber = "number"; @@ -8,7 +11,6 @@ type TypeObject = "object"; type TypeList = "list"; type TypeSelect = "select"; type TypeMultiselect = "multiselect"; -type TypePointer = "pointer"; type TypeUnion = "union"; // prettier-ignore @@ -46,20 +48,22 @@ export type GuardList = A extends { readonly type: TypeList, readonly subtype: infer B, spec?: {} } ? ReadonlyArray & ({ type: B })>> : unknown // prettier-ignore -type GuardPointer = - A extends { readonly type: TypePointer } ? (string | null) : - unknown -// prettier-ignore type GuardSelect = - A extends { readonly type: TypeSelect, readonly values: ArrayLike } ? GuardDefaultNullable : + A extends { readonly type: TypeSelect, variants: { [key in infer B & string]: string } } ? B : unknown // prettier-ignore type GuardMultiselect = - A extends { readonly type: TypeMultiselect, readonly values: ArrayLike } ? GuardDefaultNullable : + A extends { readonly type: TypeMultiselect, variants: { [key in infer B & string]: string } } ? B[] : unknown + +// prettier-ignore +type VariantValue = + A extends { name: string, spec: infer B } ? { name: A['name'], spec: TypeFromProps } : + never // prettier-ignore type GuardUnion = - A extends { readonly type: TypeUnion, readonly tag: { id: infer Id & string }, variants: infer Variants & Record } ? { [K in keyof Variants]: { [keyType in Id & string]: K } & TypeFromProps }[keyof Variants] : + A extends { readonly type: TypeUnion, variants: infer Variants & Record } ? + { [K in keyof Variants]: { [key in K]: VariantValue['spec'] } }[keyof Variants] : unknown type _ = T; @@ -68,7 +72,6 @@ export type GuardAll = GuardNumber & GuardBoolean & GuardObject & GuardList & - GuardPointer & GuardUnion & GuardSelect & GuardMultiselect; @@ -77,22 +80,25 @@ export type TypeFromProps = A extends Record ? { readonly [K in keyof A & string]: _> } : unknown; -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 isType = object({ type: string }); +const matchVariant = object({ + name: string, + spec: unknown +}) +const recordString = dictionary([string, unknown]); +const matchDefault = object({ default: unknown }); +const matchNullable = object({ nullable: literals(true) }); +const matchPattern = object({ pattern: 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 matchRange = object({ range: string }); +const matchIntegral = object({ integral: literals(true) }); +const matchSpec = object({ spec: recordString }); +const matchSubType = object({ subtype: string }); +const matchUnion = object({ + variants: dictionary([string, matchVariant]), }); -const matchValues = matches.shape({ - values: matches.arrayOf(matches.string), +const matchValues = object({ + values: dictionary([string, string]), }); function charRange(value = "") { @@ -142,15 +148,11 @@ export function generateDefault( return answer.join(""); } -function withPattern(value: unknown) { - if (matchPattern.test(value)) return matches.regex(RegExp(value.pattern)); - return matches.string; -} export function matchNumberWithRange(range: string) { const matched = rangeRegex.exec(range); - if (!matched) return matches.number; + if (!matched) return number; const [, left, leftValue, , rightValue, , right] = matched; - return matches.number + return number .validate( leftValue === "*" ? (_) => true @@ -174,7 +176,7 @@ export function matchNumberWithRange(range: string) { `lessThan${rightValue}` ); } -function withIntegral(parser: matches.Parser, value: unknown) { +function withIntegral(parser: Parser, value: unknown) { if (matchIntegral.test(value)) { return parser.validate(Number.isInteger, "isIntegral"); } @@ -184,14 +186,14 @@ function withRange(value: unknown) { if (matchRange.test(value)) { return matchNumberWithRange(value.range); } - return matches.number; + return number; } -const isGenerator = matches.shape({ - charset: matches.string, - len: matches.number, +const isGenerator = object({ + charset: string, + len: number, }).test; function defaultNullable( - parser: matches.Parser, + parser: Parser, value: unknown ) { if (matchDefault.test(value)) { @@ -216,16 +218,16 @@ function defaultNullable( */ export function guardAll( value: A -): matches.Parser> { +): Parser> { if (!isType.test(value)) { - return matches.unknown as any; + return unknown as any; } switch (value.type) { case "boolean": - return defaultNullable(matches.boolean, value) as any; + return defaultNullable(boolean, value) as any; case "string": - return defaultNullable(withPattern(value), value) as any; + return defaultNullable(string, value) as any; case "number": return defaultNullable( @@ -237,7 +239,7 @@ export function guardAll( if (matchSpec.test(value)) { return defaultNullable(typeFromProps(value.spec), value) as any; } - return matches.unknown as any; + return unknown as any; case "list": { const spec = (matchSpec.test(value) && value.spec) || {}; @@ -255,40 +257,40 @@ export function guardAll( } case "select": if (matchValues.test(value)) { + const valueKeys = Object.keys(value.values) return defaultNullable( - matches.literals(value.values[0], ...value.values), + literals(valueKeys[0], ...valueKeys), value ) as any; } - return matches.unknown as any; + return unknown as any; case "multiselect": if (matchValues.test(value)) { const rangeValidate = (matchRange.test(value) && matchNumberWithRange(value.range).test) || (() => true); + const valueKeys = Object.keys(value.values) return defaultNullable( matches - .literals(value.values[0], ...value.values) + .literals(valueKeys[0], ...valueKeys) .validate((x) => rangeValidate(x.length), "valid length"), value ) as any; } - return matches.unknown as any; + return unknown as any; 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)) + return some( + ...Object.entries(value.variants).map(([_, { spec }]) => + typeFromProps(spec) ) ) as any; } - return matches.unknown as any; + return unknown as any; } - return matches.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. @@ -300,9 +302,9 @@ export function guardAll( */ export function typeFromProps( valueDictionary: A -): matches.Parser> { - if (!recordString.test(valueDictionary)) return matches.unknown as any; - return matches.shape( +): Parser> { + if (!recordString.test(valueDictionary)) return unknown as any; + return object( Object.fromEntries( Object.entries(valueDictionary).map(([key, value]) => [ key,