diff --git a/README.md b/README.md index 24d2fb2..d6fee45 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# flex +# tinylex -flex is a tiny vanilla JavaScript library for validating [atproto lexicons](https://atproto.com/guides/lexicon). Given a set of lexicon schemata — the JSON documents that describe atproto record types — flex extracts static types and validates record data against them. It conforms to [Standard Schema](https://standardschema.dev/schema), so you can use it anywhere you'd use schema validators like Zod/Valibot/etc. +tinylex is a tiny vanilla JavaScript library for validating [atproto lexicons](https://atproto.com/guides/lexicon). Given a set of lexicon schemata — the JSON documents that describe atproto record types — tinylex extracts static types and validates record data against them. It conforms to [Standard Schema](https://standardschema.dev/schema), so you can use it anywhere you'd use schema validators like Zod/Valibot/etc. ## Installation -flex is meant to be [vendored](https://htmx.org/essays/vendoring/); simply copy and paste `flex.js` and `flex-install` into your project! +tinylex is meant to be [vendored](https://htmx.org/essays/vendoring/); simply copy and paste `tinylex.js` and `tinylex-install` into your project! ## Usage A `LexiconCatalog` holds your lexicon documents and resolves cross-references between them. You can handwrite or copy/paste lexicons inline or [fetch them by NSID](#fetching-lexicons) — either way, add them to the catalog, then call `schema(nsid)` to get a Standard Schema validator: ```js -import { LexiconCatalog } from "./flex.js"; +import { LexiconCatalog } from "./tinylex.js"; const lexicons = new LexiconCatalog().add({ lexicon: 1, @@ -45,12 +45,12 @@ else console.log(result.value.subject); // typed as string ## Fetching lexicons -`flex-install` resolves lexicons by NSID over DNS (per the [lexicon resolution spec](https://atproto.com/specs/lexicon#lexicon-resolution)) and writes them to disk: +`tinylex-install` resolves lexicons by NSID over DNS (per the [lexicon resolution spec](https://atproto.com/specs/lexicon#lexicon-resolution)) and writes them to disk: ```sh -./flex-install app.bsky.feed.post app.bsky.feed.like -./flex-install app.bsky.actor.profile --out ./schemata -./flex-install app.bsky.feed.post --js +./tinylex-install app.bsky.feed.post app.bsky.feed.like +./tinylex-install app.bsky.actor.profile --out ./schemata +./tinylex-install app.bsky.feed.post --js ``` Output defaults to `./lexicons/.json`. Pass `--js` to emit `.js` files wrapped in `export default /** @type {const} */ ({...})` so imports keep literal types (otherwise TypeScript widens the JSON and `LexiconCatalog.add()` can't infer narrow types for `result.value`). @@ -61,4 +61,4 @@ The script is a polyglot shell/JS file that picks the first available runtime am The [official guide to installing lexicons](https://atproto.com/guides/installing-lexicons) uses the [`@atproto/lex`](https://npmx.dev/package/@atproto/lex) package, which has 79 transitive dependencies and a requires a build step to actually use the lexicons in your code. [`@atcute/lex-cli`](https://npmx.dev/package/@atcute/lex-cli) is lighter weight, but requires a config file _and_ a build step. -flex is ~650 lines of vanilla JavaScript with no dependencies. It lets you use the lexicon schema JSON as-is for validation and static typing. For convenience, it also includes a ~100 line install script that works with any JavaScript runtime to fetch lexicon documents based on their NSIDs. +tinylex is ~650 lines of vanilla JavaScript with no dependencies. It lets you use the lexicon schema JSON as-is for validation and static typing. For convenience, it also includes a ~100 line install script that works with any JavaScript runtime to fetch lexicon documents based on their NSIDs. diff --git a/test.js b/test.js index bf99855..afe5058 100644 --- a/test.js +++ b/test.js @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { LexiconCatalog } from "./flex.js"; +import { LexiconCatalog } from "./tinylex.js"; /** @returns {(v: unknown) => boolean} */ const validator = (/** @type {any} */ def) => { diff --git a/flex-install b/tinylex-install similarity index 99% rename from flex-install rename to tinylex-install index 81dc14c..d02db6f 100755 --- a/flex-install +++ b/tinylex-install @@ -5,7 +5,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // -// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/flex +// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/tinylex import { writeFile, mkdir } from "node:fs/promises"; import { dirname, join } from "node:path"; diff --git a/flex.js b/tinylex.js similarity index 96% rename from flex.js rename to tinylex.js index 64aec6b..39243a8 100644 --- a/flex.js +++ b/tinylex.js @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // -// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/flex +// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/tinylex // --------------------------------------------------------------------------- // String format names (see FORMATS validators below) @@ -49,7 +49,7 @@ * @typedef {{ * readonly "~standard": { * readonly version: 1; - * readonly vendor: "flex"; + * readonly vendor: "tinylex"; * readonly validate: (value: unknown) => Result; * readonly types?: { readonly input: unknown; readonly output: T }; * }; @@ -276,8 +276,10 @@ const isPlainObject = (x) => x !== null && typeof x === "object" && !Array.isArr * @returns {{ issues: ReadonlyArray } | null} */ function checkRange(value, min, max, minLabel, maxLabel, prefix, path) { - if (min !== undefined && value < min) return fail(`${prefix} ${value} < ${minLabel} ${min}`, path); - if (max !== undefined && value > max) return fail(`${prefix} ${value} > ${maxLabel} ${max}`, path); + if (min !== undefined && value < min) + return fail(`${prefix} ${value} < ${minLabel} ${min}`, path); + if (max !== undefined && value > max) + return fail(`${prefix} ${value} > ${maxLabel} ${max}`, path); return null; } @@ -340,7 +342,7 @@ export class LexiconCatalog { return { "~standard": { version: 1, - vendor: "flex", + vendor: "tinylex", validate: (value) => /** @type {Result>} */ ( validateType(value, def, { resolveRef, nsid }, []) @@ -428,11 +430,27 @@ function validateString(data, def, _opts, path) { const ce = checkConstEnum(data, def, path); if (ce) return ce; const byteLen = TEXT_ENCODER.encode(data).length; - const lenFail = checkRange(byteLen, def.minLength, def.maxLength, "minLength", "maxLength", "Byte length", path); + const lenFail = checkRange( + byteLen, + def.minLength, + def.maxLength, + "minLength", + "maxLength", + "Byte length", + path, + ); if (lenFail) return lenFail; if (def.minGraphemes !== undefined || def.maxGraphemes !== undefined) { const count = SEGMENTER ? [...SEGMENTER.segment(data)].length : [...data].length; - const gFail = checkRange(count, def.minGraphemes, def.maxGraphemes, "minGraphemes", "maxGraphemes", "Grapheme count", path); + const gFail = checkRange( + count, + def.minGraphemes, + def.maxGraphemes, + "minGraphemes", + "maxGraphemes", + "Grapheme count", + path, + ); if (gFail) return gFail; } if (def.format !== undefined) { @@ -487,7 +505,15 @@ function validateBlob(data, def, _opts, path) { /** @type {Validator} */ function validateArray(data, def, opts, path) { if (!Array.isArray(data)) return fail(`Expected array, got ${typeof data}`, path); - const lenFail = checkRange(data.length, def.minLength, def.maxLength, "minLength", "maxLength", "Length", path); + const lenFail = checkRange( + data.length, + def.minLength, + def.maxLength, + "minLength", + "maxLength", + "Length", + path, + ); if (lenFail) return lenFail; if (!def.items) return fail(`Array schema missing "items"`, path); const issues = data.flatMap(