inquiry_attrs 1.0.1 → 1.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e38ba51363664abde16b95c0369e6c81a59d5328a5040a3ae16f6b90ebc89fca
4
- data.tar.gz: 8f90df1580bff8741d69d5654b16cf35ca81f5a31167a3a89563ebec7f2c4b10
3
+ metadata.gz: eaef756f9fda05436e3f930b7d0252712fea66dc7818b23b1238b2c098290c77
4
+ data.tar.gz: 61d886c923036fd403a0e2c780a1b45bb03de0f3f44f136a0a9cb44f48ebf7f5
5
5
  SHA512:
6
- metadata.gz: fc6265f245d6f36cba27d4f98f82e0e2778eccf657c0ea549ab46519db81fe7865a6f3c552f8a293b11bfd434124b04552f82698bacf0950750c31d01735b1b8
7
- data.tar.gz: a9acf7294f8d6c55fe5dd5ab36871d9e5b35355665888276d875ab0fd6e3a330067e6c00544b0a4a61a1f23052e5a96b59377721c3b63e7580af5b2f9efcac37
6
+ metadata.gz: 38bb6a3e26e701fbcc4e8dbe874f329ff74b245b67e4552566883b5a7520d2feef0d0c170ce5a639fe730792aba01d208a237914e1d23f746be98d8adb302719
7
+ data.tar.gz: 241b76ab67af3f051a12d40113cfaac361294e7b88a254ec12ff93d2b1f8be60309b2fa22c6e69b289d5115ce8dcd0444eb872d8bc964468beb8fe5cd8826e34
data/AGENTS.md CHANGED
@@ -92,6 +92,26 @@ database in `test/test_helper.rb`.
92
92
 
93
93
  ---
94
94
 
95
+ ## Architecture — reserved predicate names
96
+
97
+ Some predicate names are **already defined as real methods** on the returned
98
+ objects. `method_missing` is never reached for them, so calling them does **not**
99
+ test whether the attribute value equals that word.
100
+
101
+ | Predicate | Defined on | What it actually does |
102
+ |---|---|---|
103
+ | `nil?` | Ruby `Object` | `false` for any present value; `true` for `NilInquiry` |
104
+ | `blank?` | ActiveSupport | `true` when value is blank — not when it equals `"blank"` |
105
+ | `present?` | ActiveSupport | Opposite of `blank?` — not when value equals `"present"` |
106
+ | `empty?` | Ruby `String` | `true` only for `""` — not when value equals `"empty"` |
107
+ | `frozen?` | Ruby `Object` | Reflects freeze state of the object |
108
+
109
+ **When generating or reviewing code:** if an attribute's domain values include
110
+ `nil`, `blank`, `present`, `empty`, or `frozen`, flag this and suggest direct
111
+ string comparison (`== 'blank'`) rather than a predicate.
112
+
113
+ ---
114
+
95
115
  ## Architecture — the `instance_method` capture pattern
96
116
 
97
117
  This is the **most important** design decision in `concern.rb`:
@@ -166,9 +186,11 @@ auto-include anything on load — that would be implicit and hard to audit.
166
186
  ### `NilInquiry` (`lib/inquiry_attrs/nil_inquiry.rb`)
167
187
 
168
188
  - Frozen singleton: `NilInquiry::INSTANCE`
169
- - `nil?` → `true`; `blank?` → `true`; `present?` → `false`
189
+ - `nil?` → `true`; `blank?` → `true`; `empty?` → `true`; `present?` → `false`
170
190
  - Any `?`-method → `false` via `method_missing`
171
191
  - `== nil`, `== ""`, `== INSTANCE` → `true`
192
+ - `is_a?(NilClass)`, `kind_of?(NilClass)`, `instance_of?(NilClass)` → `true`
193
+ (`NilClass` cannot be subclassed; methods are overridden explicitly)
172
194
  - Implements `to_s` / `to_str` / `inspect`
173
195
 
174
196
  ### `SymbolInquiry` (`lib/inquiry_attrs/symbol_inquiry.rb`)
