Skip to content

feat: add the schema-check module - #5

Merged
mcanouil merged 8 commits into
mainfrom
feat/add-the-schema-check-module
Sep 6, 2026
Merged

mcanouil merged 8 commits into
mainfrom
feat/add-the-schema-check-module

Conversation

@mcanouil

@mcanouil mcanouil commented Sep 6, 2026

Copy link
Copy Markdown
Owner

schema-check holds the wiring an extension needs to check a document and a shortcode call against its own _schema.yml, so that thirty-eight extensions can validate without each carrying sixty lines of the same code.

The validator is injected rather than required:

local checker = check.new(validator, EXTENSION_NAME)
local defaults, resolved = checker:options(meta)
checker:call(name, args, kwargs)

new reads the schema once. options validates the document configuration and returns the resolved defaults, and second a table holding provided, merged and defaults. The second return is what tells a value an author wrote from a key they never set, which is the only way inline: false differs from an absent inline. call validates one shortcode call, and only an extension declaring a shortcodes: block needs it.

Severity lives in one table. Warnings, except an unreadable schema and a missing required argument, which are errors. Nothing here stops a render, raises, or exits, and a test asserts that no path calls os.exit. The single exception is a load-time assertion that every severity level has a reporter, whose condition depends only on constants in the file, so no document or schema can reach it and a mistyped level cannot reach a release.

The behaviour is quarto-iconify's, moved rather than rewritten. Message strings are byte-identical, and the levels were cross-checked against the lines they came from, so an extension adopting this module reports exactly what iconify reports today.

The defaults are copied on the way out, all the way down. A caller that writes into what it received cannot change what a later reader sees, and the copy is recursive because the schema vocabulary allows a collection default at any depth: quarto-remember declares an array default and quarto-revealjs-a11y an object one.

122 checks pass. Each of them was checked against a mutation of the thing it covers, so a test that could not fail was found and fixed rather than counted.

Every extension that checks its `_schema.yml` at render time needs the same
wiring.
It reads the schema once, checks the document configuration, checks a
shortcode call, and reports through `logging`.
The module holds that wiring, so an extension does not copy it.

The validator arrives as an argument rather than through `require`.
A vendored copy of this module then knows nothing about where the validator
was vendored, and the two sources stay independent.

The level of each finding lives in one table.
Nothing the module reports stops a render.
`options` returned the schema defaults alone.
An extension needs more than that: a default is always present once declared,
so the defaults cannot tell a value the document wrote from a key it never set.

It now returns the defaults first, and a table holding `provided`, `merged`
and `defaults` second.
A caller that wants the defaults alone is unchanged.
…trip

Two levels in the severity table had no test that reached them.
`option_error` needed an options pass the schema rejects, and `call_error`
needed a call that gives its required argument, so that the missing argument
path does not take over the reporting.

The two once per render checks passed whether or not the guard was there,
because the stub they used reported nothing.
The stub now reports a finding on every pass, so a second check shows as a
second message.

`_report` no longer reports an unmapped kind as a warning.
It says which kind it could not report, at error level, and the module now
checks at load that every level has a reporting function.
The validator comes from an independent source, so `options` no longer assumes
the schema it returns carries an `options` key.
`call` already made the same allowance for `shortcodes`, and the two methods
now agree on how much they trust the injected table.

`options` returned the checker's own defaults table.
One caller writing into what it received changed the defaults for the whole
render, and for every later reader.
It now returns a copy on every call.
The second return still holds the checker's own tables, and the docstring says
they must not be written to.
The module discards the first return of `validate_shortcode`, so a `valid` on
the call stub said nothing while inviting the next reader to believe it gated
the errors loop.
The stub now returns true there, and a comment says why, next to the options
pass where `valid` does gate the reporting.
A default is not always a scalar.
One extension declares `default: []` for an array option, and another declares
a mapping default, so a copy one level deep handed two callers the same inner
table and left the aliasing one level down.

The copy is now recursive.
A table already copied on the walk is reused, which keeps shared structure
shared and makes a cycle terminate.
The docstring justified the recursive copy with the two schemas that need it
today.
That is the weaker claim: the vocabulary allows `type: array` and
`type: object`, and the validator returns a declared default untouched once it
matches its type, so any option with a collection type and a literal default
resolves to a table.
The docstring now says that instead.
Both nested defaults in the previous check sit one level below the top
container and hold scalars, so they pass against a copy that recurses exactly
one level further and stops.

The new check nests a default three levels down, as an array of objects whose
properties are objects, and writes through the returned table at each level.
Nothing in the fleet declares that shape today, so it covers the format rather
than a schema in play, which is the reasoning that set the depth.
@mcanouil mcanouil self-assigned this Sep 6, 2026
@mcanouil mcanouil added the Type: Enhancement 💡 Issues related to enhancements, improvements, new features, or new analyses label Sep 6, 2026
@mcanouil
mcanouil marked this pull request as ready for review September 6, 2026 15:32
@mcanouil
mcanouil merged commit 562ced5 into main Sep 6, 2026
2 checks passed
@mcanouil
mcanouil deleted the feat/add-the-schema-check-module branch September 6, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Enhancement 💡 Issues related to enhancements, improvements, new features, or new analyses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant