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.
- checksums.yaml +7 -0
- data/AGENTS.md +58 -0
- data/CHANGELOG.md +653 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +645 -0
- data/UPGRADING.md +743 -0
- data/USAGE.md +820 -0
- data/docs/getting_started.md +337 -0
- data/lib/field_struct/base.rb +1316 -0
- data/lib/field_struct/error.rb +48 -0
- data/lib/field_struct/errors.rb +103 -0
- data/lib/field_struct/field.rb +179 -0
- data/lib/field_struct/metadata.rb +331 -0
- data/lib/field_struct/model_name.rb +48 -0
- data/lib/field_struct/rbs.rb +299 -0
- data/lib/field_struct/registry.rb +85 -0
- data/lib/field_struct/scaffold.rb +331 -0
- data/lib/field_struct/types/array.rb +156 -0
- data/lib/field_struct/types/base.rb +162 -0
- data/lib/field_struct/types/big_decimal.rb +78 -0
- data/lib/field_struct/types/binary.rb +38 -0
- data/lib/field_struct/types/boolean.rb +126 -0
- data/lib/field_struct/types/date.rb +158 -0
- data/lib/field_struct/types/datetime.rb +163 -0
- data/lib/field_struct/types/email.rb +49 -0
- data/lib/field_struct/types/float.rb +80 -0
- data/lib/field_struct/types/immutable_string.rb +18 -0
- data/lib/field_struct/types/integer.rb +133 -0
- data/lib/field_struct/types/nested.rb +82 -0
- data/lib/field_struct/types/preset_resolver.rb +37 -0
- data/lib/field_struct/types/rfc3339_format.rb +197 -0
- data/lib/field_struct/types/string.rb +73 -0
- data/lib/field_struct/types/symbol.rb +38 -0
- data/lib/field_struct/types/temporal_parser.rb +93 -0
- data/lib/field_struct/types/time.rb +168 -0
- data/lib/field_struct/types/time_format_resolver.rb +54 -0
- data/lib/field_struct/types/union.rb +96 -0
- data/lib/field_struct/types/url.rb +46 -0
- data/lib/field_struct/types/uuid.rb +46 -0
- data/lib/field_struct/types/value.rb +24 -0
- data/lib/field_struct/types/zone_option.rb +267 -0
- data/lib/field_struct/version.rb +7 -0
- data/lib/field_struct.rb +159 -0
- data/sig/field_struct.rbs +2845 -0
- data/skills/field-struct/SKILL.md +183 -0
- 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
|