Class: Axn::Validation::Fields
- Defined in:
- lib/axn/core/validation/fields.rb
Overview
THE one-off validator collector, for every declared config at every level: a top-level field validates against the context facade (which resolves model records and reads by wire key), a subfield against its canonically-resolved parent value. One (field, validations) pair per one-off class; raising/settling is the caller's concern (see Executor#_validate_inbound!).
Defined Under Namespace
Classes: GateFlipValidator
Constant Summary
Constants inherited from Base
Base::ModelValidator, Base::NonEmptinessValidator, Base::OfValidator, Base::ShapeValidator, Base::TypeValidator, Base::ValidateValidator
Class Method Summary collapse
-
.collect_errors(field:, validations:, source:, action: nil, reader: nil, config: nil) ⇒ Object
Returns the ActiveModel::Errors for one (field, validations) pair against a source (empty if valid).
-
.declaration_gate_open?(validations:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) ⇒ Boolean
Whether the DECLARATION-level shared gate alone is OPEN — the special case of validator_gate_open? with no per-validator entry in view (nested gates empty).
-
.errors_for(validator_class, source:, validations:, action: nil, reader: nil, config: nil, permit_method_call: false, shape_ancestry: nil) ⇒ Object
Runs a validator class against a source and returns its ActiveModel::Errors (empty if valid).
-
.validator_class_for(field:, validations:) ⇒ Object
Builds the one-off validator class for a (field, validations) pair.
-
.validator_gate_open?(validations:, entry_options:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) ⇒ Boolean
Whether the gate for ONE validator ENTRY is OPEN for this call — i.e.
Instance Method Summary collapse
-
#initialize(source) ⇒ Fields
constructor
A new instance of Fields.
- #read_attribute_for_validation(attr) ⇒ Object
Methods inherited from Base
acceptance_admits_nil?, declared_length_checks, declared_length_floor, effective_entry_options, emittable_length_floor?, entry_context_scoped?, entry_effective_gate_keys, entry_effective_option, entry_self_gated?, format_admits_nil?, length_admits_nil?, #method_missing, nil_accepted?, nil_tolerant_validation?, normalize_validator_options, #respond_to_missing?, set_includes_nil?, shared_validation_option_keys, type_admits_nil?, type_klass_admits_nil?, validator_entries, validator_entry_options
Constructor Details
#initialize(source) ⇒ Fields
Returns a new instance of Fields.
13 14 15 16 |
# File 'lib/axn/core/validation/fields.rb', line 13 def initialize(source) super() @source = source end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method in the class Axn::Validation::Base
Class Method Details
.collect_errors(field:, validations:, source:, action: nil, reader: nil, config: nil) ⇒ Object
Returns the ActiveModel::Errors for one (field, validations) pair against a source (empty if valid). This is THE facade call site (top-level inbound + outbound): its source is the framework's context/result facade, whose generated reader is safe, so it permits method dispatch unconditionally. (A subfield reaches read_attribute_for_validation via the reader/ resolve_value branches, never the dispatch-gated else, so the flag is a no-op for it here.)
52 53 54 |
# File 'lib/axn/core/validation/fields.rb', line 52 def self.collect_errors(field:, validations:, source:, action: nil, reader: nil, config: nil) errors_for(validator_class_for(field:, validations:), source:, validations:, action:, reader:, config:, permit_method_call: true) end |
.declaration_gate_open?(validations:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) ⇒ Boolean
Whether the DECLARATION-level shared gate alone is OPEN — the special case of validator_gate_open? with no per-validator entry in view (nested gates empty). Asks "would ANY validator on this declaration run this pass", the right question when there is no single entry to isolate (e.g. the drift-proof matrix spec, which pins the shared-tier decision directly).
146 147 148 |
# File 'lib/axn/core/validation/fields.rb', line 146 def self.declaration_gate_open?(validations:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) validator_gate_open?(validations:, entry_options: nil, action:, source:, reader:, config:, permit_method_call:) end |
.errors_for(validator_class, source:, validations:, action: nil, reader: nil, config: nil, permit_method_call: false, shape_ancestry: nil) ⇒ Object
Runs a validator class against a source and returns its ActiveModel::Errors (empty if valid).
permit_method_call: governs the dispatch gate in the else branch of
read_attribute_for_validation: the facade call site (collect_errors) passes true; a shape
member passes its own method_call: opt-in (PRO-2907). It is deliberately independent of
action: so the two can be threaded separately.
shape_ancestry: carries a nested shape walk's position — the value/shape pairs open on the current
path and how deep it has descended — across this boundary. A nested shape: recurses through
ActiveModel rather than by calling itself, so a fresh validator is built per level and the walk state
has nowhere else to live; ShapeValidator reads it back off the record it is handed, the same way it
reads the threaded action.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/axn/core/validation/fields.rb', line 83 def self.errors_for(validator_class, source:, validations:, action: nil, reader: nil, config: nil, permit_method_call: false, shape_ancestry: nil) validator = validator_class.new(source) # Set the action context for model field resolution + symbol-argument delegation validator.instance_variable_set(:@action, action) validator.instance_variable_set(:@validations, validations) validator.instance_variable_set(:@reader, reader) validator.instance_variable_set(:@config, config) validator.instance_variable_set(:@permit_method_call, permit_method_call) validator.instance_variable_set(:@shape_ancestry, shape_ancestry) validator.valid? validator.errors end |
.validator_class_for(field:, validations:) ⇒ Object
Builds the one-off validator class for a (field, validations) pair. Callers that validate the same contract repeatedly (e.g. ShapeValidator over array elements) can build this once and reuse it across sources via .errors_for, avoiding per-call class compilation.
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/axn/core/validation/fields.rb', line 59 def self.validator_class_for(field:, validations:) Class.new(self) do def self.name = "Axn::Validation::Fields::OneOff" # A field may legitimately carry no validators at all (e.g. `optional: true` with no # type/model, or an `optional:` field carrying only ActiveModel shared options like # `strict:`), which `validates` rejects with "You need to supply at least one validation" — # an empty set means nothing to enforce. Shared options (gates, strict:, …) aren't # validators, so they don't count toward the set. validates field, **validations unless Axn::Validation::Base.validator_entries(validations).empty? end end |
.validator_gate_open?(validations:, entry_options:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) ⇒ Boolean
Whether the gate for ONE validator ENTRY is OPEN for this call — i.e. whether ActiveModel
would run that entry's validator this pass. Two gate tiers apply, and AM merges them inside
validates: the declaration-level SHARED if:/unless: (validations's own gate keys) and the
ENTRY's OWN nested if:/unless: (the if: inside e.g. a model: { ..., if: } or
presence: { if: } hash). Decided by ActiveModel ITSELF, never by a hand-rolled mirror of its
merge/evaluation: we build a probe validator (subclassing this class, so it inherits the exact
method_missing delegation to the action the real validators use) carrying a single custom
validator whose NESTED options are the entry's own gates and whose SHARED options are the
declaration-level gates — the identical validates :attr, <gated validator>, **shared shape
the real declaration takes. AM applies its real defaults.merge(per_validator_options)
precedence (measured against activemodel 7.2.2.2: the entry's OWN if:/unless: OVERRIDES the
shared one PER KEY; distinct keys AND together) and the identical ActiveSupport callback
machinery for the probe and the real validators, so the arity/String/callable resolution AND
the tier precedence are AM's, not ours — the gate decision cannot drift. This is THE single
gate oracle for the two axn-side checks that live OUTSIDE ActiveModel and must waive themselves
exactly when AM would waive the real validator: the executor's model-consistency pass and
ShapeValidator's unreadable-member pre-check.
Zero-cost short-circuit: no gate key on EITHER tier → true, building nothing. Declaration-level
blank gates are canonicalized away at declaration, so a present shared key is always a real
gate. A blank NESTED gate needs no special handling — it is passed to the probe verbatim and
AM's own check_conditionals treats it as no gate (and, per the measured merge, a blank nested
same-key value still overrides/drops the shared gate before being ignored) — the probe reflects
that automatically. A non-Hash entry value (presence: true) carries no nested gates. A
condition that raises propagates exactly as during the real valid?. The receiver context
(@action/@reader/@config/@permit_method_call) is threaded onto the probe so a Symbol/Proc
condition resolves against the same self and action delegation the real validators see.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/axn/core/validation/fields.rb', line 126 def self.validator_gate_open?(validations:, entry_options:, action: nil, source: nil, reader: nil, config: nil, permit_method_call: false) gate_keys = Axn::Internal::FieldConfig::CONDITIONAL_GATE_KEYS shared_gates = validations.slice(*gate_keys) nested_gates = .is_a?(Hash) ? .slice(*gate_keys) : {} return true if shared_gates.empty? && nested_gates.empty? probe = gate_probe_class_for(shared: shared_gates, nested: nested_gates).new(source) probe.instance_variable_set(:@action, action) probe.instance_variable_set(:@validations, validations) probe.instance_variable_set(:@reader, reader) probe.instance_variable_set(:@config, config) probe.instance_variable_set(:@permit_method_call, permit_method_call) probe.valid? probe.gate_open? end |
Instance Method Details
#read_attribute_for_validation(attr) ⇒ Object
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/axn/core/validation/fields.rb', line 18 def read_attribute_for_validation(attr) # A subfield reads through the action's generated reader when one exists — the reader IS the # field's value (memoized, model-resolving, value-level-default-applying, PRO-2889), so # validation sees exactly what user code sees. The reader's memo can't be stale here: the # executor clears every subfield reader memo at the inbound-pipeline boundary # (_clear_pre_pipeline_memos!), so a value cached by an early pre-settlement read is discarded # and this read resolves against the settled wire values. A dotted-name subfield has no reader # and resolves through the same shared helper. Top-level fields keep reading their source facade. if @action && @reader && @action.respond_to?(@reader) @action.public_send(@reader) elsif @action && @config&.subfield? Axn::Core::ContractForSubfields.resolve_value(@action, @config) else # Two occupants reach here. A top-level/outbound FACADE read: its source is the framework's # own context/result facade, whose per-field reader is a safe generated accessor — method # dispatch is always permitted (it's not the caller-object dispatch the method_call gate # targets), so the facade call site (collect_errors) passes `permit_method_call: true`. A # SHAPE MEMBER read (ShapeValidator): its source is a caller-supplied element, so it honors # the member's own `method_call:` opt-in — the same gate as a subfield (PRO-2907). The # permission is carried explicitly by each call site (NOT inferred from @action presence): # threading the action into shape-member validation — e.g. to resolve a Symbol validation # arg or if:/unless: condition against the action — must not silently re-permit dispatch. # Malformed sources still read as absent (one doctrine — see FieldResolvers.extract_or_nil): # this field's own validators report against nil while the source's own type validation # classifies the bad value. Axn::Core::FieldResolvers.extract_or_nil(field: attr, provided_data: @source, permit_method_call: @permit_method_call) end end |