@@ -177,7 +199,8 @@ auto-include anything on load — that would be implicit and hard to audit.
177
199
  - Raises `ArgumentError` for non-Symbol; unwraps nested `SymbolInquiry`
178
200
  - `?`-method returns `true` iff `sym.to_s == method_name.delete_suffix('?')`
179
201
  - `==` accepts `Symbol`, `String`, or `SymbolInquiry`
180
- - `is_a?(Symbol)` and `kind_of?(Symbol)` → `true`
202
+ - `is_a?(Symbol)`, `kind_of?(Symbol)`, `instance_of?(Symbol)` → `true`
203
+ (`Symbol` cannot be subclassed; methods are overridden explicitly)
181
204
  - `nil?` → `false`; `blank?` → `false`; `present?` → `true`
182
205
 
183
206
  ### `Concern` (`lib/inquiry_attrs/concern.rb`)
@@ -219,6 +242,7 @@ auto-include anything on load — that would be implicit and hard to audit.
219
242
  | Stub `Rails.root` in tests | Use `Installer.install!(tmpdir)` instead |
220
243
  | Add a dependency on anything outside ActiveSupport/ActiveRecord/Railties | This is a Rails gem; Rails is already the dependency |
221
244
  | Introduce allocation inside the hot path (e.g. `String.new.extend(...)`) | `NilInquiry::INSTANCE` is a frozen singleton for a reason |
245
+ | Suggest `.blank?` / `.nil?` / `.present?` / `.empty?` / `.frozen?` as inquiry predicates for those exact values | These are real methods, not inquiry predicates — they test object state, not string equality. Use `== 'blank'` etc. instead |
222
246
 
223
247
  ---
224
248
 
data/CHANGELOG.md CHANGED
@@ -11,6 +11,27 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.0.2] — 2026-02-27
15
+
16
+ ### Added
17
+
18
+ - **`NilInquiry#is_a?` / `#kind_of?` / `#instance_of?`** — all three type-check
19
+ methods now return `true` when called with `NilClass` as the argument.
20
+ `NilClass` cannot be subclassed in Ruby, so the methods are overridden
21
+ explicitly (the same technique already used by `SymbolInquiry` for `Symbol`).
22
+ `is_a?(InquiryAttrs::NilInquiry)` continues to return `true`.
23
+
24
+ - **`SymbolInquiry#kind_of?` / `#instance_of?`** — aliased to the existing
25
+ `is_a?` override so all three type-check methods consistently return `true`
26
+ for `Symbol` and the `SymbolInquiry` class itself.
27
+
28
+ - **README — ⚠️ Reserved predicate names** — new section documenting that
29
+ attribute values whose names match built-in Ruby/Rails `?`-methods (`nil`,
30
+ `blank`, `present`, `empty`, `frozen`) will invoke the real method rather than
31
+ testing string equality, and explaining the safe `== 'value'` alternative.
32
+
33
+ ---
34
+
14
35
  ## [1.0.0] — 2026-02-27
15
36
 
16
37
  ### Added
@@ -80,5 +101,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
80
101
 
81
102
  ---
82
103
 
83
- [Unreleased]: https://github.com/pniemczyk/inquiry_attrs/compare/v1.0.0...HEAD
104
+ [Unreleased]: https://github.com/pniemczyk/inquiry_attrs/compare/v1.0.2...HEAD
105
+ [1.0.2]: https://github.com/pniemczyk/inquiry_attrs/compare/v1.0.0...v1.0.2
84
106
  [1.0.0]: https://github.com/pniemczyk/inquiry_attrs/releases/tag/v1.0.0
data/CLAUDE.md CHANGED
@@ -110,3 +110,28 @@ gem build inquiry_attrs.gemspec
110
110
  - **Never** call `inquirer` before `attr_accessor` in plain Ruby classes
111
111
  - **Never** broaden the `rescue` in `concern.rb`
112
112
  - **Never** stub `Rails.root` in tests — use `Installer.install!(tmpdir)` instead
113
+
114
+ ---
115
+
116
+ ## Reserved predicate names — gotcha
117
+
118
+ Some predicate names are **real methods** on the objects `inquiry_attrs` returns.
119
+ They are **never** handled by `method_missing` and do **not** test string equality.
120
+
121
+ | Predicate | Actual behaviour |
122
+ |---|---|
123
+ | `.nil?` | Always `false` for present values; always `true` for blank (`NilInquiry`) |
124
+ | `.blank?` | Tests blankness (nil / "" / whitespace) — not `value == "blank"` |
125
+ | `.present?` | Opposite of `blank?` — not `value == "present"` |
126
+ | `.empty?` | `true` only for `""` — not `value == "empty"` |
127
+ | `.frozen?` | Reflects the object's freeze state |
128
+
129
+ **When writing or reviewing code:** if a domain value matches one of the names
130
+ above, use direct comparison instead:
131
+
132
+ ```ruby
133
+ record.state == 'blank' # ✅ correct
134
+ record.state.blank? # ❌ tests blankness, not state == "blank"
135
+ ```
136
+
137
+ See `README.md` → "⚠️ Reserved predicate names" for the full worked example.
data/README.md CHANGED
@@ -121,6 +121,52 @@ sub.state.nil? # => true
121
121
 
122
122
  ---
123
123
 
124
+ ## ⚠️ Reserved predicate names
125
+
126
+ Some predicate names are **already defined as real methods** on the objects
127
+ `inquiry_attrs` returns. Calling them does **not** test whether the attribute
128
+ value equals that word — the existing method is called instead and
129
+ `method_missing` is never reached.
130
+
131
+ | Value / predicate | Already defined by | What it actually tests |
132
+ |---|---|---|
133
+ | `"nil"` / `.nil?` | Ruby `Object#nil?` | Whether the object is `nil` — always `false` for present strings, always `true` for blank values |
134
+ | `"blank"` / `.blank?` | ActiveSupport `Object#blank?` | Whether the value is blank (`nil`, `""`, whitespace) — **not** whether it equals `"blank"` |
135
+ | `"present"` / `.present?` | ActiveSupport `Object#present?` | Opposite of `blank?` — **not** whether it equals `"present"` |
136
+ | `"empty"` / `.empty?` | Ruby `String#empty?` | Whether the string is `""` — **not** whether it equals `"empty"` |
137
+ | `"frozen"` / `.frozen?` | Ruby `Object#frozen?` | Whether the object is frozen — **not** whether it equals `"frozen"` |
138
+
139
+ ### Example of the problem
140
+
141
+ ```ruby
142
+ class Order < ApplicationRecord
143
+ inquirer :state
144
+ end
145
+
146
+ # ❌ Misleading — .blank? tests blankness, not state == "blank"
147
+ order = Order.new(state: 'blank')
148
+ order.state.blank? # => false ("blank" is a non-empty string, so not blank)
149
+
150
+ # ❌ Misleading — .present? tests non-blankness, not state == "present"
151
+ order = Order.new(state: 'present')
152
+ order.state.present? # => true (any non-blank string is present)
153
+
154
+ # ❌ Misleading — .nil? tests object identity, not state == "nil"
155
+ order = Order.new(state: 'nil')
156
+ order.state.nil? # => false (it is a StringInquirer, not nil)
157
+ ```
158
+
159
+ **Rule of thumb:** if your domain uses values such as `nil`, `blank`, `present`,
160
+ `empty`, or `frozen`, use direct string comparison instead of a predicate:
161
+
162
+ ```ruby
163
+ order.state == 'blank' # ✅ reliable
164
+ order.state == 'present' # ✅ reliable
165
+ order.state == 'nil' # ✅ reliable
166
+ ```
167
+
168
+ ---
169
+
124
170
  ## How it works
125
171
 
126
172
  `.inquirer :attr` wraps the original attribute reader and returns one of three
@@ -132,31 +178,42 @@ objects based on the raw value:
132
178
  | `Symbol` | `InquiryAttrs::SymbolInquiry` | `:active.active?` → `true` |
133
179
  | Any other string | `ActiveSupport::StringInquirer` | Standard Rails inquiry |
134
180
 
181
+ > **Note:** if an attribute value shares a name with a built-in Ruby/Rails
182
+ > predicate (`"nil"`, `"blank"`, `"present"`, `"empty"`, `"frozen"`) the real
183
+ > method will be called — not a string-equality check. See
184
+ > [⚠️ Reserved predicate names](#️-reserved-predicate-names) for details.
185
+
135
186
  ### `InquiryAttrs::NilInquiry`
136
187
 
137
188
  A frozen singleton returned for blank attributes. Every `?`-method returns
138
- `false`; behaves like `nil` in comparisons and `blank?` checks.
189
+ `false`; behaves like `nil` in comparisons, `blank?` checks, and type
190
+ introspection.
139
191
 
140
192
  ```ruby
141
193
  ni = InquiryAttrs::NilInquiry::INSTANCE
142
- ni.nil? # => true
143
- ni.active? # => false
144
- ni == nil # => true
145
- ni.blank? # => true
194
+ ni.nil? # => true
195
+ ni.active? # => false
196
+ ni == nil # => true
197
+ ni.blank? # => true
198
+ ni.is_a?(NilClass) # => true
199
+ ni.kind_of?(NilClass) # => true
200
+ ni.instance_of?(NilClass) # => true
146
201
  ```
147
202
 
148
203
  ### `InquiryAttrs::SymbolInquiry`
149
204
 
150
205
  Wraps a Symbol with predicate methods; compares equal to both the symbol and
151
- its string equivalent.
206
+ its string equivalent; reports itself as a `Symbol` in all type-check methods.
152
207
 
153
208
  ```ruby
154
209
  si = InquiryAttrs::SymbolInquiry.new(:active)
155
- si.active? # => true
156
- si == :active # => true
157
- si == 'active' # => true
158
- si.is_a?(Symbol) # => true
159
- si.to_s # => "active"
210
+ si.active? # => true
211
+ si == :active # => true
212
+ si == 'active' # => true
213
+ si.is_a?(Symbol) # => true
214
+ si.kind_of?(Symbol) # => true
215
+ si.instance_of?(Symbol) # => true
216
+ si.to_s # => "active"
160
217
  ```
161
218
 
162
219
  ---
@@ -172,7 +229,6 @@ Auto-included into `ActiveRecord::Base`. Include manually in other classes.
172
229
  ```ruby
173
230
  inquirer :status # single attribute
174
231
  inquirer :status, :role, :plan # multiple attributes
175
- inquirer :status, only: :show # passes options to before_action style macros (AR scoped)
176
232
  ```
177
233
 
178
234
  ---
@@ -181,9 +237,16 @@ inquirer :status, only: :show # passes options to before_action style
181
237
 
182
238
  ```bash
183
239
  bundle install
184
- bundle exec ruby -Ilib -Itest test/inquiry_attrs/nil_inquiry_test.rb \
185
- test/inquiry_attrs/symbol_inquiry_test.rb \
186
- test/inquiry_attrs/concern_test.rb
240
+
241
+ # Full suite (preferred)
242
+ bundle exec rake
243
+
244
+ # Single file
245
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb
246
+
247
+ # Single test by name
248
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb \
249
+ --name test_matching_predicate_returns_true
187
250
  ```
188
251
 
189
252
  ---
@@ -25,6 +25,12 @@ module InquiryAttrs
25
25
  method_name.to_s.end_with?('?') || super
26
26
  end
27
27
 
28
+ # NilClass cannot be subclassed in Ruby, so we override the type-check
29
+ # methods explicitly — the same technique used by SymbolInquiry for Symbol.
30
+ def is_a?(klass) = klass == NilClass || super
31
+ alias kind_of? is_a?
32
+ alias instance_of? is_a?
33
+
28
34
  def nil? = true
29
35
  def blank? = true
30
36
  def empty? = true
@@ -42,8 +42,9 @@ module InquiryAttrs
42
42
  end
43
43
  end
44
44
 
45
- def is_a?(klass) = klass == Symbol || super
45
+ def is_a?(klass) = klass == Symbol || super
46
46
  alias kind_of? is_a?
47
+ alias instance_of? is_a?
47
48
 
48
49
  def nil? = false
49
50
  def blank? = false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InquiryAttrs
4
- VERSION = '1.0.1'.freeze
4
+ VERSION = '1.0.2'.freeze
5
5
  end
data/llms/overview.md CHANGED
@@ -64,14 +64,39 @@ inquirer :status, :role, :plan # wraps multiple readers at once
64
64
  | `Symbol` | `InquiryAttrs::SymbolInquiry.new(raw)` |
65
65
  | Any other string | `raw.to_s.inquiry` → `ActiveSupport::StringInquirer` |
66
66
 
67
+ ## Reserved predicate names
68
+
69
+ Several predicate names are **already defined as real methods** on the returned
70
+ objects. `method_missing` is never reached for them, so they **do not** test
71
+ whether the raw attribute value equals that word — they invoke the existing method.
72
+
73
+ | Predicate | Defined by | What it tests |
74
+ |---|---|---|
75
+ | `nil?` | Ruby `Object` | Whether the object is `nil` — always `false` for present values |
76
+ | `blank?` | ActiveSupport | Whether the value is blank (nil / "" / whitespace) |
77
+ | `present?` | ActiveSupport | Opposite of `blank?` |
78
+ | `empty?` | Ruby `String` / `NilInquiry` | Whether the string is `""` |
79
+ | `frozen?` | Ruby `Object` | Whether the object is frozen |
80
+
81
+ **Code generation rule:** when a model's attribute domain includes values whose
82
+ names match the table above (e.g. a `state` column that can hold `"blank"` or
83
+ `"nil"`), use direct string comparison — never a predicate:
84
+
85
+ ```ruby
86
+ record.state == 'blank' # ✅ tests string equality
87
+ record.state.blank? # ❌ tests blankness, not state == "blank"
88
+ ```
89
+
67
90
  ## Class responsibilities
68
91
 
69
92
  ### `InquiryAttrs::NilInquiry`
70
93
 
71
94
  - Frozen singleton (`INSTANCE`)
72
- - `nil?` → `true`; `blank?` → `true`; `present?` → `false`
73
- - Every `?`-method → `false` via `method_missing`
95
+ - `nil?` → `true`; `blank?` → `true`; `empty?` → `true`; `present?` → `false`
96
+ - Every `?`-method → `false` via `method_missing` (except the explicit overrides above)
74
97
  - `== nil`, `== ""`, `== INSTANCE` → `true`
98
+ - `is_a?(NilClass)`, `kind_of?(NilClass)`, `instance_of?(NilClass)` → `true`
99
+ (`NilClass` cannot be subclassed; overridden explicitly)
75
100
  - Implements `to_s`, `to_str`, `inspect`
76
101
 
77
102
  ### `InquiryAttrs::SymbolInquiry < SimpleDelegator`
@@ -80,7 +105,8 @@ inquirer :status, :role, :plan # wraps multiple readers at once
80
105
  - Unwraps nested `SymbolInquiry` on init
81
106
  - Any `?`-method returns `true` iff `sym.to_s == method_name.delete_suffix('?')`
82
107
  - `==` accepts `Symbol`, `String`, or `SymbolInquiry`
83
- - `is_a?(Symbol)` → `true` (and `kind_of?`)
108
+ - `is_a?(Symbol)`, `kind_of?(Symbol)`, `instance_of?(Symbol)` → `true`
109
+ (`Symbol` cannot be subclassed; overridden explicitly)
84
110
  - `nil?` → `false`; `blank?` → `false`; `present?` → `true`
85
111
 
86
112
  ### `InquiryAttrs::Concern`
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inquiry_attrs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk