diff --git a/Makefile b/Makefile index 395f5aa..2a34bf3 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ TS_FILES := $(shell find ./**/*.ts ) version = $(shell git tag --sort=committerdate | tail -1) -test: $(TS_FILES) +test: $(TS_FILES) utils/test/output.ts deno test test.ts deno check mod.ts +utils/test/output.ts: utils/test/config.json scripts/oldSpecToBuilder.ts + cat utils/test/config.json | deno run scripts/oldSpecToBuilder.ts "../../mod.ts" |deno fmt - > utils/test/output.ts + bundle: test fmt $(TS_FILES) echo "Version: $(version)" deno run --allow-net --allow-write --allow-env --allow-run --allow-read build.ts $(version) diff --git a/README.md b/README.md index 82e34a0..c3ff82c 100644 --- a/README.md +++ b/README.md @@ -3,5 +3,5 @@ ### Generate: Config class from legacy ConfigSpec ```sh -cat utils/test/config.json | deno run scripts/oldSpecToBuilder.ts "../../config/mod.ts" |deno fmt - > utils/test/output.ts +cat utils/test/config.json | deno run https://deno.land/x/embassyd_sdk/scripts/oldSpecToBuilder.ts "../../mod.ts" |deno fmt - > utils/test/output.ts ``` diff --git a/compat/getConfig.ts b/compat/getConfig.ts index 3cc8402..e93166a 100644 --- a/compat/getConfig.ts +++ b/compat/getConfig.ts @@ -1,4 +1,4 @@ -import { Config } from "../config/config.ts"; +import { Config } from "../config_builder/config.ts"; import { YAML } from "../dependencies.ts"; import { matches } from "../dependencies.ts"; import { ExpectedExports } from "../types.ts"; diff --git a/compat/migrations.ts b/compat/migrations.ts index 674309a..0bb35a9 100644 --- a/compat/migrations.ts +++ b/compat/migrations.ts @@ -4,7 +4,7 @@ import * as M from "../migrations.ts"; import * as util from "../util.ts"; import { EmVer } from "../emver-lite/mod.ts"; import { ConfigSpec } from "../types/config-types.ts"; -import { Config } from "../config/mod.ts"; +import { Config } from "../config_builder/mod.ts"; export interface NoRepeat { version: version; diff --git a/config/config.ts b/config/config.ts deleted file mode 100644 index 76decb5..0000000 --- a/config/config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ConfigSpec, ValueSpec } from "../types/config-types.ts"; -import { BuilderExtract, IBuilder } from "./builder.ts"; -import { Value } from "./value.ts"; - -export class Config extends IBuilder { - static empty() { - return new Config({}); - } - static withValue( - key: K, - value: Value, - ) { - return Config.empty().withValue(key, value); - } - static addValue( - key: K, - value: Value, - ) { - return Config.empty().withValue(key, value); - } - - static of }>(spec: B) { - // deno-lint-ignore no-explicit-any - const answer: { [K in keyof B]: BuilderExtract } = {} as any; - for (const key in spec) { - // deno-lint-ignore no-explicit-any - answer[key] = spec[key].build() as any; - } - return new Config(answer); - } - withValue(key: K, value: Value) { - return new Config( - { - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }, - ); - } - addValue(key: K, value: Value) { - return new Config( - { - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }, - ); - } -} diff --git a/config/variants.ts b/config/variants.ts deleted file mode 100644 index daa06e9..0000000 --- a/config/variants.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ConfigSpec } from "../types/config-types.ts"; -import { BuilderExtract, IBuilder } from "./builder.ts"; -import { Config } from "./mod.ts"; - -export class Variants - extends IBuilder { - static of< - A extends { - [key: string]: Config; - }, - >(a: A) { - // deno-lint-ignore no-explicit-any - const variants: { [K in keyof A]: BuilderExtract } = {} as any; - for (const key in a) { - // deno-lint-ignore no-explicit-any - variants[key] = a[key].build() as any; - } - return new Variants(variants); - } - - static empty() { - return Variants.of({}); - } - static withVariant( - key: K, - value: Config, - ) { - return Variants.empty().withVariant(key, value); - } - - withVariant( - key: K, - value: Config, - ) { - return new Variants( - { - ...this.a, - [key]: value.build(), - } as A & { [key in K]: B }, - ); - } -} diff --git a/config/builder.ts b/config_builder/builder.ts similarity index 100% rename from config/builder.ts rename to config_builder/builder.ts diff --git a/config_builder/config.ts b/config_builder/config.ts new file mode 100644 index 0000000..743d134 --- /dev/null +++ b/config_builder/config.ts @@ -0,0 +1,522 @@ +import { ConfigSpec, ValueSpec } from "../types/config-types.ts"; +import { typeFromProps } from "../util.ts"; +import { BuilderExtract, IBuilder } from "./builder.ts"; +import { Value } from "./value.ts"; + +/** + * Configs are the specs that are used by the os configuration form for this service. + * Here is an example of a simple configuration + ```ts + const smallConfig = Config.of({ + test: Value.boolean({ + name: "Test", + description: "This is the description for the test", + warning: null, + default: false, + }), + }); + ``` + + The idea of a config is that now the form is going to ask for + Test: [ ] and the value is going to be checked as a boolean. + There are more complex values like enums, lists, and objects. See {@link Value} + + Also, there is the ability to get a validator/parser from this config spec. + ```ts + const matchSmallConfig = smallConfig.validator(); + type SmallConfig = typeof matchSmallConfig._TYPE; + ``` + + Here is an example of a more complex configuration which came from a configuration for a service + that works with bitcoin, like c-lightning. + ```ts + + export const enable = Value.boolean({ + "name": "Enable", + "default": true, + "description": "Allow remote RPC requests.", + "warning": null, + }); + export const username = Value.string({ + "name": "Username", + "default": "bitcoin", + "description": "The username for connecting to Bitcoin over RPC.", + "warning": null, + "nullable": false, + "masked": true, + "placeholder": null, + "pattern": "^[a-zA-Z0-9_]+$", + "pattern-description": "Must be alphanumeric (can contain underscore).", + "textarea": null, + }); + export const password = Value.string({ + "name": "RPC Password", + "default": { + "charset": "a-z,2-7", + "len": 20, + }, + "description": "The password for connecting to Bitcoin over RPC.", + "warning": null, + "nullable": false, + "masked": true, + "placeholder": null, + "pattern": '^[^\\n"]*$', + "pattern-description": "Must not contain newline or quote characters.", + "textarea": null, + }); + export const authorizationList = List.string({ + "name": "Authorization", + "range": "[0,*)", + "spec": { + "masked": null, + "placeholder": null, + "pattern": "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + "pattern-description": + 'Each item must be of the form ":$".', + "textarea": false, + }, + "default": [], + "description": + "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + "warning": null, + }); + export const auth = Value.list(authorizationList); + export const serialversion = Value.enum({ + "name": "Serialization Version", + "description": + "Return raw transaction or block hex with Segwit or non-SegWit serialization.", + "warning": null, + "default": "segwit", + "values": [ + "non-segwit", + "segwit", + ], + "value-names": {}, + }); + export const servertimeout = Value.number({ + "name": "Rpc Server Timeout", + "default": 30, + "description": + "Number of seconds after which an uncompleted RPC call will time out.", + "warning": null, + "nullable": false, + "range": "[5,300]", + "integral": true, + "units": "seconds", + "placeholder": null, + }); + export const threads = Value.number({ + "name": "Threads", + "default": 16, + "description": + "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + "warning": null, + "nullable": false, + "range": "[1,64]", + "integral": true, + "units": null, + "placeholder": null, + }); + export const workqueue = Value.number({ + "name": "Work Queue", + "default": 128, + "description": + "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + "warning": null, + "nullable": false, + "range": "[8,256]", + "integral": true, + "units": "requests", + "placeholder": null, + }); + 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, + "display-as": null, + "unique-by": null, + spec: advancedSpec, + "value-names": {}, + }); + 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, + "display-as": null, + "unique-by": null, + spec: rpcSettingsSpec, + "value-names": {}, + }); + export const zmqEnabled = Value.boolean({ + "name": "ZeroMQ Enabled", + "default": true, + "description": "Enable the ZeroMQ interface", + "warning": null, + }); + export const txindex = Value.boolean({ + "name": "Transaction Index", + "default": true, + "description": "Enable the Transaction Index (txindex)", + "warning": null, + }); + export const enable1 = Value.boolean({ + "name": "Enable Wallet", + "default": true, + "description": "Load the wallet and enable wallet RPC calls.", + "warning": null, + }); + export const avoidpartialspends = Value.boolean({ + "name": "Avoid Partial Spends", + "default": true, + "description": + "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + "warning": null, + }); + export const discardfee = Value.number({ + "name": "Discard Change Tolerance", + "default": 0.0001, + "description": + "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + "warning": null, + "nullable": false, + "range": "[0,.01]", + "integral": false, + "units": "BTC/kB", + "placeholder": null, + }); + 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, + "display-as": null, + "unique-by": null, + spec: walletSpec, + "value-names": {}, + }); + export const mempoolfullrbf = Value.boolean({ + "name": "Enable Full RBF", + "default": false, + "description": + "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.md#notice-of-new-option-for-transaction-replacement-policies", + "warning": null, + }); + export const persistmempool = Value.boolean({ + "name": "Persist Mempool", + "default": true, + "description": "Save the mempool on shutdown and load on restart.", + "warning": null, + }); + export const maxmempool = Value.number({ + "name": "Max Mempool Size", + "default": 300, + "description": "Keep the transaction memory pool below megabytes.", + "warning": null, + "nullable": false, + "range": "[1,*)", + "integral": true, + "units": "MiB", + "placeholder": null, + }); + export const mempoolexpiry = Value.number({ + "name": "Mempool Expiration", + "default": 336, + "description": + "Do not keep transactions in the mempool longer than hours.", + "warning": null, + "nullable": false, + "range": "[1,*)", + "integral": true, + "units": "Hr", + "placeholder": null, + }); + 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, + "display-as": null, + "unique-by": null, + spec: mempoolSpec, + "value-names": {}, + }); + export const listen = Value.boolean({ + "name": "Make Public", + "default": true, + "description": "Allow other nodes to find your server on the network.", + "warning": null, + }); + export const onlyconnect = Value.boolean({ + "name": "Disable Peer Discovery", + "default": false, + "description": "Only connect to specified peers.", + "warning": null, + }); + export const onlyonion = Value.boolean({ + "name": "Disable Clearnet", + "default": false, + "description": "Only connect to peers over Tor.", + "warning": null, + }); + export const hostname = Value.string({ + "name": "Hostname", + "default": null, + "description": "Domain or IP address of bitcoin peer", + "warning": null, + "nullable": false, + "masked": null, + "placeholder": null, + "pattern": + "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + "pattern-description": + "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + "textarea": null, + }); + export const port = Value.number({ + "name": "Port", + "default": null, + "description": "Port that peer is listening on for inbound p2p connections", + "warning": null, + "nullable": true, + "range": "[0,65535]", + "integral": true, + "units": null, + "placeholder": null, + }); + export const addNodesSpec = Config.of({ "hostname": hostname, "port": port }); + export const addNodesList = List.obj({ + name: "Add Nodes", + range: "[0,*)", + spec: { + spec: addNodesSpec, + "display-as": null, + "unique-by": 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 peers = Value.object({ + name: "Peers", + description: "Peer Connection Settings", + warning: null, + default: null, + "display-as": null, + "unique-by": null, + spec: peersSpec, + "value-names": {}, + }); + export const dbcache = Value.number({ + "name": "Database Cache", + "default": null, + "description": + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + "warning": + "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + "nullable": true, + "range": "(0,*)", + "integral": true, + "units": "MiB", + "placeholder": null, + }); + export const disabled = Config.of({}); + export const size = Value.number({ + "name": "Max Chain Size", + "default": 550, + "description": "Limit of blockchain size on disk.", + "warning": "Increasing this value will require re-syncing your node.", + "nullable": false, + "range": "[550,1000000)", + "integral": true, + "units": "MiB", + "placeholder": null, + }); + export const automatic = Config.of({ "size": size }); + export const size1 = Value.number({ + "name": "Failsafe Chain Size", + "default": 65536, + "description": "Prune blockchain if size expands beyond this.", + "warning": null, + "nullable": false, + "range": "[550,1000000)", + "integral": true, + "units": "MiB", + "placeholder": null, + }); + 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, + "variant-names": { + "disabled": "Disabled", + "automatic": "Automatic", + "manual": "Manual", + }, + }, + "display-as": null, + "unique-by": null, + "variant-names": null, + }); + export const blockfilterindex = Value.boolean({ + "name": "Compute Compact Block Filters (BIP158)", + "default": true, + "description": + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + "warning": null, + }); + export const peerblockfilters = Value.boolean({ + "name": "Serve Compact Block Filters to Peers (BIP157)", + "default": false, + "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 blockfilters = Value.object({ + name: "Block Filters", + description: "Settings for storing and serving compact block filters", + warning: null, + default: null, + "display-as": null, + "unique-by": null, + spec: blockFiltersSpec, + "value-names": {}, + }); + 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 bloomfilters = Value.object({ + name: "Bloom Filters (BIP37)", + description: "Setting for serving Bloom Filters", + warning: null, + default: null, + "display-as": null, + "unique-by": null, + spec: bloomFiltersBip37Spec, + "value-names": {}, + }); + 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, + "display-as": null, + "unique-by": null, + spec: advancedSpec1, + "value-names": {}, + }); + export const config = Config.of({ + "rpc": rpc, + "zmq-enabled": zmqEnabled, + "txindex": txindex, + "wallet": wallet, + "advanced": advanced1, + }); + + ``` + */ +export class Config extends IBuilder { + static empty() { + return new Config({}); + } + static withValue(key: K, value: Value) { + return Config.empty().withValue(key, value); + } + static addValue(key: K, value: Value) { + return Config.empty().withValue(key, value); + } + + static of }>(spec: B) { + // deno-lint-ignore no-explicit-any + const answer: { [K in keyof B]: BuilderExtract } = {} as any; + for (const key in spec) { + // deno-lint-ignore no-explicit-any + answer[key] = spec[key].build() as any; + } + return new Config(answer); + } + withValue(key: K, value: Value) { + return new Config({ + ...this.a, + [key]: value.build(), + } as A & { [key in K]: B }); + } + addValue(key: K, value: Value) { + return new Config({ + ...this.a, + [key]: value.build(), + } as A & { [key in K]: B }); + } + + public validator() { + return typeFromProps(this.a); + } +} diff --git a/config/index.test.ts b/config_builder/index.test.ts similarity index 100% rename from config/index.test.ts rename to config_builder/index.test.ts diff --git a/config/list.ts b/config_builder/list.ts similarity index 66% rename from config/list.ts rename to config_builder/list.ts index 9ed9ef4..eac8ee2 100644 --- a/config/list.ts +++ b/config_builder/list.ts @@ -3,28 +3,37 @@ import { Config } from "./config.ts"; import { Default, NumberSpec, StringSpec } from "./value.ts"; import { Description } from "./value.ts"; import { Variants } from "./variants.ts"; -import { - ConfigSpec, - UniqueBy, - ValueSpecList, - ValueSpecListOf, -} from "../types/config-types.ts"; +import { ConfigSpec, UniqueBy, ValueSpecList, ValueSpecListOf } from "../types/config-types.ts"; -export class List extends IBuilder { - // // deno-lint-ignore ban-types - // static boolean & { range: string; spec: {}; default: boolean }>(a: A) { - // return new List({ - // type: "list" as const, - // subtype: "boolean" as const, - // ...a, - // }); - // } +/** + * Used as a subtype of Value.list +```ts - static string< - A extends Description & Default & { - range: string; - spec: StringSpec; + export const authorizationList = List.string({ + "name": "Authorization", + "range": "[0,*)", + "spec": { + "masked": null, + "placeholder": null, + "pattern": "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + "pattern-description": + 'Each item must be of the form ":$".', + "textarea": false, }, + "default": [], + "description": + "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + "warning": null, + }); +``` + */ +export class List extends IBuilder { + static string< + A extends Description & + Default & { + range: string; + spec: StringSpec; + } >(a: A) { return new List({ type: "list" as const, @@ -33,10 +42,11 @@ export class List extends IBuilder { } as ValueSpecListOf<"string">); } static number< - A extends Description & Default & { - range: string; - spec: NumberSpec; - }, + A extends Description & + Default & { + range: string; + spec: NumberSpec; + } >(a: A) { return new List({ type: "list" as const, @@ -45,10 +55,8 @@ export class List extends IBuilder { }); } static enum< - A extends - & Description - & Default - & { + A extends Description & + Default & { range: string; spec: { values: string[]; @@ -56,7 +64,7 @@ export class List extends IBuilder { [key: string]: string; }; }; - }, + } >(a: A) { return new List({ type: "list" as const, @@ -65,23 +73,19 @@ export class List extends IBuilder { }); } static obj< - A extends - & Description - & Default[]> - & { + A extends Description & + Default[]> & { range: string; spec: { spec: Config; "display-as": null | string; "unique-by": null | UniqueBy; }; - }, + } >(a: A) { const { spec: previousSpec, ...rest } = a; const { spec: previousSpecSpec, ...restSpec } = previousSpec; - const specSpec = previousSpecSpec.build() as BuilderExtract< - A["spec"]["spec"] - >; + const specSpec = previousSpecSpec.build() as BuilderExtract; const spec = { ...restSpec, spec: specSpec, @@ -97,10 +101,8 @@ export class List extends IBuilder { }); } static union< - A extends - & Description - & Default - & { + A extends Description & + Default & { range: string; spec: { tag: { @@ -118,13 +120,11 @@ export class List extends IBuilder { default: string; }; }, - B extends string, + B extends string >(a: A) { const { spec: previousSpec, ...rest } = a; const { variants: previousVariants, ...restSpec } = previousSpec; - const variants = previousVariants.build() as BuilderExtract< - A["spec"]["variants"] - >; + const variants = previousVariants.build() as BuilderExtract; const spec = { ...restSpec, variants, diff --git a/config/mod.ts b/config_builder/mod.ts similarity index 100% rename from config/mod.ts rename to config_builder/mod.ts diff --git a/config/value.ts b/config_builder/value.ts similarity index 63% rename from config/value.ts rename to config_builder/value.ts index eba93a1..6b6cffe 100644 --- a/config/value.ts +++ b/config_builder/value.ts @@ -14,9 +14,9 @@ import { export type DefaultString = | string | { - charset: string | null | undefined; - len: number; - }; + charset: string | null | undefined; + len: number; + }; export type Description = { name: string; description: string | null; @@ -46,6 +46,29 @@ export type Nullable = { nullable: boolean; }; +/** + * 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. + * in the fe it will ask for the name of value, and use the rest of the value to determine how to render it. + * While writing with a value, you will start with `Value.` then let the IDE suggest the rest. + * for things like string, the options are going to be in {}. + * Keep an eye out for another config builder types as params. + * Note, usually this is going to be used in a `Config` {@link Config} builder. + ```ts + Value.string({ + name: "Name of This Value", + description: "Going to be what the description is in the FE, hover over", + warning: "What the warning is going to be on warning situations", + default: null, + nullable: false, + masked: null, // If there is a masked, then the value is going to be masked in the FE, like a password + placeholder: null, // If there is a placeholder, then the value is going to be masked in the FE, like a password + pattern: null, // A regex pattern to validate the value + "pattern-description": null, + textarea: null + }) + ``` + */ export class Value extends IBuilder { static boolean>(a: A) { return new Value({ @@ -53,34 +76,24 @@ export class Value extends IBuilder { ...a, }); } - static string< - A extends - & Description - & NullableDefault - & Nullable - & StringSpec, - >(a: A) { + static string & Nullable & StringSpec>(a: A) { return new Value({ type: "string" as const, ...a, } as ValueSpecString); } - static number< - A extends Description & NullableDefault & Nullable & NumberSpec, - >(a: A) { + static number & Nullable & NumberSpec>(a: A) { return new Value({ type: "number" as const, ...a, } as ValueSpecNumber); } static enum< - A extends - & Description - & Default - & { + A extends Description & + Default & { values: readonly string[] | string[]; "value-names": Record; - }, + } >(a: A) { return new Value({ type: "enum" as const, @@ -97,7 +110,7 @@ export class Value extends IBuilder { "unique-by": null | string; spec: Config; "value-names": Record; - }, + } >(a: A) { const { spec: previousSpec, ...rest } = a; const spec = previousSpec.build() as BuilderExtract; @@ -108,10 +121,8 @@ export class Value extends IBuilder { }); } static union< - A extends - & Description - & Default - & { + A extends Description & + Default & { tag: { id: B; name: string; @@ -125,7 +136,7 @@ export class Value extends IBuilder { "display-as": string | null; "unique-by": UniqueBy; }, - B extends string, + B extends string >(a: A) { const { variants: previousVariants, ...rest } = a; const variants = previousVariants.build() as BuilderExtract; diff --git a/config_builder/variants.ts b/config_builder/variants.ts new file mode 100644 index 0000000..506e1a5 --- /dev/null +++ b/config_builder/variants.ts @@ -0,0 +1,69 @@ +import { ConfigSpec } from "../types/config-types.ts"; +import { BuilderExtract, IBuilder } from "./builder.ts"; +import { Config } from "./mod.ts"; + +/** + * Used in the the Value.enum { @link './value.ts' } + * to indicate the type of enums variants that are available. The key for the record passed in will be the + * key to the tag.id in the Value.enum +```ts + 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, + "variant-names": { + "disabled": "Disabled", + "automatic": "Automatic", + "manual": "Manual", + }, + }, + "display-as": null, + "unique-by": null, + "variant-names": null, + }); +``` + */ +export class Variants extends IBuilder { + static of< + A extends { + [key: string]: Config; + } + >(a: A) { + // deno-lint-ignore no-explicit-any + const variants: { [K in keyof A]: BuilderExtract } = {} as any; + for (const key in a) { + // deno-lint-ignore no-explicit-any + variants[key] = a[key].build() as any; + } + return new Variants(variants); + } + + static empty() { + return Variants.of({}); + } + static withVariant(key: K, value: Config) { + return Variants.empty().withVariant(key, value); + } + + withVariant(key: K, value: Config) { + return new Variants({ + ...this.a, + [key]: value.build(), + } as A & { [key in K]: B }); + } +} diff --git a/config_tools/config_file.ts b/config_tools/config_file.ts new file mode 100644 index 0000000..57d542a --- /dev/null +++ b/config_tools/config_file.ts @@ -0,0 +1,132 @@ +import { matches, TOML, YAML } from "../dependencies.ts"; +import * as T from "../types.ts"; + +const previousPath = /(.+?)\/([^/]*)$/; + +/** + * Used in the get config and the set config exported functions. + * The idea is that we are going to be reading/ writing to a file, or multiple files. And then we use this tool + * to keep the same path on the read and write, and have methods for helping with structured data. + * And if we are not using a structured data, we can use the raw method which forces the construction of a BiMap + * ```ts + import {configSpec} from './configSpec.ts' + import {matches, T} from '../deps.ts'; + const { object, string, number, boolean, arrayOf, array, anyOf, allOf } = matches + const someValidator = object({ + data: string + }) + const jsonFile = ConfigFile.json({ + path: 'data.json', + data: someValidator, + volume: 'main' + }) + const tomlFile = ConfigFile.toml({ + path: 'data.toml', + data: someValidator, + volume: 'main' + }) + const rawFile = ConfigFile.raw({ + path: 'data.amazingSettings', + data: someValidator, + volume: 'main' + fromData(dataIn: Data): string { + return `myDatais ///- ${dataIn.data}` + }, + toData(rawData: string): Data { + const [,data] = /myDatais \/\/\/- (.*)/.match(rawData) + return {data} + } + }) + + export const setConfig : T.ExpectedExports.setConfig= async (effects, config) => { + await jsonFile.write({ data: 'here lies data'}, effects) + } + + export const getConfig: T.ExpectedExports.getConfig = async (effects, config) => ({ + spec: configSpec, + config: nullIfEmpty({ + ...jsonFile.get(effects) + }) + ``` + */ +export class ConfigFile { + protected constructor( + private options: { + path: string; + volume: string; + writeData(dataIn: A): string; + readData(stringValue: string): A; + } + ) {} + async write(data: A, effects: T.Effects) { + let matched; + if ((matched = previousPath.exec(this.options.path))) + await effects.createDir({ volumeId: this.options.volume, path: matched[1] }); + + await effects.writeFile({ + path: this.options.path, + volumeId: this.options.volume, + toWrite: this.options.writeData(data), + }); + } + async read(effects: T.Effects) { + return this.options.readData( + await effects.readFile({ + path: this.options.path, + volumeId: this.options.volume, + }) + ); + } + static raw(options: { path: string; volume: string; fromData(dataIn: A): string; toData(rawData: string): A }) { + return new ConfigFile({ + path: options.path, + volume: options.volume, + writeData: options.fromData, + readData: options.toData, + }); + } + static json(options: { path: string; volume: string; data: matches.Validator }) { + return new ConfigFile({ + path: options.path, + volume: options.volume, + writeData(inData) { + return JSON.stringify(inData, null, 2); + }, + readData(inString) { + return options.data.unsafeCast(JSON.parse(inString)); + }, + }); + } + static toml>(options: { + path: string; + volume: string; + data: matches.Validator; + }) { + return new ConfigFile({ + path: options.path, + volume: options.volume, + writeData(inData) { + return TOML.stringify(inData); + }, + readData(inString) { + return options.data.unsafeCast(TOML.parse(inString)); + }, + }); + } + static yaml>(options: { + path: string; + volume: string; + data: matches.Validator; + }) { + return new ConfigFile({ + path: options.path, + volume: options.volume, + writeData(inData) { + return YAML.stringify(inData); + }, + readData(inString) { + return options.data.unsafeCast(YAML.parse(inString)); + }, + }); + } +} diff --git a/config_tools/mod.ts b/config_tools/mod.ts new file mode 100644 index 0000000..ad2f8bb --- /dev/null +++ b/config_tools/mod.ts @@ -0,0 +1,13 @@ +import { ConfigFile } from "./config_file.ts"; + +/** + * A useful tool when doing a getConfig. + * Look into the config {@link ConfigFile} for an example of the use. + * @param s + * @returns + */ +export function nullIfEmpty(s: Record) { + return Object.keys(s).length === 0 ? null : s; +} + +export { ConfigFile }; diff --git a/dependencies.ts b/dependencies.ts index 1eaec6f..c3ce4d1 100644 --- a/dependencies.ts +++ b/dependencies.ts @@ -1,2 +1,3 @@ export * as matches from "https://deno.land/x/ts_matches@v5.3.0/mod.ts"; -export * as YAML from "https://deno.land/std@0.140.0/encoding/yaml.ts"; +export * as YAML from "https://deno.land/std@0.177.0/encoding/yaml.ts"; +export * as TOML from "https://deno.land/std@0.177.0/encoding/toml.ts"; diff --git a/mod.ts b/mod.ts index 46b4e13..c9a2044 100644 --- a/mod.ts +++ b/mod.ts @@ -4,7 +4,7 @@ export * as compat from "./compat/mod.ts"; export * as migrations from "./migrations.ts"; export * as healthUtil from "./healthUtil.ts"; export * as util from "./util.ts"; -export * as C from "./config/mod.ts"; -export * as config from "./config/mod.ts"; +export * as configBuilder from "./config_builder/mod.ts"; export { Backups } from "./backups.ts"; export * as configTypes from "./types/config-types.ts"; +export * as configTools from "./config_tools/mod.ts"; diff --git a/scripts/oldSpecToBuilder.ts b/scripts/oldSpecToBuilder.ts index 7d38b38..f805c61 100644 --- a/scripts/oldSpecToBuilder.ts +++ b/scripts/oldSpecToBuilder.ts @@ -10,12 +10,15 @@ if (!Deno.isatty(Deno.stdin.rid)) { } console.log(` - import {Config, Value, List, Variants} from '${await Deno.args[0]}'; + import {configBuilder} from '${await Deno.args[0]}'; + const {Config, Value, List, Variants} = configBuilder; `); const data = JSON.parse(list.join("\n")); const namedConsts = new Set(["Config", "Value", "List"]); -newConst("config", convertConfigSpec(data)); +const configName = newConst("config", convertConfigSpec(data)); +const configMatcherName = newConst("matchConfig", `${configName}.validator()`); +console.log(`export type Config = typeof ${configMatcherName}._TYPE;`); function newConst(key: string, data: string) { const variableName = getNextConstName(camelCase(key)); @@ -34,79 +37,68 @@ function convertConfigSpec(data: any) { function convertValueSpec(value: any): string { switch (value.type) { case "string": { - return `Value.string(${ - JSON.stringify( - { - name: value.name || null, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, - nullable: value.nullable || false, - masked: value.masked || null, - placeholder: value.placeholder || null, - pattern: value.pattern || null, - "pattern-description": value["pattern-description"] || null, - textarea: value.textarea || null, - }, - null, - 2, - ) - })`; + return `Value.string(${JSON.stringify( + { + name: value.name || null, + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + nullable: value.nullable || false, + masked: value.masked || null, + placeholder: value.placeholder || null, + pattern: value.pattern || null, + "pattern-description": value["pattern-description"] || null, + textarea: value.textarea || null, + }, + null, + 2 + )})`; } case "number": { - return `Value.number(${ - JSON.stringify( - { - name: value.name || null, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, - nullable: value.nullable || false, - range: value.range || null, - integral: value.integral || false, - units: value.units || null, - placeholder: value.placeholder || null, - }, - null, - 2, - ) - })`; + return `Value.number(${JSON.stringify( + { + name: value.name || null, + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + nullable: value.nullable || false, + range: value.range || null, + integral: value.integral || false, + units: value.units || null, + placeholder: value.placeholder || null, + }, + null, + 2 + )})`; } case "boolean": { - return `Value.boolean(${ - JSON.stringify( - { - name: value.name || null, - default: value.default || false, - description: value.description || null, - warning: value.warning || null, - }, - null, - 2, - ) - })`; + return `Value.boolean(${JSON.stringify( + { + name: value.name || null, + default: value.default || false, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2 + )})`; } case "enum": { - return `Value.enum(${ - JSON.stringify( - { - name: value.name || null, - description: value.description || null, - warning: value.warning || null, - default: value.default || null, - values: value.values || null, - "value-names": value["value-names"] || null, - }, - null, - 2, - ) - })`; + return `Value.enum(${JSON.stringify( + { + name: value.name || null, + description: value.description || null, + warning: value.warning || null, + default: value.default || null, + values: value.values || null, + "value-names": value["value-names"] || null, + }, + null, + 2 + )})`; } case "object": { - const specName = newConst( - value.name + "_spec", - convertConfigSpec(value.spec), - ); + const specName = newConst(value.name + "_spec", convertConfigSpec(value.spec)); return `Value.object({ name: ${JSON.stringify(value.name || null)}, description: ${JSON.stringify(value.description || null)}, @@ -119,37 +111,27 @@ function convertValueSpec(value: any): string { })`; } case "union": { - const variants = newConst( - value.name + "_variants", - convertVariants(value.variants), - ); + const variants = newConst(value.name + "_variants", convertVariants(value.variants)); return `Value.union({ name: ${JSON.stringify(value.name || null)}, description: ${JSON.stringify(value.description || null)}, warning: ${JSON.stringify(value.warning || null)}, default: ${JSON.stringify(value.default || null)}, variants: ${variants}, - tag: ${ - JSON.stringify({ - "id": value?.tag?.["id"] || null, - "name": value?.tag?.["name"] || null, - "description": value?.tag?.["description"] || null, - "warning": value?.tag?.["warning"] || null, + tag: ${JSON.stringify({ + id: value?.tag?.["id"] || null, + name: value?.tag?.["name"] || null, + description: value?.tag?.["description"] || null, + warning: value?.tag?.["warning"] || null, "variant-names": value?.tag?.["variant-names"] || {}, - }) - }, + })}, "display-as": ${JSON.stringify(value["display-as"] || null)}, "unique-by": ${JSON.stringify(value["unique-by"] || null)}, - "variant-names": ${ - JSON.stringify(value["variant-names"] as any || null) - }, + "variant-names": ${JSON.stringify((value["variant-names"] as any) || null)}, })`; } case "list": { - const list = newConst( - value.name + "_list", - convertList(value), - ); + const list = newConst(value.name + "_list", convertList(value)); return `Value.list(${list})`; } case "pointer": { @@ -162,81 +144,69 @@ function convertValueSpec(value: any): string { function convertList(value: any) { switch (value.subtype) { case "string": { - return `List.string(${ - JSON.stringify( - { - name: value.name || null, - range: value.range || null, - spec: { - "masked": value?.spec?.["masked"] || null, - "placeholder": value?.spec?.["placeholder"] || null, - "pattern": value?.spec?.["pattern"] || null, - "pattern-description": value?.spec?.["pattern-description"] || - null, - "textarea": value?.spec?.["textarea"] || false, - }, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, + return `List.string(${JSON.stringify( + { + name: value.name || null, + range: value.range || null, + spec: { + masked: value?.spec?.["masked"] || null, + placeholder: value?.spec?.["placeholder"] || null, + pattern: value?.spec?.["pattern"] || null, + "pattern-description": value?.spec?.["pattern-description"] || null, + textarea: value?.spec?.["textarea"] || false, }, - null, - 2, - ) - })`; + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2 + )})`; } case "number": { - return `List.number(${ - JSON.stringify( - { - name: value.name || null, - range: value.range || null, - spec: { - range: value?.spec?.range || null, - integral: value?.spec?.integral || false, - units: value?.spec?.units || null, - placeholder: value?.spec?.placeholder || null, - }, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, + return `List.number(${JSON.stringify( + { + name: value.name || null, + range: value.range || null, + spec: { + range: value?.spec?.range || null, + integral: value?.spec?.integral || false, + units: value?.spec?.units || null, + placeholder: value?.spec?.placeholder || null, }, - null, - 2, - ) - })`; + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2 + )})`; } case "enum": { - return `List.enum(${ - JSON.stringify( - { - name: value.name || null, - range: value.range || null, - spec: { - "values": value?.spec?.["values"] || null, - "value-names": value?.spec?.["value-names"] || {}, - }, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, + return `List.enum(${JSON.stringify( + { + name: value.name || null, + range: value.range || null, + spec: { + values: value?.spec?.["values"] || null, + "value-names": value?.spec?.["value-names"] || {}, }, - null, - 2, - ) - })`; + default: value.default || null, + description: value.description || null, + warning: value.warning || null, + }, + null, + 2 + )})`; } case "object": { - const specName = newConst( - value.name + "_spec", - convertConfigSpec(value.spec.spec), - ); + const specName = newConst(value.name + "_spec", convertConfigSpec(value.spec.spec)); return `List.obj({ name: ${JSON.stringify(value.name || null)}, range: ${JSON.stringify(value.range || null)}, spec: { spec: ${specName}, - "display-as": ${ - JSON.stringify(value?.spec?.["display-as"] || null) - }, + "display-as": ${JSON.stringify(value?.spec?.["display-as"] || null)}, "unique-by": ${JSON.stringify(value?.spec?.["unique-by"] || null)}, }, default: ${JSON.stringify(value.default || null)}, @@ -245,10 +215,7 @@ function convertList(value: any) { })`; } case "union": { - const variants = newConst( - value.name + "_variants", - convertConfigSpec(value.spec.variants), - ); + const variants = newConst(value.name + "_variants", convertConfigSpec(value.spec.variants)); return `List.union( { name:${JSON.stringify(value.name || null)}, @@ -256,26 +223,14 @@ function convertList(value: any) { 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) - }, - "variant-names": ${ - JSON.stringify(value?.spec?.tag?.["variant-names"] || {}) - }, + "name": ${JSON.stringify(value?.spec?.tag?.["name"] || null)}, + "description": ${JSON.stringify(value?.spec?.tag?.["description"] || null)}, + "warning": ${JSON.stringify(value?.spec?.tag?.["warning"] || null)}, + "variant-names": ${JSON.stringify(value?.spec?.tag?.["variant-names"] || {})}, }, variants: ${variants}, - "display-as": ${ - JSON.stringify(value?.spec?.["display-as"] || null) - }, - "unique-by": ${ - JSON.stringify(value?.spec?.["unique-by"] || null) - }, + "display-as": ${JSON.stringify(value?.spec?.["display-as"] || null)}, + "unique-by": ${JSON.stringify(value?.spec?.["unique-by"] || null)}, default: ${JSON.stringify(value?.spec?.["default"] || null)}, }, default: ${JSON.stringify(value.default || null)}, diff --git a/test.ts b/test.ts index afe6e4a..c0f63c4 100644 --- a/test.ts +++ b/test.ts @@ -1,3 +1,3 @@ import "./emver-lite/test.ts"; import "./utils/propertiesMatcher.test.ts"; -import "./config/index.test.ts"; +import "./config_builder/index.test.ts"; diff --git a/utils/test/output.ts b/utils/test/output.ts index 817ffcc..a01882c 100644 --- a/utils/test/output.ts +++ b/utils/test/output.ts @@ -1,4 +1,5 @@ -import { Config, List, Value, Variants } from "../../config/mod.ts"; +import { configBuilder } from "../../mod.ts"; +const { Config, Value, List, Variants } = configBuilder; export const enable = Value.boolean({ "name": "Enable", @@ -449,3 +450,5 @@ export const config = Config.of({ "wallet": wallet, "advanced": advanced1, }); +export const matchConfig = config.validator(); +export type Config = typeof matchConfig._TYPE;