field_struct 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +58 -0
  3. data/CHANGELOG.md +653 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/README.md +645 -0
  6. data/UPGRADING.md +743 -0
  7. data/USAGE.md +820 -0
  8. data/docs/getting_started.md +337 -0
  9. data/lib/field_struct/base.rb +1316 -0
  10. data/lib/field_struct/error.rb +48 -0
  11. data/lib/field_struct/errors.rb +103 -0
  12. data/lib/field_struct/field.rb +179 -0
  13. data/lib/field_struct/metadata.rb +331 -0
  14. data/lib/field_struct/model_name.rb +48 -0
  15. data/lib/field_struct/rbs.rb +299 -0
  16. data/lib/field_struct/registry.rb +85 -0
  17. data/lib/field_struct/scaffold.rb +331 -0
  18. data/lib/field_struct/types/array.rb +156 -0
  19. data/lib/field_struct/types/base.rb +162 -0
  20. data/lib/field_struct/types/big_decimal.rb +78 -0
  21. data/lib/field_struct/types/binary.rb +38 -0
  22. data/lib/field_struct/types/boolean.rb +126 -0
  23. data/lib/field_struct/types/date.rb +158 -0
  24. data/lib/field_struct/types/datetime.rb +163 -0
  25. data/lib/field_struct/types/email.rb +49 -0
  26. data/lib/field_struct/types/float.rb +80 -0
  27. data/lib/field_struct/types/immutable_string.rb +18 -0
  28. data/lib/field_struct/types/integer.rb +133 -0
  29. data/lib/field_struct/types/nested.rb +82 -0
  30. data/lib/field_struct/types/preset_resolver.rb +37 -0
  31. data/lib/field_struct/types/rfc3339_format.rb +197 -0
  32. data/lib/field_struct/types/string.rb +73 -0
  33. data/lib/field_struct/types/symbol.rb +38 -0
  34. data/lib/field_struct/types/temporal_parser.rb +93 -0
  35. data/lib/field_struct/types/time.rb +168 -0
  36. data/lib/field_struct/types/time_format_resolver.rb +54 -0
  37. data/lib/field_struct/types/union.rb +96 -0
  38. data/lib/field_struct/types/url.rb +46 -0
  39. data/lib/field_struct/types/uuid.rb +46 -0
  40. data/lib/field_struct/types/value.rb +24 -0
  41. data/lib/field_struct/types/zone_option.rb +267 -0
  42. data/lib/field_struct/version.rb +7 -0
  43. data/lib/field_struct.rb +159 -0
  44. data/sig/field_struct.rbs +2845 -0
  45. data/skills/field-struct/SKILL.md +183 -0
  46. metadata +139 -0
