@studnicky/config
Configuration parsing, errors, and clamping utilities.
Install
pnpm add @studnicky/configRequires @studnicky:registry=https://npm.pkg.github.com in .npmrc.
Usage
Parse external configuration through an entity's intake function. Intake supplies schema defaults and removes undeclared properties, without coercing a value's type — a wrong-typed field is rejected, not silently converted:
namespace ServerConfigEntity {
export const Schema = {
'additionalProperties': false,
'properties': {
'debug': { 'default': false, 'type': 'boolean' },
'host': { 'minLength': 1, 'type': 'string' },
'maximumRetries': { 'default': 3, 'minimum': 0, 'type': 'integer' },
'port': { 'default': 8080, 'maximum': 65_535, 'minimum': 1, 'type': 'integer' }
},
'required': ['host'],
'type': 'object'
} as const satisfies JSONSchema;
export type Type = FromSchema<typeof Schema>;
export const validate: ValidateFunction<Type> = SchemaValidator.compile<Type>(Schema);
export const intake: SchemaIntakeFunctionInterface<Type> = SchemaValidator.compileIntake<Type>(Schema);
export const create: SchemaCreateFunctionInterface<Type> = SchemaValidator.compileCreate<Type>(Schema);
}
const config = ServerConfigEntity.intake({ 'host': 'localhost', 'ignored': true, 'port': 8081 });
console.log('Parsed config:', config);
assert.deepEqual(config, {
'debug': false,
'host': 'localhost',
'maximumRetries': 3,
'port': 8081
});
const localConfig = ServerConfigEntity.create({ 'host': 'test.local' });
assert.deepEqual(localConfig, {
'debug': false,
'host': 'test.local',
'maximumRetries': 3,
'port': 8080
});Public API
Import ClampedConfig and ConfigurationError from @studnicky/config; import clamping schemas from @studnicky/config/entities.
Try it
The output shows a typed configuration with defaults applied and undeclared properties removed.
Configuration errors
Build a ConfigurationError with an Error cause when an already-parsed configuration cannot be used:
import { ConfigurationError } from '../src/index.js';
const cause = RuntimeError.create('environment variable CONFIG_URL is not set');
const configurationError = ConfigurationError.create('configuration is invalid', cause);
console.log('Configuration error:', configurationError.message);
console.log('Cause:', configurationError.cause);Clamping
ClampedConfig applies declarative {min, max, reason} rules to a flat configuration object. apply returns a new object with out-of-range numeric fields clamped into range. Fields not present in the rule table, not numeric, or already in range are copied through unchanged; the input is never mutated.
import { ClampedConfig } from '@studnicky/config';
import { ClampRuleEntity } from '@studnicky/config/entities';
interface WorkerConfig {
timeoutMs: number;
concurrency: number;
}
const rules: Record<string, ClampRuleEntity.Type> = {
timeoutMs: { min: 100, max: 5000, reason: 'timeout must stay within safe bounds' },
concurrency: { min: 1, max: 8, reason: 'concurrency must stay within pool capacity' },
};
const raw: WorkerConfig = { timeoutMs: 10, concurrency: 4 };
const clamped = ClampedConfig.apply(raw, rules);
// clamped.timeoutMs === 100, clamped.concurrency === 4, raw is unchangedOverride the protected onClamp static method to observe clamp events — logging is the caller's responsibility, ClampedConfig has no dependency on any logging package:
import { ClampedConfig } from '@studnicky/config';
import { ClampEventEntity } from '@studnicky/config/entities';
class LoggingClampedConfig extends ClampedConfig {
protected static override onClamp(event: ClampEventEntity.Type): void {
console.warn(`[config] clamped ${event.field}: ${event.raw} -> ${event.clamped} (${event.reason})`);
}
}
LoggingClampedConfig.apply(raw, rules);Entities
@studnicky/config/entities exports clamping rule and event schemas.
import { ClampRuleEntity } from '@studnicky/config/entities';Exports
| Symbol | Purpose | Import path |
|---|---|---|
ClampedConfig | Applies declarative numeric clamping rules. | @studnicky/config |
ConfigurationError | Represents invalid configuration values. | @studnicky/config |