Skip to content

Add format: presets, which export the JSON Schema format keyword - #20

Open
VSN2015 wants to merge 1 commit into
masterfrom
feature/format-presets
Open

Add format: presets, which export the JSON Schema format keyword#20
VSN2015 wants to merge 1 commit into
masterfrom
feature/format-presets

Conversation

@VSN2015

@VSN2015 VSN2015 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The regexps every app writes by hand, named once — mirroring how normalize: already works.

required :email,   :string, format: :email
required :id,      :string, format: :uuid
optional :website, :string, format: :url
optional :slug,    :string, format: :slug
optional :host,    :string, format: :hostname
Preset Matches Exported format
:email Exactly URI::MailTo::EMAIL_REGEXP email
:uuid Canonical 8-4-4-4-12, either case uuid
:url An http/https URL — a shape check, not reachability, but it does reject javascript: and other schemes uri
:slug Lowercase, digits, single hyphens between segments
:hostname A DNS hostname (label rules, no trailing dot) hostname

:email is deliberately URI::MailTo::EMAIL_REGEXP itself — the regexp Rails apps already paste into their contracts — so adopting the preset cannot change which addresses an endpoint accepts.

A preset name resolves to its Regexp at class load, so request-time matching stays a plain Regexp#match? and an authored default:/example: is checked against the resolved pattern like any other.

The real reason to prefer a preset

It carries something a hand-written Regexp cannot: the JSON Schema format keyword the wider ecosystem understands.

{ "type": "string", "format": "uuid", "minLength": 1,
  "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...$" }

pattern is still emitted next to it — in draft 2020-12 format is an annotation unless a validator opts into asserting it, so the pattern is what actually enforces.

One narrow exporter change worth reviewing

A preset's pattern is authored by this gem, not by the app, so it skips the deliberately over-eager UNTRANSLATABLE scan (vouched: true).

It has to. The RFC-derived :email pattern contains *+ inside a character class:

\A[a-zA-Z0-9.!\#$%&'*+/=?^_`{|}~-]+@...
                        ^^

which the scan reads as a possessive quantifier — so the single most common format in Rails would otherwise have published x-permittable-pattern and no real pattern at all. App-authored regexps keep the conservative treatment completely unchanged: the point of that heuristic is that a wrong pattern in published docs is worse than a missing one, and the gem can vouch for its own patterns without loosening it for anyone else's.

A latent trap closed

format: that is neither a Regexp nor a preset name now fails at class load. Previously a String was silently accepted and behaved as String#match? — an unanchored substring test — which is not what anyone writing format: "..." meant.

Also

  • The matcher speaks both spellings: matching(:email) asserts the preset by name, matching(/re/) the Regexp, and a mismatch says which of the two the contract declares.

Verification

  • 214 examples, 0 failures (15 new across the contract, exporter and matcher specs, written before the implementation), including the rejection sets for each preset
  • RuboCop clean

@VSN2015 VSN2015 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR #20: Named format: presets eliminate regex boilerplate while enabling native JSON Schema format keywords in OpenAPI output.

Comment thread lib/permittable.rb
# extension. :url and :hostname are shape checks, not reachability
# guarantees.
FORMATS = {
email: { pattern: URI::MailTo::EMAIL_REGEXP, json: "email" }.freeze,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using URI::MailTo::EMAIL_REGEXP under the hood for :email maintains exact parity with the regexp Rails developers commonly use, avoiding unexpected validation divergences.

# which UNTRANSLATABLE reads — deliberately over-eagerly — as a
# possessive quantifier, and the most common format in Rails would
# otherwise publish no pattern at all.
def apply_pattern!(schema, regexp, vouched: false)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vouched: true parameter thoughtfully permits gem-authored presets to bypass the overly conservative regex translation checks, ensuring RFC email patterns emit valid JSON Schema patterns.

The regexps every app writes by hand, named once — :email, :uuid,
:url, :slug, :hostname — mirroring how normalize: already works.

:email is deliberately URI::MailTo::EMAIL_REGEXP itself, the regexp
Rails apps already paste into their contracts, so adopting the preset
cannot change which addresses an endpoint accepts. The rest avoid flags
and Ruby-only constructs so they translate to ECMA-262 rather than
exporting as an extension.

A preset name resolves to its Regexp at class load, so request-time
matching stays a plain Regexp#match? and an authored default:/example:
is checked against the resolved pattern like any other. The preset name
is kept on the frozen field so exporters and the matcher can speak in
presets.

Two mistakes now fail at class load: an unknown preset (listing the
presets) and a format: that is neither a Regexp nor a preset name. That
second one was a latent trap — a String was silently accepted and
behaved as String#match?, i.e. an unanchored substring test, which is
not what anyone writing `format: "..."` meant.

The reason to prefer a preset over a hand-written Regexp is what it
carries into the export: the JSON Schema `format` keyword the wider
ecosystem understands, next to the `pattern` that still does the
asserting (in draft 2020-12 `format` is an annotation unless a
validator opts in).

That required one narrow change to the exporter. A preset's pattern is
authored by this gem rather than by the app, so it skips the
deliberately over-eager untranslatable-construct scan. It has to: the
RFC-derived :email pattern contains `*+` inside a character class,
which the scan reads as a possessive quantifier, so the single most
common format in Rails would otherwise have published no pattern at
all. App-authored regexps keep the conservative treatment unchanged —
the point of that heuristic is that a wrong pattern in published docs
is worse than a missing one, and this gem can vouch for its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VSN2015
VSN2015 force-pushed the feature/format-presets branch from bbf31cc to 84c8577 Compare September 11, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant