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,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: field-struct
|
|
3
|
+
description: Author, edit, and debug FieldStruct value classes in projects that depend on the field_struct gem — choosing field types and options, wiring coercion and validation, reading errors, and generating RBS for the models.
|
|
4
|
+
when_to_use: Use when defining or modifying a class that inherits from FieldStruct::Base (any field / required / optional declaration), when picking a field type or option, when debugging why an instance is invalid or a value didn't coerce, or when wiring JSON serialization or cross-field validation with field_struct.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Working with FieldStruct
|
|
8
|
+
|
|
9
|
+
FieldStruct builds typed POROs: declare fields on a `FieldStruct::Base` subclass and
|
|
10
|
+
get coerced, typed accessors plus presence/format validation. **Construction and
|
|
11
|
+
validity are separate** — you can build an invalid instance; ask `valid?`.
|
|
12
|
+
|
|
13
|
+
## Read the bundled reference first
|
|
14
|
+
|
|
15
|
+
The gem ships a dense, example-first reference covering *every* type, option, and
|
|
16
|
+
macro. Read it before answering a non-trivial question — it matches the installed
|
|
17
|
+
version:
|
|
18
|
+
|
|
19
|
+
- Run `bundle show field_struct` and open `USAGE.md` at that path, or
|
|
20
|
+
- https://github.com/Paymentbox-com/field_struct/blob/main/USAGE.md
|
|
21
|
+
|
|
22
|
+
This skill is the **workflow and the mistakes to avoid**; `USAGE.md` is the catalog.
|
|
23
|
+
|
|
24
|
+
## Defining a model
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class User < FieldStruct::Base
|
|
28
|
+
required :name, :string # presence-checked
|
|
29
|
+
optional :age, :integer # not presence-checked
|
|
30
|
+
optional :email, :email # format-validated string subtype
|
|
31
|
+
optional :tags, :array, of: :string # arrays need `of:`
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
u = User.new(name: 'Alice', age: '30') # Hash arg; Symbol or String keys both work
|
|
35
|
+
u.age # => 30 (coerced from "30")
|
|
36
|
+
u.valid? # => true
|
|
37
|
+
u.attributes # => { name: "Alice", age: 30, email: nil, tags: nil }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Steps when asked to build one:
|
|
41
|
+
1. Subclass `FieldStruct::Base`.
|
|
42
|
+
2. Use `required` for presence-checked fields, `optional` (or bare `field`) otherwise.
|
|
43
|
+
3. Pick a **registered type symbol** — `:string :integer :float :big_decimal`
|
|
44
|
+
(`:decimal`) `:boolean :date :time :datetime :symbol :uuid :url :email :binary
|
|
45
|
+
:value :array :union` — or pass another `FieldStruct::Base` subclass for a nested
|
|
46
|
+
struct.
|
|
47
|
+
4. Add options (see scoping below). Common: `default:` (literal or a callable like
|
|
48
|
+
`-> { Time.now }`), `desc:`.
|
|
49
|
+
5. For composites: `:array` requires `of: <type>`; `:union` requires `of: [a, b, …]`
|
|
50
|
+
with ≥2 members; nested = pass the class.
|
|
51
|
+
|
|
52
|
+
## Mistakes to avoid (these are the ones that bite)
|
|
53
|
+
|
|
54
|
+
1. **Setters are permissive — don't "fix" them.** `user.age = "30"` is *valid*; the
|
|
55
|
+
type coerces. Readers are typed; writers accept anything coercible. `nil` passes
|
|
56
|
+
through as `nil` for nearly every type.
|
|
57
|
+
2. **`required` does NOT guarantee non-nil at runtime.** A required field can hold
|
|
58
|
+
`nil` (coercion failed, or `:replace` policy, or never assigned). Validity is
|
|
59
|
+
separate from construction — gate on `valid?`, never assume a required reader is
|
|
60
|
+
present.
|
|
61
|
+
3. **Whitespace-only strings count as "missing"** for `:string` and its subtypes
|
|
62
|
+
(`:uuid :url :email :immutable_string`). So `required :name, :string` with
|
|
63
|
+
`name: " "` is **invalid**. `:binary` is the exception (its whitespace bytes are
|
|
64
|
+
real data — only nil/empty are missing).
|
|
65
|
+
4. **`:array` needs `of:`; `:union` needs `of: [...]` (≥2).** Omitting raises
|
|
66
|
+
`ArgumentError` at class-definition time.
|
|
67
|
+
5. **Option scoping is enforced at declaration** (misuse raises `ArgumentError`):
|
|
68
|
+
- `format:` → string-shaped + `:date`/`:time`/`:datetime` (regex, strftime string, or preset Symbol)
|
|
69
|
+
- `enum:` → `:string`/`:symbol` (and subtypes) only — an `Array` of allowed values
|
|
70
|
+
- `in:` → `:integer`/`:float`/`:decimal`/`:date`/`:time`/`:datetime` — an `Array` or `Range`
|
|
71
|
+
- `round:` → `:float`/`:decimal` (Integer places, half up; keeps the field's type)
|
|
72
|
+
- `base:` → `:integer` (radix 2..36 for strings; default 10, so `"010"` is 10)
|
|
73
|
+
- `values:` → `:boolean` (a `{truthy:, falsy:}` Hash or a preset Symbol)
|
|
74
|
+
- `strict:` → `:string` (and subtypes) / `:integer` (Boolean — refuse loose input instead of `to_s` / truncating)
|
|
75
|
+
- `zone:` → `:time`/`:datetime` (`"UTC"`, `"±HH:MM"` or an IANA name like `"America/Denver"` for zone-less strings; no Rails names or abbreviations)
|
|
76
|
+
- `sensitive:` → any type (mask the value in `inspect`)
|
|
77
|
+
- `element:` → `:array` (a Hash of the element type's own options, e.g.
|
|
78
|
+
`of: :integer, element: {strict: true}`; bad elements are reported by index — every one, the first five listed)
|
|
79
|
+
6. **Cross-field rules use `validate` + `errors.add(:base, …)`**, run at `valid?`.
|
|
80
|
+
`errors[:base]` is cleared each `valid?`; per-field errors are owned by the setter.
|
|
81
|
+
```ruby
|
|
82
|
+
validate :ends_after_start
|
|
83
|
+
def ends_after_start
|
|
84
|
+
errors.add(:base, 'end before start') if start && finish && finish < start
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
7. **JSON**: `serialize :json, internal: 'externalKey'` maps keys; `as_json`/`to_json`
|
|
88
|
+
apply it forward, `Klass.from_json(str)` reverse-maps. Everything round-trips
|
|
89
|
+
**except `:value`** (no type info to restore). Uses **Oj**, not stdlib JSON.
|
|
90
|
+
8. **Unknown input keys are dropped by default** (`unknown_attributes :ignore`); set
|
|
91
|
+
`unknown_attributes :raise` on the class to reject them. Do this at payment/API
|
|
92
|
+
boundaries, on **every** struct that reads input: nested classes don't inherit it
|
|
93
|
+
from the struct that holds them.
|
|
94
|
+
9. **Messages never echo values** (payments data stays out of logs): errors and raised
|
|
95
|
+
errors name the field, expected type and input *class*. `sensitive: true` (any
|
|
96
|
+
type) shows `[FILTERED]` in `inspect`/`pp`; readers, `to_h`, `as_json`/`to_json`
|
|
97
|
+
still return the real value.
|
|
98
|
+
10. **Validity is computed on assignment.** Mutating a nested struct or array element
|
|
99
|
+
in place (`rec.address.city = nil`, `rec.tags << 5`) doesn't refresh the parent's
|
|
100
|
+
errors, and an in-place push isn't coerced. Assign the field again (or use
|
|
101
|
+
`rec.with(address: ...)`) to re-validate.
|
|
102
|
+
11. **Immutable/frozen instances change by copy**: `rec.with(field: value)` returns a
|
|
103
|
+
new instance; changed fields run the setter pipeline. Shallow, like `dup`.
|
|
104
|
+
|
|
105
|
+
## Upgrading field_struct
|
|
106
|
+
|
|
107
|
+
When bumping the gem, follow `UPGRADING.md` (shipped with the gem, next to
|
|
108
|
+
`USAGE.md`; or https://github.com/Paymentbox-com/field_struct/blob/main/UPGRADING.md).
|
|
109
|
+
It lists every visible change since 0.10 as **Symptom / Find / Fix / Why**; don't
|
|
110
|
+
guess from memory.
|
|
111
|
+
|
|
112
|
+
1. Read your old version from `Gemfile.lock` (`field_struct (x.y.z)`) and the target
|
|
113
|
+
tag. Open `UPGRADING.md` (repo root, or `bundle show field_struct` after bumping)
|
|
114
|
+
and read every section from the one after your old version to the target.
|
|
115
|
+
2. Bump the Gemfile tag. If the project uses **actionable** >= 1.6, field_struct
|
|
116
|
+
must be >= 0.12 (its gemspec says so); if `oj` is pinned below 3.11.2, loosen it.
|
|
117
|
+
`bundle update field_struct`.
|
|
118
|
+
3. Check `rg --pcre2-version` works in your shell. Run the Find of every entry
|
|
119
|
+
the TL;DR lists as **silent**, from the project root, and read each hit using the
|
|
120
|
+
entry's narrowing rule. Run the other entries' Finds only for code the suite
|
|
121
|
+
doesn't reach; those changes raise or fail a spec.
|
|
122
|
+
4. Apply the matching **Fix** to each real hit. Don't change behaviour you weren't
|
|
123
|
+
asked to — e.g. keep a renamed reserved field's wire name with `serialize :json`.
|
|
124
|
+
5. Run the full suite. Match each failure against UPGRADING.md's symptom index and
|
|
125
|
+
fix it; if nothing matches, follow its "If something else breaks" section.
|
|
126
|
+
6. If the project generates RBS with `FieldStruct::RBS.generate`, or with a task that
|
|
127
|
+
wraps it (`rg -n 'FieldStruct::RBS' lib Rakefile`), regenerate it and run Steep if
|
|
128
|
+
the project does.
|
|
129
|
+
7. Report: old → new version, each entry that applied (file:line, fix), each Find
|
|
130
|
+
with no hits, and anything left unexplained.
|
|
131
|
+
|
|
132
|
+
## Debugging an invalid instance
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
i = Thing.new(...)
|
|
136
|
+
i.valid? # => false
|
|
137
|
+
i.errors.to_h # => { name: ["is required"], email: ["is invalid"] }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- `"is required"` → field is `required` and the value is *missing* (nil/empty/whitespace
|
|
141
|
+
per the type). Often a whitespace string or a failed coercion that stored nil.
|
|
142
|
+
- `"is invalid"` → present but fails `format:`/`enum:`/`in:`, or a nested struct/array
|
|
143
|
+
element is itself invalid (validity propagates up).
|
|
144
|
+
- `"could not be coerced: …"` → coercion raised; under the default `:keep_raw` the raw
|
|
145
|
+
value is kept, under `:replace` it's nilled. Check the field's/class's
|
|
146
|
+
`coercion_policy`.
|
|
147
|
+
|
|
148
|
+
## Extending types (namespaces)
|
|
149
|
+
|
|
150
|
+
A module exposes `self.field_types` (a registry parented to `FieldStruct.types`);
|
|
151
|
+
classes inside it resolve names through it.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
module Acme
|
|
155
|
+
def self.field_types
|
|
156
|
+
@field_types ||= FieldStruct.new_registry do
|
|
157
|
+
register :money, Acme::Types::Money # a FieldStruct::Types::Base subclass
|
|
158
|
+
register :dec, :big_decimal # alias to an existing name
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
A custom type subclasses `FieldStruct::Types::Base` and implements `#coerce` and
|
|
165
|
+
`#ruby_type` (override `#missing?` for a broader-than-nil notion).
|
|
166
|
+
|
|
167
|
+
## Typing the models (RBS)
|
|
168
|
+
|
|
169
|
+
`FieldStruct::RBS.generate(Klass)` emits RBS for the per-field accessors so Steep /
|
|
170
|
+
Solargraph can check `user.name`:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
puts FieldStruct::RBS.generate(User)
|
|
174
|
+
# class User < ::FieldStruct::Base
|
|
175
|
+
# attr_reader name: ::String
|
|
176
|
+
# def name=: (::String? value) -> ::String?
|
|
177
|
+
# ...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Reader = the field's Ruby type, nullable when `optional`; the setter takes and
|
|
181
|
+
returns that type, nilable (since 0.12 — pass loose input through `new` /
|
|
182
|
+
`assign_attributes`, whose values are `Object?`). `rbs_type:` overrides a field's type. Generate RBS for nested/element classes too so references resolve. See
|
|
183
|
+
the README "Type signatures" section for a Rake-task wiring example.
|
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: field_struct
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.15.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Adrian Madrid
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bigdecimal
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: oj
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 3.11.2
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 3.11.2
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tzinfo
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
description: |
|
|
55
|
+
FieldStruct is a Ruby library for building Plain Old Ruby Objects with
|
|
56
|
+
declared attributes that have enforced types, presence checks, and
|
|
57
|
+
per-field validation. Each class collects its declarations into an
|
|
58
|
+
inspectable Metadata object; types are pluggable via a registry that
|
|
59
|
+
namespaces can extend or replace.
|
|
60
|
+
|
|
61
|
+
Not a database layer, not a form object, not an ActiveModel replacement.
|
|
62
|
+
Mirrors the AM interface shape where it helps adoption, but reuses no AM
|
|
63
|
+
code.
|
|
64
|
+
email:
|
|
65
|
+
- amadrid@pmtbox.com
|
|
66
|
+
executables: []
|
|
67
|
+
extensions: []
|
|
68
|
+
extra_rdoc_files: []
|
|
69
|
+
files:
|
|
70
|
+
- AGENTS.md
|
|
71
|
+
- CHANGELOG.md
|
|
72
|
+
- CODE_OF_CONDUCT.md
|
|
73
|
+
- README.md
|
|
74
|
+
- UPGRADING.md
|
|
75
|
+
- USAGE.md
|
|
76
|
+
- docs/getting_started.md
|
|
77
|
+
- lib/field_struct.rb
|
|
78
|
+
- lib/field_struct/base.rb
|
|
79
|
+
- lib/field_struct/error.rb
|
|
80
|
+
- lib/field_struct/errors.rb
|
|
81
|
+
- lib/field_struct/field.rb
|
|
82
|
+
- lib/field_struct/metadata.rb
|
|
83
|
+
- lib/field_struct/model_name.rb
|
|
84
|
+
- lib/field_struct/rbs.rb
|
|
85
|
+
- lib/field_struct/registry.rb
|
|
86
|
+
- lib/field_struct/scaffold.rb
|
|
87
|
+
- lib/field_struct/types/array.rb
|
|
88
|
+
- lib/field_struct/types/base.rb
|
|
89
|
+
- lib/field_struct/types/big_decimal.rb
|
|
90
|
+
- lib/field_struct/types/binary.rb
|
|
91
|
+
- lib/field_struct/types/boolean.rb
|
|
92
|
+
- lib/field_struct/types/date.rb
|
|
93
|
+
- lib/field_struct/types/datetime.rb
|
|
94
|
+
- lib/field_struct/types/email.rb
|
|
95
|
+
- lib/field_struct/types/float.rb
|
|
96
|
+
- lib/field_struct/types/immutable_string.rb
|
|
97
|
+
- lib/field_struct/types/integer.rb
|
|
98
|
+
- lib/field_struct/types/nested.rb
|
|
99
|
+
- lib/field_struct/types/preset_resolver.rb
|
|
100
|
+
- lib/field_struct/types/rfc3339_format.rb
|
|
101
|
+
- lib/field_struct/types/string.rb
|
|
102
|
+
- lib/field_struct/types/symbol.rb
|
|
103
|
+
- lib/field_struct/types/temporal_parser.rb
|
|
104
|
+
- lib/field_struct/types/time.rb
|
|
105
|
+
- lib/field_struct/types/time_format_resolver.rb
|
|
106
|
+
- lib/field_struct/types/union.rb
|
|
107
|
+
- lib/field_struct/types/url.rb
|
|
108
|
+
- lib/field_struct/types/uuid.rb
|
|
109
|
+
- lib/field_struct/types/value.rb
|
|
110
|
+
- lib/field_struct/types/zone_option.rb
|
|
111
|
+
- lib/field_struct/version.rb
|
|
112
|
+
- sig/field_struct.rbs
|
|
113
|
+
- skills/field-struct/SKILL.md
|
|
114
|
+
homepage: https://github.com/Paymentbox-com/field_struct
|
|
115
|
+
licenses: []
|
|
116
|
+
metadata:
|
|
117
|
+
allowed_push_host: https://rubygems.org
|
|
118
|
+
homepage_uri: https://github.com/Paymentbox-com/field_struct
|
|
119
|
+
source_code_uri: https://github.com/Paymentbox-com/field_struct
|
|
120
|
+
changelog_uri: https://github.com/Paymentbox-com/field_struct/blob/main/CHANGELOG.md
|
|
121
|
+
rubygems_mfa_required: 'true'
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: 3.0.0
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 4.0.20
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: Typed PORO foundation with declared fields, presence checks, and validation.
|
|
139
|
+
test_files: []
|