@@ -0,0 +1,2845 @@
1
+ # Typed PORO foundation: declare fields with enforced types, presence checks,
2
+ # and per-field validation. Subclass {FieldStruct::Base} to get a value object
3
+ # whose attributes coerce, validate on assignment, and introspect cleanly.
4
+ module FieldStruct
5
+ FILTERED: String
6
+ VERSION: String
7
+
8
+ # The library's root directory — the parent of +lib/+. Computed from this
9
+ # file's location, so it resolves to the repo root in development and to the
10
+ # installed gem directory when packaged.
11
+ #
12
+ # FieldStruct.root.join('lib', 'field_struct.rb') # => #<Pathname …>
13
+ #
14
+ # _@return_ — absolute path to the gem root
15
+ def self.root: () -> Pathname
16
+
17
+ # Build a new {Registry}, parented to +parent+ (defaults to
18
+ # {FieldStruct.types}), and optionally configure it with a block
19
+ # evaluated in the new registry's instance scope.
20
+ #
21
+ # module Acme
22
+ # def self.field_types
23
+ # @field_types ||= FieldStruct.new_registry do
24
+ # register :money, Acme::Types::Money
25
+ # end
26
+ # end
27
+ # end
28
+ #
29
+ # Equivalent to the longhand:
30
+ #
31
+ # FieldStruct::Registry.new(FieldStruct.types).tap do |r|
32
+ # r.register :money, Acme::Types::Money
33
+ # end
34
+ #
35
+ # Pass +parent: nil+ for an unparented registry (no fallback chain).
36
+ #
37
+ # _@param_ `parent` — the parent registry; defaults to {FieldStruct.types}. Pass +nil+ explicitly for no parent.
38
+ def self.new_registry: (?Registry? parent) -> Registry
39
+
40
+ # The base type registry, seeded with every v1 scalar type and the
41
+ # +:decimal+ alias for +:big_decimal+. Namespace registries should be
42
+ # built with this as their parent (see {Registry}).
43
+ def self.types: () -> Registry
44
+
45
+ # Opt-in short alias for the +FieldStruct+ module.
46
+ #
47
+ # Defines a top-level constant (default: +FS+) that points back to
48
+ # +FieldStruct+, *and* swaps the prefix used by every +#inspect+
49
+ # method in the library so IRB output reads with the short name too:
50
+ #
51
+ # FieldStruct.use_alias!
52
+ # FS::Base # => FieldStruct::Base
53
+ # User.metadata.inspect # => "#<FS::Metadata fields=[...]>"
54
+ #
55
+ # Off by default — the user has to ask for it. Idempotent. If the
56
+ # chosen constant is already defined to something other than
57
+ # FieldStruct, raises +NameError+ rather than clobbering.
58
+ #
59
+ # _@param_ `name` — the top-level constant to define (default +:FS+)
60
+ #
61
+ # _@return_ — for chaining
62
+ def self.use_alias!: (?Symbol name) -> self
63
+
64
+ # The current prefix that library +#inspect+ methods should render.
65
+ # Defaults to +"FieldStruct"+; flipped to the alias string (e.g.
66
+ # +"FS"+) by {.use_alias!}.
67
+ def self.inspect_namespace: () -> String
68
+
69
+ # Generates RBS type signatures for the *generated* instance surface of
70
+ # user-defined {FieldStruct::Base} subclasses — the per-field accessors the
71
+ # +field+ DSL defines dynamically.
72
+ #
73
+ # Sord generates +sig/field_struct.rbs+ from the library's static source, so
74
+ # it covers {Base}, {Types}, {Registry}, … but it can never see +User#name+
75
+ # or +User#age=+: those methods don't exist until a consumer declares the
76
+ # fields. This is the deferred "track 2" of the plan's RBS strategy (D13) —
77
+ # walk a subclass's {Metadata} and emit signatures for its accessors so
78
+ # downstream Steep / Solargraph can type-check code that touches them.
79
+ #
80
+ # class User < FieldStruct::Base
81
+ # required :name, :string
82
+ # optional :age, :integer
83
+ # end
84
+ #
85
+ # puts FieldStruct::RBS.generate(User)
86
+ # # # Generated by FieldStruct::RBS. ...
87
+ # # class User < ::FieldStruct::Base
88
+ # # attr_reader name: ::String
89
+ # # def name=: (::String? value) -> ::String?
90
+ # #
91
+ # # attr_reader age: ::Integer?
92
+ # # def age=: (::Integer? value) -> ::Integer?
93
+ # # end
94
+ #
95
+ # Design choices:
96
+ #
97
+ # * **Types describe a valid instance.** The reader type is the field's
98
+ # {Types::Base#ruby_type} (or an override, below). Nullability follows
99
+ # {Field#required?}: required → +T+, optional → +T?+. An invalid
100
+ # instance — a required field left unset, or a +:keep_raw+ coercion
101
+ # failure holding the raw input — can hold other values; check +valid?+
102
+ # before trusting the signature. The generated header says so.
103
+ # * **Writer** is +(T? value) -> T?+: the runtime setter accepts +nil+ and
104
+ # returns the coerced value. Loose input the type would coerce
105
+ # (+user.age = "30"+) is valid Ruby but a type error to Steep — cast or
106
+ # coerce at the boundary.
107
+ # * **Generic arguments** are filled in: arrays as +::Array[Element]+
108
+ # (a scalar element as +Element?+, since a valid instance can hold nil
109
+ # elements; a nested element stays non-nil, since nil makes it invalid),
110
+ # unions as +(A | B)+ (a boolean member as +bool+, a member's own
111
+ # {Types::Base#rbs_type} first), nested structs by qualified name; an
112
+ # anonymous nested class raises +ArgumentError+. A custom type whose +ruby_type+ is a bare generic
113
+ # class (+::Hash+) gets +untyped+ arguments unless it declares
114
+ # {Types::Base#rbs_type}.
115
+ # * **Anything-valued fields** (+:value+, or a union with a +:value+
116
+ # member) are +untyped+ — reader and writer, never +untyped?+.
117
+ # * **Overrides:** a field's +rbs_type:+ option, then the type's
118
+ # {Types::Base#rbs_type}, replace the derived type +T+ verbatim;
119
+ # nullability is still added for optional fields and writers.
120
+ # * **+new+ / +initialize+** are inherited from {Base}, which already
121
+ # accepts +User.new(name: ...)+.
122
+ #
123
+ # Each call emits one class. Referenced types (nested structs, array
124
+ # elements) appear as qualified names (+::Address+) — generate RBS for those
125
+ # classes too so the references resolve. {.type_for} exposes the per-field
126
+ # mapping for other generators.
127
+ module RBS
128
+ BOOL_CLASSES: ::Array[Class]
129
+ HEADER: String
130
+ GENERIC_ARITY: ::Hash[Class, Integer]
131
+
132
+ # Generate RBS source for a single FieldStruct subclass.
133
+ #
134
+ # _@param_ `klass`
135
+ #
136
+ # _@return_ — RBS declaring the class (wrapped in its module nesting)
137
+ # and a typed reader and writer for every field it declares, after a
138
+ # comment header stating that the types describe a valid instance
139
+ def self.generate: (singleton(FieldStruct::Base) klass) -> String
140
+
141
+ # The RBS type of one field's value, as {.generate} emits it — for other
142
+ # generators (e.g. one typing a method that takes or returns the field)
143
+ # that need the same mapping:
144
+ #
145
+ # FieldStruct::RBS.type_for(Order.metadata[:items])
146
+ # # => "::Array[::LineItem]?"
147
+ # FieldStruct::RBS.type_for(Order.metadata[:items], nullable: false)
148
+ # # => "::Array[::LineItem]"
149
+ #
150
+ # The type comes from the field's +rbs_type:+ option, else its type's
151
+ # {Types::Base#rbs_type}, else its {Types::Base#ruby_type} with generic
152
+ # arguments filled in. Qualified names (+::LineItem+) assume the
153
+ # referenced classes get RBS too.
154
+ #
155
+ # _@param_ `field` — a declared field, e.g. +Order.metadata[:items]+
156
+ #
157
+ # _@param_ `nullable` — append +?+ (the reader type of an optional field, and every writer's); +nil+, the default, means +!field.required?+. Never applied to +untyped+, which already admits +nil+.
158
+ #
159
+ # _@return_ — an RBS type expression
160
+ def self.type_for: (Field field, ?nullable: bool?) -> String
161
+
162
+ # Build the +class ... end+ block (unindented, no surrounding modules).
163
+ #
164
+ # _@param_ `klass`
165
+ def self.class_block: (singleton(FieldStruct::Base) klass) -> String
166
+
167
+ # Fields declared on +klass+ itself — inherited fields (the same {Field}
168
+ # objects merged in at inheritance time) belong to the parent's RBS.
169
+ #
170
+ # _@param_ `klass`
171
+ def self.own_fields: (singleton(FieldStruct::Base) klass) -> ::Array[Field]
172
+
173
+ # An override, parenthesized when it is a union, intersection or proc
174
+ # type, so a trailing +?+ applies to the whole of it.
175
+ #
176
+ # _@param_ `rbs` — a field's +rbs_type:+ or a type's +rbs_type+
177
+ #
178
+ # _@return_ — +nil+ when there is no override
179
+ def self.override_expression: (String? rbs) -> String?
180
+
181
+ # _@param_ `rbs` — a stripped RBS type expression
182
+ #
183
+ # _@return_ — whether one parenthesized group spans all of +rbs+
184
+ # (optionally followed by +?+), so wrapping it again adds nothing
185
+ def self.wrapped?: (String rbs) -> bool
186
+
187
+ # _@param_ `base` — an RBS type expression
188
+ #
189
+ # _@return_ — +base+ made nullable, unless it already admits +nil+
190
+ def self.optional: (String base) -> String
191
+
192
+ # Map a type instance (plus its field options, needed for arrays) to an
193
+ # RBS type expression.
194
+ #
195
+ # _@param_ `type_instance`
196
+ #
197
+ # _@param_ `options`
198
+ def self.expression_for: (Types::Base type_instance, ::Hash[Symbol, Object?] options) -> String
199
+
200
+ # A union as its members' types, each member's own +rbs_type+ first.
201
+ #
202
+ # _@param_ `union`
203
+ #
204
+ # _@return_ — +untyped+ when any member is
205
+ def self.union_expression: (Types::Union union) -> String
206
+
207
+ # _@param_ `classes` — a {Types::Base#ruby_type}, as a list
208
+ #
209
+ # _@return_ — one RBS type per class, with +TrueClass+ /
210
+ # +FalseClass+ together collapsed to +bool+ where the pair appears
211
+ def self.classes_to_rbs: (::Array[Class] classes) -> ::Array[String]
212
+
213
+ # The element type of an array field, derived from its +:of_type+ (a
214
+ # +Types::Base+ subclass for stock types, or a built instance for
215
+ # parameterized ones like {Types::Nested}).
216
+ #
217
+ # _@param_ `of_type`
218
+ def self.element_type: ((Class | Types::Base)? of_type) -> String
219
+
220
+ # _@param_ `klass` — a Ruby class from a {Types::Base#ruby_type}
221
+ #
222
+ # _@return_ — qualified RBS name (with +untyped+ type arguments for
223
+ # a generic class), or +untyped+ for the value type's +::Object+
224
+ def self.class_to_rbs: (Class klass) -> String
225
+
226
+ # Wrap an inner block in the +module ... end+ nesting implied by a
227
+ # qualified class name, indenting two spaces per level.
228
+ #
229
+ # _@param_ `full_name` — e.g. +"Acme::Order"+
230
+ #
231
+ # _@param_ `inner` — the class block
232
+ #
233
+ # _@return_ — RBS source ending in a trailing newline
234
+ def self.wrap_namespace: (String full_name, String inner) -> String
235
+
236
+ # _@param_ `text`
237
+ #
238
+ # _@return_ — +text+ with every non-blank line indented two spaces
239
+ def self.indent: (String text) -> String
240
+ end
241
+
242
+ # Superclass for FieldStruct value classes.
243
+ #
244
+ # class User < FieldStruct::Base
245
+ # field :name, :string
246
+ # field :age, :integer
247
+ # end
248
+ #
249
+ # u = User.new(name: 'Alice', age: '30')
250
+ # u.name # => 'Alice'
251
+ # u.age # => 30 (coerced from the string)
252
+ # u.attributes # => { name: 'Alice', age: 30 }
253
+ #
254
+ # Phase 1 wires up the +field+ macro, getter/setter accessors with
255
+ # coercion via the resolved type, +initialize+ from a hash, +attributes+
256
+ # / +attribute_names+, and metadata inheritance via the +inherited+
257
+ # hook. Macros and validation layer on in later slices.
258
+ class Base
259
+ VALID_COERCION_POLICIES: ::Array[Symbol]
260
+ VALID_UNKNOWN_POLICIES: ::Array[Symbol]
261
+ RESERVED_FIELD_NAMES: ::Array[Symbol]
262
+ UNSET: Object
263
+ JSON_LOAD_OPTIONS: ::Hash[Symbol, Object]
264
+ JSON_DUMP_OPTIONS: ::Hash[Symbol, Object]
265
+ UNIVERSAL_FIELD_OPTIONS: ::Array[Symbol]
266
+
267
+ # _@return_ — the per-class field collection (memoized)
268
+ def self.metadata: () -> Metadata
269
+
270
+ # Mark this class (and its descendants) immutable. Setters called
271
+ # after {#initialize} completes raise {ImmutableError}. One-way
272
+ # switch: there is no companion +mutable!+ macro in Phase 1.
273
+ def self.immutable!: () -> bool
274
+
275
+ # _@return_ — whether this class is marked immutable
276
+ # (locally or via an ancestor)
277
+ def self.immutable?: () -> bool
278
+
279
+ # Mark this class (and its descendants) frozen-on-construct.
280
+ # Instances will be Ruby-frozen at the end of {#initialize} —
281
+ # any subsequent attempt to mutate ivars raises +FrozenError+.
282
+ #
283
+ # Independent of {.immutable!}. +immutable!+ blocks our setters
284
+ # with a custom error; +frozen!+ engages Ruby's frozen-state
285
+ # mechanism, which also locks the instance against any other
286
+ # ivar mutation. Pick the one that matches the level of
287
+ # strictness you need (or stack both).
288
+ def self.frozen!: () -> bool
289
+
290
+ # _@return_ — whether instances of this class are frozen at
291
+ # the end of {#initialize} (locally or via an ancestor)
292
+ def self.frozen_on_construct?: () -> bool
293
+
294
+ # Class macro: get or set the policy used by the setter pipeline
295
+ # when a value can't be coerced into a field's declared type.
296
+ #
297
+ # coercion_policy :keep_raw # default — store raw, add error
298
+ # coercion_policy :replace # store nil, add error
299
+ # coercion_policy :raise # raise CoercionError
300
+ #
301
+ # Inherited by subclasses; descendants can override. Default on
302
+ # Base itself is +:keep_raw+.
303
+ #
304
+ # _@param_ `value` — policy to set; omit to read
305
+ #
306
+ # _@return_ — the resolved policy
307
+ def self.coercion_policy: (?Symbol value) -> Symbol
308
+
309
+ # Class macro: get or set how {Base#initialize} responds to input
310
+ # keys that do not match any declared field.
311
+ #
312
+ # unknown_attributes :ignore # default — silently drop them
313
+ # unknown_attributes :raise # raise UnknownAttributeError
314
+ #
315
+ # Inherited by subclasses; descendants can override.
316
+ #
317
+ # _@param_ `value` — policy to set; omit to read
318
+ #
319
+ # _@return_ — the resolved policy
320
+ def self.unknown_attributes: (?Symbol value) -> Symbol
321
+
322
+ # _@return_ — declared field names in insertion order
323
+ def self.attribute_names: () -> ::Array[Symbol]
324
+
325
+ # Declare a field. Resolves +type_arg+ — a registered symbol or a
326
+ # {FieldStruct::Base} subclass — into a type class (and, for
327
+ # parameterized types like {Types::Nested}, a pre-built instance),
328
+ # builds a {Field}, adds it to the class's {Metadata}, and defines
329
+ # the getter/setter pair.
330
+ #
331
+ # _@param_ `name` — canonical field name
332
+ #
333
+ # _@param_ `type_arg` — a registered name in the resolving registry, OR a +Types::Base+ subclass, OR a +FieldStruct::Base+ subclass (auto-wrapped in {Types::Nested})
334
+ #
335
+ # _@param_ `options` — each keyword option's value: +required:+ (Boolean), +default:+ (literal or callable), +coercion_policy:+ (Symbol), +description:+ / +desc:+ (String), +rbs_type:+ (String — an RBS type expression {RBS.generate} emits for this field instead of the derived one), +sensitive:+ (Boolean — mask the value in +inspect+; see {Field#sensitive?}), plus type-specific options (e.g. +format:+, +round:+, +of:+, +values:+, +enum:+, +in:+) — see each type's +coerce+ for the options it consumes
336
+ #
337
+ # _@return_ — the class-level metadata, so the DSL reads
338
+ # cleanly in IRB (each line prints the running set of declared
339
+ # fields rather than just the most recently added one). The
340
+ # added Field is still available as +metadata[name]+.
341
+ def self.field: ((Symbol | String) name, (Symbol | singleton(Types::Base) | singleton(FieldStruct::Base)) type_arg, **Object? options) -> Metadata
342
+
343
+ # Sugar for {.field} with +required: true+.
344
+ #
345
+ # _@param_ `name`
346
+ #
347
+ # _@param_ `type_name`
348
+ #
349
+ # _@param_ `options` — keyword options, as for {.field}
350
+ def self.required: ((Symbol | String) name, (Symbol | singleton(Types::Base) | singleton(FieldStruct::Base)) type_name, **Object? options) -> Metadata
351
+
352
+ # Sugar for {.field} with +required: false+ (the default).
353
+ #
354
+ # _@param_ `name`
355
+ #
356
+ # _@param_ `type_name`
357
+ #
358
+ # _@param_ `options` — keyword options, as for {.field}
359
+ def self.optional: ((Symbol | String) name, (Symbol | singleton(Types::Base) | singleton(FieldStruct::Base)) type_name, **Object? options) -> Metadata
360
+
361
+ # Declare an external-name mapping for a serialization format.
362
+ # The mapping reads internal-field-name (Symbol) on the left, the
363
+ # external name (String) on the right; it applies to both
364
+ # directions (import reverse-maps, export forward-maps).
365
+ #
366
+ # serialize :json,
367
+ # first_name: 'firstName',
368
+ # last_name: 'lastName'
369
+ #
370
+ # Fields not listed in the mapping serialize under their canonical
371
+ # name (identity). Repeating +serialize :json, ...+ on the same
372
+ # class replaces the prior mapping (last-write-wins).
373
+ #
374
+ # All mapping keys must be declared fields at the moment +serialize+
375
+ # is called — declare fields first, then declare serializations.
376
+ # Mapping values are normalized to Strings.
377
+ #
378
+ # The +:json+ format ships in-gem (Phase B wires it into +to_json+ /
379
+ # +from_json+). Future formats (CSV, XML, etc.) will be supported
380
+ # by downstream gems reading the same metadata.
381
+ #
382
+ # _@param_ `name` — the format name (e.g. +:json+)
383
+ #
384
+ # _@param_ `mapping` — each external key, passed as a keyword named for the internal field. Symbol values are normalized to Strings.
385
+ def self.serialize: ((Symbol | String) name, **(String | Symbol) mapping) -> self
386
+
387
+ # Resolve a type name through the registry chain visible to this
388
+ # class. Walks the containing modules of the class's name in
389
+ # outermost-first order, looking for the nearest one that responds
390
+ # to +field_types+. Falls back to {FieldStruct.types}.
391
+ #
392
+ # _@param_ `type_name`
393
+ #
394
+ # _@return_ — a Types::Base subclass
395
+ def self.resolve_type: (Symbol type_name) -> Class
396
+
397
+ # Resolve the second positional argument of {.field}.
398
+ #
399
+ # _@param_ `type_arg`
400
+ #
401
+ # _@return_ — +[type_class, type_instance]+ —
402
+ # +type_instance+ is +nil+ for stock symbolic types (Field will
403
+ # call +type_class.new+ itself); for nested-class args (and for
404
+ # symbol args that resolve to a FieldStruct::Base subclass) it's
405
+ # a pre-built {Types::Nested} instance.
406
+ def self.resolve_type_arg: ((Symbol | Class) type_arg) -> [Class, Types::Base]
407
+
408
+ # _@return_ — an ActiveModel::Name-shaped value for this class
409
+ def self.model_name: () -> ModelName
410
+
411
+ # A human/agent-readable summary of this class's schema: the class name
412
+ # followed by one line per field (type, required-ness, and the native
413
+ # options that field's type accepts). A single call that answers "what
414
+ # does this model look like, and what can I put where" without reading
415
+ # source. See {Metadata#describe} for the field-line format.
416
+ def self.describe: () -> String
417
+
418
+ # Parse a JSON string and build an instance.
419
+ #
420
+ # Person.from_json('{"name":"Alice","address":{"street":"1","city":"NYC"}}')
421
+ # # => #<Person name: "Alice", address: #<Address ...>>
422
+ #
423
+ # The parsed hash is fed through {#initialize} like any other input,
424
+ # so the full setter pipeline runs: coercion (strings re-coerce to
425
+ # Date/Time/BigDecimal), nested Hash → Types::Nested instantiation,
426
+ # +unknown_attributes+ policy, +coercion_policy+. Invalid JSON
427
+ # raises the underlying Oj parse error's class (+EncodingError+ or a
428
+ # subclass) with its position but not the input, which Oj's own
429
+ # message quotes; a non-object root (+[...]+, +"hi"+, +42+) raises
430
+ # +ArgumentError+ naming its class.
431
+ #
432
+ # JSON numbers with a fraction or exponent parse as +BigDecimal+, never
433
+ # through a Float, so a +:decimal+ field gets the exact digits sent.
434
+ # +:float+ fields still convert to Float; +:value+ fields (and numbers
435
+ # inside their Hashes and Arrays) receive the +BigDecimal+.
436
+ #
437
+ # _@param_ `json_string`
438
+ def self.from_json: (String json_string) -> Base
439
+
440
+ # Oj quotes the whole input in its parse-error message, and that input
441
+ # may be payments data. Re-raise the same class with Oj's description
442
+ # and position only, and without the original as +cause+ (which error
443
+ # reporters print).
444
+ #
445
+ # _@param_ `json_string`
446
+ #
447
+ # _@return_ — the parsed JSON value
448
+ def self.parse_json: (String json_string) -> Object?
449
+
450
+ # Walk a parsed JSON hash (string-keyed, possibly with external
451
+ # names) and reverse-map every key to its canonical Symbol via
452
+ # +metadata.serialization(:json)+. Recurses into nested
453
+ # FieldStruct values and arrays of nested. Unmapped keys are
454
+ # converted to Symbols and passed through — they'll be handled
455
+ # by +new+'s +unknown_attributes+ check.
456
+ #
457
+ # _@param_ `hash` — parsed JSON object
458
+ #
459
+ # _@return_ — canonical-keyed input ready for +.new+
460
+ def self.canonicalize_serialized_hash: (::Hash[(String | Symbol), Object?] hash) -> ::Hash[Symbol, Object?]
461
+
462
+ # Reverse-map a single serialized value for its field, recursing into
463
+ # nested FieldStructs and arrays of nested. Companion to
464
+ # {.canonicalize_serialized_hash}.
465
+ #
466
+ # _@param_ `value` — the parsed JSON value
467
+ #
468
+ # _@param_ `field` — the field it is for; +nil+ for an unknown key
469
+ #
470
+ # _@return_ — +value+, with nested Hashes reverse-mapped
471
+ def self.canonicalize_serialized_value: (Object? value, Field? field) -> Object?
472
+
473
+ # Which external names apply depends on which member the Hash is for,
474
+ # so try the members in declared order, as {Types::Union#coerce} does:
475
+ # a struct member gets the Hash reverse-mapped through its own
476
+ # +serialize :json+ mapping, and the first member that accepts wins. A
477
+ # winning struct is returned built (the union's coerce passes it
478
+ # through to that member); a winning non-struct member gets the Hash
479
+ # unchanged. When nothing accepts it, the Hash goes on as-is and the
480
+ # setter's +coercion_policy+ handles the failure.
481
+ #
482
+ # _@param_ `hash` — parsed JSON object for a union field
483
+ #
484
+ # _@param_ `union`
485
+ def self.canonicalize_serialized_union_hash: (::Hash[(String | Symbol), Object?] hash, Types::Union union) -> (Base | ::Hash[(String | Symbol), Object?])
486
+
487
+ # _@return_ — the +field_types+ of the nearest enclosing
488
+ # module that defines one, or +nil+ to fall back to {FieldStruct.types}
489
+ def self.namespace_field_types: () -> Registry?
490
+
491
+ # _@param_ `type_class`
492
+ #
493
+ # _@param_ `options` — the field's declared options
494
+ #
495
+ # _@return_ — for an +:array+ field, +of:+ resolved
496
+ # into +of_type:+; otherwise +options+ unchanged
497
+ def self.resolve_array_options: (singleton(Types::Base) type_class, ::Hash[Symbol, Object?] options) -> ::Hash[Symbol, Object?]
498
+
499
+ # Check an array field's +element:+ options against the element type's
500
+ # own schema, exactly as a scalar field of that type is checked, and
501
+ # store them resolved and frozen. An empty or absent +element:+ leaves
502
+ # no +:element+ key, so an array without one is unchanged.
503
+ #
504
+ # _@param_ `name` — the field name, for error messages
505
+ #
506
+ # _@param_ `options` — the array field's resolved options
507
+ #
508
+ # _@return_ — +options+ with +:element+ resolved
509
+ def self.resolve_element_options: ((Symbol | String) name, ::Hash[Symbol, Object?] options) -> ::Hash[Symbol, Object?]
510
+
511
+ # _@param_ `element_class` — the array's element type
512
+ #
513
+ # _@param_ `element` — the declared +element:+ options
514
+ #
515
+ # _@return_ — +element+, every key native to +element_class+
516
+ def self.element_options_checked: (singleton(Types::Base) element_class, ::Hash[Symbol, Object?] element) -> ::Hash[Symbol, Object?]
517
+
518
+ # _@param_ `word`
519
+ #
520
+ # _@return_ — +"an"+ before a vowel, otherwise +"a"+
521
+ def self.article: (String word) -> String
522
+
523
+ # _@param_ `type_class`
524
+ #
525
+ # _@param_ `options` — the field's declared options
526
+ #
527
+ # _@return_ — the built union
528
+ # and the options without +of:+
529
+ def self.build_union_instance: (singleton(Types::Union) type_class, ::Hash[Symbol, Object?] options) -> [Types::Union, ::Hash[Symbol, Object?]]
530
+
531
+ # _@param_ `type_class`
532
+ #
533
+ # _@param_ `options` — the field's declared options
534
+ #
535
+ # _@return_ — +options+ with the type's default
536
+ # +format:+ added when it has one and none was declared
537
+ def self.apply_default_format: (singleton(Types::Base) type_class, ::Hash[Symbol, Object?] options) -> ::Hash[Symbol, Object?]
538
+
539
+ # Pull +description:+ (canonical) or +desc:+ (alias) out of the
540
+ # field-declaration options. Raises if both are given — pick one.
541
+ #
542
+ # _@param_ `options` — mutated in place (keys deleted)
543
+ def self.extract_description!: (::Hash[Symbol, Object?] options) -> String?
544
+
545
+ # Three-way option check (design invariant 7): validate native, pass
546
+ # through foreign. For each declared option:
547
+ #
548
+ # 1. **Native to this type** (in its +option_schema+) → validate the
549
+ # value's shape against the descriptor.
550
+ # 2. **Native to *another* registered type but not this one** → raise,
551
+ # naming the types it does apply to (the old "wrong family" errors,
552
+ # now schema-driven).
553
+ # 3. **Not a FieldStruct option at all** → leave it untouched. Foreign
554
+ # options persist on the Field for downstream tooling (e.g. an Avro
555
+ # schema exporter).
556
+ #
557
+ # _@param_ `type_class`
558
+ #
559
+ # _@param_ `options` — the resolved per-field options
560
+ def self.validate_options!: (singleton(Types::Base) type_class, ::Hash[Symbol, Object?] options) -> void
561
+
562
+ # Every option name FieldStruct treats as its own: the union of every
563
+ # registered type's native options plus the universal field options.
564
+ # Anything outside this set is foreign and passes through.
565
+ #
566
+ # _@param_ `registry`
567
+ def self.reserved_option_names: (Registry registry) -> ::Array[Symbol]
568
+
569
+ # _@param_ `type_class` — the field's type
570
+ #
571
+ # _@param_ `key` — the option name
572
+ #
573
+ # _@param_ `value` — the declared option value
574
+ #
575
+ # _@param_ `descriptor` — the option's schema entry
576
+ def self.validate_option_value!: (
577
+ singleton(Types::Base) type_class,
578
+ Symbol key,
579
+ Object? value,
580
+ ::Hash[Symbol, Object] descriptor
581
+ ) -> void
582
+
583
+ # _@param_ `key` — the misapplied option name
584
+ #
585
+ # _@param_ `type_class` — the field's type
586
+ #
587
+ # _@param_ `registry` — the registry the field resolves through
588
+ #
589
+ # _@return_ — the message for a native-elsewhere option, naming
590
+ # the registered types that do accept it
591
+ def self.wrong_family_message: (Symbol key, singleton(Types::Base) type_class, Registry registry) -> String
592
+
593
+ # _@param_ `type_class` — a type class or an option-value class
594
+ #
595
+ # _@return_ — the unqualified class name (+"Integer"+, not
596
+ # +"FieldStruct::Types::Integer"+) for readable messages
597
+ def self.short_type_name: (Module type_class) -> String
598
+
599
+ # _@param_ `name` — the field name being declared
600
+ def self.refuse_reserved_name!: ((Symbol | String) name) -> void
601
+
602
+ # _@param_ `name` — the format name
603
+ #
604
+ # _@param_ `mapping` — internal → external names
605
+ def self.validate_serialize_mapping!: ((Symbol | String) name, ::Hash[Symbol, (String | Symbol)] mapping) -> void
606
+
607
+ # _@param_ `value` — the declared field-level +coercion_policy:+
608
+ def self.validate_coercion_policy_override!: (Object value) -> void
609
+
610
+ # _@param_ `value` — the declared +rbs_type:+
611
+ def self.validate_rbs_type!: (Object value) -> void
612
+
613
+ # _@param_ `value` — the declared +sensitive:+
614
+ def self.validate_sensitive!: (Object value) -> void
615
+
616
+ # _@param_ `field`
617
+ def self.define_field_accessors: (Field field) -> void
618
+
619
+ # _@return_ — the registered cross-field validators
620
+ # for this class. Each entry is a proc that takes the
621
+ # record. Subclasses start with a +dup+ of the parent's list at
622
+ # inheritance time and accumulate their own.
623
+ def self.validators: () -> ::Array[Proc]
624
+
625
+ # Declare a cross-field validator.
626
+ #
627
+ # class Schedule < FieldStruct::Base
628
+ # required :start_date, :date
629
+ # required :end_date, :date
630
+ #
631
+ # validate :ensure_chronological
632
+ # validate do |record|
633
+ # record.errors.add(:base, '...') if record.something_else?
634
+ # end
635
+ #
636
+ # def ensure_chronological
637
+ # return unless start_date && end_date
638
+ #
639
+ # errors.add(:base, 'end_date must not precede start_date') if start_date > end_date
640
+ # end
641
+ # end
642
+ #
643
+ # Multiple symbols can be passed in a single call. Method-symbol
644
+ # and block forms are interchangeable — the symbol form is sugar
645
+ # for +validate { |r| r.send(:name) }+. Validators run on every
646
+ # call to {#valid?}, in declaration order, after per-field
647
+ # validation.
648
+ #
649
+ # Per convention, validators add errors via
650
+ # +record.errors.add(:base, '...')+ — +errors[:base]+ is cleared
651
+ # at the start of each {#valid?} run, so prior cross-field errors
652
+ # don't pile up. Adding to a field-specific key bypasses that
653
+ # auto-clearing; the user is then responsible for clearing those
654
+ # entries.
655
+ #
656
+ # _@param_ `method_names` — zero or more instance method names to invoke on each record at +valid?+ time
657
+ def self.validate: (*Symbol method_names) ?{ (Base record) -> void } -> self
658
+
659
+ # Ruby hook: give each subclass its own metadata seeded from the
660
+ # parent's (merged copy) plus inherited macro settings, so field
661
+ # declarations and config flow down the class chain.
662
+ #
663
+ # _@param_ `subclass`
664
+ def self.inherited: (singleton(Base) subclass) -> void
665
+
666
+ # _@param_ `attrs` — input values, by symbol or string key. Anything responding to +to_hash+ (e.g. a permitted +ActionController::Parameters+) is converted first; +nil+ is treated as no input.
667
+ def initialize: (?(::Hash[(Symbol | String), Object?] | _ToHash[untyped, untyped])? attrs) -> void
668
+
669
+ # Bulk-update declared attributes from a hash. Routes each value
670
+ # through the same setter pipeline used everywhere else — so coercion,
671
+ # presence checks, and the +coercion_policy+ all run as normal.
672
+ #
673
+ # Respects {.unknown_attributes} on input and {.immutable?} on the
674
+ # setter side. Keys must match declared field names. For data arriving
675
+ # in an external naming convention, see {Base.serialize} and
676
+ # {Base.from_json}.
677
+ #
678
+ # _@param_ `attrs` — anything responding to +to_hash+ is converted first
679
+ def assign_attributes: ((::Hash[(Symbol | String), Object?] | _ToHash[untyped, untyped]) attrs) -> self
680
+
681
+ # A new instance with the given attributes changed; the receiver is left
682
+ # untouched. Like +Data#with+.
683
+ #
684
+ # moved = person.with(city: 'Provo', zip: '84601')
685
+ #
686
+ # Changed fields run through the normal setter pipeline (coercion, the
687
+ # field's +coercion_policy+, its errors rewritten), then cross-field
688
+ # validators rerun. Unchanged fields are carried over exactly as they
689
+ # are, errors included, and callable defaults are not called again.
690
+ # The copy is shallow, as with +dup+: an unchanged nested struct or
691
+ # array is the same object in both. Unknown keys follow
692
+ # {.unknown_attributes}. Works on +immutable!+ and frozen instances;
693
+ # the copy is an instance of the receiver's class, immutable when the
694
+ # class is, and frozen when the receiver is.
695
+ #
696
+ # _@param_ `positional` — refused — changes are keywords only
697
+ #
698
+ # _@param_ `changes` — new values, by field name
699
+ #
700
+ # _@return_ — a new instance of the receiver's class
701
+ def with: (*Object positional, **Object? changes) -> Base
702
+
703
+ # _@return_ — a fresh hash of current attribute values
704
+ # keyed by canonical field name. For format-aware export (with the
705
+ # declared +:json+ mapping applied), see {#as_json} / {#to_json}.
706
+ def attributes: () -> ::Hash[Symbol, Object?]
707
+
708
+ def attribute_names: () -> ::Array[Symbol]
709
+
710
+ # _@return_ — the per-instance error collection
711
+ def errors: () -> Errors
712
+
713
+ # Run the class's cross-field validators (if any), then report
714
+ # whether the record has any errors.
715
+ #
716
+ # Each call clears +errors[:base]+ before running validators, so
717
+ # cross-field errors don't accumulate across calls. Field-level
718
+ # errors written by setters are NOT cleared by +valid?+ — Phase 1's
719
+ # "setter owns its field's errors" contract is preserved.
720
+ #
721
+ # For classes that declare no +validate+ blocks, +valid?+ stays a
722
+ # cheap +errors.empty?+ read.
723
+ def valid?: () -> bool
724
+
725
+ # _@return_ — true when {#valid?} is false
726
+ def invalid?: () -> bool
727
+
728
+ # Structural equality: same class AND same {#attributes}. Errors are
729
+ # ignored — equality is about value, not validity.
730
+ #
731
+ # _@param_ `other`
732
+ def ==: (Object other) -> bool
733
+
734
+ # _@return_ — consistent with {#==} so instances work as Hash
735
+ # keys and Set members
736
+ def hash: () -> Integer
737
+
738
+ # _@return_ — alias of {#attributes}
739
+ def to_h: () -> ::Hash[Symbol, Object?]
740
+
741
+ # _@param_ `_options` — unused — accepted for ActiveSupport-style symmetry
742
+ #
743
+ # _@return_ — a JSON-ready hash with the
744
+ # +:json+ serialize mapping applied. Date/Time/DateTime convert
745
+ # to ISO-8601 strings, BigDecimal to its plain-form string,
746
+ # Symbol to String. Arrays recurse. Nested FieldStructs walk
747
+ # through their own +as_json+, applying their own mapping.
748
+ # Covers exactly the declared fields, each read through its reader
749
+ # (as {#attributes} reads them) in one pass; an override of
750
+ # {#attributes} does not change it.
751
+ def as_json: (?Object? _options) -> ::Hash[Symbol, Object?]
752
+
753
+ # _@param_ `_options` — unused — accepted for ActiveSupport-style symmetry
754
+ #
755
+ # _@return_ — JSON representation, via Oj
756
+ def to_json: (?Object? _options) -> String
757
+
758
+ # Readable representation. Valid instances render as
759
+ # +#<ClassName field: value, ...>+. An *invalid* instance additionally
760
+ # surfaces its errors — +#<ClassName name: "" errors: {name: ["is
761
+ # required"]}>+ — so the most-read debug output never hides invalidity
762
+ # (design invariant 7). This reads the current {#errors}; it does not
763
+ # re-run validation, so printing an instance never mutates it.
764
+ #
765
+ # A +sensitive: true+ field renders {FieldStruct::FILTERED} in place of
766
+ # a non-nil value (+nil+ still shows, so a missing value stays visible).
767
+ # +pp+ goes through here too.
768
+ def inspect: () -> String
769
+
770
+ def model_name: () -> ModelName
771
+
772
+ # _@return_ — for ActiveModel-shaped helpers that call .to_model
773
+ def to_model: () -> self
774
+
775
+ # Ruby pattern-matching hash protocol. Returns the declared field
776
+ # values keyed by canonical name, sliced down to +keys+ if the
777
+ # pattern asked for specific ones.
778
+ #
779
+ # case user
780
+ # in { name: 'Alice', age: }
781
+ # ...
782
+ # in { name: String => name }
783
+ # ...
784
+ # end
785
+ #
786
+ # Nested FieldStruct values respond to the same protocol, so deep
787
+ # patterns (+{ address: { city: 'NYC' } }+) work without extra code.
788
+ # Aliases (per the field-name aliases feature) do *not* participate;
789
+ # pattern matching uses canonical names, the same as Ruby-side
790
+ # accessors.
791
+ #
792
+ # _@param_ `keys` — keys the pattern requested, or +nil+ for the +**rest+ pattern (every declared field)
793
+ def deconstruct_keys: (::Array[Symbol]? keys) -> ::Hash[Symbol, Object?]
794
+
795
+ # Ruby pattern-matching array protocol. Returns the declared field
796
+ # values in declaration order — useful for positional unpacking of
797
+ # small POROs whose field order is meaningful.
798
+ #
799
+ # class Point < FieldStruct::Base
800
+ # required :x, :integer
801
+ # required :y, :integer
802
+ # end
803
+ #
804
+ # case point
805
+ # in [x, y]
806
+ # Math.hypot(x, y)
807
+ # end
808
+ def deconstruct: () -> ::Array[Object?]
809
+
810
+ # Ruby hook behind +dup+ / +clone+: give the copy its own {Errors},
811
+ # seeded with the original's messages, so validating or reassigning
812
+ # one instance never changes the other's validity.
813
+ #
814
+ # _@param_ `source` — the instance being copied
815
+ def initialize_copy: (self source) -> void
816
+
817
+ # Return +attrs+ as a Hash, converting via +to_hash+ when it isn't one.
818
+ # The message names the input's class, never its value.
819
+ #
820
+ # _@param_ `attrs` — the caller's input
821
+ #
822
+ # _@param_ `method_label` — the public method, for the message
823
+ def hash_input!: (Object? attrs, String method_label) -> ::Hash[(Symbol | String), Object?]
824
+
825
+ # _@param_ `value` — a field value, or an element inside one
826
+ #
827
+ # _@param_ `field` — the field it belongs to, for its +format:+
828
+ #
829
+ # _@return_ — a JSON-ready value for Oj
830
+ def json_value: (Object? value, ?Field? field) -> Object?
831
+
832
+ # A declared +format:+ is now stored as written, so a preset name has to be
833
+ # expanded before strftime sees it — and only the field's own type knows
834
+ # its preset table. An :array field's elements use the +format:+ in its
835
+ # +element:+ options, read through the element type, so +from_json+ reads
836
+ # back what +to_json+ wrote. With no declared format (an :array without
837
+ # one, a :value field holding a date) the answer is ISO-8601, as it has
838
+ # always been.
839
+ #
840
+ # _@param_ `value`
841
+ #
842
+ # _@param_ `field`
843
+ def temporal_json_value: ((::Date | ::Time | ::DateTime) value, Field? field) -> String
844
+
845
+ # _@param_ `field`
846
+ #
847
+ # _@return_ — the
848
+ # options holding the +format:+ for this field's temporal values and
849
+ # the type class that resolves it — an array's +element:+ options and
850
+ # element type, or the field's own
851
+ def temporal_format_source: (Field? field) -> ([::Hash[Symbol, Object?], Class] | [NilClass, Class] | [NilClass, NilClass])
852
+
853
+ # Give every field its default, through the setter. A field with no
854
+ # default whose key +attrs+ carries only gets its ivar (so the ivar order
855
+ # stays the declaration order): the assignment that follows runs its
856
+ # setter, so a nil pass first would only be undone.
857
+ #
858
+ # _@param_ `attrs` — the input about to be assigned
859
+ def apply_defaults: (::Hash[(Symbol | String), Object?] attrs) -> void
860
+
861
+ def run_cross_field_validators: () -> void
862
+
863
+ # _@param_ `attrs`
864
+ def reject_unknown_attributes!: (::Hash[(Symbol | String), Object?] attrs) -> void
865
+
866
+ # _@param_ `field`
867
+ #
868
+ # _@param_ `value` — the coerced value just stored
869
+ def validate_field: (Field field, Object? value) -> void
870
+
871
+ # _@param_ `field`
872
+ #
873
+ # _@param_ `value` — a present (non-missing) coerced value
874
+ def field_value_invalid?: (Field field, Object? value) -> bool
875
+
876
+ # Whether +value+ breaks a +format:+ Regexp, +enum:+ or +in:+ in +options+.
877
+ #
878
+ # _@param_ `options` — a field's options, or an array field's +element:+ options
879
+ #
880
+ # _@param_ `value` — a present coerced value
881
+ def outside_options?: (::Hash[Symbol, Object?] options, Object? value) -> bool
882
+
883
+ # _@param_ `field`
884
+ #
885
+ # _@param_ `value` — a present coerced value
886
+ #
887
+ # _@return_ — the indexes of every non-nil element
888
+ # that breaks the array field's +element:+ +format:+ / +enum:+ / +in:+,
889
+ # or +nil+ when there is none (or the field declares no +element:+)
890
+ def invalid_element_indexes: (Field field, Object? value) -> ::Array[Integer]?
891
+
892
+ # `in:` with a Range means a BOUNDS check, so ask the Range for its bounds.
893
+ #
894
+ # Range#include? ENUMERATES a non-numeric range rather than comparing
895
+ # endpoints. For a Date range that is merely wasteful — 365 successor steps
896
+ # to answer one comparison. For a DateTime range it is wrong: the range
897
+ # steps by whole days, so a value that isn't exactly midnight is reported
898
+ # outside a range that plainly contains it.
899
+ #
900
+ # (DateTime.new(2026, 1, 1)..DateTime.new(2026, 12, 31))
901
+ # .include?(DateTime.new(2026, 7, 3, 10, 30)) # => false
902
+ # .cover?(DateTime.new(2026, 7, 3, 10, 30)) # => true
903
+ #
904
+ # An Array stays a membership test. There is no string-range case to worry
905
+ # about: the DSL refuses `in:` on String fields outright.
906
+ #
907
+ # _@param_ `allowed` — the field's +in:+ option (an Array for the stock types; a custom type may declare another)
908
+ #
909
+ # _@param_ `value`
910
+ def within_allowed?: ((::Range[Object] | ::Enumerable[Object]) allowed, Object? value) -> bool
911
+
912
+ # _@param_ `value`
913
+ #
914
+ # _@return_ — whether +value+ is, or is an Array holding, an
915
+ # invalid FieldStruct
916
+ def nested_invalid?: (Object? value) -> bool
917
+
918
+ # _@param_ `field`
919
+ #
920
+ # _@param_ `raw_value` — the value that failed to coerce
921
+ #
922
+ # _@param_ `error` — what the type raised
923
+ #
924
+ # _@return_ — what the setter stored: +raw_value+ under
925
+ # +:keep_raw+, +nil+ under +:replace+
926
+ def apply_coercion_policy: (Field field, Object? raw_value, StandardError error) -> Object?
927
+
928
+ # _@param_ `field`
929
+ #
930
+ # _@param_ `stored_value` — what to store in the field
931
+ #
932
+ # _@param_ `error` — what the type raised
933
+ def record_coercion_failure: (Field field, Object? stored_value, StandardError error) -> void
934
+ end
935
+
936
+ # Base class for every error FieldStruct raises. Catch this to handle
937
+ # any library error generically.
938
+ class Error < StandardError
939
+ end
940
+
941
+ # Raised by the setter pipeline when a value cannot be coerced into a
942
+ # field's declared type and the class's +coercion_policy+ is +:raise+.
943
+ #
944
+ # Carries the field name and the original underlying error for callers
945
+ # that want to inspect what happened.
946
+ class CoercionError < FieldStruct::Error
947
+ # _@param_ `message`
948
+ #
949
+ # _@param_ `field_name`
950
+ #
951
+ # _@param_ `original`
952
+ def initialize: (String message, ?field_name: Symbol?, ?original: StandardError?) -> void
953
+
954
+ # _@return_ — the field whose setter triggered the failure
955
+ attr_reader field_name: Symbol?
956
+
957
+ # _@return_ — the underlying error from the type's coerce
958
+ attr_reader original: StandardError?
959
+ end
960
+
961
+ # Raised when a field setter on a class marked +immutable!+ is called
962
+ # after construction.
963
+ class ImmutableError < FieldStruct::Error
964
+ end
965
+
966
+ # Raised by {Base#initialize} / {Base#assign_attributes} when the input
967
+ # hash includes a key that is not a declared field on the class and
968
+ # the +unknown_attributes+ policy is +:raise+.
969
+ class UnknownAttributeError < FieldStruct::Error
970
+ # _@param_ `message`
971
+ #
972
+ # _@param_ `keys`
973
+ def initialize: (String message, ?keys: ::Array[Symbol]) -> void
974
+
975
+ # _@return_ — the offending keys
976
+ attr_reader keys: ::Array[Symbol]
977
+ end
978
+
979
+ # A single declared attribute on a FieldStruct class.
980
+ #
981
+ # Holds the field's name, the resolved type class, whether it's
982
+ # required, an optional default, and any remaining keyword options
983
+ # (e.g. +format:+). Frozen after construction — the DSL is expected
984
+ # to build a fresh Field rather than mutate one.
985
+ class Field
986
+ # _@param_ `name` — canonical field name
987
+ #
988
+ # _@param_ `type` — the resolved type class
989
+ #
990
+ # _@param_ `type_instance` — optional pre-built instance — used by the DSL for parameterized types (e.g. {Types::Nested}) where +type.new+ wouldn't be enough to capture the parameterization. When nil, +type.new+ is called.
991
+ #
992
+ # _@param_ `required`
993
+ #
994
+ # _@param_ `default` — literal default value, or a parameterless callable (Proc/Lambda/Method) that returns one. Callables are invoked once per instance during apply_defaults.
995
+ #
996
+ # _@param_ `coercion_policy` — override the class-level coercion policy for this one field; +nil+ means "use whatever the class says"
997
+ #
998
+ # _@param_ `description` — human-readable description of the field; for documentation purposes only
999
+ #
1000
+ # _@param_ `rbs_type` — RBS type expression overriding the derived one in generated signatures (see {#rbs_type})
1001
+ #
1002
+ # _@param_ `sensitive` — mask the value in {Base#inspect} (see {#sensitive?})
1003
+ #
1004
+ # _@param_ `options` — extra keyword type/field options forwarded to coerce (e.g. +format:+, +of_type:+, +round:+, +values:+, +enum:+, +in:+)
1005
+ def initialize: (
1006
+ name: (Symbol | String),
1007
+ _type: singleton(Types::Base),
1008
+ ?type_instance: Types::Base?,
1009
+ ?required: bool,
1010
+ ?default: Object?,
1011
+ ?coercion_policy: Symbol?,
1012
+ ?description: String?,
1013
+ ?rbs_type: String?,
1014
+ ?sensitive: bool,
1015
+ **Object? options
1016
+ ) -> void
1017
+
1018
+ def required?: () -> bool
1019
+
1020
+ # Whether the field holds data that must not reach a log — a card
1021
+ # number, a CVV, a token. {Base#inspect} (and so +pp+) renders
1022
+ # {FieldStruct::FILTERED} in place of a non-nil value. Data paths —
1023
+ # the reader, +attributes+, +to_h+, +as_json+, +to_json+, pattern
1024
+ # matching — return the real value, so round-trips stay exact.
1025
+ # Declared with the +sensitive:+ field option.
1026
+ def sensitive?: () -> bool
1027
+
1028
+ # Concise one-line representation. Designed to be readable in IRB
1029
+ # without dumping every ivar (the default Object#inspect is the
1030
+ # reason +optional :x, :string+ in a console looks like a wall of
1031
+ # text).
1032
+ def inspect: () -> String
1033
+
1034
+ # Delegates pretty-print to {#inspect} so IRB / pp don't fall back
1035
+ # to the default reflection-based output.
1036
+ #
1037
+ # _@param_ `pp` — a PP-shaped sink (responds to +#text+)
1038
+ def pretty_print: (Object pp) -> void
1039
+
1040
+ # _@return_ — the type's short name, or +Nested(...)+ / +Union(...)+
1041
+ def type_repr: () -> String
1042
+
1043
+ # Render an option's value in inspect output. Type-class values
1044
+ # (e.g. +:of_type+ on an Array field) get their basename instead
1045
+ # of the full +FieldStruct::Types::String+ class.inspect.
1046
+ #
1047
+ # _@param_ `value` — an option value
1048
+ def option_repr: (Object? value) -> String
1049
+
1050
+ # _@return_ — canonical field name
1051
+ attr_reader name: Symbol
1052
+
1053
+ # _@return_ — the resolved type class
1054
+ attr_reader type: singleton(Types::Base)
1055
+
1056
+ # _@return_ — the configured default value, or a
1057
+ # parameterless callable returning one, or +nil+ if no default
1058
+ attr_reader default: (Object | untyped)?
1059
+
1060
+ # _@return_ — frozen, holds the remaining keyword
1061
+ # options forwarded to {Types::Base#coerce} (e.g. +:format+,
1062
+ # +:round+, +:values+, +:of_type+, +:enum+, +:in+)
1063
+ attr_reader options: ::Hash[Symbol, Object?]
1064
+
1065
+ # _@return_ — an eagerly-built instance of {#type}, reused by
1066
+ # the setter pipeline so we don't allocate a fresh type per coercion
1067
+ attr_reader type_instance: Types::Base
1068
+
1069
+ # _@return_ — this field's coercion-
1070
+ # failure policy override, or +nil+ to defer to the class-level
1071
+ # setting on {Base.coercion_policy}.
1072
+ attr_reader coercion_policy: Symbol?
1073
+
1074
+ # _@return_ — human-readable description of the field;
1075
+ # intended for downstream documentation generators. Not part of
1076
+ # the data surface — does not appear in +attributes+ / +as_json+ /
1077
+ # pattern matches.
1078
+ attr_reader description: String?
1079
+
1080
+ # _@return_ — an RBS type expression that {RBS.generate} and
1081
+ # {RBS.type_for} emit for this field instead of deriving one from the
1082
+ # type (e.g. +"::ActiveRecord::Relation"+ on a +:value+ field), or
1083
+ # +nil+ to derive it. Declared with the +rbs_type:+ field option;
1084
+ # signatures only — it never affects coercion or validation.
1085
+ attr_reader rbs_type: String?
1086
+
1087
+ # _@return_ — the setter's method name (+:name=+), built once here
1088
+ # so the setter pipeline doesn't intern a dynamic Symbol per call
1089
+ attr_reader setter_name: Symbol
1090
+
1091
+ # _@return_ — the instance variable holding the value (+:@name+),
1092
+ # built once for the same reason as {#setter_name}
1093
+ attr_reader ivar_name: Symbol
1094
+ end
1095
+
1096
+ # Per-instance collection of validation messages, keyed by field name.
1097
+ #
1098
+ # Shaped after ActiveModel::Errors but reuses none of its code. Each
1099
+ # FieldStruct instance gets its own Errors object; the setter pipeline
1100
+ # clears and rewrites a field's entry on every assignment.
1101
+ class Errors
1102
+ def initialize: () -> void
1103
+
1104
+ # Ruby hook behind +dup+ / +clone+: copy the message lists too, so
1105
+ # adding to the copy never changes the original.
1106
+ #
1107
+ # _@param_ `source` — the collection being copied
1108
+ def initialize_copy: (self source) -> void
1109
+
1110
+ # _@param_ `field_name`
1111
+ #
1112
+ # _@return_ — messages for the field, or an empty array
1113
+ def []: ((Symbol | String) field_name) -> ::Array[String]
1114
+
1115
+ # _@param_ `field_name`
1116
+ #
1117
+ # _@param_ `message`
1118
+ #
1119
+ # _@return_ — the field's updated message list
1120
+ def add: ((Symbol | String) field_name, String message) -> ::Array[String]
1121
+
1122
+ # Drop every message for a field.
1123
+ #
1124
+ # _@param_ `field_name`
1125
+ def clear: ((Symbol | String) field_name) -> void
1126
+
1127
+ # _@return_ — true when no field has any messages
1128
+ def empty?: () -> bool
1129
+
1130
+ # _@return_ — snapshot of field → messages,
1131
+ # skipping fields with empty message lists
1132
+ def to_h: () -> ::Hash[Symbol, ::Array[String]]
1133
+
1134
+ # Every message rendered as a full, human-readable sentence: the
1135
+ # humanized field name prepended to each message. Messages on +:base+
1136
+ # (the cross-field convention) pass through unprefixed — they are
1137
+ # already complete sentences. Order follows field insertion order, then
1138
+ # message order within a field.
1139
+ #
1140
+ # errors.add(:first_name, 'is required')
1141
+ # errors.full_messages # => ["First name is required"]
1142
+ def full_messages: () -> ::Array[String]
1143
+
1144
+ def inspect: () -> String
1145
+
1146
+ # Turn a field name into a display label for {#full_messages}:
1147
+ # underscores become spaces and the first character is upcased.
1148
+ # Deliberately minimal — no inflection, no +_id+ stripping, no i18n.
1149
+ #
1150
+ # _@param_ `field`
1151
+ def humanize: (Symbol field) -> String
1152
+ end
1153
+
1154
+ # The per-class collection of {Field} declarations.
1155
+ #
1156
+ # Mutable while a class is being defined — the DSL adds fields as it
1157
+ # encounters them — and queried thereafter by name or via iteration.
1158
+ # Subclass inheritance is handled by {#merge}: a fresh child Metadata
1159
+ # is seeded with the parent's fields, then accumulates its own; child
1160
+ # declarations win on conflict.
1161
+ class Metadata
1162
+ include ::Enumerable[Field]
1163
+ EMPTY_SERIALIZATION: ::Hash[Symbol, String]
1164
+
1165
+ def initialize: () -> void
1166
+
1167
+ # _@param_ `field`
1168
+ def add: (Field field) -> self
1169
+
1170
+ # _@param_ `name`
1171
+ def []: ((Symbol | String) name) -> Field?
1172
+
1173
+ # _@return_ — field names in insertion order
1174
+ def names: () -> ::Array[Symbol]
1175
+
1176
+ # Yields each {Field} in insertion order.
1177
+ #
1178
+ # _@return_ — when no block is given
1179
+ def each: () ?{ (Field field) -> void } -> ::Enumerator[Field]
1180
+
1181
+ # A copy-pasteable schema view of the declared fields, keyed by field
1182
+ # name. Each value is a plain Hash of primitives/strings (no live type
1183
+ # objects), so the result reads cleanly in a console and is safe to
1184
+ # `pp` or dump — handy for seeing a model's shape without reading its
1185
+ # source. Option values that are classes or type instances are rendered
1186
+ # as short names; a callable default shows as +"<callable>"+.
1187
+ #
1188
+ # User.metadata.to_h
1189
+ # # => { name: { type: "String", ruby_type: "String", required: true,
1190
+ # # default: nil, options: {}, description: nil }, ... }
1191
+ #
1192
+ # When a block is given this falls back to +Enumerable#to_h+ (mapping
1193
+ # each {Field} to a pair), preserving existing callers.
1194
+ #
1195
+ # _@return_ — field name → schema, or
1196
+ # the block-mapped pairs when a block is supplied
1197
+ def to_h: () -> ::Hash[Symbol, ::Hash[Symbol, Object?]]
1198
+
1199
+ # Record a serialization mapping for the named format. Repeats on
1200
+ # the same format name replace the prior mapping (last-write-wins).
1201
+ # The mapping is frozen on its way in.
1202
+ #
1203
+ # _@param_ `name`
1204
+ #
1205
+ # _@param_ `mapping`
1206
+ def add_serialization: ((Symbol | String) name, ::Hash[Symbol, String] mapping) -> self
1207
+
1208
+ # Look up the mapping for a format. Returns an empty hash when the
1209
+ # format hasn't been declared — the implicit identity mapping that
1210
+ # serializers fall back to.
1211
+ #
1212
+ # _@param_ `name`
1213
+ #
1214
+ # _@return_ — frozen mapping (empty hash when undeclared)
1215
+ def serialization: ((Symbol | String) name) -> ::Hash[Symbol, String]
1216
+
1217
+ # The reverse of {#serialization}: external name → field name, which
1218
+ # {Base.from_json} needs on every call. Built once per mapping and reused
1219
+ # while {#serialization} still returns that same frozen mapping, so any
1220
+ # writer that installs a new one ({#add_serialization}, {#merge}, or a
1221
+ # direct write to {#serializations}) is picked up on the next call.
1222
+ #
1223
+ # _@param_ `name`
1224
+ #
1225
+ # _@return_ — frozen; empty when the format is undeclared
1226
+ def reverse_serialization: ((Symbol | String) name) -> ::Hash[String, Symbol]
1227
+
1228
+ # Copy fields and serializations from +parent+ into self, leaving
1229
+ # already-declared entries untouched (child wins). Order: existing
1230
+ # self fields stay where they are; parent fields are appended in
1231
+ # iteration order if not present.
1232
+ #
1233
+ # _@param_ `parent`
1234
+ def merge: (Metadata parent) -> self
1235
+
1236
+ # Concise one-line representation. Lists the declared field names
1237
+ # and, when present, the declared serialization formats — without
1238
+ # dumping the underlying ivar hashes.
1239
+ def inspect: () -> String
1240
+
1241
+ # Multi-line representation suitable for IRB / pp. Renders each
1242
+ # field on its own indented line using {Field#inspect}, with the
1243
+ # closing +>+ on its own line.
1244
+ #
1245
+ # _@param_ `pp` — a PP-shaped sink (responds to +#text+)
1246
+ def pretty_print: (Object pp) -> void
1247
+
1248
+ # Human/agent-readable summary of the declared fields: one line each
1249
+ # with the field's type, required-ness, and the native options its type
1250
+ # accepts (with value shapes, required options starred, and any presets).
1251
+ # Answers "what can I put here?" without reading source. For the
1252
+ # structured form use {#to_h}; for a class-titled version use
1253
+ # {Base.describe}.
1254
+ #
1255
+ # name (String, required) — accepts format (Regexp | Symbol), enum (Array)
1256
+ def describe: () -> String
1257
+
1258
+ # _@param_ `field`
1259
+ #
1260
+ # _@return_ — one +describe+ line for a field
1261
+ def describe_field: (Field field) -> String
1262
+
1263
+ # What this field actually declares, as written.
1264
+ #
1265
+ # Without this, +describe+ answered only "what COULD go in this field" —
1266
+ # the type's option vocabulary — and a field declaring
1267
+ # +format: :iso8601, enum: %w[USD EUR]+ rendered identically to one
1268
+ # declaring nothing at all. The option schema belongs to the type; this
1269
+ # belongs to the field.
1270
+ #
1271
+ # _@param_ `field`
1272
+ #
1273
+ # _@return_ — +nil+ when the field declares no options
1274
+ def declared_options_summary: (Field field) -> String?
1275
+
1276
+ # _@param_ `value` — a declared option value
1277
+ #
1278
+ # _@return_ — a declared option value, rendered for a human. Type
1279
+ # values (an Array field's +of_type:+) show their bare type name rather
1280
+ # than a quoted string, so the line reads like the declaration it
1281
+ # describes.
1282
+ def declared_option_repr: (Object? value) -> String
1283
+
1284
+ # What an array field's +element:+ may hold — the names of its element
1285
+ # type's native options — so the line answers "what can I pass to each
1286
+ # element" too.
1287
+ #
1288
+ # _@param_ `field`
1289
+ #
1290
+ # _@return_ — +nil+ for a non-array field, or an element type
1291
+ # with no options (a nested struct)
1292
+ def element_accepts_summary: (Field field) -> String?
1293
+
1294
+ # _@param_ `type_class`
1295
+ #
1296
+ # _@return_ — the type's native option vocabulary, or +nil+ when
1297
+ # the type declares none
1298
+ def accepts_summary: (singleton(Types::Base) type_class) -> String?
1299
+
1300
+ # _@param_ `type_class`
1301
+ #
1302
+ # _@return_ — the type's native options as +name(shapes)+, required
1303
+ # ones starred, presets appended — empty when the type has none
1304
+ def native_options_summary: (singleton(Types::Base) type_class) -> String
1305
+
1306
+ # Render one field as a primitive-only schema Hash for {#to_h}.
1307
+ #
1308
+ # _@param_ `field`
1309
+ def field_schema: (Field field) -> ::Hash[Symbol, Object?]
1310
+
1311
+ # A single class, or +A | B+ for a multi-class ruby_type (boolean, union).
1312
+ #
1313
+ # _@param_ `ruby_type` — a type's {Types::Base#ruby_type}
1314
+ def ruby_type_repr: ((Class | ::Array[Class]) ruby_type) -> String
1315
+
1316
+ # Reduce an option/default value to something printable: short class
1317
+ # names for type references, +"<callable>"+ for procs, recursion into
1318
+ # Array/Hash, everything else as-is.
1319
+ #
1320
+ # _@param_ `value` — an option or default value
1321
+ def schema_value: (Object? value) -> Object?
1322
+
1323
+ # _@param_ `klass`
1324
+ #
1325
+ # _@return_ — the last segment of the class name
1326
+ def short_name: (Module klass) -> String
1327
+
1328
+ # _@return_ — declared
1329
+ # external-name mappings keyed by format name. Each inner hash
1330
+ # maps an internal field name (Symbol) to its external name
1331
+ # (String). Fields not listed in an inner hash serialize under
1332
+ # their canonical name.
1333
+ attr_reader serializations: ::Hash[Symbol, ::Hash[Symbol, String]]
1334
+ end
1335
+
1336
+ # Name → type-class map with optional parent fallback.
1337
+ #
1338
+ # Holds the resolution table that maps symbolic type names (`:string`,
1339
+ # `:integer`, …) to the {Types} classes that implement them. A namespace
1340
+ # can build its own registry parented to the base ({FieldStruct.types})
1341
+ # to extend or override the default set without mutating it.
1342
+ class Registry
1343
+ # _@param_ `parent` — the registry to fall back to when a name isn't found locally
1344
+ def initialize: (?Registry? parent) -> void
1345
+
1346
+ # Register a type class — or an alias to an already-registered name.
1347
+ #
1348
+ # registry.register(:integer, MyInteger) # type class
1349
+ # registry.register(:decimal, :big_decimal) # alias (resolved now)
1350
+ #
1351
+ # Aliases are eagerly resolved through {#lookup}, so the alias target
1352
+ # must already be registered (locally or in a parent). A later
1353
+ # registration of the same name overwrites silently — this is the
1354
+ # mechanism that lets a namespace registry shadow a parent's type.
1355
+ #
1356
+ # _@param_ `name` — the name to register
1357
+ #
1358
+ # _@param_ `target` — a type class, or an existing registered name
1359
+ def register: ((Symbol | String) name, (Class | Symbol) target) -> self
1360
+
1361
+ # Resolve a name to its type class, walking the parent chain.
1362
+ #
1363
+ # _@param_ `name`
1364
+ #
1365
+ # _@return_ — the type class
1366
+ def lookup: ((Symbol | String) name) -> Class
1367
+
1368
+ # Whether the name resolves to anything (locally or via the parent chain).
1369
+ #
1370
+ # _@param_ `name`
1371
+ def key?: ((Symbol | String) name) -> bool
1372
+
1373
+ # Every distinct type class resolvable through this registry, including
1374
+ # those inherited from the parent chain. Local entries come first;
1375
+ # duplicates (e.g. an alias pointing at an already-listed class, or a
1376
+ # parent class also registered locally) are removed.
1377
+ #
1378
+ # _@return_ — deduplicated, local-first
1379
+ def type_classes: () -> ::Array[Class]
1380
+
1381
+ # _@return_ — one-line summary of the locally-registered names
1382
+ # (does not enumerate the parent chain), with a +parent+ marker
1383
+ # when the registry has one
1384
+ def inspect: () -> String
1385
+
1386
+ # _@return_ — the parent registry, or +nil+ for an unparented one
1387
+ attr_reader parent: Registry?
1388
+ end
1389
+
1390
+ # Scaffolds FieldStruct class source from one or more JSON example objects.
1391
+ #
1392
+ # The goal is a *working prototype* you then refine by hand — the generator
1393
+ # is deliberately conservative about types and never guesses past the
1394
+ # evidence. Pass a single object to seed the shape, or an array of
1395
+ # same-shape objects to gather more signal (empty fields get resolved from
1396
+ # other samples, presence drives required/optional, and small repeated
1397
+ # vocabularies surface as enum candidates). A single object is just the
1398
+ # one-sample case of the same engine.
1399
+ #
1400
+ # FieldStruct::Scaffold.from_json(<<~JSON, class_name: 'User')
1401
+ # {"name": "Al", "age": 30, "active": true}
1402
+ # JSON
1403
+ # # class User < FieldStruct::Base
1404
+ # # required :name, :string
1405
+ # # required :age, :integer
1406
+ # # required :active, :boolean
1407
+ # # end
1408
+ #
1409
+ # Inference rules (conservative on purpose):
1410
+ #
1411
+ # * JSON object → a nested FieldStruct class (emitted before its parent).
1412
+ # * JSON array → +:array, of: <element type>+ (element class for arrays of objects).
1413
+ # * JSON +true+/+false+ → +:boolean+; integer → +:integer+; real → +:float+.
1414
+ # * JSON string → +:string+ — *including numeric-looking strings* (an
1415
+ # +authorization_code+ must not become an Integer). Hints like "maybe
1416
+ # :big_decimal" are emitted as trailing comments, not committed types.
1417
+ # * required when present and non-blank across *every* sample; else optional.
1418
+ #
1419
+ # The developer adds enums, descriptions, formats, and final scalar types;
1420
+ # the comments point at where.
1421
+ module Scaffold
1422
+ NUMERIC_STRING: Regexp
1423
+ ENUM_MAX_DISTINCT: Integer
1424
+ RESERVED: ::Array[String]
1425
+
1426
+ # Generate FieldStruct class source from JSON example(s).
1427
+ #
1428
+ # _@param_ `input` — a JSON string (object or array root), a parsed Hash, or an Array of same-shape Hashes
1429
+ #
1430
+ # _@param_ `class_name` — the root class name
1431
+ #
1432
+ # _@return_ — Ruby source defining the class and any nested classes
1433
+ def self.from_json: ((String | ::Hash[Object, Object?] | ::Array[::Hash[Object, Object?]]) input, ?class_name: String) -> String
1434
+
1435
+ # _@param_ `input`
1436
+ #
1437
+ # _@return_ — the sample set (a lone object becomes a set of
1438
+ # one); {.from_json} checks every element is a Hash
1439
+ def self.normalize: ((String | ::Hash[Object, Object?] | ::Array[::Hash[Object, Object?]]) input) -> ::Array[Object]
1440
+
1441
+ # Build the class for +samples+ as +class_name+, appending nested classes
1442
+ # to +classes+ first (post-order, so references resolve), and return the
1443
+ # (deduplicated) class name used.
1444
+ #
1445
+ # _@param_ `class_name` — the name wanted
1446
+ #
1447
+ # _@param_ `samples`
1448
+ #
1449
+ # _@param_ `classes` — class sources emitted so far (appended to)
1450
+ #
1451
+ # _@param_ `used` — class names taken so far (appended to)
1452
+ def self.emit_object: (
1453
+ String class_name,
1454
+ ::Array[::Hash[Object, Object?]] samples,
1455
+ ::Array[String] classes,
1456
+ ::Array[String] used
1457
+ ) -> String
1458
+
1459
+ # _@param_ `samples`
1460
+ #
1461
+ # _@param_ `key` — a sample key
1462
+ #
1463
+ # _@return_ — +{ present: Integer, values: Array }+
1464
+ # for +key+ across samples
1465
+ def self.observe: (::Array[::Hash[Object, Object?]] samples, Object key) -> ::Hash[Symbol, (Integer | ::Array[Object?])]
1466
+
1467
+ # _@param_ `attr` — the attribute name
1468
+ #
1469
+ # _@param_ `obs` — from {.observe}
1470
+ #
1471
+ # _@param_ `total` — the sample count
1472
+ #
1473
+ # _@param_ `classes` — class sources emitted so far
1474
+ #
1475
+ # _@param_ `used` — class names taken so far
1476
+ #
1477
+ # _@return_ — the indented DSL line
1478
+ def self.field_line: (
1479
+ String attr,
1480
+ ::Hash[Symbol, (Integer | ::Array[Object?])] obs,
1481
+ Integer total,
1482
+ ::Array[String] classes,
1483
+ ::Array[String] used
1484
+ ) -> String
1485
+
1486
+ # _@param_ `attr` — the attribute name
1487
+ #
1488
+ # _@param_ `non_nil` — the attribute's non-nil sample values
1489
+ #
1490
+ # _@param_ `classes` — class sources emitted so far
1491
+ #
1492
+ # _@param_ `used` — class names taken so far
1493
+ #
1494
+ # _@return_ — +[type_part, comment_hint]+
1495
+ def self.type_for: (
1496
+ String attr,
1497
+ ::Array[Object] non_nil,
1498
+ ::Array[String] classes,
1499
+ ::Array[String] used
1500
+ ) -> [String, String]
1501
+
1502
+ # _@param_ `attr` — the attribute name
1503
+ #
1504
+ # _@param_ `elements` — every element of the attribute's arrays
1505
+ #
1506
+ # _@param_ `classes` — class sources emitted so far
1507
+ #
1508
+ # _@param_ `used` — class names taken so far
1509
+ #
1510
+ # _@return_ — +of:+ value and a comment hint
1511
+ def self.array_element: (
1512
+ String attr,
1513
+ ::Array[Object?] elements,
1514
+ ::Array[String] classes,
1515
+ ::Array[String] used
1516
+ ) -> [String, String]
1517
+
1518
+ # _@param_ `values` — non-nil scalar sample values
1519
+ #
1520
+ # _@return_ — inferred scalar type and a comment hint
1521
+ def self.infer_scalar: (::Array[Object] values) -> [Symbol, String]
1522
+
1523
+ # _@param_ `values`
1524
+ #
1525
+ # _@return_ — +:string+ and a comment hint
1526
+ def self.string_inference: (::Array[String] values) -> [Symbol, String]
1527
+
1528
+ # _@param_ `value`
1529
+ #
1530
+ # _@return_ — +:boolean+, +:integer+, +:float+, +:string+ or +:other+
1531
+ def self.scalar_kind: (Object value) -> Symbol
1532
+
1533
+ # +serialize+ refuses undeclared fields, so the mapping goes after them.
1534
+ #
1535
+ # _@param_ `name` — the class name
1536
+ #
1537
+ # _@param_ `mappings` — attribute name → original sample key
1538
+ #
1539
+ # _@param_ `lines` — the field lines
1540
+ #
1541
+ # _@return_ — the class source
1542
+ def self.build_class: (String name, ::Hash[String, Object] mappings, ::Array[String] lines) -> String
1543
+
1544
+ # _@param_ `declaration` — the DSL call, without indentation
1545
+ #
1546
+ # _@param_ `comment_parts` — trailing-comment fragments; nil and empty ones are dropped
1547
+ #
1548
+ # _@return_ — the indented line, with a +#+ comment when any part remains
1549
+ def self.decorate: (String declaration, *String? comment_parts) -> String
1550
+
1551
+ # _@param_ `present` — samples holding the key
1552
+ #
1553
+ # _@param_ `total` — the sample count
1554
+ #
1555
+ # _@param_ `missing` — nil or blank values among them
1556
+ def self.availability_comment: (Integer present, Integer total, Integer missing) -> String?
1557
+
1558
+ # First-seen union of keys across all samples.
1559
+ #
1560
+ # _@param_ `samples`
1561
+ def self.ordered_keys: (::Array[::Hash[Object, Object?]] samples) -> ::Array[Object]
1562
+
1563
+ # _@param_ `base` — the name wanted
1564
+ #
1565
+ # _@param_ `used` — names taken so far (appended to)
1566
+ #
1567
+ # _@return_ — +base+, or +base+ with a numeric suffix
1568
+ def self.unique_name: (String base, ::Array[String] used) -> String
1569
+
1570
+ # _@param_ `key` — an attribute name
1571
+ #
1572
+ # _@return_ — a constant-safe class name
1573
+ def self.class_name_for: (String key) -> String
1574
+
1575
+ # _@param_ `str`
1576
+ def self.camelize: (String str) -> String
1577
+
1578
+ # Naive: strip one trailing "s". Good enough for a prototype name; the
1579
+ # developer renames as needed.
1580
+ #
1581
+ # _@param_ `str`
1582
+ def self.singularize: (String str) -> String
1583
+
1584
+ # _@param_ `key` — a sample key
1585
+ #
1586
+ # _@return_ — a snake_case attribute name that +field+ accepts
1587
+ def self.safe_attr: (Object key) -> String
1588
+
1589
+ # _@param_ `value`
1590
+ def self.blank_string?: (Object? value) -> bool
1591
+ end
1592
+
1593
+ # Namespace for the built-in coercion types ({Types::String}, {Types::Integer},
1594
+ # …) and the {Types::Base} contract that custom types implement.
1595
+ module Types
1596
+ # URL type — a {Types::String} that pre-fills the field's +format:+
1597
+ # option with a practical http(s) URL pattern. Same shape as
1598
+ # {Types::UUID}: bad-format input is a Phase 1 validation error,
1599
+ # not a coercion failure.
1600
+ #
1601
+ # The default pattern matches +http://+ and +https://+ URLs with a
1602
+ # non-empty host. It is intentionally pragmatic, not RFC 3986
1603
+ # exhaustive — users can override via +format: /.../+ if they need
1604
+ # different schemes or stricter host rules.
1605
+ class URL < FieldStruct::Types::String
1606
+ # Inherits +format:+ / +enum:+ from {Types::String}, advertising the
1607
+ # +:format+ presets this subtype resolves (see {.presets}).
1608
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1609
+
1610
+ # _@return_ — practical http(s) pattern; override per-field via +format:+
1611
+ def self.default_format: () -> Regexp
1612
+
1613
+ # _@return_ — named presets for +format:+
1614
+ def self.presets: () -> ::Hash[::Symbol, Regexp]
1615
+
1616
+ # Resolve a Symbol +format:+ preset (see {.presets}) to its Regexp at
1617
+ # field-declaration time; a user-supplied Regexp passes through.
1618
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
1619
+ end
1620
+
1621
+ # Abstract base for all FieldStruct type classes.
1622
+ #
1623
+ # Subclasses must implement {#coerce} and {#ruby_type}. They may override
1624
+ # {#missing?} when their notion of "missing" is broader than nil-only
1625
+ # (e.g. strings consider empty/whitespace missing, arrays consider empty
1626
+ # missing).
1627
+ class Base
1628
+ # Native per-field options this type understands, as a frozen Hash of
1629
+ # option-name (Symbol) → descriptor. Each descriptor is built by
1630
+ # {.option} and carries the accepted value classes, whether the option
1631
+ # is required at declaration time, and any Symbol presets it accepts.
1632
+ #
1633
+ # This merges {.own_option_schema} down the class chain so a subtype
1634
+ # inherits its parent's options for free (e.g. {Types::UUID} picks up
1635
+ # {Types::String}'s +format:+ / +enum:+). The merged result is frozen and
1636
+ # memoized per type class — a type's option vocabulary is fixed at load,
1637
+ # so this is computed once rather than on every field declaration.
1638
+ #
1639
+ # This is the single source of truth for both *validation* (the DSL
1640
+ # checks declared values against these descriptors) and *discoverability*
1641
+ # (an agent or human can ask a type what it accepts without reading
1642
+ # source). Options *not* listed here are treated as foreign and pass
1643
+ # through onto the Field untouched, available to downstream tooling.
1644
+ def self.option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1645
+
1646
+ # The options this type adds on top of its superclass's. Subclasses
1647
+ # override *this* (not {.option_schema}) to declare their native options;
1648
+ # the empty default means "adds nothing." {.option_schema} handles the
1649
+ # inheritance merge.
1650
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1651
+
1652
+ # Build a normalized descriptor for {.option_schema}.
1653
+ #
1654
+ # _@param_ `type` — the Ruby class(es) a declared value may be an instance of
1655
+ #
1656
+ # _@param_ `required` — whether the option must be present at field-declaration time
1657
+ #
1658
+ # _@param_ `presets` — Symbol preset names the option also accepts
1659
+ #
1660
+ # _@return_ — a frozen descriptor: +{ type:, required:, presets: }+
1661
+ def self.option: (_type: (::Class | ::Array[::Class]), ?required: bool, ?presets: ::Array[::Symbol]) -> ::Hash[::Symbol, Object]
1662
+
1663
+ # Resolve symbolic preset references on the per-field options hash
1664
+ # at field-declaration time. Default implementation passes options
1665
+ # through unchanged; concrete types override to interpret their
1666
+ # own options (e.g. +format: :iso8601+ → strftime string).
1667
+ #
1668
+ # _@param_ `options` — the per-field options hash, as written by the user
1669
+ #
1670
+ # _@return_ — possibly-modified options to store on the Field
1671
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
1672
+
1673
+ # Resolve a declared +format:+ into the form coercion and rendering use.
1674
+ #
1675
+ # Declared options are kept exactly as the user wrote them — see
1676
+ # {TimeFormatResolver} for why — so a Symbol preset name has to be
1677
+ # expanded somewhere, and the type owning the preset table is the only
1678
+ # thing that can do it. This default serves types with no preset table:
1679
+ # a String passes through, a Symbol is refused.
1680
+ #
1681
+ # _@param_ `format` — the format as declared
1682
+ def self.resolve_format: ((::String | ::Symbol)? format) -> ::String?
1683
+
1684
+ # Coerce an input value into the type's domain.
1685
+ #
1686
+ # The contract here is intentionally loose — subclasses override with
1687
+ # explicit kwargs for the options they consume (e.g. +format:+ for
1688
+ # Date, +round:+ for Float, +of_type:+ for Array), plus a trailing
1689
+ # +**+ catch-all so a call site that splats +field.options+ can pass
1690
+ # keys that some types ignore. See each subclass's +coerce+ for the
1691
+ # exact accepted options.
1692
+ #
1693
+ # _@param_ `value` — raw input — each subclass narrows this to the inputs it actually accepts
1694
+ #
1695
+ # _@return_ — the coerced value
1696
+ def coerce: (Object value) -> Object
1697
+
1698
+ # Whether the given value should be treated as "missing" for the purpose
1699
+ # of presence checks. The default is nil-only; subclasses override for
1700
+ # emptier notions.
1701
+ #
1702
+ # _@param_ `value`
1703
+ def missing?: (Object value) -> bool
1704
+
1705
+ # The native Ruby class a coerced value will be an instance of.
1706
+ # Used by the (deferred) RBS generator and for introspection.
1707
+ def ruby_type: () -> Class
1708
+
1709
+ # The RBS type expression {FieldStruct::RBS} emits for fields of this
1710
+ # type, or +nil+ (the default) to derive one from {#ruby_type}. Override
1711
+ # in a custom type whose values {#ruby_type} can't describe on its own —
1712
+ # typically a generic class, which RBS needs type arguments for:
1713
+ #
1714
+ # def ruby_type = ::Hash
1715
+ # def rbs_type = '::Hash[::Symbol, ::String]'
1716
+ #
1717
+ # Without it a bare generic +ruby_type+ is emitted with +untyped+
1718
+ # arguments (+::Hash[untyped, untyped]+). A field's own +rbs_type:+
1719
+ # option wins over this. Fully qualify constants (+::Hash+, not
1720
+ # +Hash+): the RBS is emitted inside the struct's module nesting.
1721
+ def rbs_type: () -> ::String?
1722
+
1723
+ # Concise type-instance representation. The default shows just the
1724
+ # type-class name (e.g. +#<FieldStruct::Types::String>+). Subclasses
1725
+ # that carry parameter state (Nested, Union, …) override to surface
1726
+ # what they wrap.
1727
+ def inspect: () -> ::String
1728
+
1729
+ # _@param_ `pp` — a PP-shaped sink (responds to +#text+)
1730
+ def pretty_print: (Object pp) -> void
1731
+ end
1732
+
1733
+ # Date type. Returns a +Date+ as-is, narrows a +DateTime+ or +Time+ down
1734
+ # to a plain Date, reads a String via +Date.parse+ (or +strptime+ when the
1735
+ # field declares a +format:+), and refuses everything else.
1736
+ #
1737
+ # Per-field +format:+ option (String or Symbol-preset) controls both
1738
+ # parsing (strptime when input is a String) and serialization
1739
+ # (strftime when emitting via +as_json+ / +to_json+). When no format
1740
+ # is set, the type uses +Date.parse+ for input and ISO-8601 for output.
1741
+ class Date < FieldStruct::Types::Base
1742
+ # Native options: a +format:+ strftime/strptime String (or Symbol
1743
+ # preset; see {.presets}) and an +in:+ Array or Range of allowed values.
1744
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1745
+
1746
+ # _@return_ — field-level strftime/strptime format; +nil+
1747
+ # leaves the type using ISO-8601 in/out
1748
+ def self.default_format: () -> ::String?
1749
+
1750
+ # Named presets for the +format:+ option.
1751
+ #
1752
+ # +:iso8601+ is an {Rfc3339Format} rather than a strftime String: the
1753
+ # contract cannot be written as one (see that class). The display presets
1754
+ # stay strftime-based — they are for humans, not wire formats.
1755
+ def self.presets: () -> ::Hash[::Symbol, Object]
1756
+
1757
+ # Resolve a Symbol +format:+ preset (see {.presets}) to its strftime
1758
+ # string at field-declaration time; other forms pass through.
1759
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
1760
+
1761
+ # Resolve a declared +format:+ (a Symbol preset name or a strftime
1762
+ # String) into the String that {#coerce} and JSON rendering actually
1763
+ # use. Declarations are stored exactly as written, so this is where a
1764
+ # preset name becomes usable. See {TimeFormatResolver}.
1765
+ #
1766
+ # _@param_ `format` — the format as declared
1767
+ #
1768
+ # _@return_ — +nil+ when the field declares no format
1769
+ def self.resolve_format: ((::String | ::Symbol)? format) -> ::String?
1770
+
1771
+ # Dispatch is on explicit stdlib classes, never on a +respond_to?+ probe
1772
+ # against the value. ActiveSupport defines +String#to_date+, so a probe
1773
+ # that is false on plain Ruby is TRUE under Rails, and the type would take
1774
+ # a different branch there than it does in its own suite — which is
1775
+ # exactly how a declared +format:+ came to be ignored under Rails while
1776
+ # every spec here passed. +Date.parse+ and +Date.strptime+ are not
1777
+ # redefined by ActiveSupport, so dispatching on them gives one behaviour
1778
+ # in both worlds.
1779
+ #
1780
+ # _@param_ `value` — +nil+ → +nil+; a String is read through +strptime+ when a +format:+ is set and +Date.parse+ otherwise; a +Date+ passes through; a +DateTime+ or +Time+ (including ActiveSupport's TimeWithZone) is narrowed to a plain +Date+. Anything else is refused.
1781
+ #
1782
+ # _@param_ `format` — strptime/strftime format; +nil+ uses {.default_format} (which itself defaults to +nil+ = ISO-8601 path)
1783
+ def coerce: ((::String | ::Date | ::DateTime | ::Time)? value, ?format: ::String?) -> ::Date?
1784
+
1785
+ # _@return_ — the top-level +::Date+ class
1786
+ def ruby_type: () -> Class
1787
+
1788
+ # _@param_ `value` — the raw string
1789
+ #
1790
+ # _@param_ `format` — strptime format, when the field declares one
1791
+ def parse_string: (::String value, ::String? format) -> ::Date
1792
+
1793
+ # +Date.parse+ and +Date.strptime+ build the Date straight from year,
1794
+ # month and day whenever the parts carry all three and neither a Julian
1795
+ # day (+:jd+) nor a day of the year (+:yday+) — date_core.c's fast path
1796
+ # in +d_new_by_frags+ — so this returns the same Date. Any other shape is
1797
+ # left to the stdlib (+nil+).
1798
+ #
1799
+ # _@param_ `parts` — already checked by {TemporalParser}
1800
+ def date_from_parts: (::Hash[::Symbol, Object] parts) -> ::Date?
1801
+ end
1802
+
1803
+ # Time type. Returns a +Time+ as-is, builds one from a +Date+ or
1804
+ # +DateTime+'s components, reads a String via +Time.parse+ (or +strptime+
1805
+ # when the field declares a +format:+), and refuses everything else.
1806
+ #
1807
+ # Per-field +format:+ option (String or Symbol-preset) controls both
1808
+ # parsing (strptime when input is a String) and serialization
1809
+ # (strftime via +as_json+).
1810
+ class Time < FieldStruct::Types::Base
1811
+ # Native options: a +format:+ strftime/strptime String (or Symbol
1812
+ # preset; see {.presets}), an +in:+ Array or Range of allowed values,
1813
+ # and a +zone:+ String (+"UTC"+, +"±HH:MM"+ or an IANA name; see {ZoneOption}).
1814
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1815
+
1816
+ def self.default_format: () -> ::String?
1817
+
1818
+ # Named presets for the +format:+ option.
1819
+ #
1820
+ # +:iso8601+ is an {Rfc3339Format} rather than a strftime String: +%z+
1821
+ # renders +\+0000+, which RFC 3339 rejects, and no strftime string can
1822
+ # express optional fractional seconds. The interchange and display
1823
+ # presets stay strftime-based.
1824
+ def self.presets: () -> ::Hash[::Symbol, Object]
1825
+
1826
+ # Validate a Symbol +format:+ preset (see {.presets}) and a +zone:+
1827
+ # (see {ZoneOption}) at field-declaration time.
1828
+ #
1829
+ # _@param_ `options` — the per-field options
1830
+ #
1831
+ # _@return_ — the options, unchanged
1832
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
1833
+
1834
+ # Resolve a declared +format:+ (a Symbol preset name or a strftime
1835
+ # String) into the String that {#coerce} and JSON rendering actually
1836
+ # use. Declarations are stored exactly as written, so this is where a
1837
+ # preset name becomes usable. See {TimeFormatResolver}.
1838
+ #
1839
+ # _@param_ `format` — the format as declared
1840
+ #
1841
+ # _@return_ — +nil+ when the field declares no format
1842
+ def self.resolve_format: ((::String | ::Symbol)? format) -> ::String?
1843
+
1844
+ # Dispatch is on explicit stdlib classes, never on a +respond_to?+ probe
1845
+ # against the value. ActiveSupport defines +String#to_time+, and it
1846
+ # returns +nil+ for a string with no time in it rather than raising — so
1847
+ # under Rails "not-a-time" was coerced to nil and reported VALID. The
1848
+ # probe is gone; +Time.parse+ and +Time.strptime+ behave the same with or
1849
+ # without ActiveSupport loaded.
1850
+ #
1851
+ # Cross-class conversion builds a Time from components rather than
1852
+ # calling +to_time+, which ActiveSupport redefines: under AS 7.2
1853
+ # +DateTime#to_time+ converts to the system-local zone and emits a
1854
+ # deprecation warning, while 8.x preserves the offset. Constructing from
1855
+ # parts gives the same instant and the same offset in all three
1856
+ # environments, which is what makes the behaviour host-independent rather
1857
+ # than merely documented.
1858
+ #
1859
+ # _@param_ `value` — +nil+ → +nil+; a String is read through +strptime+ when a +format:+ is set and +Time.parse+ otherwise; a +Time+ passes through; a +DateTime+, +Date+ or Time-like value (ActiveSupport's TimeWithZone) is converted. Anything else is refused.
1860
+ #
1861
+ # _@param_ `format` — strptime/strftime format; +nil+ uses {.default_format} (which itself defaults to +nil+ = ISO-8601 path)
1862
+ #
1863
+ # _@param_ `zone` — +"UTC"+, +"±HH:MM"+ or an IANA name: the offset for a String that names none, and for a +Date+'s midnight. Input carrying its own offset keeps it. +nil+ means the process zone. See {ZoneOption}.
1864
+ def coerce: ((::String | ::Time | ::Date | ::DateTime)? value, ?format: ::String?, ?zone: ::String?) -> ::Time?
1865
+
1866
+ # _@return_ — the top-level +::Time+ class
1867
+ def ruby_type: () -> Class
1868
+
1869
+ # _@param_ `value` — the raw string
1870
+ #
1871
+ # _@param_ `format` — strptime format, when the field declares one
1872
+ #
1873
+ # _@param_ `zone` — the field's +zone:+, if declared
1874
+ def parse_string: (::String value, ::String? format, ::String? zone) -> ::Time
1875
+
1876
+ # _@param_ `value` — the value to convert
1877
+ #
1878
+ # _@return_ — the same instant, at the same offset, sub-second
1879
+ # precision preserved
1880
+ def time_from_datetime: (::DateTime value) -> ::Time
1881
+ end
1882
+
1883
+ # UUID type — a {Types::String} that pre-fills the field's +format:+
1884
+ # option with a canonical RFC-4122 pattern. Bad-format input becomes
1885
+ # a Phase 1-style validation error (+"is invalid"+); coercion itself
1886
+ # still just hands back the string.
1887
+ #
1888
+ # Users can override the pattern by passing +format: /.../+ on the
1889
+ # field declaration.
1890
+ class UUID < FieldStruct::Types::String
1891
+ # Inherits +format:+ / +enum:+ from {Types::String}, advertising the
1892
+ # +:format+ presets this subtype resolves (see {.presets}).
1893
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1894
+
1895
+ # Hex-digit-only canonical RFC-4122 form. Permits any version.
1896
+ #
1897
+ # _@return_ — applied at the field level when no +format:+ is given
1898
+ def self.default_format: () -> Regexp
1899
+
1900
+ # _@return_ — named presets for +format:+
1901
+ def self.presets: () -> ::Hash[::Symbol, Regexp]
1902
+
1903
+ # Resolve a Symbol +format:+ preset (see {.presets}) to its Regexp at
1904
+ # field-declaration time; a user-supplied Regexp passes through.
1905
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
1906
+ end
1907
+
1908
+ # Array type. Coerces each element through the registered element
1909
+ # type named by the +:of+ DSL option (resolved at field-declaration
1910
+ # time and stashed as +options[:of_type]+ — a class, not a symbol).
1911
+ #
1912
+ # An optional +:element+ Hash carries the element type's own options
1913
+ # (+strict:+, +base:+, +format:+, …), passed to every element's coerce:
1914
+ #
1915
+ # optional :ids, :array, of: :integer, element: {strict: true, base: 16}
1916
+ #
1917
+ # A refused element is reported with its index — +element at index 2: …+ —
1918
+ # and several with all of theirs (see {.elements_at}).
1919
+ #
1920
+ # Missing: +nil+ or empty array. ruby_type: +::Array+.
1921
+ class Array < FieldStruct::Types::Base
1922
+ INDEXES_LISTED: ::Integer
1923
+
1924
+ # Native options: a required +of:+ naming the element type — a
1925
+ # registered Symbol or a Class (a +Types::Base+ or +FieldStruct::Base+
1926
+ # subclass) — and an +element:+ Hash of that element type's options.
1927
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
1928
+
1929
+ # The element type class behind an +of_type+ option, whether it holds
1930
+ # a class (+:integer+ → {Types::Integer}) or a built instance (a
1931
+ # {Types::Nested}). Its +option_schema+ is what +element:+ accepts.
1932
+ #
1933
+ # _@param_ `of_type` — an array field's +:of_type+ option
1934
+ #
1935
+ # _@return_ — a {Types::Base} subclass
1936
+ def self.element_class: ((Class | Types::Base) of_type) -> singleton(Types::Base)
1937
+
1938
+ # _@param_ `value` — anything else raises TypeError
1939
+ #
1940
+ # _@param_ `of_type` — the element type — a Types::Base subclass for stock types, or an already-built instance for parameterized types like {Types::Nested}. The DSL stashes this when the user writes +of: :string+ / +of: SomeStruct+. Required at the type level; nil here means the type was called directly without DSL help.
1941
+ #
1942
+ # _@param_ `element` — the element type's options, passed to every element's coerce (the DSL's +element:+, already validated against the element type's schema)
1943
+ #
1944
+ # _@return_ — each element coerced by +of_type+
1945
+ def coerce: (::Array[Object?]? value, ?of_type: (Class | Types::Base)?, ?element: ::Hash[::Symbol, Object?]?) -> ::Array[Object?]?
1946
+
1947
+ # Name a set of element indexes for an error message: one reads
1948
+ # +element at index 2+, several +elements at index 2, 5+, and past
1949
+ # {INDEXES_LISTED} the rest are counted (+… and 7 more+). Indexes only,
1950
+ # never elements, so a message carries no value.
1951
+ #
1952
+ # _@param_ `indexes` — ascending, at least one
1953
+ def self.elements_at: (::Array[::Integer] indexes) -> ::String
1954
+
1955
+ # _@param_ `value`
1956
+ #
1957
+ # _@return_ — +true+ when +nil+ or empty
1958
+ def missing?: (Object value) -> bool
1959
+
1960
+ # _@return_ — the top-level +::Array+ class
1961
+ def ruby_type: () -> Class
1962
+
1963
+ # Coerce the elements after the first refused one, so the error names
1964
+ # every refused index, then raise the first element's error with them.
1965
+ # The same message reaches every coercion_policy, :raise included —
1966
+ # finishing the scan costs a refused array no more than an accepted one.
1967
+ #
1968
+ # _@param_ `value` — the input array
1969
+ #
1970
+ # _@param_ `first` — the index of the first refused element
1971
+ #
1972
+ # _@param_ `error` — what the element type raised for it
1973
+ #
1974
+ # _@param_ `element_type` — the element type instance
1975
+ #
1976
+ # _@param_ `element` — element options
1977
+ def raise_refused!: (
1978
+ ::Array[Object?] value,
1979
+ ::Integer first,
1980
+ StandardError error,
1981
+ Types::Base element_type,
1982
+ ::Hash[::Symbol, Object?]? element
1983
+ ) -> void
1984
+
1985
+ # +of_type+ may be either a Types::Base subclass (the DSL form for
1986
+ # stock types like +:string+) or an already-built Types::Base
1987
+ # instance (the DSL form for parameterized types like
1988
+ # {Types::Nested}). Both produce a single cached instance per
1989
+ # configured element type.
1990
+ #
1991
+ # _@param_ `type_or_instance`
1992
+ def element_type_for: ((singleton(Types::Base) | Types::Base) type_or_instance) -> Types::Base
1993
+ end
1994
+
1995
+ # Email type — a {Types::String} that pre-fills the field's +format:+
1996
+ # option with a pragmatic email pattern. Same shape as
1997
+ # {Types::UUID} / {Types::URL}: bad-format input is a validation
1998
+ # error, not a coercion failure.
1999
+ #
2000
+ # The default pattern is deliberately not RFC 5322 strict — those
2001
+ # regexes are unwieldy and reject real-world addresses. It checks
2002
+ # +local@domain.tld+ shape with no internal whitespace or extra +@+
2003
+ # characters; users can override via +format: /.../+ for stricter
2004
+ # rules.
2005
+ class Email < FieldStruct::Types::String
2006
+ # Inherits +format:+ / +enum:+ from {Types::String}, advertising the
2007
+ # +:format+ presets this subtype resolves (see {.presets}).
2008
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2009
+
2010
+ # _@return_ — pragmatic +local@domain.tld+ shape, no internal
2011
+ # whitespace or extra +@+; deliberately not RFC 5322 strict.
2012
+ # Override per-field via +format:+ (Regexp or Symbol preset).
2013
+ def self.default_format: () -> Regexp
2014
+
2015
+ # _@return_ — named presets for +format:+
2016
+ def self.presets: () -> ::Hash[::Symbol, Regexp]
2017
+
2018
+ # Resolve a Symbol +format:+ preset (see {.presets}) to its Regexp at
2019
+ # field-declaration time; a user-supplied Regexp passes through.
2020
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
2021
+ end
2022
+
2023
+ # Float type. Coerces via +Kernel#Float+, which parses numeric strings
2024
+ # strictly and rejects anything non-numeric. Strings are base 10, as for
2025
+ # {Types::Integer}: a hex string like "0x1A", which +Kernel#Float+ would
2026
+ # read as 26.0, is refused.
2027
+ class Float < FieldStruct::Types::Base
2028
+ HEX_PREFIX: ::Regexp
2029
+
2030
+ # Native options: a +round:+ Integer precision and an +in:+ Array or
2031
+ # Range of allowed values.
2032
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2033
+
2034
+ # _@return_ — field-level rounding precision; +nil+ disables.
2035
+ def self.default_round: () -> ::Integer?
2036
+
2037
+ # _@param_ `value` — +nil+ → +nil+; everything else flows through +Kernel#Float+ — strict on string parsing, rejects non-numeric strings, Symbols, Booleans, Arrays, and hex strings ("0x1A").
2038
+ #
2039
+ # _@param_ `round` — decimal places, rounding half up (ties away from zero); +nil+ leaves the value unrounded. The result is a Float even at +round: 0+.
2040
+ #
2041
+ # _@return_ — +nil+ for +nil+ input, otherwise the parsed float
2042
+ def coerce: ((Numeric | ::String)? value, ?round: ::Integer?) -> ::Float?
2043
+
2044
+ # _@return_ — the top-level +::Float+ class
2045
+ def ruby_type: () -> Class
2046
+
2047
+ # _@param_ `value` — a non-Float value
2048
+ def parse: (Object value) -> ::Float
2049
+ end
2050
+
2051
+ # Union type — a value that may be any of several types. Each
2052
+ # member is tried in declared order; the first one whose +coerce+
2053
+ # doesn't raise wins. Member-coercion errors are caught broadly
2054
+ # (ArgumentError, TypeError, FieldStruct::Error) so a member's
2055
+ # rejection lets the next member have a try; unrelated bugs (e.g.
2056
+ # +NoMethodError+) still propagate.
2057
+ #
2058
+ # optional :payload, :union, of: [Payload, :boolean]
2059
+ #
2060
+ # If every member rejects the value, the union raises a +TypeError+
2061
+ # which the parent's +coercion_policy+ handles like any
2062
+ # shape-level coercion failure.
2063
+ #
2064
+ # Order matters: +of: [String, Integer]+ coerces "42" to "42"
2065
+ # (String wins first); +of: [Integer, String]+ coerces "42" to 42.
2066
+ class Union < FieldStruct::Types::Base
2067
+ RESCUABLE: ::Array[singleton(::StandardError)]
2068
+
2069
+ # Native options: a required +of:+ Array of member types (the DSL
2070
+ # additionally requires at least two members).
2071
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2072
+
2073
+ # _@param_ `member_types` — member instances, in priority order
2074
+ def initialize: (::Array[Types::Base] member_types) -> void
2075
+
2076
+ # _@param_ `value` — anything — each member type's coerce is tried in declared order; the first that doesn't raise wins
2077
+ #
2078
+ # _@return_ — whatever the first successful member returns
2079
+ def coerce: (Object? value) -> Object?
2080
+
2081
+ # _@param_ `value`
2082
+ #
2083
+ # _@return_ — true only for nil — union doesn't try to
2084
+ # collectively reason about "missing" across its members
2085
+ def missing?: (Object value) -> bool
2086
+
2087
+ # Flat list of member +ruby_type+ values. Members whose
2088
+ # ruby_type is already an Array (like Boolean's
2089
+ # +[TrueClass, FalseClass]+) get spliced in; duplicates are
2090
+ # removed.
2091
+ def ruby_type: () -> ::Array[Class]
2092
+
2093
+ def inspect: () -> ::String
2094
+
2095
+ # _@return_ — the configured member type instances
2096
+ attr_reader member_types: ::Array[Types::Base]
2097
+ end
2098
+
2099
+ # Value type. A no-coercion passthrough — whatever you give it comes
2100
+ # back unchanged. Useful as an escape hatch when you want a field
2101
+ # declaration to participate in metadata without committing to a
2102
+ # specific shape.
2103
+ class Value < FieldStruct::Types::Base
2104
+ # _@param_ `value` — anything — passthrough, no coercion
2105
+ #
2106
+ # _@return_ — the value, unchanged
2107
+ def coerce: (Object value) -> Object
2108
+
2109
+ # _@return_ — +Object+ — the broadest sensible class
2110
+ def ruby_type: () -> Class
2111
+ end
2112
+
2113
+ # Binary type — coerces input to a String and forces +ASCII-8BIT+
2114
+ # encoding. Intended for raw bytes (file contents, BLOBs, etc.)
2115
+ # where whitespace bytes (+\x20+, +\n+, etc.) are meaningful data,
2116
+ # not "missing" content.
2117
+ #
2118
+ # +missing?+ is therefore +nil+-or-empty only — unlike
2119
+ # {Types::String} which treats whitespace-only as missing.
2120
+ class Binary < FieldStruct::Types::String
2121
+ # _@param_ `value` — anything stringable; +nil+ stays +nil+
2122
+ #
2123
+ # _@return_ — +nil+ when input is +nil+, otherwise a
2124
+ # fresh ASCII-8BIT-encoded copy of +value.to_s+
2125
+ def coerce: (_ToS? value) -> ::String?
2126
+
2127
+ # Missing if nil or empty. Whitespace bytes are meaningful in
2128
+ # binary data, so they don't count as missing.
2129
+ #
2130
+ # _@param_ `value`
2131
+ def missing?: (Object value) -> bool
2132
+ end
2133
+
2134
+ # Type wrapper for nested FieldStruct fields.
2135
+ #
2136
+ # class Address < FieldStruct::Base
2137
+ # required :street, :string
2138
+ # required :city, :string
2139
+ # end
2140
+ #
2141
+ # class Person < FieldStruct::Base
2142
+ # required :address, Address
2143
+ # end
2144
+ #
2145
+ # Built and held per-Field by the DSL when it sees a class argument
2146
+ # that descends from {FieldStruct::Base}. Each instance is
2147
+ # parameterized by its target +struct_class+; instances are not
2148
+ # interchangeable across different nested classes.
2149
+ class Nested < FieldStruct::Types::Base
2150
+ # _@param_ `struct_class` — a FieldStruct::Base subclass
2151
+ def initialize: (Class struct_class) -> void
2152
+
2153
+ # Coerce input into a {#struct_class} instance.
2154
+ #
2155
+ # - +nil+ → +nil+
2156
+ # - already a {#struct_class} (or subclass) instance → passthrough
2157
+ # - +Hash+ → +struct_class.new(hash)+
2158
+ # - anything else → +TypeError+ (which the parent's coercion_policy
2159
+ # handles like any shape-level coercion failure)
2160
+ #
2161
+ # Errors raised inside +struct_class.new+ (e.g.
2162
+ # {UnknownAttributeError}, {CoercionError}) propagate to the caller
2163
+ # rather than being caught by the parent's policy — those are
2164
+ # structural rejections of the nested record's input, not parent-
2165
+ # shape coercion failures.
2166
+ #
2167
+ # _@param_ `value` — +nil+ → +nil+; an instance of {#struct_class} (or subclass) → passthrough; a +Hash+ → constructs via +struct_class.new(hash)+. Anything else raises TypeError.
2168
+ def coerce: ((FieldStruct::Base | ::Hash[(::Symbol | ::String), Object?])? value) -> FieldStruct::Base?
2169
+
2170
+ # _@param_ `value`
2171
+ #
2172
+ # _@return_ — true only when value is nil — an instance with
2173
+ # all required fields missing is not "missing" here, it is just
2174
+ # invalid. Invalidity is surfaced by the parent's validate_field.
2175
+ def missing?: (Object value) -> bool
2176
+
2177
+ # _@return_ — the +struct_class+ itself, so introspection sees
2178
+ # the actual nested type rather than a generic FieldStruct::Base
2179
+ def ruby_type: () -> Class
2180
+
2181
+ def inspect: () -> ::String
2182
+
2183
+ # _@return_ — the FieldStruct::Base subclass this type wraps
2184
+ attr_reader struct_class: Class
2185
+ end
2186
+
2187
+ # String type. Coerces any non-nil value via +to_s+; treats nil, empty,
2188
+ # and whitespace-only strings as missing. A field declared +strict: true+
2189
+ # accepts only scalars (String, Symbol, Integer) and objects
2190
+ # responding to +to_str+, so a Hash or Array is refused rather than stored
2191
+ # as its +inspect+ text.
2192
+ class String < FieldStruct::Types::Base
2193
+ # Native options: a +format:+ Regexp (or Symbol preset, used by the
2194
+ # +:uuid+ / +:url+ / +:email+ subtypes), an +enum:+ Array of
2195
+ # allowed values, and a +strict:+ Boolean.
2196
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2197
+
2198
+ # _@param_ `value` — anything stringable (everything in Ruby responds to +to_s+); +nil+ stays +nil+
2199
+ #
2200
+ # _@param_ `strict` — when true, refuse anything but a String, Symbol, Integer or +to_str+ object (a Float, BigDecimal or Rational is refused: its +to_s+ is +"0.12e2"+ / +"1/2"+, not the caller's text). Strict never changes what an accepted value becomes — it is still +value.to_s+.
2201
+ #
2202
+ # _@return_ — +nil+ when input is +nil+, otherwise +value.to_s+
2203
+ def coerce: (_ToS? value, ?strict: bool?) -> ::String?
2204
+
2205
+ # Missing if nil, empty, or whitespace-only.
2206
+ #
2207
+ # _@param_ `value`
2208
+ def missing?: (Object value) -> bool
2209
+
2210
+ # _@return_ — the top-level +::String+ class
2211
+ def ruby_type: () -> Class
2212
+
2213
+ # _@param_ `value` — a non-nil value
2214
+ def refuse_non_scalar!: (Object value) -> void
2215
+ end
2216
+
2217
+ # Symbol type. Accepts an existing +Symbol+ or a +String+. Anything else
2218
+ # raises +TypeError+, which the parent's +coercion_policy+ handles.
2219
+ #
2220
+ # Empty symbols (+:""+) are unusual; like other scalar non-string types,
2221
+ # +missing?+ is nil-only — an empty symbol counts as present.
2222
+ class Symbol < FieldStruct::Types::Base
2223
+ # Native options: an +enum:+ Array of allowed values (symbols are
2224
+ # string-like, so they share +enum:+ with {Types::String}).
2225
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2226
+
2227
+ # _@param_ `value` — anything else raises TypeError
2228
+ def coerce: ((::Symbol | ::String)? value) -> ::Symbol?
2229
+
2230
+ # _@return_ — the top-level +::Symbol+ class
2231
+ def ruby_type: () -> Class
2232
+ end
2233
+
2234
+ # Boolean type. Accepts the literal +true+/+false+, the strings
2235
+ # "true"/"false" (case-insensitive), and the integers/strings 1/0.
2236
+ # Anything else raises +ArgumentError+.
2237
+ #
2238
+ # Per-field +values:+ option customizes the truthy/falsy vocabulary:
2239
+ #
2240
+ # required :active, :boolean, values: {truthy: %w[on yes y],
2241
+ # falsy: %w[off no n]}
2242
+ # required :flag, :boolean, values: :english_yes_no
2243
+ #
2244
+ # Hash form takes +:truthy+ and +:falsy+ Arrays of strings (matched
2245
+ # case-insensitively). Symbol form names a built-in preset; see
2246
+ # {.presets} for the available names.
2247
+ #
2248
+ # Ruby has no Boolean class, so {#ruby_type} returns the pair
2249
+ # +[TrueClass, FalseClass]+.
2250
+ class Boolean < FieldStruct::Types::Base
2251
+ # Native options: a +values:+ Hash with +:truthy+ / +:falsy+ keys, or a
2252
+ # Symbol naming one of {.presets}.
2253
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2254
+
2255
+ # _@return_ — case-insensitive strings that coerce to true
2256
+ def self.default_truthy: () -> ::Array[::String]
2257
+
2258
+ # _@return_ — case-insensitive strings that coerce to false
2259
+ def self.default_falsy: () -> ::Array[::String]
2260
+
2261
+ # _@return_ — named presets for the +values:+ option
2262
+ def self.presets: () -> ::Hash[::Symbol, ::Hash[::Symbol, ::Array[::String]]]
2263
+
2264
+ # Resolve a Symbol +values:+ to its preset Hash at field-declaration
2265
+ # time. Pass-through for Hash form; raises for unknown preset names.
2266
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
2267
+
2268
+ # _@param_ `value` — accepts literal +true+/+false+; the integers +1+/+0+; or a String matched case-insensitively against the truthy/falsy vocabularies. Anything else raises ArgumentError.
2269
+ #
2270
+ # _@param_ `values` — +{truthy: [...], falsy: [...]}+ custom vocabulary; +nil+ uses {.default_truthy} / {.default_falsy}
2271
+ def coerce: ((bool | ::Integer | ::String)? value, ?values: ::Hash[::Symbol, ::Array[::String]]?) -> bool?
2272
+
2273
+ # _@return_ — +[TrueClass, FalseClass]+
2274
+ def ruby_type: () -> [Class, Class]
2275
+
2276
+ # The accepted vocabulary is declared schema, so it is listed; the
2277
+ # refused value is named by class only — it may be payments data.
2278
+ #
2279
+ # _@param_ `value` — the refused input
2280
+ #
2281
+ # _@param_ `accepted` — the truthy then falsy strings
2282
+ def refusal_message: (Object value, ::Array[::String] accepted) -> ::String
2283
+
2284
+ # _@param_ `values` — the field's +values:+
2285
+ #
2286
+ # _@return_ — +[truthy, falsy]+, downcased
2287
+ def truthy_and_falsy_for: (::Hash[::Symbol, ::Array[::String]]? values) -> [::Array[::String], ::Array[::String]]
2288
+ end
2289
+
2290
+ # Integer type. Strings parse in base 10 unless the field declares a
2291
+ # +base:+, so "010" is ten (not octal eight) and "0x1A" is refused rather
2292
+ # than read as hex. Float-shaped strings like "3.14" raise rather than
2293
+ # silently truncate. Float numerics, by contrast, are truncated — that
2294
+ # matches +Kernel#Integer+'s own behavior on numeric input — unless the
2295
+ # field declares +strict: true+, which refuses any non-integral value.
2296
+ class Integer < FieldStruct::Types::Base
2297
+ BASES: ::Range[::Integer]
2298
+ MAX_DECIMAL_DIGITS: ::Integer
2299
+
2300
+ # Native options: an +in:+ Array or Range of allowed values, a
2301
+ # +base:+ Integer radix (2..36) for string input, and a +strict:+
2302
+ # Boolean.
2303
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2304
+
2305
+ # Refuse a +base:+ outside 2..36 at field-declaration time. A
2306
+ # non-Integer +base:+ is left for the option-shape check to report.
2307
+ #
2308
+ # _@param_ `options` — the per-field options hash
2309
+ #
2310
+ # _@return_ — the options, unchanged
2311
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
2312
+
2313
+ # _@param_ `value` — +nil+ → +nil+; +Integer+ passes through; a String parses in +base+ (surrounding whitespace and digit underscores allowed; float-shaped or out-of-base strings raise ArgumentError); other numerics flow through +Kernel#Integer+ — Floats are truncated; Symbols/Booleans/Arrays raise TypeError.
2314
+ #
2315
+ # _@param_ `base` — radix (2..36) for String input; +nil+ means 10
2316
+ #
2317
+ # _@param_ `strict` — when true, refuse a value that is not a whole number (+12.7+, a +Time+) instead of truncating it; an integral one (+12.0+) still becomes its Integer
2318
+ #
2319
+ # _@return_ — +nil+ for +nil+ input, otherwise the parsed integer
2320
+ def coerce: ((::Integer | Numeric | ::String)? value, ?base: ::Integer?, ?strict: bool?) -> ::Integer?
2321
+
2322
+ # _@return_ — the top-level +::Integer+ class
2323
+ def ruby_type: () -> Class
2324
+
2325
+ # _@param_ `value`
2326
+ def refuse_huge_decimal!: (Object value) -> void
2327
+
2328
+ # _@param_ `value` — the input, already read by +Kernel#Integer+
2329
+ #
2330
+ # _@param_ `whole` — what +Kernel#Integer+ made of it
2331
+ def refuse_inexact!: (Object value, ::Integer whole) -> void
2332
+
2333
+ # _@param_ `value`
2334
+ #
2335
+ # _@param_ `base`
2336
+ def parse_string: (::String value, ::Integer base) -> ::Integer
2337
+ end
2338
+
2339
+ # DateTime type. Returns a +DateTime+ as-is, builds one from a +Date+ or
2340
+ # +Time+'s components, reads a String via +DateTime.parse+ (or +strptime+
2341
+ # when the field declares a +format:+), and refuses everything else.
2342
+ #
2343
+ # Per-field +format:+ option (String or Symbol-preset) controls both
2344
+ # parsing (strptime when input is a String) and serialization
2345
+ # (strftime via +as_json+).
2346
+ class DateTime < FieldStruct::Types::Base
2347
+ # Native options: a +format:+ strftime/strptime String (or Symbol
2348
+ # preset; see {.presets}), an +in:+ Array or Range of allowed values,
2349
+ # and a +zone:+ String (+"UTC"+, +"±HH:MM"+ or an IANA name; see {ZoneOption}).
2350
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2351
+
2352
+ # _@return_ — field-level strftime/strptime format; +nil+
2353
+ # leaves the type using ISO-8601 in/out
2354
+ def self.default_format: () -> ::String?
2355
+
2356
+ # Named presets for the +format:+ option.
2357
+ #
2358
+ # +:iso8601+ is an {Rfc3339Format} rather than a strftime String: +%z+
2359
+ # renders +\+0000+, which RFC 3339 rejects, and no strftime string can
2360
+ # express optional fractional seconds. The interchange and display
2361
+ # presets stay strftime-based.
2362
+ def self.presets: () -> ::Hash[::Symbol, Object]
2363
+
2364
+ # Validate a Symbol +format:+ preset (see {.presets}) and a +zone:+
2365
+ # (see {ZoneOption}) at field-declaration time.
2366
+ #
2367
+ # _@param_ `options` — the per-field options
2368
+ #
2369
+ # _@return_ — the options, unchanged
2370
+ def self.resolve_options: (::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
2371
+
2372
+ # Resolve a declared +format:+ (a Symbol preset name or a strftime
2373
+ # String) into the String that {#coerce} and JSON rendering actually
2374
+ # use. Declarations are stored exactly as written, so this is where a
2375
+ # preset name becomes usable. See {TimeFormatResolver}.
2376
+ #
2377
+ # _@param_ `format` — the format as declared
2378
+ #
2379
+ # _@return_ — +nil+ when the field declares no format
2380
+ def self.resolve_format: ((::String | ::Symbol)? format) -> ::String?
2381
+
2382
+ # Dispatch is on explicit stdlib classes, never on a +respond_to?+ probe
2383
+ # against the value: ActiveSupport defines +String#to_datetime+, so a
2384
+ # probe that is false on plain Ruby is TRUE under Rails.
2385
+ # +DateTime.parse+ and +DateTime.strptime+ are not redefined by
2386
+ # ActiveSupport. See {Types::Time#coerce} for why cross-class conversion
2387
+ # is built from components rather than +to_datetime+.
2388
+ #
2389
+ # _@param_ `value` — +nil+ → +nil+; a String is read through +strptime+ when a +format:+ is set and +DateTime.parse+ otherwise; a +DateTime+ passes through; a +Date+ becomes midnight and a +Time+ (including ActiveSupport's TimeWithZone) keeps its instant and offset. Anything else is refused.
2390
+ #
2391
+ # _@param_ `format` — strptime/strftime format; +nil+ uses {.default_format} (which itself defaults to +nil+ = ISO-8601 path)
2392
+ #
2393
+ # _@param_ `zone` — +"UTC"+, +"±HH:MM"+ or an IANA name: the offset for a String that names none, and for a +Date+'s midnight. Input carrying its own offset keeps it. +nil+ means UTC. See {ZoneOption}.
2394
+ def coerce: ((::String | ::DateTime | ::Date | ::Time)? value, ?format: ::String?, ?zone: ::String?) -> ::DateTime?
2395
+
2396
+ # _@return_ — the top-level +::DateTime+ class
2397
+ def ruby_type: () -> Class
2398
+
2399
+ # _@param_ `value` — the raw string
2400
+ #
2401
+ # _@param_ `format` — strptime format, when the field declares one
2402
+ #
2403
+ # _@param_ `zone` — the field's +zone:+, if declared
2404
+ def parse_string: (::String value, ::String? format, ::String? zone) -> ::DateTime
2405
+
2406
+ # _@param_ `value` — the value to convert
2407
+ #
2408
+ # _@return_ — the same instant, at the same offset, sub-second
2409
+ # precision preserved
2410
+ def datetime_from_time: (::Time value) -> ::DateTime
2411
+ end
2412
+
2413
+ # BigDecimal type. Accepts a {::BigDecimal}, +Integer+, +Float+, or a
2414
+ # numeric string. Float input is converted with +Float::DIG+ precision
2415
+ # to avoid the bare +BigDecimal(Float)+ "can't omit precision" error.
2416
+ class BigDecimal < FieldStruct::Types::Base
2417
+ # Native options: a +round:+ Integer precision and an +in:+ Array or
2418
+ # Range of allowed values.
2419
+ def self.own_option_schema: () -> ::Hash[::Symbol, ::Hash[::Symbol, Object]]
2420
+
2421
+ # _@return_ — the field-level rounding precision, or +nil+
2422
+ # for no rounding. Subclasses override.
2423
+ def self.default_round: () -> ::Integer?
2424
+
2425
+ # _@param_ `value` — +nil+ → +nil+; a +BigDecimal+ passes through; +Float+ uses +Float::DIG+ precision; everything else goes through +Kernel.BigDecimal(value.to_s)+ — non-numeric strings raise ArgumentError.
2426
+ #
2427
+ # _@param_ `round` — decimal places, rounding half up (ties away from zero) whatever the global +BigDecimal.mode+; +nil+ leaves the value unrounded. The result is a BigDecimal even at +round: 0+.
2428
+ def coerce: ((::BigDecimal | Numeric | ::String)? value, ?round: ::Integer?) -> ::BigDecimal?
2429
+
2430
+ # _@return_ — the top-level +::BigDecimal+ class
2431
+ def ruby_type: () -> Class
2432
+
2433
+ # _@param_ `value` — a value that is neither a BigDecimal nor a Float
2434
+ def parse: (Object value) -> ::BigDecimal
2435
+ end
2436
+
2437
+ # The +zone:+ option shared by {Time} and {DateTime}.
2438
+ #
2439
+ # A zone-less string (+"2026-10-22 10:00"+) names a wall-clock time, not an
2440
+ # instant: by default +:time+ reads it in the *process* zone and +:datetime+
2441
+ # in UTC, so the same input meant different instants on different servers.
2442
+ # +zone:+ supplies the missing offset, so it means one instant everywhere.
2443
+ #
2444
+ # It only fills in an offset that is MISSING. A string with an offset, a
2445
+ # +Time+, a +DateTime+ (and ActiveSupport's TimeWithZone) keep theirs; a
2446
+ # +Date+, which has none, becomes midnight in the zone.
2447
+ #
2448
+ # Accepted values are +"UTC"+, fixed offsets +"±HH:MM"+ and IANA
2449
+ # Area/Location names (+"America/Denver"+). A fixed offset is the same all
2450
+ # year. A name is read through tzinfo's zone database, so a wall-clock time
2451
+ # gets the offset in force at that date and an instant (an epoch) the
2452
+ # offset in force at that instant. tzinfo is required only when a field
2453
+ # declares a name.
2454
+ #
2455
+ # Daylight-saving edges, for a name:
2456
+ #
2457
+ # - A wall-clock time the zone skips (02:30 on a spring-forward day) is
2458
+ # refused, as a 30 February is: it names no instant. ActiveSupport moves
2459
+ # it an hour forward instead; FieldStruct does not guess.
2460
+ # - A wall-clock time the zone repeats (01:30 on a fall-back day) is read
2461
+ # at its daylight-time offset — the earlier instant — which is what
2462
+ # ActiveSupport's TimeWithZone picks, so a Rails app sees the same answer.
2463
+ # - A +Date+ whose midnight the zone skips becomes the first moment of that
2464
+ # day (01:00 in Santiago on its spring-forward day), as
2465
+ # +Date#in_time_zone+ does.
2466
+ #
2467
+ # The value returned is a plain +::Time+ (or +::DateTime+) carrying the
2468
+ # offset, never a +TZInfo::TimeWithOffset+; it does not carry the zone
2469
+ # itself, so arithmetic on it keeps that offset rather than following DST.
2470
+ #
2471
+ # Refused: abbreviations and legacy aliases with no +/+ (+"MST"+ is a fixed
2472
+ # -07:00 in the database, not Mountain time with DST) and Rails' friendly
2473
+ # names (+"Mountain Time (US & Canada)"+), which only exist when
2474
+ # ActiveSupport is loaded and so would make a declaration valid in one
2475
+ # process and invalid in another (invariant 7).
2476
+ module ZoneOption
2477
+ OFFSET: ::Regexp
2478
+
2479
+ # Refuse a +zone:+ value FieldStruct cannot read, at declaration. A
2480
+ # non-String is left for the option-shape check to report.
2481
+ #
2482
+ # _@param_ `type_name` — the type's short name, for the message
2483
+ #
2484
+ # _@param_ `options` — the per-field options
2485
+ #
2486
+ # _@return_ — the options, unchanged
2487
+ def self.validate!: (::String type_name, ::Hash[::Symbol, Object?] options) -> ::Hash[::Symbol, Object?]
2488
+
2489
+ # _@param_ `type_name` — the type's short name
2490
+ #
2491
+ # _@param_ `zone` — the refused +zone:+
2492
+ #
2493
+ # _@return_ — the declaration error, with a hint for the
2494
+ # shape of +zone+
2495
+ def self.refusal: (::String type_name, ::String zone) -> ::String
2496
+
2497
+ # Look up (and cache) a zone by name, requiring tzinfo on first use.
2498
+ #
2499
+ # _@param_ `type_name` — the type's short name, for the message
2500
+ #
2501
+ # _@param_ `zone` — an IANA name
2502
+ #
2503
+ # _@return_ — the +TZInfo::Timezone+
2504
+ def self.load_timezone: (::String type_name, ::String zone) -> Object
2505
+
2506
+ # _@param_ `zone` — an IANA name {.validate!} accepted
2507
+ #
2508
+ # _@return_ — the cached +TZInfo::Timezone+
2509
+ def self.timezone: (::String zone) -> Object
2510
+
2511
+ # _@param_ `zone` — a declared +zone:+
2512
+ #
2513
+ # _@return_ — seconds east of UTC, or +nil+ when +zone+ is
2514
+ # not a value {.validate!} accepts
2515
+ def self.offset_seconds: (::String zone) -> ::Integer?
2516
+
2517
+ # Whether parsed fragments leave the offset to +zone:+.
2518
+ #
2519
+ # _@param_ `parts` — fragments from +Date._parse+ or +Date._strptime+
2520
+ #
2521
+ # _@return_ — +true+ when the string named no offset at all
2522
+ def self.zoneless?: (::Hash[::Symbol, Object] parts) -> bool
2523
+
2524
+ # Read a zone-less String as a wall-clock time in +zone+.
2525
+ #
2526
+ # _@param_ `value` — a string {.zoneless?} accepted
2527
+ #
2528
+ # _@param_ `format` — a resolved strptime format, or +nil+ for the no-format path (which has already required a whole day)
2529
+ #
2530
+ # _@param_ `zone` — a declared +zone:+
2531
+ #
2532
+ # _@param_ `parts` — the fragments the string parsed to; an epoch (+%s+ / +%Q+) shows up as +:seconds+
2533
+ #
2534
+ # _@return_ — at +zone+'s offset (+utc?+ for +"UTC"+)
2535
+ def self.time_at: (
2536
+ ::String value,
2537
+ ::String? format,
2538
+ ::String zone,
2539
+ ::Hash[::Symbol, Object] parts
2540
+ ) -> ::Time
2541
+
2542
+ # {.time_at} for an IANA name: the offset depends on the date.
2543
+ #
2544
+ # _@param_ `value` — the zone-less string
2545
+ #
2546
+ # _@param_ `format` — a resolved strptime format, or +nil+
2547
+ #
2548
+ # _@param_ `timezone` — a +TZInfo::Timezone+
2549
+ #
2550
+ # _@param_ `parts` — the parsed fragments
2551
+ def self.named_time_at: (
2552
+ ::String value,
2553
+ ::String? format,
2554
+ Object timezone,
2555
+ ::Hash[::Symbol, Object] parts
2556
+ ) -> ::Time
2557
+
2558
+ # A wall-clock time in a named zone.
2559
+ #
2560
+ # _@param_ `timezone` — a +TZInfo::Timezone+
2561
+ #
2562
+ # _@param_ `civil` — year, month, day, hour, minute and seconds (with any fraction)
2563
+ #
2564
+ # _@return_ — at the offset in force at that wall-clock time
2565
+ def self.local: (Object timezone, ::Array[::Numeric] civil) -> ::Time
2566
+
2567
+ # _@param_ `time` — an instant
2568
+ #
2569
+ # _@param_ `timezone` — a +TZInfo::Timezone+
2570
+ #
2571
+ # _@return_ — the same instant at the zone's offset at that instant
2572
+ def self.in_zone: (::Time time, Object timezone) -> ::Time
2573
+
2574
+ # _@param_ `value` — a plain Date
2575
+ #
2576
+ # _@param_ `zone` — a declared +zone:+
2577
+ #
2578
+ # _@return_ — midnight at +zone+'s offset — for a name, the first
2579
+ # moment of that day
2580
+ def self.midnight: (::Date value, ::String zone) -> ::Time
2581
+
2582
+ # _@param_ `value` — a plain Date
2583
+ #
2584
+ # _@param_ `timezone` — a +TZInfo::Timezone+
2585
+ #
2586
+ # _@return_ — the first moment of that day in the zone
2587
+ def self.named_midnight: (::Date value, Object timezone) -> ::Time
2588
+
2589
+ # _@param_ `seconds` — seconds east of UTC
2590
+ #
2591
+ # _@return_ — +"+HH:MM"+ / +"-HH:MM"+
2592
+ def self.offset_text: (::Integer seconds) -> ::String
2593
+ end
2594
+
2595
+ # RFC 3339 — the profile of ISO 8601 that "ISO-8601" means in an API
2596
+ # contract, and what JSON Schema's +date-time+ refers to. This is what the
2597
+ # +:iso8601+ preset resolves to for {Date}, {Time} and {DateTime}.
2598
+ #
2599
+ # FieldStruct parses and renders it itself, for two reasons.
2600
+ #
2601
+ # **It cannot be written as a strftime string.** +%z+ renders +\+0000+,
2602
+ # which RFC 3339 rejects — so the preset named +iso8601+ used to emit a
2603
+ # value its own documented format would refuse. And no strftime string can
2604
+ # express "optional fractional seconds", which most JSON APIs emit.
2605
+ #
2606
+ # **The stdlib helpers disagree with each other.** Routing +Time+ through
2607
+ # +Time.iso8601+ and +DateTime+ through +DateTime.iso8601+ would give the
2608
+ # same declared preset two different contracts:
2609
+ #
2610
+ # Time.iso8601('2026-02-30T10:30:00Z') # => 2026-03-02 10:30 UTC
2611
+ # DateTime.iso8601('2026-02-30T10:30:00Z') # => refused
2612
+ # Time.iso8601('2026-07-03T10:30Z') # => refused
2613
+ # DateTime.iso8601('2026-07-03T10:30Z') # => accepted
2614
+ # DateTime.iso8601('10:30:00Z') # => TODAY at 10:30
2615
+ #
2616
+ # +Time.iso8601+ reading 30 February as 2 March is the exact
2617
+ # silently-wrong-day defect this release exists to remove, so it cannot be
2618
+ # the thing that implements the strict preset.
2619
+ #
2620
+ # The pattern is deliberately narrow: fixed-width fields, a mandatory
2621
+ # offset, and no week dates, ordinal dates or basic (unseparated) forms.
2622
+ # That narrowness is the point — it is what lets a caller replace a
2623
+ # hand-rolled +/\A\d{4}-\d{2}-\d{2}\z/+ with a declared format.
2624
+ class Rfc3339Format
2625
+ DATE: ::Regexp
2626
+ TIMESTAMP: ::Regexp
2627
+
2628
+ # _@return_ — the date-only form, for {Types::Date}
2629
+ def self.date: () -> Rfc3339Format
2630
+
2631
+ # _@return_ — the timestamp form, for {Types::Time} and {Types::DateTime}
2632
+ def self.timestamp: () -> Rfc3339Format
2633
+
2634
+ # _@param_ `kind` — +:date+ or +:timestamp+
2635
+ def initialize: (::Symbol kind) -> void
2636
+
2637
+ # _@return_ — the preset name this format implements
2638
+ def name: () -> ::Symbol
2639
+
2640
+ # The accepted shape, as a regular expression whose source is also valid
2641
+ # as an ECMA pattern — so a schema or documentation generator can publish
2642
+ # the enforced contract rather than a prose approximation of it.
2643
+ def pattern: () -> ::Regexp
2644
+
2645
+ # _@return_ — the format's name, for error messages and inspection
2646
+ def to_s: () -> ::String
2647
+
2648
+ # Parse a String into date/time fragments.
2649
+ #
2650
+ # _@param_ `value` — the raw string
2651
+ #
2652
+ # _@return_ — +:year+, +:mon+, +:mday+ and, for a
2653
+ # timestamp, +:hour+, +:min+, +:sec+, +:sec_fraction+, +:offset+
2654
+ def parse: (::String value) -> ::Hash[::Symbol, Object]
2655
+
2656
+ # Render a coerced value back to RFC 3339.
2657
+ #
2658
+ # _@param_ `value` — the coerced value
2659
+ def render: ((::Date | ::Time | ::DateTime) value) -> ::String
2660
+
2661
+ # _@param_ `match` — a {DATE} match
2662
+ def date_parts: (::MatchData match) -> ::Hash[::Symbol, ::Integer]
2663
+
2664
+ # _@param_ `match` — a {TIMESTAMP} match
2665
+ def timestamp_parts: (::MatchData match) -> ::Hash[::Symbol, Object]
2666
+
2667
+ # _@param_ `text` — the +.digits+ capture, if any
2668
+ #
2669
+ # _@return_ — seconds after the whole second, exact
2670
+ def fraction_from: (::String? text) -> ::Rational
2671
+
2672
+ # _@param_ `text` — the offset capture: +Z+ or +±hh:mm+ / +±hhmm+
2673
+ #
2674
+ # _@return_ — seconds east of UTC
2675
+ def offset_from: (::String text) -> ::Integer
2676
+
2677
+ # _@param_ `value`
2678
+ #
2679
+ # _@return_ — +".123"+, or empty when the value is on a whole second
2680
+ def fraction_of: ((::Time | ::DateTime) value) -> ::String
2681
+
2682
+ # _@param_ `value`
2683
+ #
2684
+ # _@return_ — +"Z"+ for UTC, otherwise +"+HH:MM"+
2685
+ def offset_of: ((::Time | ::DateTime) value) -> ::String
2686
+
2687
+ # _@return_ — +:date+ or +:timestamp+
2688
+ attr_reader kind: ::Symbol
2689
+ end
2690
+
2691
+ # Generic preset resolution for a single named option on a single
2692
+ # type. Symbol values are looked up in the supplied presets hash;
2693
+ # anything else passes through. Unknown preset names raise
2694
+ # +ArgumentError+ at field-declaration time.
2695
+ #
2696
+ # Used by string-shaped types (Email/UUID/URL) for +format:+ and by
2697
+ # time-shaped types (Date/Time/DateTime) via {TimeFormatResolver}
2698
+ # (which adds a stricter set of allowed value types).
2699
+ module PresetResolver
2700
+ # Resolve +options[key]+ when it's a Symbol preset name, leaving other
2701
+ # values untouched.
2702
+ #
2703
+ # _@param_ `options` — the per-field options
2704
+ #
2705
+ # _@param_ `key` — the option to resolve (e.g. +:format+)
2706
+ #
2707
+ # _@param_ `presets` — preset name → concrete value
2708
+ #
2709
+ # _@param_ `label` — used in the unknown-preset error message
2710
+ #
2711
+ # _@return_ — options with +key+ resolved (a copy when changed)
2712
+ def self.call: (
2713
+ ::Hash[::Symbol, Object?] options,
2714
+ ::Symbol key,
2715
+ ::Hash[::Symbol, Object] presets,
2716
+ label: ::String
2717
+ ) -> ::Hash[::Symbol, Object?]
2718
+ end
2719
+
2720
+ # Shared String-parsing rules for {Date}, {Time} and {DateTime}.
2721
+ #
2722
+ # Three rules the stdlib parsers leave to their caller. Each of them is a
2723
+ # case where the stdlib answers a question it was not asked, confidently.
2724
+ #
2725
+ # **Anchoring.** +strptime+ matches a PREFIX and discards the rest.
2726
+ # +Date.strptime('2026-07-031', '%Y-%m-%d')+ returns the 3rd and throws the
2727
+ # trailing +1+ away; +'2026-07-03T10:30:00+0000-NONSENSE'+ parses clean
2728
+ # under the +iso8601+ preset. A declared +format:+ is a contract about the
2729
+ # whole string, so anything left over is refused. (Leading whitespace needs
2730
+ # no rule — +Date._strptime+ already returns +nil+ for it.)
2731
+ #
2732
+ # **Civil validity.** +Date.strptime+ and +DateTime.strptime+ refuse the
2733
+ # 30th of February. +Time+ does not: +Time.parse+, +Time.strptime+ and
2734
+ # +Time.new+ all roll it forward to 2 March and say nothing. All three
2735
+ # types refuse it here.
2736
+ #
2737
+ # **Completeness.** With no declared format the stdlib parsers fill in
2738
+ # whatever the string omits, from *today*: +Time.parse('10:30')+ is today
2739
+ # at 10:30, +'July'+ is the 1st of July this year, +'12'+ is the 12th of
2740
+ # this month. A value whose meaning depends on when it was parsed is not a
2741
+ # value, so a string with no declared format must name a whole day.
2742
+ #
2743
+ # Anchoring is trailing-junk rejection and nothing more. Widths, sign and
2744
+ # case stay strptime-lenient for a hand-written format: +%m+ and +%d+
2745
+ # accept one or two digits, which is strptime's contract rather than a
2746
+ # defect. Strictness for the built-in presets is a separate concern.
2747
+ module TemporalParser
2748
+ # Validate a String against a declared format.
2749
+ #
2750
+ # _@param_ `value` — the raw string
2751
+ #
2752
+ # _@param_ `format` — a resolved strftime/strptime format
2753
+ #
2754
+ # _@return_ — the parsed fragments
2755
+ def self.strptime_parts: (::String value, ::String format) -> ::Hash[::Symbol, Object]
2756
+
2757
+ # Validate a String with no declared format.
2758
+ #
2759
+ # _@param_ `value` — the raw string
2760
+ #
2761
+ # _@return_ — the parsed fragments
2762
+ def self.parse_parts: (::String value) -> ::Hash[::Symbol, Object]
2763
+
2764
+ # _@param_ `parts` — fragments from +Date._parse+
2765
+ def self.ensure_whole_day!: (::Hash[::Symbol, Object] parts) -> void
2766
+
2767
+ # _@param_ `parts` — fragments from +Date._parse+ or +Date._strptime+
2768
+ def self.ensure_real_day!: (::Hash[::Symbol, Object] parts) -> void
2769
+ end
2770
+
2771
+ # Like {Types::String}, but the coerced value is frozen. Useful when
2772
+ # downstream code must not mutate a stored string.
2773
+ class ImmutableString < FieldStruct::Types::String
2774
+ # _@param_ `value` — anything stringable; +nil+ stays +nil+
2775
+ #
2776
+ # _@return_ — frozen string, or +nil+ if input is +nil+
2777
+ def coerce: (_ToS? value) -> ::String?
2778
+ end
2779
+
2780
+ # Shared +format:+ option resolution for {Date}/{Time}/{DateTime}.
2781
+ # Each of those types calls +TimeFormatResolver.call(options, presets)+
2782
+ # from its +resolve_options+ class method.
2783
+ #
2784
+ # A declared +format:+ is VALIDATED at declaration time and kept exactly as
2785
+ # the user wrote it; it is resolved to a strftime string later, at coerce
2786
+ # and render time, by {.resolve}.
2787
+ #
2788
+ # Keeping the declaration is the point. The Symbol used to be overwritten
2789
+ # with the strftime String it expands to, which destroyed the only fact a
2790
+ # documentation generator actually wants — that the field is +:iso8601+,
2791
+ # not some anonymous String of percent-escapes. Recovering the name by
2792
+ # reverse-mapping the String worked only while no two presets shared a
2793
+ # value, which is luck rather than a contract.
2794
+ module TimeFormatResolver
2795
+ # Validate a declared +format:+ and return the options unchanged.
2796
+ #
2797
+ # _@param_ `options` — the per-field options as written
2798
+ #
2799
+ # _@param_ `presets` — the type's preset table
2800
+ #
2801
+ # _@return_ — +options+, unmodified
2802
+ def self.call: (::Hash[::Symbol, Object?] options, ::Hash[::Symbol, ::String] presets) -> ::Hash[::Symbol, Object?]
2803
+
2804
+ # Resolve a declared +format:+ to a strftime/strptime String.
2805
+ #
2806
+ # _@param_ `format` — as declared
2807
+ #
2808
+ # _@param_ `presets` — the type's preset table
2809
+ #
2810
+ # _@return_ — +nil+ when no format is declared
2811
+ def self.resolve: ((::String | ::Symbol)? format, ::Hash[::Symbol, ::String] presets) -> ::String?
2812
+ end
2813
+ end
2814
+
2815
+ # Minimal ActiveModel::Name-shaped value for {Base.model_name}.
2816
+ #
2817
+ # We mirror only what Rails-adjacent callers commonly reach for:
2818
+ # +name+, +singular+, +plural+, +element+, and string coercion.
2819
+ # No ActiveModel code is reused; pluralization is naive +s+-suffix
2820
+ # English. If a richer Name is needed, a downstream gem can build on
2821
+ # this without changing the surface.
2822
+ class ModelName
2823
+ # _@param_ `class_name`
2824
+ def initialize: (String? class_name) -> void
2825
+
2826
+ def to_s: () -> String
2827
+
2828
+ # _@param_ `str` — a bare class name
2829
+ #
2830
+ # _@return_ — its snake_case form
2831
+ def camel_to_snake: (String str) -> String
2832
+
2833
+ # _@return_ — the fully-qualified class name (or "" for anonymous classes)
2834
+ attr_reader name: String
2835
+
2836
+ # _@return_ — last segment, snake_case (e.g. "user_account")
2837
+ attr_reader singular: String
2838
+
2839
+ # _@return_ — naive +s+-suffixed plural of {#singular}
2840
+ attr_reader plural: String
2841
+
2842
+ # _@return_ — same as {#singular} (the AM convention for last segment)
2843
+ attr_reader element: String
2844
+ end
2845
+ end