inquiry_attrs 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 615f0028c78433e7cd13171ce70724af8e5c535c9bc3ddac9127b6121cec6cc2
4
+ data.tar.gz: 95d371d0ee8d484e6f80b353a62dc49c92d19df63eeb08c35abaccccacb1738f
5
+ SHA512:
6
+ metadata.gz: f1327f64dc6cc2aec05aa5ae6b97619ba1bfa95cfe394633c44f7e7ca67a8d2fc4da5cb9cd58c114b234dde4eef3e5db973a35ed19e9678c23befd188b4d4b5b
7
+ data.tar.gz: a4ce3e81aab2b778aff6abf4c8532e5ed74765f457b49efc9a90134bf803d94f4396e5a3706875b73fa32159634e816779f4ad973b365f0ea105bc7ecd4bb8e5
data/AGENTS.md ADDED
@@ -0,0 +1,245 @@
1
+ # AGENTS.md — inquiry_attrs
2
+
3
+ This file is the primary context document for AI agents and LLMs working on
4
+ this gem. Read it fully before making any changes.
5
+
6
+ ---
7
+
8
+ ## What this gem does
9
+
10
+ `inquiry_attrs` adds predicate-style inquiry methods to Rails model attributes.
11
+
12
+ ```ruby
13
+ # Without inquiry_attrs
14
+ user.status == 'active'
15
+
16
+ # With inquiry_attrs
17
+ user.status.active? # => true
18
+ user.status.inactive? # => false
19
+ user.status.nil? # => true when blank — never raises NoMethodError
20
+ ```
21
+
22
+ It is a **Rails-only** gem (depends on `activesupport >= 7`, `activerecord >= 7`,
23
+ `railties >= 7`). All Rails APIs (`blank?`, `ActiveSupport::Concern`,
24
+ `ActiveSupport.on_load`, `String#inquiry`) are available and should be preferred
25
+ over reinventing them.
26
+
27
+ ---
28
+
29
+ ## File map
30
+
31
+ ```
32
+ lib/
33
+ inquiry_attrs.rb # Entry point — requires everything, loads Railtie
34
+ inquiry_attrs/
35
+ version.rb # VERSION = '1.0.0'
36
+ nil_inquiry.rb # NilInquiry::INSTANCE — frozen singleton for blank values
37
+ symbol_inquiry.rb # SymbolInquiry < SimpleDelegator — wraps Symbol attrs
38
+ concern.rb # Concern — adds .inquirer class macro
39
+ installer.rb # Installer — file-system logic for the rake tasks
40
+ railtie.rb # Railtie — wires rake tasks into the host Rails app
41
+ tasks/
42
+ inquiry_attrs.rake # Shell rake tasks (install / uninstall)
43
+
44
+ test/
45
+ test_helper.rb # Minitest setup + SQLite in-memory AR connection
46
+ inquiry_attrs/
47
+ nil_inquiry_test.rb # Unit tests for NilInquiry
48
+ symbol_inquiry_test.rb # Unit tests for SymbolInquiry
49
+ concern_test.rb # Integration tests: AR, StoreModel, plain Ruby, Symbol
50
+ install_task_test.rb # Unit tests for Installer (no Rake machinery needed)
51
+
52
+ llms/
53
+ overview.md # Architecture deep-dive for LLMs
54
+ usage.md # Common patterns and recipes
55
+
56
+ AGENTS.md # This file
57
+ README.md
58
+ CHANGELOG.md
59
+ ```
60
+
61
+ ---
62
+
63
+ ## How to run tests
64
+
65
+ ```bash
66
+ # Full suite (preferred)
67
+ bundle exec rake
68
+
69
+ # Single file
70
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb
71
+
72
+ # Single test by name
73
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb \
74
+ --name test_matching_predicate_returns_true
75
+ ```
76
+
77
+ Tests use **Minitest** (`minitest-reporters` with SpecReporter). There is no
78
+ Rails application — ActiveRecord is wired directly to an in-memory SQLite
79
+ database in `test/test_helper.rb`.
80
+
81
+ ---
82
+
83
+ ## Architecture — the three return types
84
+
85
+ `.inquirer :attr` overrides the original attribute reader and returns one of:
86
+
87
+ | Raw value | Return type | Key behaviour |
88
+ |---|---|---|
89
+ | `nil` or any `blank?` value | `NilInquiry::INSTANCE` | `nil?` true, all predicates false |
90
+ | `Symbol` | `SymbolInquiry.new(raw)` | predicate matches symbol name |
91
+ | Everything else | `raw.to_s.inquiry` → `ActiveSupport::StringInquirer` | standard Rails inquiry |
92
+
93
+ ---
94
+
95
+ ## Architecture — the `instance_method` capture pattern
96
+
97
+ This is the **most important** design decision in `concern.rb`:
98
+
99
+ ```ruby
100
+ def inquirer(*attrs)
101
+ attrs.each do |attr|
102
+ original = method_defined?(attr) ? instance_method(attr) : nil
103
+
104
+ # Remove the method from this class's own table before redefining it to
105
+ # silence Ruby's "method redefined" warning. The original is already
106
+ # safely captured in `original` above.
107
+ remove_method(attr) if instance_methods(false).include?(attr)
108
+
109
+ define_method(attr) do
110
+ raw = original ? original.bind_call(self) : super()
111
+ # … return type logic
112
+ end
113
+ end
114
+ end
115
+ ```
116
+
117
+ **Why capture + remove + redefine:** `inquirer :status` must replace the original
118
+ reader. Both AR and StoreModel define their readers via `define_method`; calling
119
+ `define_method` again on the same method triggers Ruby's "method redefined"
120
+ warning. Calling `remove_method` first clears it from the class's own method
121
+ table so the subsequent `define_method` is seen as a fresh definition.
122
+ `remove_method` is safe here because it only removes the method from *this*
123
+ class, not from superclasses, and the original is already preserved in `original`.
124
+
125
+ **Consequence:** `inquirer` must always be called **after** the attribute reader
126
+ is defined. Violating this order produces a `NoMethodError` in plain Ruby classes
127
+ (AR and StoreModel define readers early enough for this not to matter).
128
+
129
+ ---
130
+
131
+ ## Architecture — Railtie / rake task / Installer split
132
+
133
+ ```
134
+ Rails app inquiry_attrs gem
135
+ ───────────────── ──────────────────────────────────────────────────
136
+ Gemfile ──requires──▶ lib/inquiry_attrs.rb
137
+ └── lib/inquiry_attrs/railtie.rb (only if Rails::Railtie defined)
138
+ └── rake_tasks { load 'lib/tasks/inquiry_attrs.rake' }
139
+
140
+ $ rails inquiry_attrs:install
141
+ ──▶ task :install
142
+ └── InquiryAttrs::Installer.install!(Rails.root)
143
+ └── writes config/initializers/inquiry_attrs.rb
144
+ ```
145
+
146
+ **Why Installer is a separate class:** The rake task calls `Rails.root`, which is
147
+ unavailable in tests without a full Rails boot. By pushing all logic into
148
+ `Installer.install!(root)`, tests can pass any `Pathname` as the root — no
149
+ stubbing, no Rake DSL needed.
150
+
151
+ **The generated initializer** contains:
152
+
153
+ ```ruby
154
+ ActiveSupport.on_load(:active_record) do
155
+ include InquiryAttrs::Concern
156
+ end
157
+ ```
158
+
159
+ This is the **only** place where `on_load` is used. The gem itself does not
160
+ auto-include anything on load — that would be implicit and hard to audit.
161
+
162
+ ---
163
+
164
+ ## Key classes — quick reference
165
+
166
+ ### `NilInquiry` (`lib/inquiry_attrs/nil_inquiry.rb`)
167
+
168
+ - Frozen singleton: `NilInquiry::INSTANCE`
169
+ - `nil?` → `true`; `blank?` → `true`; `present?` → `false`
170
+ - Any `?`-method → `false` via `method_missing`
171
+ - `== nil`, `== ""`, `== INSTANCE` → `true`
172
+ - Implements `to_s` / `to_str` / `inspect`
173
+
174
+ ### `SymbolInquiry` (`lib/inquiry_attrs/symbol_inquiry.rb`)
175
+
176
+ - Subclasses `SimpleDelegator`, wraps a `Symbol`
177
+ - Raises `ArgumentError` for non-Symbol; unwraps nested `SymbolInquiry`
178
+ - `?`-method returns `true` iff `sym.to_s == method_name.delete_suffix('?')`
179
+ - `==` accepts `Symbol`, `String`, or `SymbolInquiry`
180
+ - `is_a?(Symbol)` and `kind_of?(Symbol)` → `true`
181
+ - `nil?` → `false`; `blank?` → `false`; `present?` → `true`
182
+
183
+ ### `Concern` (`lib/inquiry_attrs/concern.rb`)
184
+
185
+ - `extend ActiveSupport::Concern`
186
+ - Single class method: `inquirer(*attrs)` — see `instance_method` capture pattern above
187
+ - Uses `blank?` (Rails) instead of `nil? || == ""`
188
+
189
+ ### `Installer` (`lib/inquiry_attrs/installer.rb`)
190
+
191
+ - `Installer::INITIALIZER_PATH` — `Pathname` relative path to the initializer
192
+ - `Installer::INITIALIZER_CONTENT` — the exact string written to disk
193
+ - `Installer.install!(rails_root)` → `:created` or `:skipped`
194
+ - `Installer.uninstall!(rails_root)` → `:removed` or `:skipped`
195
+ - Accepts `Pathname` or `String` as `rails_root`
196
+
197
+ ---
198
+
199
+ ## Adding a new feature — checklist
200
+
201
+ 1. **Write the test first** in the appropriate `test/inquiry_attrs/*_test.rb` file.
202
+ 2. Implement in `lib/inquiry_attrs/`.
203
+ 3. If the feature touches `Installer` (file operations), test via `InstallerTest`
204
+ passing a `Dir.mktmpdir` path — never stub `Rails.root`.
205
+ 4. If the feature is a new public API, document it in `llms/overview.md` and
206
+ `llms/usage.md` and update `README.md`.
207
+ 5. Run the full test suite and confirm 0 failures.
208
+ 6. Update `CHANGELOG.md`.
209
+
210
+ ---
211
+
212
+ ## Things to never do
213
+
214
+ | Don't | Why |
215
+ |---|---|
216
+ | Add `on_load` back to `lib/inquiry_attrs.rb` | Makes the gem implicitly modify every AR model; hard to audit |
217
+ | Broaden the `rescue` in `concern.rb` | Swallows real errors in attribute readers |
218
+ | Call `inquirer` before `attr_accessor` in plain Ruby | `instance_method` capture returns `nil`; reader will be `nil` |
219
+ | Stub `Rails.root` in tests | Use `Installer.install!(tmpdir)` instead |
220
+ | Add a dependency on anything outside ActiveSupport/ActiveRecord/Railties | This is a Rails gem; Rails is already the dependency |
221
+ | Introduce allocation inside the hot path (e.g. `String.new.extend(...)`) | `NilInquiry::INSTANCE` is a frozen singleton for a reason |
222
+
223
+ ---
224
+
225
+ ## Dependencies
226
+
227
+ | Gem | Version | Why |
228
+ |---|---|---|
229
+ | `activesupport` | `>= 7.0` | `Concern`, `blank?`, `String#inquiry`, `on_load` |
230
+ | `activerecord` | `>= 7.0` | AR integration (attr readers, `on_load` hook) |
231
+ | `railties` | `>= 7.0` | `Rails::Railtie` for exposing rake tasks |
232
+ | `sqlite3` | dev only | In-memory DB for AR tests |
233
+ | `store_model` | dev only | Integration tests for StoreModel |
234
+ | `minitest` + `minitest-reporters` | dev only | Test framework |
235
+
236
+ ---
237
+
238
+ ## Test conventions
239
+
240
+ - Use `Minitest::Test` (not `ActiveSupport::TestCase`)
241
+ - AR schema is created in `setup` with `force: true` and dropped in `teardown`
242
+ - `InstallerTest` uses `Dir.mktmpdir` — always clean up in `teardown`
243
+ - `assert` / `refute` preferred over `assert_equal true/false`
244
+ - Group related tests with comment banners: `# ── install! ── #`
245
+ - Test method names describe the exact behaviour: `test_install_skips_when_initializer_already_exists`
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] — 2026-02-27
4
+
5
+ ### Added
6
+ - `InquiryAttrs::Concern` with `.inquirer(*attrs)` class macro
7
+ - `InquiryAttrs::SymbolInquiry` — predicate wrapper for Symbol attributes
8
+ - `InquiryAttrs::NilInquiry::INSTANCE` — frozen singleton for blank/nil attributes
9
+ - Auto-include into `ActiveRecord::Base` via `ActiveSupport.on_load(:active_record)`
10
+ - Support for StoreModel and plain Ruby classes via explicit `include`
11
+ - LLM context files in `llms/`
data/CLAUDE.md ADDED
@@ -0,0 +1,112 @@
1
+ # CLAUDE.md — inquiry_attrs
2
+
3
+ ## Start here
4
+
5
+ Before writing any code, read these files in order:
6
+
7
+ 1. **@AGENTS.md** — architecture, design decisions, guardrails, test conventions
8
+ 2. **@llms/overview.md** — class responsibilities and internal design notes
9
+ 3. **@llms/usage.md** — common patterns and recipes
10
+
11
+ ---
12
+
13
+ ## Project context
14
+
15
+ | | |
16
+ |---|---|
17
+ | **Gem name** | `inquiry_attrs` |
18
+ | **Type** | Rails gem (Ruby only, no frontend) |
19
+ | **Ruby** | ≥ 3.0 |
20
+ | **Rails deps** | `activesupport`, `activerecord`, `railties` — all ≥ 7.0 |
21
+ | **Test framework** | Minitest (`minitest-reporters`, SpecReporter) |
22
+ | **Database** | SQLite in-memory (tests only) |
23
+
24
+ ---
25
+
26
+ ## Running tests
27
+
28
+ ```bash
29
+ # Full suite (preferred)
30
+ bundle exec rake
31
+
32
+ # Full suite (explicit)
33
+ bundle exec rake test
34
+
35
+ # Single file
36
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb
37
+
38
+ # Single test by name
39
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/concern_test.rb \
40
+ --name test_matching_predicate_returns_true
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Key files — one-line each
46
+
47
+ ```
48
+ lib/inquiry_attrs/nil_inquiry.rb # NilInquiry::INSTANCE — frozen singleton for blank values
49
+ lib/inquiry_attrs/symbol_inquiry.rb # SymbolInquiry < SimpleDelegator — wraps Symbol attrs
50
+ lib/inquiry_attrs/concern.rb # .inquirer macro — instance_method capture pattern
51
+ lib/inquiry_attrs/installer.rb # Installer.install!/uninstall! — file-system logic
52
+ lib/inquiry_attrs/railtie.rb # loads rake tasks into the host Rails app
53
+ lib/tasks/inquiry_attrs.rake # rails inquiry_attrs:install / :uninstall
54
+ test/test_helper.rb # Minitest setup + SQLite + on_load simulation
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Code style
60
+
61
+ Follow **@~/.agent-os/standards/code-style.md** and **@~/.agent-os/standards/best-practices.md**.
62
+
63
+ Key rules that apply to this gem:
64
+
65
+ - 2-space indentation, no tabs
66
+ - Single quotes for strings; double quotes only for interpolation
67
+ - `snake_case` methods/variables, `PascalCase` classes, `UPPER_SNAKE_CASE` constants
68
+ - Comment the *why*, not the *what*; never remove existing comments unless removing the associated code
69
+ - Keep it simple — fewest lines possible, no over-engineering
70
+
71
+ ---
72
+
73
+ ## Workflow
74
+
75
+ ### Fixing a bug or adding a feature
76
+
77
+ 1. Read **@AGENTS.md** → section "Adding a new feature — checklist"
78
+ 2. Write the failing test first
79
+ 3. Implement the fix
80
+ 4. Run the full suite — must be 0 failures before committing
81
+
82
+ ### Testing the rake task
83
+
84
+ The rake tasks are thin shells over `InquiryAttrs::Installer`. Test `Installer`
85
+ directly — pass a `Dir.mktmpdir` path as `rails_root`. Never stub `Rails.root`.
86
+
87
+ ```ruby
88
+ def test_something
89
+ Dir.mktmpdir do |tmpdir|
90
+ root = Pathname.new(tmpdir)
91
+ FileUtils.mkdir_p(root.join('config', 'initializers'))
92
+ result = InquiryAttrs::Installer.install!(root)
93
+ assert_equal :created, result
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Building the gem
99
+
100
+ ```bash
101
+ gem build inquiry_attrs.gemspec
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Hard rules (from @AGENTS.md)
107
+
108
+ - **Never** add `ActiveSupport.on_load` back to `lib/inquiry_attrs.rb` — the
109
+ on_load lives only in the generated initializer
110
+ - **Never** call `inquirer` before `attr_accessor` in plain Ruby classes
111
+ - **Never** broaden the `rescue` in `concern.rb`
112
+ - **Never** stub `Rails.root` in tests — use `Installer.install!(tmpdir)` instead
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # inquiry_attrs
2
+
3
+ Predicate-style inquiry methods for Rails model attributes.
4
+
5
+ Instead of comparing strings:
6
+
7
+ ```ruby
8
+ user.status == 'active'
9
+ user.role == 'admin'
10
+ ```
11
+
12
+ Write expressive predicates:
13
+
14
+ ```ruby
15
+ user.status.active?
16
+ user.role.admin?
17
+ ```
18
+
19
+ Nil/blank attributes safely return `false` for every predicate — no more
20
+ `NoMethodError` on nil.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```ruby
27
+ # Gemfile
28
+ gem 'inquiry_attrs'
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quick start
34
+
35
+ ### ActiveRecord — zero configuration
36
+
37
+ `inquiry_attrs` auto-includes itself into every `ActiveRecord::Base` subclass
38
+ via `ActiveSupport.on_load(:active_record)`. Just call `.inquirer` in any model:
39
+
40
+ ```ruby
41
+ class User < ApplicationRecord
42
+ inquirer :status, :kind
43
+ end
44
+
45
+ user = User.new(status: "active", kind: "admin")
46
+ user.status.active? # => true
47
+ user.status.inactive? # => false
48
+ user.kind.admin? # => true
49
+ user.kind.user? # => false
50
+ ```
51
+
52
+ ### Nil / blank attributes
53
+
54
+ ```ruby
55
+ user = User.new(status: nil)
56
+
57
+ user.status.nil? # => true
58
+ user.status.active? # => false ← no NoMethodError!
59
+ user.status == nil # => true
60
+ ```
61
+
62
+ ### String comparison and String methods still work
63
+
64
+ ```ruby
65
+ user.status # => "active"
66
+ user.status == 'active' # => true
67
+ user.status.include?('act') # => true
68
+ user.status.upcase # => "ACTIVE"
69
+ ```
70
+
71
+ ---
72
+
73
+ ## StoreModel
74
+
75
+ Include `InquiryAttrs::Concern` explicitly for non-AR classes:
76
+
77
+ ```ruby
78
+ class ShippingAddress
79
+ include StoreModel::Model
80
+ include InquiryAttrs::Concern
81
+
82
+ attribute :kind, :string # "shipping", "billing", "return"
83
+ inquirer :kind
84
+ end
85
+
86
+ address = ShippingAddress.new(kind: "billing")
87
+ address.kind.billing? # => true
88
+ address.kind.shipping? # => false
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Plain Ruby
94
+
95
+ ```ruby
96
+ class Subscription
97
+ include InquiryAttrs::Concern
98
+
99
+ attr_accessor :plan, :state
100
+
101
+ def initialize(plan:, state: nil)
102
+ @plan = plan
103
+ @state = state
104
+ end
105
+
106
+ # Call inquirer AFTER attr_accessor — the original reader must exist first.
107
+ inquirer :plan, :state
108
+ end
109
+
110
+ sub = Subscription.new(plan: 'enterprise')
111
+ sub.plan.enterprise? # => true
112
+ sub.state.nil? # => true
113
+ ```
114
+
115
+ ---
116
+
117
+ ## How it works
118
+
119
+ `.inquirer :attr` wraps the original attribute reader and returns one of three
120
+ objects based on the raw value:
121
+
122
+ | Raw value | Return type | Key behaviour |
123
+ |---|---|---|
124
+ | `nil` or any blank value | `InquiryAttrs::NilInquiry::INSTANCE` | `nil?` → `true`, all predicates → `false` |
125
+ | `Symbol` | `InquiryAttrs::SymbolInquiry` | `:active.active?` → `true` |
126
+ | Any other string | `ActiveSupport::StringInquirer` | Standard Rails inquiry |
127
+
128
+ ### `InquiryAttrs::NilInquiry`
129
+
130
+ A frozen singleton returned for blank attributes. Every `?`-method returns
131
+ `false`; behaves like `nil` in comparisons and `blank?` checks.
132
+
133
+ ```ruby
134
+ ni = InquiryAttrs::NilInquiry::INSTANCE
135
+ ni.nil? # => true
136
+ ni.active? # => false
137
+ ni == nil # => true
138
+ ni.blank? # => true
139
+ ```
140
+
141
+ ### `InquiryAttrs::SymbolInquiry`
142
+
143
+ Wraps a Symbol with predicate methods; compares equal to both the symbol and
144
+ its string equivalent.
145
+
146
+ ```ruby
147
+ si = InquiryAttrs::SymbolInquiry.new(:active)
148
+ si.active? # => true
149
+ si == :active # => true
150
+ si == 'active' # => true
151
+ si.is_a?(Symbol) # => true
152
+ si.to_s # => "active"
153
+ ```
154
+
155
+ ---
156
+
157
+ ## API
158
+
159
+ ### `InquiryAttrs::Concern`
160
+
161
+ Auto-included into `ActiveRecord::Base`. Include manually in other classes.
162
+
163
+ ### `.inquirer(*attribute_names)`
164
+
165
+ ```ruby
166
+ inquirer :status # single attribute
167
+ inquirer :status, :role, :plan # multiple attributes
168
+ inquirer :status, only: :show # passes options to before_action style macros (AR scoped)
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ bundle install
177
+ bundle exec ruby -Ilib -Itest test/inquiry_attrs/nil_inquiry_test.rb \
178
+ test/inquiry_attrs/symbol_inquiry_test.rb \
179
+ test/inquiry_attrs/concern_test.rb
180
+ ```
181
+
182
+ ---
183
+
184
+ ## License
185
+
186
+ MIT
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ # +ActiveSupport::Concern+ that adds the +.inquirer+ macro to any class.
5
+ #
6
+ # For ActiveRecord models this is automatically included via
7
+ # +ActiveSupport.on_load(:active_record)+ — no explicit include needed:
8
+ #
9
+ # class User < ApplicationRecord
10
+ # inquirer :status, :role
11
+ # end
12
+ #
13
+ # For StoreModel, plain Ruby, or any other class, include it manually:
14
+ #
15
+ # class Address
16
+ # include StoreModel::Model
17
+ # include InquiryAttrs::Concern
18
+ #
19
+ # attribute :kind, :string
20
+ # inquirer :kind
21
+ # end
22
+ #
23
+ module Concern
24
+ extend ActiveSupport::Concern
25
+
26
+ class_methods do
27
+ # Wraps the named attribute readers with inquiry behaviour.
28
+ #
29
+ # Returns one of three objects depending on the raw value:
30
+ #
31
+ # * +InquiryAttrs::NilInquiry::INSTANCE+ — when the value is +blank?+
32
+ # * +InquiryAttrs::SymbolInquiry+ — when the value is a +Symbol+
33
+ # * +ActiveSupport::StringInquirer+ — for any other present value
34
+ # (identical to calling +value.inquiry+ on a string)
35
+ #
36
+ # @param attrs [Array<Symbol>] attribute reader names to wrap
37
+ def inquirer(*attrs)
38
+ attrs.each do |attr|
39
+ # Capture the original reader before we overwrite it.
40
+ # This covers attr_accessor, StoreModel, and AR attribute methods
41
+ # that are already defined by the time inquirer is called.
42
+ original = method_defined?(attr) ? instance_method(attr) : nil
43
+
44
+ # Remove the method from this class's own table (not from superclasses)
45
+ # so the define_method below is seen as a fresh definition rather than
46
+ # a redefinition — silencing Ruby's "method redefined" warning.
47
+ remove_method(attr) if instance_methods(false).include?(attr)
48
+
49
+ define_method(attr) do
50
+ raw = if original
51
+ original.bind_call(self)
52
+ else
53
+ # Lazy AR attribute methods or Dry::Struct hash access.
54
+ begin
55
+ super()
56
+ rescue NoMethodError
57
+ begin
58
+ self[attr]
59
+ rescue NoMethodError, TypeError
60
+ nil
61
+ end
62
+ end
63
+ end
64
+
65
+ if raw.blank?
66
+ NilInquiry::INSTANCE
67
+ elsif raw.is_a?(Symbol)
68
+ SymbolInquiry.new(raw)
69
+ else
70
+ raw.to_s.inquiry
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ # Manages the lifecycle of the inquiry_attrs initializer inside a Rails app.
5
+ #
6
+ # This class holds all file-system logic so it can be unit-tested without
7
+ # requiring a full Rails boot or Rake DSL. The rake tasks are thin shells
8
+ # that delegate here.
9
+ #
10
+ # @example From a Rake task (or Rails generator):
11
+ # InquiryAttrs::Installer.install!(Rails.root)
12
+ # InquiryAttrs::Installer.uninstall!(Rails.root)
13
+ #
14
+ class Installer
15
+ INITIALIZER_PATH = Pathname.new('config/initializers/inquiry_attrs.rb')
16
+
17
+ INITIALIZER_CONTENT = <<~RUBY
18
+ # frozen_string_literal: true
19
+
20
+ # Auto-include InquiryAttrs::Concern into every ActiveRecord model so that
21
+ # the .inquirer macro is available without an explicit include in each class.
22
+ #
23
+ # Generated by: rails inquiry_attrs:install
24
+ #
25
+ # To opt out of auto-include, remove this file and add
26
+ # include InquiryAttrs::Concern
27
+ # to whichever models need it.
28
+ ActiveSupport.on_load(:active_record) do
29
+ include InquiryAttrs::Concern
30
+ end
31
+ RUBY
32
+
33
+ # Write the initializer to +rails_root/config/initializers/inquiry_attrs.rb+.
34
+ #
35
+ # @param rails_root [Pathname, String] the root directory of the Rails app
36
+ # @return [:created, :skipped]
37
+ def self.install!(rails_root)
38
+ destination = Pathname.new(rails_root).join(INITIALIZER_PATH)
39
+
40
+ return :skipped if destination.exist?
41
+
42
+ destination.write(INITIALIZER_CONTENT)
43
+ :created
44
+ end
45
+
46
+ # Remove the initializer if it exists.
47
+ #
48
+ # @param rails_root [Pathname, String]
49
+ # @return [:removed, :skipped]
50
+ def self.uninstall!(rails_root)
51
+ destination = Pathname.new(rails_root).join(INITIALIZER_PATH)
52
+
53
+ return :skipped unless destination.exist?
54
+
55
+ destination.delete
56
+ :removed
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ # Returned by an inquired attribute when its raw value is blank.
5
+ #
6
+ # Every +?+ predicate returns +false+, so callers never receive a
7
+ # +NoMethodError+ and nil comparisons work naturally.
8
+ #
9
+ # user.status # => InquiryAttrs::NilInquiry::INSTANCE (when nil/blank)
10
+ # user.status.nil? # => true
11
+ # user.status.active? # => false
12
+ # user.status == nil # => true
13
+ #
14
+ class NilInquiry
15
+ INSTANCE = new.freeze
16
+
17
+ # Any +?+ method returns false — no NoMethodError on blank attributes.
18
+ def method_missing(method_name, *_args, &_block)
19
+ return false if method_name.to_s.end_with?('?')
20
+
21
+ super
22
+ end
23
+
24
+ def respond_to_missing?(method_name, include_private = false)
25
+ method_name.to_s.end_with?('?') || super
26
+ end
27
+
28
+ def nil? = true
29
+ def blank? = true
30
+ def empty? = true
31
+ def present? = false
32
+ def to_s = ''
33
+ def to_str = ''
34
+ def inspect = 'nil'
35
+
36
+ def ==(other)
37
+ other.nil? || other == '' || other.equal?(INSTANCE)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :inquiry_attrs
6
+
7
+ # Expose `rails inquiry_attrs:install` to the host application.
8
+ rake_tasks do
9
+ load File.expand_path('../../tasks/inquiry_attrs.rake', __dir__)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ # Wraps a Symbol to provide the same predicate interface as
5
+ # +ActiveSupport::StringInquirer+.
6
+ #
7
+ # inquiry = SymbolInquiry.new(:active)
8
+ # inquiry.active? # => true
9
+ # inquiry.inactive? # => false
10
+ # inquiry == :active # => true
11
+ # inquiry == 'active' # => true
12
+ # inquiry.is_a?(Symbol) # => true
13
+ #
14
+ class SymbolInquiry < SimpleDelegator
15
+ # @param sym [Symbol]
16
+ # @raise [ArgumentError] if the argument is not a Symbol
17
+ def initialize(sym)
18
+ raise ArgumentError, "Must be a Symbol, got #{sym.class}" unless sym.is_a?(Symbol)
19
+
20
+ super(sym.is_a?(SymbolInquiry) ? sym.__getobj__ : sym)
21
+ end
22
+
23
+ def method_missing(method_name, *_args, &_block)
24
+ if method_name.to_s.end_with?('?')
25
+ __getobj__.to_s == method_name.to_s.delete_suffix('?')
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def respond_to_missing?(method_name, include_private = false)
32
+ method_name.to_s.end_with?('?') || super
33
+ end
34
+
35
+ # Compares against Symbol, String, or another SymbolInquiry.
36
+ def ==(other)
37
+ case other
38
+ when SymbolInquiry then __getobj__ == other.__getobj__
39
+ when Symbol then __getobj__ == other
40
+ when String then __getobj__.to_s == other
41
+ else false
42
+ end
43
+ end
44
+
45
+ def is_a?(klass) = klass == Symbol || super
46
+ alias kind_of? is_a?
47
+
48
+ def nil? = false
49
+ def blank? = false
50
+ def present? = true
51
+ def to_s = __getobj__.to_s
52
+ def to_sym = __getobj__
53
+ def inspect = ":#{__getobj__}"
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InquiryAttrs
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/concern'
5
+ require 'active_support/core_ext/object/blank'
6
+ require 'active_support/core_ext/string/inquiry'
7
+
8
+ require_relative 'inquiry_attrs/version'
9
+ require_relative 'inquiry_attrs/nil_inquiry'
10
+ require_relative 'inquiry_attrs/symbol_inquiry'
11
+ require_relative 'inquiry_attrs/concern'
12
+ require_relative 'inquiry_attrs/installer'
13
+
14
+ # InquiryAttrs adds predicate-style inquiry methods to Rails model attributes.
15
+ #
16
+ # Instead of comparing strings:
17
+ #
18
+ # user.status == 'active'
19
+ #
20
+ # Write expressive predicates:
21
+ #
22
+ # user.status.active?
23
+ #
24
+ # Nil/blank values safely return +false+ for all predicates — no more
25
+ # +NoMethodError+ on nil.
26
+ #
27
+ # Run the install task to wire the gem into your Rails app:
28
+ #
29
+ # rails inquiry_attrs:install
30
+ #
31
+ # That creates +config/initializers/inquiry_attrs.rb+ which calls
32
+ # +ActiveSupport.on_load(:active_record)+ so that every ActiveRecord model
33
+ # gets the +.inquirer+ macro with no explicit include.
34
+ #
35
+ # For StoreModel or plain Ruby classes, include the concern manually:
36
+ #
37
+ # class ShippingAddress
38
+ # include StoreModel::Model
39
+ # include InquiryAttrs::Concern
40
+ #
41
+ # attribute :kind, :string
42
+ # inquirer :kind
43
+ # end
44
+ #
45
+ module InquiryAttrs
46
+ end
47
+
48
+ # Register the Railtie when running inside a Rails application.
49
+ # The Railtie exposes the `rails inquiry_attrs:install` rake task.
50
+ require 'inquiry_attrs/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'inquiry_attrs/installer'
4
+
5
+ namespace :inquiry_attrs do
6
+ INITIALIZER_RELATIVE = InquiryAttrs::Installer::INITIALIZER_PATH.to_s
7
+
8
+ desc 'Install an initializer that auto-includes InquiryAttrs::Concern into ActiveRecord'
9
+ task :install do
10
+ result = InquiryAttrs::Installer.install!(Rails.root)
11
+
12
+ case result
13
+ when :created then printf " %-10s %s\n", 'create', INITIALIZER_RELATIVE
14
+ when :skipped then printf " %-10s %s\n", 'skip', "#{INITIALIZER_RELATIVE} already exists"
15
+ end
16
+ end
17
+
18
+ desc 'Remove the inquiry_attrs initializer'
19
+ task :uninstall do
20
+ result = InquiryAttrs::Installer.uninstall!(Rails.root)
21
+
22
+ case result
23
+ when :removed then printf " %-10s %s\n", 'remove', INITIALIZER_RELATIVE
24
+ when :skipped then printf " %-10s %s\n", 'skip', "#{INITIALIZER_RELATIVE} not found"
25
+ end
26
+ end
27
+ end
data/llms/overview.md ADDED
@@ -0,0 +1,129 @@
1
+ # inquiry_attrs — LLM Context Overview
2
+
3
+ > Load this file before modifying or extending the gem.
4
+
5
+ ## Purpose
6
+
7
+ `inquiry_attrs` adds predicate-style inquiry methods to Rails model attributes.
8
+ It is a Rails-only gem (depends on ActiveSupport ≥ 7.0 and ActiveRecord ≥ 7.0).
9
+
10
+ ## File map
11
+
12
+ ```
13
+ lib/
14
+ inquiry_attrs.rb # Entry point — requires everything, fires on_load hook
15
+ inquiry_attrs/
16
+ version.rb # VERSION constant
17
+ nil_inquiry.rb # NilInquiry::INSTANCE — returned for blank values
18
+ symbol_inquiry.rb # SymbolInquiry — wraps Symbol attributes
19
+ concern.rb # Concern — provides .inquirer class macro
20
+ test/
21
+ test_helper.rb # Minitest setup, SQLite in-memory AR connection
22
+ inquiry_attrs/
23
+ nil_inquiry_test.rb
24
+ symbol_inquiry_test.rb
25
+ concern_test.rb # Integration: AR, StoreModel, plain Ruby, Symbol attrs
26
+ llms/
27
+ overview.md # This file
28
+ usage.md # Common patterns and recipes
29
+ ```
30
+
31
+ ## Public surface
32
+
33
+ ### Auto-include for ActiveRecord
34
+
35
+ ```ruby
36
+ # lib/inquiry_attrs.rb
37
+ ActiveSupport.on_load(:active_record) do
38
+ include InquiryAttrs::Concern
39
+ end
40
+ ```
41
+
42
+ This fires once when ActiveRecord is first loaded, so every
43
+ `ApplicationRecord` subclass automatically has access to `.inquirer` — no
44
+ explicit `include` needed in AR models.
45
+
46
+ ### `InquiryAttrs::Concern` (opt-in for non-AR classes)
47
+
48
+ ```ruby
49
+ include InquiryAttrs::Concern # adds .inquirer class method
50
+ ```
51
+
52
+ ### `.inquirer(*attrs)` — the only public class-level API
53
+
54
+ ```ruby
55
+ inquirer :status # wraps :status reader
56
+ inquirer :status, :role, :plan # wraps multiple readers at once
57
+ ```
58
+
59
+ ## Return type decision table
60
+
61
+ | Raw value (from original reader) | Return type |
62
+ |---|---|
63
+ | `nil`, `""`, or any `blank?` value | `InquiryAttrs::NilInquiry::INSTANCE` |
64
+ | `Symbol` | `InquiryAttrs::SymbolInquiry.new(raw)` |
65
+ | Any other string | `raw.to_s.inquiry` → `ActiveSupport::StringInquirer` |
66
+
67
+ ## Class responsibilities
68
+
69
+ ### `InquiryAttrs::NilInquiry`
70
+
71
+ - Frozen singleton (`INSTANCE`)
72
+ - `nil?` → `true`; `blank?` → `true`; `present?` → `false`
73
+ - Every `?`-method → `false` via `method_missing`
74
+ - `== nil`, `== ""`, `== INSTANCE` → `true`
75
+ - Implements `to_s`, `to_str`, `inspect`
76
+
77
+ ### `InquiryAttrs::SymbolInquiry < SimpleDelegator`
78
+
79
+ - Wraps a `Symbol`; raises `ArgumentError` for non-symbols
80
+ - Unwraps nested `SymbolInquiry` on init
81
+ - Any `?`-method returns `true` iff `sym.to_s == method_name.delete_suffix('?')`
82
+ - `==` accepts `Symbol`, `String`, or `SymbolInquiry`
83
+ - `is_a?(Symbol)` → `true` (and `kind_of?`)
84
+ - `nil?` → `false`; `blank?` → `false`; `present?` → `true`
85
+
86
+ ### `InquiryAttrs::Concern`
87
+
88
+ The `inquirer` class method:
89
+
90
+ 1. Captures the **original** reader with `instance_method(attr)` before
91
+ redefining — this is the critical design choice that makes plain Ruby
92
+ `attr_accessor` and StoreModel work correctly.
93
+ 2. Defines a new method via `define_method` that calls `original.bind_call(self)`
94
+ to read the raw value.
95
+ 3. Falls back to `super()` → `self[attr]` → `nil` when no original exists
96
+ (e.g., lazy AR attribute definitions or Dry::Struct).
97
+ 4. Applies the return type decision table above.
98
+
99
+ ## Why `instance_method` capture instead of `super()`?
100
+
101
+ When `inquirer :status` is called after `attr_accessor :status`, the new
102
+ `define_method` *replaces* the attr_accessor reader. There is then no superclass
103
+ method to call via `super()`, which raises `NoMethodError`. Capturing
104
+ `instance_method(:status)` before the redefinition avoids this entirely.
105
+
106
+ For AR models, `ActiveModel::Attributes` pre-defines readers before `inquirer`
107
+ is typically called, so `instance_method` finds them too.
108
+
109
+ ## Why `blank?` instead of `nil? || == ""`?
110
+
111
+ Since this is a Rails-only gem, `blank?` (from ActiveSupport) is always
112
+ available. It handles `nil`, `""`, whitespace-only strings, and any object
113
+ that defines `blank?`, all in one call.
114
+
115
+ ## Rails conventions used
116
+
117
+ | Convention | Where used |
118
+ |---|---|
119
+ | `ActiveSupport::Concern` | `InquiryAttrs::Concern` |
120
+ | `ActiveSupport.on_load(:active_record)` | `lib/inquiry_attrs.rb` — auto-include in AR |
121
+ | `ActiveSupport::StringInquirer` via `String#inquiry` | `concern.rb` — wraps string values |
122
+ | `Object#blank?` | `concern.rb` — nil/blank detection |
123
+
124
+ ## Test setup
125
+
126
+ - **Framework:** Minitest with `minitest-reporters` (SpecReporter)
127
+ - **Database:** SQLite in-memory (`adapter: 'sqlite3', database: ':memory:'`)
128
+ - **AR schema:** defined inline in `setup` with `force: true`, dropped in `teardown`
129
+ - **Run command:** `bundle exec ruby -Ilib -Itest test/inquiry_attrs/*.rb`
data/llms/usage.md ADDED
@@ -0,0 +1,204 @@
1
+ # inquiry_attrs — Usage Patterns
2
+
3
+ > Common patterns and recipes for LLMs helping users work with this gem.
4
+
5
+ ## 1. ActiveRecord — no setup needed
6
+
7
+ ```ruby
8
+ # Just call inquirer in any AR model.
9
+ # No include required — auto-added via ActiveSupport.on_load(:active_record).
10
+
11
+ class User < ApplicationRecord
12
+ inquirer :status, :role, :plan
13
+ end
14
+
15
+ user = User.new(status: 'active', role: 'admin', plan: 'pro')
16
+
17
+ user.status.active? # => true
18
+ user.status.inactive? # => false
19
+ user.role.admin? # => true
20
+ user.plan.pro? # => true
21
+ user.plan.free? # => false
22
+
23
+ # String methods still work
24
+ user.status == 'active' # => true
25
+ user.status.include?('act') # => true
26
+ user.status.upcase # => "ACTIVE"
27
+ ```
28
+
29
+ ## 2. Nil / blank safety
30
+
31
+ ```ruby
32
+ user = User.new(status: nil)
33
+
34
+ user.status.nil? # => true
35
+ user.status.blank? # => true
36
+ user.status.active? # => false ← no NoMethodError!
37
+ user.status == nil # => true
38
+ user.status.to_s # => ""
39
+
40
+ # Guard pattern
41
+ if user.status.nil?
42
+ # handle blank
43
+ elsif user.status.active?
44
+ # handle active
45
+ end
46
+ ```
47
+
48
+ ## 3. Replace case/when string comparisons
49
+
50
+ ```ruby
51
+ # Before
52
+ case user.status
53
+ when 'active' then grant_access(user)
54
+ when 'suspended' then deny_access(user)
55
+ when nil, '' then redirect_to login_path
56
+ end
57
+
58
+ # After
59
+ grant_access(user) if user.status.active?
60
+ deny_access(user) if user.status.suspended?
61
+ redirect_to login_path if user.status.nil?
62
+ ```
63
+
64
+ ## 4. Scopes and conditions
65
+
66
+ ```ruby
67
+ class User < ApplicationRecord
68
+ inquirer :status
69
+
70
+ scope :active, -> { where(status: 'active') }
71
+ scope :suspended, -> { where(status: 'suspended') }
72
+ end
73
+
74
+ # In views or controllers
75
+ User.all.select { |u| u.status.active? }
76
+ ```
77
+
78
+ ## 5. StoreModel
79
+
80
+ ```ruby
81
+ class ShippingAddress
82
+ include StoreModel::Model
83
+ include InquiryAttrs::Concern # required for non-AR classes
84
+
85
+ attribute :kind, :string
86
+ inquirer :kind
87
+ end
88
+
89
+ class Order < ApplicationRecord
90
+ attribute :shipping_address, ShippingAddress.to_type
91
+ end
92
+
93
+ order.shipping_address.kind.billing? # => true/false
94
+ order.shipping_address.kind.nil? # => true when blank
95
+ ```
96
+
97
+ ## 6. Plain Ruby class
98
+
99
+ ```ruby
100
+ class Subscription
101
+ include InquiryAttrs::Concern
102
+
103
+ attr_accessor :plan, :state
104
+
105
+ def initialize(plan:, state: nil)
106
+ @plan = plan
107
+ @state = state
108
+ end
109
+
110
+ # IMPORTANT: call inquirer AFTER attr_accessor
111
+ inquirer :plan, :state
112
+ end
113
+
114
+ sub = Subscription.new(plan: 'enterprise')
115
+ sub.plan.enterprise? # => true
116
+ sub.state.nil? # => true
117
+ ```
118
+
119
+ ## 7. Symbol attributes (e.g., Dry::Struct, enums stored as symbols)
120
+
121
+ ```ruby
122
+ # If a reader returns a Symbol, you get SymbolInquiry
123
+ si = InquiryAttrs::SymbolInquiry.new(:active)
124
+ si.active? # => true
125
+ si.inactive? # => false
126
+ si == :active # => true
127
+ si == 'active' # => true
128
+ si.is_a?(Symbol) # => true
129
+ si.to_s # => "active"
130
+ si.to_sym # => :active
131
+ ```
132
+
133
+ ## 8. Testing models with inquirer
134
+
135
+ ```ruby
136
+ # Minitest
137
+ class UserTest < ActiveSupport::TestCase
138
+ test 'status predicate' do
139
+ user = User.new(status: 'active')
140
+ assert user.status.active?
141
+ refute user.status.inactive?
142
+ assert_equal 'active', user.status
143
+ end
144
+
145
+ test 'nil status is safe' do
146
+ user = User.new(status: nil)
147
+ assert user.status.nil?
148
+ refute user.status.active?
149
+ end
150
+ end
151
+ ```
152
+
153
+ ## 9. Detecting NilInquiry in code
154
+
155
+ ```ruby
156
+ # Use nil? or == nil — do NOT use is_a?(NilClass)
157
+ user.status.nil? # => true ✅
158
+ user.status == nil # => true ✅
159
+ user.status.is_a?(NilClass) # => false ❌ (it's NilInquiry, not NilClass)
160
+
161
+ # Or compare to the instance directly
162
+ user.status.equal?(InquiryAttrs::NilInquiry::INSTANCE) # => true
163
+ ```
164
+
165
+ ## 10. Common mistakes
166
+
167
+ ```ruby
168
+ # ❌ Calling inquirer before attr_accessor in plain Ruby
169
+ class Broken
170
+ include InquiryAttrs::Concern
171
+ inquirer :status # no original reader to capture yet!
172
+ attr_accessor :status
173
+ end
174
+
175
+ # ✅ Always after
176
+ class Fixed
177
+ include InquiryAttrs::Concern
178
+ attr_accessor :status
179
+ inquirer :status
180
+ end
181
+
182
+ # ❌ Forgetting include for non-AR classes
183
+ class MyStoreModel
184
+ include StoreModel::Model
185
+ # include InquiryAttrs::Concern ← missing!
186
+ inquirer :status # => NoMethodError
187
+ end
188
+
189
+ # ✅ Explicit include for StoreModel / plain Ruby
190
+ class MyStoreModel
191
+ include StoreModel::Model
192
+ include InquiryAttrs::Concern # ← required
193
+ inquirer :status
194
+ end
195
+ ```
196
+
197
+ ## 11. Difference from `String#inquiry` (ActiveSupport)
198
+
199
+ | Feature | `"active".inquiry` | `inquirer :status` |
200
+ |---|---|---|
201
+ | Nil/blank safety | Raises `NoMethodError` | Returns `NilInquiry::INSTANCE` |
202
+ | Symbol support | Manual conversion | Automatic `SymbolInquiry` |
203
+ | Model integration | Manual wrapping | Declarative macro |
204
+ | AR auto-include | No | Yes, via `on_load` |
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inquiry_attrs
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Pawel Niemczyk
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: railties
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.0'
54
+ description: |
55
+ InquiryAttrs wraps ActiveRecord/ActiveModel (and StoreModel/Dry::Struct) attributes with
56
+ predicate-style inquiry methods. Write user.status.active? instead of
57
+ user.status == "active". Blank/nil values safely return false for every
58
+ predicate — no more NoMethodError on nil. Run `rails inquiry_attrs:install`
59
+ to generate an initializer that auto-includes the concern into every
60
+ ActiveRecord model.
61
+ email: []
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - AGENTS.md
67
+ - CHANGELOG.md
68
+ - CLAUDE.md
69
+ - LICENSE
70
+ - README.md
71
+ - lib/inquiry_attrs.rb
72
+ - lib/inquiry_attrs/concern.rb
73
+ - lib/inquiry_attrs/installer.rb
74
+ - lib/inquiry_attrs/nil_inquiry.rb
75
+ - lib/inquiry_attrs/railtie.rb
76
+ - lib/inquiry_attrs/symbol_inquiry.rb
77
+ - lib/inquiry_attrs/version.rb
78
+ - lib/tasks/inquiry_attrs.rake
79
+ - llms/overview.md
80
+ - llms/usage.md
81
+ homepage: https://github.com/your-org/inquiry_attrs
82
+ licenses:
83
+ - MIT
84
+ metadata:
85
+ homepage_uri: https://github.com/your-org/inquiry_attrs
86
+ source_code_uri: https://github.com/your-org/inquiry_attrs
87
+ changelog_uri: https://github.com/your-org/inquiry_attrs/blob/main/CHANGELOG.md
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '3.0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubygems_version: 3.6.9
103
+ specification_version: 4
104
+ summary: Predicate-style inquiry methods for Rails model attributes
105
+ test_files: []