Skip to content
Merged

Dev #51

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4bae352
chore: Updated dependencies
erayhanoglu Aug 11, 2026
c51144f
feat: Integrate `graphify` for knowledge graph generation and add rel…
erayhanoglu Sep 8, 2026
e313dd3
fix: correct several validator edge-case bugs
erayhanoglu Sep 8, 2026
36ec1b4
chore: update dependencies (npm packages)
erayhanoglu Sep 8, 2026
f96c5ae
fix: more validator correctness fixes and a doc correction
erayhanoglu Sep 8, 2026
bf875e4
fix: stateful regex and wrong property lookup in two rules
erayhanoglu Sep 8, 2026
bf76ca5
fix: forward options in allOf/oneOf and fix iif's success check
erayhanoglu Sep 8, 2026
fc7d6b3
test: raise coverage to ~99% and fix bugs found along the way
erayhanoglu Sep 8, 2026
7c17682
chore: remove dead helper files
erayhanoglu Sep 8, 2026
c8660e7
feat: add a benchmark tool and fix real perf/correctness issues it found
erayhanoglu Sep 8, 2026
d60ee5d
perf: fix remaining wrong-slot nested calls, benchmark rest of utilit…
erayhanoglu Sep 8, 2026
ce39180
test: add benchmarks for all format-rules and fix two stale JSDoc com…
erayhanoglu Sep 8, 2026
9bfb49c
feat: write benchmark results to BENCHMARKS.md and show live progress
erayhanoglu Sep 8, 2026
01cf98e
fix: stop the memory pass's own forceGc() from erasing what it measures
erayhanoglu Sep 8, 2026
21940b8
fix: bugs found while writing the API reference docs
erayhanoglu Sep 8, 2026
96f6697
docs: add a detailed API reference and rewrite the README
erayhanoglu Sep 8, 2026
74dd529
chore: refresh BENCHMARKS.md from a new run
erayhanoglu Sep 8, 2026
f84935a
7.0.0
erayhanoglu Sep 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
node_modules
/build
*.tsbuildinfo
graphify-out
.claude

# environment variables
.env
Expand Down
7 changes: 0 additions & 7 deletions .madgerc

This file was deleted.

14 changes: 14 additions & 0 deletions .run/All Tests.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="mocha-javascript-test-runner">
<node-interpreter>project</node-interpreter>
<node-options />
<mocha-package>$PROJECT_DIR$/node_modules/mocha</mocha-package>
<working-directory>$PROJECT_DIR$/</working-directory>
<pass-parent-env>true</pass-parent-env>
<ui>bdd</ui>
<extra-mocha-options />
<test-kind>PATTERN</test-kind>
<test-pattern />
<method v="2" />
</configuration>
</component>
5 changes: 5 additions & 0 deletions .run/benchmark.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="benchmark" type="NodeJSConfigurationType" application-parameters="-s isObject" node-parameters="--import @swc-node/register/esm-register" path-to-js-file="cli.ts" working-dir="$PROJECT_DIR$/benchmark">
<method v="2" />
</configuration>
</component>
243 changes: 243 additions & 0 deletions BENCHMARKS.md

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
## Changelog

### [v6.2.2](https://github.com/panates/valgen/compare/v6.2.1...v6.2.2) -
### [v7.0.0](https://github.com/panates/valgen/compare/v6.2.2...v7.0.0) -

#### 🚀 New Features

- feat: add a benchmark tool and fix real perf/correctness issues it found @Eray Hanoğlu
- feat: write benchmark results to BENCHMARKS.md and show live progress @Eray Hanoğlu
- feat: Integrate `graphify` for knowledge graph generation and add related documentation and scripts @Eray Hanoğlu

#### 🪲 Fixes

- fix: bugs found while writing the API reference docs @Eray Hanoğlu
- fix: stop the memory pass's own forceGc() from erasing what it measures @Eray Hanoğlu
- fix: forward options in allOf/oneOf and fix iif's success check @Eray Hanoğlu
- fix: more validator correctness fixes and a doc correction @Eray Hanoğlu
- fix: correct several validator edge-case bugs @Eray Hanoğlu
- fix: stateful regex and wrong property lookup in two rules @Eray Hanoğlu

#### 📖 Documentation Changes

- docs: add a detailed API reference and rewrite the README @Eray Hanoğlu

#### 🧪 Changes to Test Assests

- test: raise coverage to ~99% and fix bugs found along the way @Eray Hanoğlu
- test: add benchmarks for all format-rules and fix two stale JSDoc comments @Eray Hanoğlu

#### ⚡️ Performance Improvments

- perf: fix remaining wrong-slot nested calls, benchmark rest of utility-rules @Eray Hanoğlu

### [v6.2.2](https://github.com/panates/valgen/compare/v6.2.1...v6.2.2) - 21 May 2026

