Module: Axn::Core::Contract::ClassMethods
- Includes:
- Redaction, ShapeDeclaration
- Defined in:
- lib/axn/core/contract.rb
Constant Summary
Constants included from Redaction
Redaction::SENSITIVE_FILTERED_MASK
Instance Method Summary collapse
- #_declared_fields(direction) ⇒ Object
-
#expects(*fields, on: nil, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, preprocess: nil, sensitive: false, as: nil, prefix: nil, user_facing: false, method_call: false, &block) ⇒ Object
rubocop:disable Metrics/ParameterLists.
-
#exposes(*fields, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, sensitive: false, &block) ⇒ Object
rubocop:enable Metrics/ParameterLists.
Methods included from Redaction
#_build_instance_filter, #_context_slice, #_has_dynamic_sensitive_fields?, #_mask_unfilterable_shape_value, #_mask_unfilterable_shapes, #_resolve_sensitive_value, #_sensitive_ambient_shape_paths, #_sensitive_member_names, #inspection_filter, #sensitive_fields
Instance Method Details
#_declared_fields(direction) ⇒ Object
537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/axn/core/contract.rb', line 537 def _declared_fields(direction) raise ArgumentError, "Invalid direction: #{direction}" unless direction.nil? || %i[inbound outbound].include?(direction) configs = case direction when :inbound then internal_field_configs when :outbound then external_field_configs else (internal_field_configs + external_field_configs) end configs.map(&:field) end |
#expects(*fields, on: nil, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, preprocess: nil, sensitive: false, as: nil, prefix: nil, user_facing: false, method_call: false, &block) ⇒ Object
rubocop:disable Metrics/ParameterLists
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 |
# File 'lib/axn/core/contract.rb', line 370 def expects( *fields, on: nil, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, preprocess: nil, sensitive: false, as: nil, prefix: nil, user_facing: false, method_call: false, **, &block ) # Canonicalize the wire key to a symbol up front so everything downstream — config.field, # reader names, duplicate detection, the inbound read path — is symbol-keyed by construction. # `expects "note"` and `expects :note` are the same field; a dotted subfield key (`"a.b"`) # symbolizes harmlessly (it's only ever compared/split via `.to_s`). See PRO-2790. fields = _canonical_field_names!(fields, kind: "a field name", names: "an inbound field") # A subfield's ROUTE is canonicalized on the same terms, and here — before the first guard reads it. # A route is judged as written (its root must name a declared reader; `_duplicate_fields` keys a config # by it) and then split again by every consumer: `SubfieldTree`, `resolve_parent`'s recipe, the ambient # checks, the executor's parent memo. So a caller value whose rendering answered differently on # successive reads had one layer judging one route while the tree built another — two subfields # silently landing on ONE node, merged as if they were two routes to one wire slot, where the same # declaration written honestly is a duplicate. A Symbol's `to_s` is Ruby's own, so afterwards every # read is the same answer, and no message renders a route by running the route's own code. Dotted # paths symbolize harmlessly, exactly as a dotted `on:` always did: they are only ever split via `to_s`. # # Absent stays absent — `nil`, `false`, an empty or whitespace-only String and the empty Symbol all # mean "no route", which is the whole of what the `present?` this replaced answered — and that verdict # is reached WITHOUT running the route's own code, because `present?`/`blank?` are ActiveSupport # methods on Object that a String subclass overrides: one answering "blank" here and "present" to a # later reader skipped this line entirely and was stored raw, reinstating the split this # canonicalization exists to close. Absent canonicalizes to `nil` rather than being left as written, # so that every reader below is asking nil-or-Symbol — otherwise the SAME split reopens one line # down, with this deciding "absent" and a `present?` on the caller's own value deciding to route. # # A supplied route that is not a name at all is rejected here rather than left to `to_sym` (see # Contract.validate_name_option!): the option and the offending class are what the author needs, and # `NoMethodError` named neither. on = if Internal::NativeMethods.absent_value?(on) nil else # Canonicalized through the shared rule, which also holds the encoding of what `to_sym` ANSWERS — # this is the value every consumer then splits on `.`, and a wide one raised from the split. Contract.canonical_name!(on, option: "on:", names: "a parent reader", fix: "Pass the parent's name (dotted for a nested path), or omit `on:` " \ "to declare a top-level field.", encoding_fix: "Name the parent in UTF-8 (or any other ASCII-compatible " \ "encoding).") end fields.each do |field| raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPECTATIONS.include?(field.to_s) end # A field's wire key always names a single key; the nested-path capability lives entirely in a # dotted `on:` (`expects :b, on: "a"`). A dotted field NAME is therefore never valid — reject it # unconditionally, pointed at the dotted-`on:` spelling (PRO-2926). A dotted `on:` VALUE is fine. _reject_dotted_field_name!(fields, on:) _validate_user_facing!(user_facing) # `method_call:` governs how a SUBFIELD's segment is resolved from its parent (invoke vs. # read). A top-level field reads its literal wire key from the context Hash — it never # method-dispatches — so `method_call: true` without `on:` could never take effect; reject # it rather than accept a silently-inert option (matching the ambient default:/coerce: # rejections). `method_call: false` is the default, so it's a harmless no-op anywhere. if method_call && on.nil? raise ArgumentError, "`method_call: true` is only meaningful on a subfield (declared with `on:`) — a top-level field " \ "reads its wire key and never invokes a method. Add `on:` to name the parent, or drop `method_call:`." end reader_names = _resolve_reader_names(fields, as:, prefix:) _validate_reader_names!(reader_names) validations, = (fields, **) validations[:shape] = _build_shape(fields, validations:, &block) if block _snapshot_declared_shape!(validations, fields) # `on` is nil-or-Symbol by construction here (canonicalized above), so routing asks the canonical # value rather than re-deciding presence on whatever the caller passed. if on return _expects_subfields(*fields, on:, allow_blank:, allow_nil:, allow_empty:, optional:, default:, preprocess:, sensitive:, metadata:, reader_names:, user_facing:, method_call:, **validations) end _parse_field_configs(*fields, allow_blank:, allow_nil:, allow_empty:, optional:, default:, preprocess:, sensitive:, metadata:, reader_names:, user_facing:, **validations).tap do |configs| _reject_duplicate_fields!(internal_field_configs, configs) # Declaring a top-level field can RE-ANCHOR existing subfields (a new root takes precedence over # a same-named subfield reader), so the resolved check runs here too rather than only where # subfields are declared. # Every declaration check has passed; NOW mutate the class (matching _expects_subfields' # validate-before-commit ordering), so a rescued declaration error never leaves the class # carrying an orphaned config or generated reader. Copy-on-write + freeze: `<<` would # mutate the superclass's contract, and identity-keyed caching relies on replacement. self.internal_field_configs = (internal_field_configs + configs).freeze _define_field_readers!(configs) end end |
#exposes(*fields, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, sensitive: false, &block) ⇒ Object
rubocop:enable Metrics/ParameterLists
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 |
# File 'lib/axn/core/contract.rb', line 480 def exposes( *fields, allow_blank: false, allow_nil: false, allow_empty: nil, optional: false, default: nil, sensitive: false, **, &block ) # Symbolize the wire key (see `expects`) so exposes shares the same symbol-keyed contract. fields = _canonical_field_names!(fields, kind: "an exposure name", names: "an outbound field") # Stays pre-build, unlike every other declared name: an exposed field name is a property in the # SERIALIZED BODY (`Values.serialize_exposed` iterates these configs and raises on an unrenderable # one) as well as in `output_schema`, so it must be rejected whatever the schema emits. _reject_unrenderable_field_names!(fields) fields.each do |field| raise ContractViolation::ReservedAttributeError, field if RESERVED_FIELD_NAMES_FOR_EXPOSURES.include?(field.to_s) end # exposes has no `on:`/subfields, so a dotted name has no valid meaning at all (see expects). _reject_dotted_field_name!(fields, on: nil, kind: "exposes") validations, = (fields, **) validations[:shape] = _build_shape(fields, validations:, outbound: true, &block) if block # Ahead of the `user_facing:` walk below so a member carrying both an unusable name and a rejected # option is reported as the naming defect it is. That ordering only governs these two walks over # resolved members: in the block form the option error surfaces first, raised inside # `_build_shape_member` while `_build_shape` above is still assembling the members. _snapshot_declared_shape!(validations, fields) # The block form rejects a `user_facing:` member inside `_build_shape_member` (above), but a # raw `shape:` kwarg supplies pre-built member objects that never route through it — so walk # the resolved members here to close that path too (see _reject_outbound_shape_user_facing!). _reject_outbound_shape_user_facing!(validations[:shape]) _parse_field_configs(*fields, allow_blank:, allow_nil:, allow_empty:, optional:, default:, preprocess: nil, sensitive:, metadata:, **validations).tap do |configs| if configs.any? { |c| c.validations.dig(:type, :coerce) } raise ArgumentError, "coerce: is not supported on exposes (outbound fields are serialized, not coerced)." end _reject_duplicate_fields!(external_field_configs, configs) # The outbound claim space. `exposes` has no `on:`, so there are no routes to resolve — but a shape # member (and a `Data` type's own members) still name properties under an exposure, and those pairs # collapse in `output_schema` exactly as their inbound counterparts do. # Copy-on-write + freeze (see internal_field_configs above). self.external_field_configs = (external_field_configs + configs).freeze end end |