field_struct 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +58 -0
  3. data/CHANGELOG.md +653 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/README.md +645 -0
  6. data/UPGRADING.md +743 -0
  7. data/USAGE.md +820 -0
  8. data/docs/getting_started.md +337 -0
  9. data/lib/field_struct/base.rb +1316 -0
  10. data/lib/field_struct/error.rb +48 -0
  11. data/lib/field_struct/errors.rb +103 -0
  12. data/lib/field_struct/field.rb +179 -0
  13. data/lib/field_struct/metadata.rb +331 -0
  14. data/lib/field_struct/model_name.rb +48 -0
  15. data/lib/field_struct/rbs.rb +299 -0
  16. data/lib/field_struct/registry.rb +85 -0
  17. data/lib/field_struct/scaffold.rb +331 -0
  18. data/lib/field_struct/types/array.rb +156 -0
  19. data/lib/field_struct/types/base.rb +162 -0
  20. data/lib/field_struct/types/big_decimal.rb +78 -0
  21. data/lib/field_struct/types/binary.rb +38 -0
  22. data/lib/field_struct/types/boolean.rb +126 -0
  23. data/lib/field_struct/types/date.rb +158 -0
  24. data/lib/field_struct/types/datetime.rb +163 -0
  25. data/lib/field_struct/types/email.rb +49 -0
  26. data/lib/field_struct/types/float.rb +80 -0
  27. data/lib/field_struct/types/immutable_string.rb +18 -0
  28. data/lib/field_struct/types/integer.rb +133 -0
  29. data/lib/field_struct/types/nested.rb +82 -0
  30. data/lib/field_struct/types/preset_resolver.rb +37 -0
  31. data/lib/field_struct/types/rfc3339_format.rb +197 -0
  32. data/lib/field_struct/types/string.rb +73 -0
  33. data/lib/field_struct/types/symbol.rb +38 -0
  34. data/lib/field_struct/types/temporal_parser.rb +93 -0
  35. data/lib/field_struct/types/time.rb +168 -0
  36. data/lib/field_struct/types/time_format_resolver.rb +54 -0
  37. data/lib/field_struct/types/union.rb +96 -0
  38. data/lib/field_struct/types/url.rb +46 -0
  39. data/lib/field_struct/types/uuid.rb +46 -0
  40. data/lib/field_struct/types/value.rb +24 -0
  41. data/lib/field_struct/types/zone_option.rb +267 -0
  42. data/lib/field_struct/version.rb +7 -0
  43. data/lib/field_struct.rb +159 -0
  44. data/sig/field_struct.rbs +2845 -0
  45. data/skills/field-struct/SKILL.md +183 -0
  46. metadata +139 -0
@@ -0,0 +1,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: []