Module: Axn::Core::Contract
- Defined in:
- lib/axn/core/contract.rb,
lib/axn/core/contract/redaction.rb,
lib/axn/core/contract/shape_declaration.rb,
lib/axn/core/contract/subfield_contradictions.rb
Defined Under Namespace
Modules: ClassMethods, FieldOptionality, InstanceMethods, Redaction, ShapeDeclaration, SubfieldContradictions Classes: FieldConfig, ShapeBuilder, ShapeConfig
Constant Summary collapse
- GENERATED_READER_SOURCE_PATH =
Every top-level reader and boolean predicate alias is defined in this file, so reflection can verify a Symbol condition still resolves to the framework-generated reader — an alias shares its source_location with the aliased definition, so a user method of the same name (a pre-existing predicate target that suppressed generation, or a plain reader redefined after
expects) reports a different source and is rejected (declarative-emission would otherwise condition on the wire value while runtime evaluates the user method — the looser direction). __FILE__- RESERVED_EXECUTION_CONTEXT_KEYS =
Keys the framework owns in the execution/exception-report context, so they can't be set via set_execution_context or the additional_execution_context hook: :inputs/:outputs are the structural pair, and :async/:ambient_context/:axn_stack/:tags/:dimensions are framework-populated in execution_context / Internal::ExceptionContext.build — reserving them here prevents a user value from being silently overwritten when they're assigned after merging the user's extra keys. :tags/:dimensions carry the resolved
tag/dimensionfacets (PRO-2853). %i[inputs outputs async ambient_context axn_stack tags dimensions].freeze
Class Method Summary collapse
-
.canonical_name!(value, option:, names:, fix:, encoding_fix:) ⇒ Object
Canonicalize a declared name to the Symbol every consumer downstream reads, holding both rules first.
- .included(base) ⇒ Object
-
.validate_name_encoding!(value, kind:, fix:) ⇒ Object
A name of the right TYPE can still be written in bytes no declaration can work with.
-
.validate_name_option!(value, option:, names:, fix:) ⇒ Object
An option whose value NAMES something (
Axn::Factory.build'sexpose_return_as:, a subfield'son:) is canonicalized to a Symbol the moment it is read, so the only values it can carry are the two that have a Symbol as their canonical spelling: a String and a Symbol. -
.validate_sensitive!(sensitive) ⇒ Object
The grammar of a
sensitive:value:true/false, a Symbol (an action method name), or a Proc — plusnil, which meansfalse(sosensitive: some_flagreads naturally when the flag is unset). -
.validate_shape_member_name!(name) ⇒ Object
A shape member's name has to serve as TWO things: the JSON property it renders as (via
to_s, which the declaration guard canonicalizes) and the schema property key (viato_sym, which Internal::Reflection::Schema#member_properties emits). -
.validate_user_facing!(user_facing) ⇒ Object
The grammar of a
user_facing:value:true/false, a String, a Symbol (an action method name), or a callable (Proc) — the fullerror/fail!/fails_onhandler shape.
Class Method Details
.canonical_name!(value, option:, names:, fix:, encoding_fix:) ⇒ Object
Canonicalize a declared name to the Symbol every consumer downstream reads, holding both rules first. THE
single place a name becomes a Symbol, so the rules and the conversion cannot drift apart — a site that
canonicalized on its own is how as:, prefix: and the field names each ended up with a different answer
for the same bad value.
Scope, deliberately: these rules serve a name a developer actually WROTE — a Symbol, a String, or the
nil/[]/123 that a variable holding the wrong thing produces. They do not try to survive a String
subclass whose to_sym lies (answering with a wide Symbol, a non-Symbol, or by raising). Verifying that a
caller's object BEHAVES is unbounded — every round of verification is defeated by the next case — and the
honest boundary is that such a class is not a contract axn can be asked to hold. What IS guaranteed is that
nothing here consults the value's own is_a?, inspect or encoding to reach a verdict, so an ordinary
mistake is always diagnosed as one.
254 255 256 257 258 259 |
# File 'lib/axn/core/contract.rb', line 254 def self.canonical_name!(value, option:, names:, fix:, encoding_fix:) validate_name_option!(value, option:, names:, fix:) validate_name_encoding!(value, kind: option, fix: encoding_fix) value.to_sym end |
.included(base) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/axn/core/contract.rb', line 22 def self.included(base) base.class_eval do # Copy-on-write stores, frozen at every assignment: declaration replaces the array (`+`, # never `<<` — which would now raise FrozenError rather than silently mutating the # superclass's contract), so the per-class resolved-subfield cache can key on array # identity and concurrent readers always see an immutable snapshot. class_attribute :internal_field_configs, :external_field_configs, default: [].freeze extend ClassMethods include InstanceMethods end end |
.validate_name_encoding!(value, kind:, fix:) ⇒ Object
A name of the right TYPE can still be written in bytes no declaration can work with. Runs immediately
after the type rule above, at every site that takes a name, and BEFORE anything compares the name to
anything: the questions a declaration asks — is this path dotted, is this reader reserved — are asked
against axn's own ASCII patterns, and a wide encoding (UTF-16, UTF-32) makes the comparison itself
raise Encoding::CompatibilityError instead of answering. That was the whole diagnosis such a name got:
an encoding error from "a.b".include?("."), naming neither the option nor what was wrong with it.
Rejected rather than accommodated, because there is no working declaration behind it. The name interns
to a Symbol DISTINCT from its UTF-8 twin ("ab".encode("UTF-16LE").to_sym != :ab) while canonicalizing
to the same JSON property, so the schema advertises "ab" and no caller can satisfy it: supplying that
property, its Symbol, or the wide Symbol itself each raise from the read path. An ASCII-COMPATIBLE
non-UTF-8 name (Latin-N) is a different case entirely and stays legal — it compares, it reads, and it
renders as the property it canonicalizes to.
The encoding is read from bound base implementations (NativeMethods.ascii_compatible_name?) for the same
reason the rest of this layer does — a dispatch inside a verdict is a dispatch the verdict did not need —
and the encoding is named in the message from Encoding's own object rather than from anything the caller
supplied.
232 233 234 235 236 237 238 239 240 |
# File 'lib/axn/core/contract.rb', line 232 def self.validate_name_encoding!(value, kind:, fix:) return if Axn::Internal::NativeMethods.ascii_compatible_name?(value) raise ArgumentError, "#{kind} must be written in an ASCII-compatible encoding (got one encoded as " \ "#{Axn::Internal::NativeMethods.name_encoding(value).name}) — a name in a wide encoding interns to a " \ "different Symbol than the UTF-8 property it renders as, so nothing a caller sends can match it, and " \ "every check the declaration makes against it raises rather than answering. #{fix}" end |
.validate_name_option!(value, option:, names:, fix:) ⇒ Object
An option whose value NAMES something (Axn::Factory.build's expose_return_as:, a subfield's on:)
is canonicalized to a Symbol the moment it is read, so the only values it can carry are the two that
have a Symbol as their canonical spelling: a String and a Symbol. Anything else used to be left to the
caller's own to_sym, and was therefore diagnosed as whatever that happened to raise —
NoMethodError: undefined method 'to_sym' for an instance of Array, which names neither the option nor
what was wrong with the value — the same non-diagnosis as a mistyped option key surfacing at call time
as Unknown validator: 'TpyeValidator', and rejected for the same reason.
Worse, an exotic object that merely ANSWERS to_sym was not diagnosed at all: it silently
named whatever its to_sym invented, independently of what the object renders as — the
two-independent-conversions defect validate_shape_member_name! rejects for a member name.
Runs AFTER the absent check at each call site, so every spelling of "not supplied"
(nil, false, an empty or whitespace-only String, the empty Symbol) still means the option was
omitted rather than hitting a type error — see Internal::NativeMethods.absent_value?.
case/when decides the type through Module#=== (a C-level check) rather than is_a?, which a
value can override to route around a guard, and the offender is named by CLASS through the renderer
rather than by its own inspect, exactly as the sensitive:/user_facing: guards above — a
declaration guard must not let the value it is judging raise INSTEAD of the verdict.
203 204 205 206 207 208 209 210 211 212 |
# File 'lib/axn/core/contract.rb', line 203 def self.validate_name_option!(value, option:, names:, fix:) case value when ::String, ::Symbol then return end raise ArgumentError, "#{option} must be a String or Symbol naming #{names} (got a value of class " \ "#{Axn::Internal::Reflection::PropertyNames.renderable_class_name(value)}) — any other object has no single " \ "name to canonicalize to. #{fix}" end |
.validate_sensitive!(sensitive) ⇒ Object
The grammar of a sensitive: value: true/false, a Symbol (an action method name), or a Proc — plus
nil, which means false (so sensitive: some_flag reads naturally when the flag is unset). Anything
else is rejected at declaration, because the runtime failure mode is a LEAK rather than an error: only
those values are resolution rules, so sensitive: "yes" or sensitive: 1 left the field out of the
name-based redaction set entirely and logged the secret in the clear, with no signal to the author.
Closing the value space is also what makes one predicate enough to decide whether redaction needs an
action instance: over this grammar, "carries no Proc or Symbol" and "resolves to the same answer with
and without an instance" are the same question (see _has_dynamic_sensitive_fields?).
case/when consults the real class through Module#=== (a C-level check) and compares the literals
by identity, so nothing a caller-supplied value defines gets to answer whether it is a valid rule —
and the offender is named by CLASS rather than by inspect, which would be running its code while its
own error is being built. A Proc is the callable form the resolver actually implements
(instance_exec(&sensitive)); a non-Proc callable would be truthiness-tested, i.e. always sensitive.
Enforced in FieldConfig's constructor, which every field, subfield and ambient subfield is built by, and
in the DECLARATION WALK for every shape member, whatever its class — the walk reads the value once, on its
way into the snapshot, so the check and the stored value are the same read
(_snapshot_member_attributes!). ShapeConfig's constructor checks it too, for the block form.
141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/axn/core/contract.rb', line 141 def self.validate_sensitive!(sensitive) case sensitive when true, false, nil, ::Symbol, ::Proc then return end raise ArgumentError, "sensitive: must be true, false, a Symbol naming an action method, or a Proc (got a value of " \ "class #{Axn::Internal::Reflection::PropertyNames.renderable_class_name(sensitive)}) — any other value is not a redaction rule, and " \ "a truthy one would silently leave the value logged in the clear rather than raise. Use " \ "`sensitive: true` to always redact, or a Symbol/Proc predicate to decide per call." end |
.validate_shape_member_name!(name) ⇒ Object
A shape member's name has to serve as TWO things: the JSON property it renders as (via to_s, which
the declaration guard canonicalizes) and the schema property key (via to_sym, which
Internal::Reflection::Schema#member_properties emits). A String and a Symbol are the only types for which those
conversions are each other's inverse, so they are the only names that mean one property. Any other
object defines the two independently, and one whose to_s and to_sym disagree makes the guard and
the schema compare different property names for the same member: the guard sees no collision while
the schema keys one property for two members and silently discards the first.
Rejected rather than reconciled, because there is nothing to reconcile — an object whose two
renderings disagree has no single property name to be. Named by class rather than by inspect, which
is the offender's own code running while its error is built.
Enforced in the DECLARATION WALK, unconditionally, for every member the class will store — the one point a
member of any class passes through, and the point before the name is converted. Every STORED member is a
ShapeConfig, whose constructor holds the same rule and normalizes a String to its Symbol, so a stored
name is always the Symbol the schema keys by; that conversion is what keeps parallel to_s/to_sym
readings of an unnormalized name from being two questions that can disagree.
170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/axn/core/contract.rb', line 170 def self.validate_shape_member_name!(name) case name when ::String, ::Symbol then return end raise ArgumentError, "a shape member name must be a String or a Symbol (got a name of class " \ "#{Axn::Internal::Reflection::PropertyNames.renderable_class_name(name)}) — a member name is both the JSON " \ "property it renders as " \ "and the schema property key it is emitted under, and any other object converts to those two " \ "independently. Declare the member under a String or Symbol name." end |
.validate_user_facing!(user_facing) ⇒ Object
The grammar of a user_facing: value: true/false, a String, a Symbol (an action method
name), or a callable (Proc) — the full error/fail!/fails_on handler shape. Anything else
is a programmer error, rejected at declaration. Single-sourced here so the expects/exposes field-level
check, FieldConfig's and ShapeConfig's own construction, the declaration walk that reads every member's
value on its way into the snapshot (_snapshot_member_attributes!), and ShapeValidator's read of a
member axn never snapshotted hold members and fields to one grammar — a member built via the block form,
via a raw shape: kwarg, or by the caller's own class is validated identically.
A value outside the grammar is not inert: the executor treats a truthy one as a resolution rule, so it
RECLASSIFIES the violation as user-facing (the contract bug is never reported) and then renders as the
caller's own error message — user_facing: 123 surfaced the literal "123". That is why the check lives
at every point a value can enter, not only at the DSL.
The three literal arms are decided by case/when (Module#===, a C-level check) rather than by is_a?,
and the offender is named by CLASS rather than by inspect, exactly as the sensitive: guard beside it —
a declaration guard must not let the value being judged raise INSTEAD of the verdict, and outside
StandardError that exception escapes every rescue above. It also closes a divergence: the executor picks
the String arm with case/when too (_resolve_user_facing_override), so a value whose own is_a?
claimed String passed this check and was then rendered as a literal — the failure the guard exists to
prevent.
The CALLABLE arm cannot be decided that way, and deliberately is not narrowed to when ::Proc, ::Method.
What may be declared here is what Handlers::Invoker will actually invoke, decided by the invoker's own
callable? so there is no second, divergent predicate — and that set is open-ended duck typing
(to_proc + arity). A case/when list would reject a custom callable that works today, and asking
the method table instead would ACCEPT one whose respond_to? answers false, which the invoker would then
treat as a literal value and render as the caller's error message. So the dispatch stays and is GUARDED
instead: an object that raises while being asked cannot be established as invokable, so it is refused by
axn's own error naming its class (see _invokable_user_facing?).
89 90 91 92 93 94 95 96 97 98 |
# File 'lib/axn/core/contract.rb', line 89 def self.validate_user_facing!(user_facing) case user_facing when true, false, ::String, ::Symbol then return end return if _invokable_user_facing?(user_facing) raise ArgumentError, "user_facing: must be true, a String, a Symbol, or a Proc (got a value of class " \ "#{Axn::Internal::Reflection::PropertyNames.renderable_class_name(user_facing)})" end |