Class: Phlex::Reactive::ParamSchema

Inherits:
Object
  • Object
show all
Defined in:
lib/phlex/reactive/param_schema.rb

Overview

A COMPILED param schema (issue #109). The coerce family used to live inline in ActionsController; here it's a standalone unit built ONCE at declaration (Component.action -> ParamSchema.compile) so:

* an unknown type symbol raises Phlex::Reactive::UnknownParamType at class
load instead of silently `to_s`-ing at request time, and
* the drop-don't-fabricate coercion is unit-testable without a booted
request spec.

The behavior is byte-for-byte what the controller did: the built-ins keep their exact semantics ("abc".to_i => 0, not DROP; :file duck-type DROP; the array/hash DROP rules), the bracket-expansion/deep-merge matrix (#16/#21/ #24/#39) is UNCHANGED, and the verbose_errors dropped-param collector (#82/ #87) threads through unchanged — a nil collector is the zero-cost prod path.

A type is one of:

* a scalar symbol           (:string/:integer/:float/:boolean/:file/:date/
                           :datetime/:decimal, or an app-registered type)
* a Hash schema             ({ id: :integer, ... })   — nested object
* a one-element Array       ([:integer] / [{ ... }])  — array of that

Constant Summary collapse

DROP =

Sentinel: a declared key whose value can't be coerced to its type is DROPPED (not assigned), so the method's keyword default applies — exactly as if the client had omitted the key. Distinct from a coerced nil/[]. A custom param_type callable returns this to reject a value while keeping the drop-don't-fabricate contract, so it is PUBLIC.

Object.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema) ⇒ ParamSchema

Returns a new instance of ParamSchema.



145
146
147
# File 'lib/phlex/reactive/param_schema.rb', line 145

def initialize(schema)
  @schema = schema
end

Instance Attribute Details

#schemaObject (readonly)

Returns the value of attribute schema.



143
144
145
# File 'lib/phlex/reactive/param_schema.rb', line 143

def schema
  @schema
end

Class Method Details

.built_in_typesObject

The frozen-semantics built-in registry, rebuilt on demand (the module dups it so app registration doesn't mutate this template). Each entry is a callable(value) -> coerced | DROP. The scalar casts here are the EXACT ones the controller ran, so the existing request-spec matrix is unchanged. rubocop:disable Style/ItBlockParameter, Style/SymbolProc Explicit (value) params, NOT it or &:to_s: the date/datetime/decimal casters wrap an INNER parse_or_drop { ... } block, and it there would bind to that inner block's (absent) param, not the caster's value — the #109 autocorrect trap. Keep the WHOLE family explicit so it reads uniformly and no caster can be collapsed to a zero-arity lambda.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/phlex/reactive/param_schema.rb', line 63

def built_in_types
  {
    string: ->(value) { value.to_s },
    integer: ->(value) { value.to_i },
    float: ->(value) { value.to_f },
    boolean: ->(value) { BOOLEAN_TYPE.cast(value) },
    # :file and the composite types (Hash/Array) are handled structurally
    # in #coerce, not via a scalar callable — the registry entry exists so
    # compile-time validation recognizes the symbol. A nil callable means
    # "handled structurally"; #coerce never dispatches it here.
    file: nil,
    date: ->(value) { parse_or_drop { Date.iso8601(value.to_s) } },
    datetime: ->(value) { parse_or_drop { DateTime.iso8601(value.to_s) } },
    decimal: ->(value) { parse_or_drop { BigDecimal(value.to_s) } }
  }
end

.compile(schema) ⇒ Object

Validate schema recursively against the param-type registry and return a compiled ParamSchema. Raises UnknownParamType at the FIRST unknown type symbol (naming the full bracketed path), so a typo fails loudly at declaration. A blank schema compiles to an empty schema (the action drops everything the client posts).



46
47
48
49
50
# File 'lib/phlex/reactive/param_schema.rb', line 46

def compile(schema)
  schema = {} if schema.nil?
  validate!(schema, nil)
  new(schema)
end

Instance Method Details

#coerce(params, dropped = nil) ⇒ Object

Coerce client params against this schema. Anything not in the schema is dropped — no raw mass assignment reaches the component. The top-level params (an ActionController::Parameters or a plain Hash) coerce against the hash schema (same recursion as nested hashes).

dropped is the verbose_errors diagnostics collector: an Array that accumulates [bracketed_path, :undeclared|:uncoercible] entries. Pass nil (the production path) and every diagnostic branch early-returns — zero extra work.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/phlex/reactive/param_schema.rb', line 158

def coerce(params, dropped = nil)
  # A schema-less action drops EVERYTHING the client posted. Under a
  # verbose collector, record every posted key as :undeclared (forgetting
  # `params:` entirely is the loudest silent drop — #87); otherwise it's a
  # cost-free {} return on the production path.
  if @schema.empty?
    collect_undeclared(dropped, to_param_hash(params), {}, nil) if dropped
    return {}
  end

  # The TOP-LEVEL params container is the request's `params[:params]` — a
  # coerced-away malformed top level (a stray non-hash) is normalized back
  # to {} here, never DROP: the whole point of the top level is to hold the
  # coerced kwargs, so a bad container yields "no params", not a dropped
  # action argument. Nested malformed hashes DO drop (see coerce_hash).
  coerced = coerce_hash(params, @schema, dropped, nil)
  coerced.equal?(DROP) ? {} : coerced
end