#### 🚀 New Features

Expand Down
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## graphify

This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.

Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
117 changes: 102 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,119 @@
[![CI Tests][ci-test-image]][ci-test-url]
[![Test Coverage][coveralls-image]][coveralls-url]

Fast runtime type validator, converter and io (encoding/decoding) library.
Fast runtime type validator, converter and io (encoding/decoding) library for TypeScript and
JavaScript.

- **Composable** - build complex schemas out of small validators with `allOf`, `oneOf`, `pipe`,
`optional`, `nullable`, `isObject`, `isArray`, ...
- **Coercing** - most rules can convert compatible input (`"42"` -> `42`, `"true"` -> `true`, ...)
instead of just rejecting it, via the `coerce` option.
- **Two calling conventions** - call a validator directly and catch a `ValidationError`, or use
`.silent(...)` to get back `{ value }` / `{ errors }` without throwing.
- **Typed** - every validator infers its output type, so a passing call narrows the type of its
return value.
- **Fast** - the core dispatch path and every rule are covered by a dedicated benchmark suite (see
[Benchmarking](#benchmarking) below); performance regressions are something this project
actively measures, not just hopes for.

## Installation

`$ npm install valgen --save`
```sh
npm install valgen --save
```

## Quick start

```ts
import { vg, isEmail, ValidationError } from 'valgen';

// Individual, ready-to-use validators
isEmail('a@b.com'); // => 'a@b.com'
isEmail('not-an-email'); // throws ValidationError

// A full object schema, built from composable rules
const userSchema = vg.isObject({
id: vg.isUUID(),
email: vg.isEmail(),
age: vg.optional(vg.pipe([vg.isNumber({ coerce: true }), vg.isGt(0)])),
role: vg.isEnum(['admin', 'user']),
});

try {
const user = userSchema({
id: 'e6a3b1c0-70b6-4a3e-9b34-1e2f2e3d1a11',
email: 'a@b.com',
age: '30', // coerced to a number
role: 'admin',
});
} catch (e) {
if (e instanceof ValidationError) {
console.error(e.issues); // one entry per failing field
}
}

// Or avoid the try/catch entirely
const result = userSchema.silent({ email: 'not-an-email' });
if (result.errors) {
// result.errors: ErrorIssue[]
}
```

## Documentation

The full API reference lives under [`docs/`](docs/api.md):

- **[API overview](docs/api.md)** - the `Validator` shape, `.silent()`, pre-built instances vs.
factories, `ExecutionOptions`, error shape, composition patterns, and how to write a custom rule.
- **[Type Rules](docs/api/type-rules.md)** - `isString`, `isNumber`, `isObject`, `isArray`,
`isTuple`, `isEnum`, `isDate`, `isRecord`, `isInstanceOf`, ...
- **[Logical Rules](docs/api/logical-rules.md)** - `isEqual`, `isGt`/`isGte`/`isLt`/`isLte`,
`range`, `lengthMin`/`lengthMax`, `isEmpty`/`isNotEmpty`, `isDefined`.
- **[Utility Rules](docs/api/utility-rules.md)** - `allOf`, `oneOf`, `pipe`, `optional`,
`nullable`, `required`, `fixed`, `getLength`, `forwardRef`, `iif`, string helpers.
- **[Format Rules](docs/api/format-rules.md)** - `isEmail`, `isURL`, `isUUID`, `isIBAN`,
`isMACAddress`, `isCreditCard`, and every other string-format check.

## Scripts

| Command | Description |
|---|---|
| `npm test` | Run the test suite (mocha). |
| `npm run citest` | Run the test suite with coverage (c8). |
| `npm run qc` | Lint + circular-dependency check. |
| `npm run bench` | Run the benchmark suite (see below). |
| `npm run build` | Type-check and compile to `build/`. |

## Benchmarking

Every validator rule has a dedicated benchmark case measuring throughput (ops/sec) and per-call
memory allocation (heap/RSS), under `benchmark/rules/`.

```sh
# Run every rule
npm run bench

# Run one or more rules (case-insensitive, comma-separated, "*" wildcards allowed)
npm run bench -- -s isEmail,isURL
npm run bench -- -s "is*"
```

Results print to the console as they complete and are also written to [`BENCHMARKS.md`](BENCHMARKS.md)
at the end of the run.

## Node Compatibility

- node `>= 16.0`;
- node `>= 20.0`

### License

[MIT](LICENSE)

[npm-image]: https://img.shields.io/npm/v/valgen.svg
[npm-image]: https://img.shields.io/npm/v/valgen
[npm-url]: https://npmjs.org/package/valgen
[downloads-image]: https://img.shields.io/npm/dm/valgen.svg
[downloads-url]: https://npmjs.org/package/valgen
[ci-test-image]: https://github.com/panates/valgen/actions/workflows/test.yml/badge.svg
[ci-test-url]: https://github.com/panates/valgen/actions/workflows/test.yml
[coveralls-image]: https://img.shields.io/coveralls/panates/valgen/master.svg
[coveralls-url]: https://coveralls.io/r/panates/valgen
[downloads-image]: https://img.shields.io/npm/dm/valgen.svg
[downloads-url]: https://npmjs.org/package/valgen
[gitter-image]: https://badges.gitter.im/panates/valgen.svg
[gitter-url]: https://gitter.im/panates/valgen?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
[dependencies-image]: https://david-dm.org/panates/valgen/status.svg
[dependencies-url]:https://david-dm.org/panates/valgen
[devdependencies-image]: https://david-dm.org/panates/valgen/dev-status.svg
[devdependencies-url]:https://david-dm.org/panates/valgen?type=dev
[quality-image]: http://npm.packagequality.com/shield/valgen.png
[quality-url]: http://packagequality.com/#?package=valgen
131 changes: 131 additions & 0 deletions benchmark/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { readdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import c from 'ansi-colors';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import {
type BenchCase,
type BenchResult,
printResults,
resultsToMarkdown,
runBench,
} from './harness.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rulesDir = path.join(__dirname, 'rules');
const BENCH_EXT = '.bench.ts';
const OUTPUT_MD_PATH = path.join(__dirname, '..', 'BENCHMARKS.md');

/** Every rule name that has a bench/rules/<name>.bench.ts file. */
function discoverRuleNames(): string[] {
return readdirSync(rulesDir)
.filter(f => f.endsWith(BENCH_EXT))
.map(f => f.slice(0, -BENCH_EXT.length))
.sort();
}

/**
* Expands a comma-separated `-s` value into actual rule names.
* "all" / "none" are special-cased (case-insensitively); otherwise each
* comma-separated item is either an exact name or a wildcard pattern - a
* leading and/or trailing "*" matches by suffix / prefix / substring, e.g.
* "isDate*", "*Address", "*is*". Matching is always case-insensitive
* (rule names are the validator's function name, e.g. "isDefined"), but the
* original casing from `all` is what's returned.
*/
function resolveList(value: string, all: readonly string[]): string[] {
const valueLower = value.toLowerCase();
if (valueLower === 'all') return [...all];
if (valueLower === 'none') return [];
const items = valueLower
.split(',')
.map(s => s.trim())
.filter(Boolean);
const seen = new Set<string>();
for (const item of items) {
const matches = all.filter(v => {
const vLower = v.toLowerCase();
return item.startsWith('*') && item.endsWith('*')
? vLower.includes(item.replaceAll('*', ''))
: item.startsWith('*')
? vLower.endsWith(item.replaceAll('*', ''))
: item.endsWith('*')
? vLower.startsWith(item.replaceAll('*', ''))
: vLower === item;
});
matches.forEach(v => seen.add(v));
}
return Array.from(seen);
}

async function main(): Promise<void> {
const ruleNames = discoverRuleNames();

const argv = await yargs(hideBin(process.argv))
.scriptName('bench')
.usage('$0 [options]')
.option('rule', {
alias: 's',
type: 'string',
default: 'all',
describe:
'Comma-separated rule names, "all", or "none" - case-insensitive, ' +
'a leading and/or trailing "*" matches by prefix/suffix/substring, ' +
`e.g. "isDefined*" (${ruleNames.join(', ')})`,
})
.strict()
.help()
.parse();

const selected = resolveList(argv.rule, ruleNames);
if (selected.length === 0) {
console.error(
c.red(
`No rule matched "${argv.rule}". Available: ${ruleNames.join(', ')}`,
),
);
process.exitCode = 1;
return;
}

console.log(c.bold.cyan(`Benchmarking: ${selected.join(', ')}`) + '\n');

const results: BenchResult[] = [];
for (const name of selected) {
const mod = (await import(
pathToFileURL(path.join(rulesDir, `${name}${BENCH_EXT}`)).href
)) as {
cases: BenchCase[];
};
for (const benchCase of mod.cases) {
results.push(runBench(benchCase));
// Each round/warmup/memory pass runs synchronously and can take a
// while (esp. across "all" rules), so print progress as each case
// finishes instead of leaving the console silent until the end.
console.log(c.dim(` ✓ ${benchCase.name}`));
}
}
console.log();
printResults(results);

const markdown = [
'# Benchmarks',
'',
`Generated ${new Date().toISOString()} - Node ${process.version}.`,
'',
resultsToMarkdown(results),
].join('\n');
writeFileSync(OUTPUT_MD_PATH, markdown);
console.log(
c.dim(
`\nResults written to ${path.relative(process.cwd(), OUTPUT_MD_PATH)}`,
),
);
}

main().catch(err => {
console.error(c.red(String(err?.stack ?? err)));
process.exit(1);
});
Loading