Module: Ruact::ComponentContract
- Defined in:
- lib/ruact/component_contract.rb
Overview
Story 13.5 (FR100) — compile-time component contract (HEEx-style attr/
slot). Validates a <Component .../> ERB call site against the
component's OPT-IN contract at preprocess time, so a missing required prop,
a typo'd prop name, or a missing required slot surfaces an error at the call
site (file:line) BEFORE the page renders — not as a silent undefined in
the browser.
This is the CONSUMER-side mirror of ruact_props (which validates the
PRODUCER side at class-load, serializable.rb). It is NAME-level only:
prop VALUES are arbitrary render-time Ruby expressions the preprocessor
never evaluates, so only names + presence + slots are checkable here (value
typing for the server boundary already landed in Story 13.4).
The contract Hash is the one the Vite plugin extracted into the manifest:
{
"props" => { "postId" => "required", "initialCount" => "optional" },
"slots" => { "header" => "optional" }, # optional
"passthrough" => false # optional
}
Pure + stateless (explicit class methods) so it is trivially testable in isolation and carries no global state.
Class Method Summary collapse
-
.validate(component_name:, prop_names:, contract:, at: {}) ⇒ void
Validate a call site's prop NAMES against
contract.
Class Method Details
.validate(component_name:, prop_names:, contract:, at: {}) ⇒ void
This method returns an undefined value.
Validate a call site's prop NAMES against contract. Raises
Ruact::ComponentContractError (a PreprocessorError) on the
first violation. A nil contract is a no-op (fail open / opt-in).
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/ruact/component_contract.rb', line 41 def self.validate(component_name:, prop_names:, contract:, at: {}) return if contract.nil? props = normalize(contract["props"]) slots = normalize(contract["slots"]) given = Array(prop_names).map(&:to_s) where = { component: component_name, file: at[:file], line: at[:line], snippet: at[:snippet] } unknown = contract["passthrough"] == true ? [] : (given - (props.keys + slots.keys)) # Priority order so the highest-signal message wins. A near-miss typo is # reported FIRST even when it leaves a required prop "missing" — e.g. # `<LikeButton postID={1} />` (postID typo of the required postId) should # say "unknown prop \"postID\" — did you mean \"postId\"?", not the less # helpful "missing required prop \"postId\"" (FR100's canonical typo case). check_typo(unknown, props.keys + slots.keys, where: where) check_missing_required(props, given, kind: "prop", where: where) check_missing_required(slots, given, kind: "slot", where: where) check_unknown(unknown.first, where: where) if unknown.any? end |