Comparing version 0.4.0 to 0.4.1
@@ -5,2 +5,12 @@ # Change log | ||
## 0.4.1 | ||
**fixes** | ||
- `getDenoConfigOptions`, `getDenoConfiguration`, `getNodeConfigOptions` and `getNodeConfiguration` all have a default options of `{}` | ||
- The Configuration markdown tables calculates the width properly when there are non-strings (URLs) in there | ||
- The `Structure.boolean` method correctly types the optional fallback argument. | ||
- Add experimental `Structure.literal` construct | ||
- `Structure.object` fails if there are additional fields or the value is an instance of a class | ||
## 0.4.0 | ||
@@ -7,0 +17,0 @@ |
@@ -41,3 +41,3 @@ /** | ||
} | ||
export class _LiteralSpec { | ||
export class _PrimativeSpec { | ||
/** | ||
@@ -53,7 +53,7 @@ * @param {string} type | ||
fields: { | ||
name: any; | ||
type: string; | ||
fallback: any; | ||
variable?: string; | ||
flag?: string; | ||
fallback: any; | ||
name: any; | ||
type: string; | ||
}[]; | ||
@@ -60,0 +60,0 @@ }; |
@@ -73,3 +73,7 @@ import { formatMarkdownTable } from "./utilities.js"; | ||
export class _LiteralSpec { | ||
// | ||
// NOTE: describe() calls should return the actual value in "fallback" | ||
// and the string-value in fields | ||
// | ||
export class _PrimativeSpec { | ||
/** | ||
@@ -86,3 +90,10 @@ * @param {string} type | ||
fallback: this.options.fallback, | ||
fields: [{ name, type: this.type, ...this.options }], | ||
fields: [ | ||
{ | ||
...this.options, | ||
name, | ||
type: this.type, | ||
fallback: this.options.fallback?.toString(), | ||
}, | ||
], | ||
}; | ||
@@ -155,3 +166,3 @@ } | ||
const struct = Structure.string(this._getValue(options).value); | ||
struct[Configuration.spec] = new _LiteralSpec("string", options); | ||
struct[Configuration.spec] = new _PrimativeSpec("string", options); | ||
return struct; | ||
@@ -171,3 +182,3 @@ } | ||
const struct = Structure.number(fallback); | ||
struct[Configuration.spec] = new _LiteralSpec("number", options); | ||
struct[Configuration.spec] = new _PrimativeSpec("number", options); | ||
return struct; | ||
@@ -187,3 +198,3 @@ } | ||
const struct = Structure.boolean(fallback); | ||
struct[Configuration.spec] = new _LiteralSpec("boolean", options); | ||
struct[Configuration.spec] = new _PrimativeSpec("boolean", options); | ||
return struct; | ||
@@ -204,3 +215,3 @@ } | ||
const struct = Structure.url(this._getValue(options).value); | ||
struct[Configuration.spec] = new _LiteralSpec("url", { | ||
struct[Configuration.spec] = new _PrimativeSpec("url", { | ||
...options, | ||
@@ -207,0 +218,0 @@ fallback: new URL(options.fallback), |
@@ -59,3 +59,3 @@ import { Configuration } from "./configuration.js"; | ||
flag: "--age", | ||
fallback: 42, | ||
fallback: "42", | ||
}, | ||
@@ -385,3 +385,3 @@ ], | ||
config.url({ | ||
fallback: "https://example.com", | ||
fallback: "https://example.com/", | ||
variable: "SELF_URL", | ||
@@ -397,3 +397,3 @@ flag: "--self-url", | ||
type: "url", | ||
fallback: new URL("https://example.com"), | ||
fallback: "https://example.com/", | ||
variable: "SELF_URL", | ||
@@ -400,0 +400,0 @@ flag: "--self-url", |
@@ -22,15 +22,4 @@ export class StructError extends Error { | ||
/** | ||
* @typedef {Record<string,unknown>} Schema | ||
*/ | ||
/** | ||
* @template T | ||
* @typedef {(input?: unknown, context?: StructContext) => T} StructExec | ||
*/ | ||
/** | ||
* @template T | ||
* @typedef {T extends Structure<infer U> ? U : never} Infer | ||
*/ | ||
/** | ||
* @template T | ||
*/ | ||
export class Structure<T> { | ||
@@ -48,6 +37,6 @@ /** | ||
/** | ||
* @param {boolean} fallback | ||
* @param {boolean} [fallback] | ||
* @returns {Structure<boolean>} | ||
*/ | ||
static boolean(fallback: boolean): Structure<boolean>; | ||
static boolean(fallback?: boolean): Structure<boolean>; | ||
/** | ||
@@ -73,2 +62,10 @@ * @param {string | URL} [fallback] | ||
/** | ||
* **UNSTABLE** use at your own risk | ||
* | ||
* @template {string|number|boolean} T | ||
* @param {T} value | ||
* @returns {Structure<T>} | ||
*/ | ||
static literal<T_1 extends string | number | boolean>(value: T_1): Structure<T_1>; | ||
/** | ||
* @param {Schema} schema | ||
@@ -78,3 +75,3 @@ * @param {StructExec<T>} process | ||
constructor(schema: Schema, process: StructExec<T>); | ||
schema: Schema; | ||
schema: Record<string, unknown>; | ||
process: StructExec<T>; | ||
@@ -85,2 +82,5 @@ getSchema(): { | ||
} | ||
export type Schema = Record<string, unknown>; | ||
export type StructExec<T> = (input?: unknown, context?: StructContext) => T; | ||
export type Infer<T> = T extends Structure<infer U> ? U : never; | ||
export type StructContext = { | ||
@@ -94,4 +94,1 @@ path: string[]; | ||
}; | ||
export type Schema = Record<string, unknown>; | ||
export type StructExec<T> = (input?: unknown, context?: StructContext) => T; | ||
export type Infer<T> = T extends Structure<infer U> ? U : never; |
@@ -88,2 +88,7 @@ /** @typedef {{ path: string[] }} StructContext */ | ||
function _additionalProperties(fields, input) { | ||
const allowed = new Set(Object.keys(fields)); | ||
return Array.from(Object.keys(input)).filter((key) => !allowed.has(key)); | ||
} | ||
/** | ||
@@ -152,3 +157,3 @@ * @template T | ||
/** | ||
* @param {boolean} fallback | ||
* @param {boolean} [fallback] | ||
* @returns {Structure<boolean>} | ||
@@ -211,2 +216,5 @@ */ | ||
} | ||
if (Object.getPrototypeOf(input) !== Object.getPrototypeOf({})) { | ||
throw new StructError("Should not have a prototype", path); | ||
} | ||
const output = {}; | ||
@@ -222,2 +230,9 @@ const errors = []; | ||
} | ||
for (const key of _additionalProperties(fields, input)) { | ||
errors.push( | ||
new StructError("Additional field not allowed", [...path, key]), | ||
); | ||
} | ||
if (errors.length > 0) { | ||
@@ -264,2 +279,26 @@ throw new StructError("Object does not match schema", path, errors); | ||
} | ||
/** | ||
* **UNSTABLE** use at your own risk | ||
* | ||
* @template {string|number|boolean} T | ||
* @param {T} value | ||
* @returns {Structure<T>} | ||
*/ | ||
static literal(value) { | ||
const schema = { type: typeof value, const: value }; | ||
return new Structure(schema, (input, context = undefined) => { | ||
if (input === undefined) { | ||
throw new StructError("Missing value", context?.path); | ||
} | ||
if (input !== value) { | ||
throw new StructError( | ||
`Expected ${schema.type} literal: ${value}`, | ||
context?.path, | ||
); | ||
} | ||
return value; | ||
}); | ||
} | ||
} |
@@ -428,2 +428,40 @@ import { StructError, Structure } from "./structures.js"; | ||
}); | ||
it("throws for unknown fields", () => { | ||
const struct = Structure.object({ | ||
key: Structure.string("fallback"), | ||
}); | ||
const error = assertThrows( | ||
() => | ||
struct.process( | ||
{ key: "value", something: "else" }, | ||
{ path: ["some", "path"] }, | ||
), | ||
StructError, | ||
); | ||
assertEquals(error.message, "Object does not match schema"); | ||
assertEquals(error.path, ["some", "path"], "should capture the context"); | ||
assertEquals(error.children[0].message, "Additional field not allowed"); | ||
assertEquals( | ||
error.children[0].path, | ||
["some", "path", "something"], | ||
"should capture the context", | ||
); | ||
}); | ||
it("throws for non-null prototypes", () => { | ||
const struct = Structure.object({ | ||
key: Structure.string("fallback"), | ||
}); | ||
class Injector { | ||
key = "value"; | ||
} | ||
const error = assertThrows( | ||
() => struct.process(new Injector(), { path: ["some", "path"] }), | ||
StructError, | ||
); | ||
assertEquals(error.message, "Should not have a prototype"); | ||
assertEquals(error.path, ["some", "path"], "should capture the context"); | ||
}); | ||
}); | ||
@@ -475,2 +513,52 @@ | ||
}); | ||
describe("literal", () => { | ||
it("creates a structure", () => { | ||
const struct = Structure.literal(42); | ||
assertInstanceOf(struct, Structure); | ||
}); | ||
it("allows that value", () => { | ||
const struct = Structure.literal(42); | ||
assertEquals(struct.process(42), 42, "should pass the value through"); | ||
}); | ||
it("throws for different values", () => { | ||
const struct = Structure.literal(42); | ||
const error = assertThrows( | ||
() => struct.process(69, { path: ["some", "path"] }), | ||
StructError, | ||
); | ||
assertEquals( | ||
error, | ||
new StructError("Expected number literal: 42", ["some", "path"]), | ||
"should throw a StructError and capture the context", | ||
); | ||
}); | ||
it("throws for different types", () => { | ||
const struct = Structure.literal(42); | ||
const error = assertThrows( | ||
() => struct.process("nice", { path: ["some", "path"] }), | ||
StructError, | ||
); | ||
assertEquals( | ||
error, | ||
new StructError("Expected number literal: 42", ["some", "path"]), | ||
"should throw a StructError and capture the context", | ||
); | ||
}); | ||
it("throws for missing values", () => { | ||
const struct = Structure.literal(42); | ||
const error = assertThrows( | ||
() => struct.process(undefined, { path: ["some", "path"] }), | ||
StructError, | ||
); | ||
assertEquals( | ||
error, | ||
new StructError("Missing value", ["some", "path"]), | ||
"should throw a StructError and capture the context", | ||
); | ||
}); | ||
}); | ||
}); |
@@ -31,3 +31,3 @@ { | ||
}, | ||
"version": "0.4.0" | ||
"version": "0.4.1" | ||
} |
@@ -5,3 +5,3 @@ /** | ||
/** @param {NodeConfigurationOptions} options */ | ||
export function getNodeConfigOptions(options: NodeConfigurationOptions): { | ||
export function getNodeConfigOptions(options?: NodeConfigurationOptions): { | ||
readTextFile(url: any): Promise<Buffer>; | ||
@@ -17,5 +17,5 @@ getEnvironmentVariable(key: any): string; | ||
*/ | ||
export function getNodeConfiguration(options: NodeConfigurationOptions): Configuration; | ||
export function getNodeConfiguration(options?: NodeConfigurationOptions): Configuration; | ||
export { Configuration }; | ||
export type NodeConfigurationOptions = object; | ||
import { Configuration } from "../core/configuration.js"; |
@@ -14,3 +14,3 @@ import fs from "node:fs"; | ||
/** @param {NodeConfigurationOptions} options */ | ||
export function getNodeConfigOptions(options) { | ||
export function getNodeConfigOptions(options = {}) { | ||
const args = util.parseArgs({ | ||
@@ -47,4 +47,4 @@ args: process.args, | ||
*/ | ||
export function getNodeConfiguration(options) { | ||
export function getNodeConfiguration(options = {}) { | ||
return new Configuration(getNodeConfigOptions(options)); | ||
} |
143140
3415