activerecord-hashlike_access 0.1.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: 3b639e40814dc74be8707c77afa1ae4e356482c1d287ff736e9b13b2a4333ffb
4
+ data.tar.gz: 7dfabf54b88b563a9583e4cfebb11e825e2143ce08b66988752d3b3783eee954
5
+ SHA512:
6
+ metadata.gz: 7b7a842f6c8385d8f288d5fdf90b07bc26f9d4ccc09c66c48e8d8533e602324086a9894bcb005c71be50e28336670967fb32a601425581c2ac016f15abe33ff1
7
+ data.tar.gz: d5e8c429394c07c2b5191d297ef394c122b1b0aa35506df9fc0755f069a6ba2b9f8af133f3187f841dbb226488b3747585791c821822c4a06b3cdd9c83296b49
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,75 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+
7
+ Layout/HashAlignment:
8
+ # Align the elements of a hash literal if they span more than one line.
9
+ EnforcedHashRocketStyle: table
10
+ EnforcedColonStyle: table
11
+
12
+
13
+ Lint/DuplicateBranch:
14
+ # Checks that there are no repeated bodies within `if/unless`, `case-when` and `rescue` constructs.
15
+ Enabled: false
16
+
17
+
18
+ Metrics/AbcSize:
19
+ # Checks that the ABC size of methods is not higher than the configured maximum.
20
+ Max: 20
21
+
22
+ Metrics/BlockLength:
23
+ # Avoid long blocks with many lines.
24
+ Exclude:
25
+ - spec/**/*
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ # A complexity metric that is strongly correlated to the number of test cases needed to validate a method.
29
+ Max: 10
30
+
31
+ Metrics/MethodLength:
32
+ # Avoid methods longer than 25 lines of code.
33
+ Max: 25
34
+
35
+
36
+ Naming/VariableNumber:
37
+ # Use the configured style when numbering symbols, methods and variables.
38
+ Enabled: false
39
+
40
+
41
+ Style/AsciiComments:
42
+ # Use only ASCII symbols in comments.
43
+ #
44
+ # NLC: We're allowing non-breaking spaces (' '), as they're needed to prevent
45
+ # browsers from collapsing multiple spaces in documentation code blocks.
46
+ AllowedChars:
47
+ - ' '
48
+
49
+ Style/FormatString:
50
+ # Enforce the use of Kernel#sprintf, Kernel#format or String#%.
51
+ EnforcedStyle: percent
52
+
53
+ Style/FrozenStringLiteralComment:
54
+ # Enforces use of the frozen_string_literal comment in every file.
55
+ EnforcedStyle: never
56
+
57
+ Style/NumericLiterals:
58
+ # Add underscores to large numeric literals to improve their readability.
59
+ Enabled: false
60
+
61
+ Style/PreferredHashMethods:
62
+ # Checks use of `has_key?` and `has_value?` Hash methods.
63
+ EnforcedStyle: verbose
64
+
65
+ Style/RegexpLiteral:
66
+ # Use / or %r around regular expressions.
67
+ EnforcedStyle: percent_r
68
+
69
+ Style/RescueModifier:
70
+ # Avoid using rescue in its modifier form.
71
+ Enabled: false
72
+
73
+ Style/StringLiterals:
74
+ # Check for use of ' vs ".
75
+ ConsistentQuotesInMultiline: true
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --format=html
2
+ --markup=markdown
3
+ --output-dir=./docs
4
+ --private
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.0] - 2026-08-06
4
+
5
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'irb'
5
+ gem 'rake', '~> 13'
6
+ gem 'rspec', '~> 3'
7
+ gem 'rubocop'
8
+ gem 'sqlite3'
9
+ gem 'yard'
data/Gemfile.lock ADDED
@@ -0,0 +1,207 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ activerecord-hashlike_access (0.0.0)
5
+ activerecord (>= 6)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (8.1.3.1)
11
+ activesupport (= 8.1.3.1)
12
+ activerecord (8.1.3.1)
13
+ activemodel (= 8.1.3.1)
14
+ activesupport (= 8.1.3.1)
15
+ timeout (>= 0.4.0)
16
+ activesupport (8.1.3.1)
17
+ base64
18
+ bigdecimal
19
+ concurrent-ruby (~> 1.0, >= 1.3.1)
20
+ connection_pool (>= 2.2.5)
21
+ drb
22
+ i18n (>= 1.6, < 2)
23
+ json
24
+ logger (>= 1.4.2)
25
+ minitest (>= 5.1)
26
+ securerandom (>= 0.3)
27
+ tzinfo (~> 2.0, >= 2.0.5)
28
+ uri (>= 0.13.1)
29
+ ast (2.4.3)
30
+ base64 (0.3.0)
31
+ bigdecimal (4.1.2)
32
+ concurrent-ruby (1.3.8)
33
+ connection_pool (3.0.2)
34
+ diff-lcs (1.6.2)
35
+ drb (2.2.3)
36
+ erb (6.0.7)
37
+ i18n (1.15.2)
38
+ concurrent-ruby (~> 1.0)
39
+ io-console (0.9.2)
40
+ irb (1.18.0)
41
+ pp (>= 0.6.0)
42
+ prism (>= 1.3.0)
43
+ rdoc (>= 4.0.0)
44
+ reline (>= 0.4.2)
45
+ json (2.21.2)
46
+ language_server-protocol (3.17.0.6)
47
+ lint_roller (1.1.0)
48
+ logger (1.7.0)
49
+ minitest (6.0.6)
50
+ drb (~> 2.0)
51
+ prism (~> 1.5)
52
+ parallel (2.1.0)
53
+ parser (3.3.12.0)
54
+ ast (~> 2.4.1)
55
+ racc
56
+ pp (0.6.4)
57
+ prettyprint
58
+ prettyprint (0.2.0)
59
+ prism (1.9.0)
60
+ racc (1.8.1)
61
+ rainbow (3.1.1)
62
+ rake (13.4.2)
63
+ rbs (4.1.3)
64
+ logger
65
+ prism (>= 1.6.0)
66
+ tsort
67
+ rdoc (8.0.0)
68
+ erb
69
+ prism (>= 1.6.0)
70
+ rbs (>= 4.0.0)
71
+ tsort
72
+ regexp_parser (2.12.0)
73
+ reline (0.7.0)
74
+ io-console (~> 0.5)
75
+ rspec (3.13.2)
76
+ rspec-core (~> 3.13.0)
77
+ rspec-expectations (~> 3.13.0)
78
+ rspec-mocks (~> 3.13.0)
79
+ rspec-core (3.13.6)
80
+ rspec-support (~> 3.13.0)
81
+ rspec-expectations (3.13.5)
82
+ diff-lcs (>= 1.2.0, < 2.0)
83
+ rspec-support (~> 3.13.0)
84
+ rspec-mocks (3.13.8)
85
+ diff-lcs (>= 1.2.0, < 2.0)
86
+ rspec-support (~> 3.13.0)
87
+ rspec-support (3.13.7)
88
+ rubocop (1.89.0)
89
+ json (~> 2.3)
90
+ language_server-protocol (~> 3.17.0.2)
91
+ lint_roller (~> 1.1.0)
92
+ parallel (>= 1.10)
93
+ parser (>= 3.3.0.2)
94
+ rainbow (>= 2.2.2, < 4.0)
95
+ regexp_parser (>= 2.9.3, < 3.0)
96
+ rubocop-ast (>= 1.49.0, < 2.0)
97
+ ruby-progressbar (~> 1.7)
98
+ unicode-display_width (>= 2.4.0, < 4.0)
99
+ rubocop-ast (1.50.0)
100
+ parser (>= 3.3.7.2)
101
+ prism (~> 1.7)
102
+ ruby-progressbar (1.13.0)
103
+ securerandom (0.4.1)
104
+ sqlite3 (2.9.5-aarch64-linux-gnu)
105
+ sqlite3 (2.9.5-aarch64-linux-musl)
106
+ sqlite3 (2.9.5-arm-linux-gnu)
107
+ sqlite3 (2.9.5-arm-linux-musl)
108
+ sqlite3 (2.9.5-arm64-darwin)
109
+ sqlite3 (2.9.5-x86-linux-gnu)
110
+ sqlite3 (2.9.5-x86-linux-musl)
111
+ sqlite3 (2.9.5-x86_64-darwin)
112
+ sqlite3 (2.9.5-x86_64-linux-gnu)
113
+ sqlite3 (2.9.5-x86_64-linux-musl)
114
+ timeout (0.6.1)
115
+ tsort (0.2.0)
116
+ tzinfo (2.0.6)
117
+ concurrent-ruby (~> 1.0)
118
+ unicode-display_width (3.2.0)
119
+ unicode-emoji (~> 4.1)
120
+ unicode-emoji (4.2.0)
121
+ uri (1.1.1)
122
+ yard (0.9.45)
123
+
124
+ PLATFORMS
125
+ aarch64-linux-gnu
126
+ aarch64-linux-musl
127
+ arm-linux-gnu
128
+ arm-linux-musl
129
+ arm64-darwin
130
+ x86-linux-gnu
131
+ x86-linux-musl
132
+ x86_64-darwin
133
+ x86_64-linux-gnu
134
+ x86_64-linux-musl
135
+
136
+ DEPENDENCIES
137
+ activerecord-hashlike_access!
138
+ irb
139
+ rake (~> 13)
140
+ rspec (~> 3)
141
+ rubocop
142
+ sqlite3
143
+ yard
144
+
145
+ CHECKSUMS
146
+ activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88
147
+ activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b
148
+ activerecord-hashlike_access (0.0.0)
149
+ activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376
150
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
151
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
152
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
153
+ bundler (4.0.14) sha256=d09a0a965cf772266a7e49e83610be7c2f4e49e61134c42a56804bb383cc24b8
154
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
155
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
156
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
157
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
158
+ erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
159
+ i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5
160
+ io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
161
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
162
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
163
+ language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
164
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
165
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
166
+ minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
167
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
168
+ parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
169
+ pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
170
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
171
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
172
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
173
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
174
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
175
+ rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4
176
+ rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
177
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
178
+ reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
179
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
180
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
181
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
182
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
183
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
184
+ rubocop (1.89.0) sha256=4dee8e3ee9c45e474834efd9e8d6fd031e8331c8dacdff0de4ad65ae0a6faae7
185
+ rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
186
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
187
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
188
+ sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
189
+ sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
190
+ sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b
191
+ sqlite3 (2.9.5-arm-linux-musl) sha256=bae1109d12b2e9f588455967729b008e1ff4feb7761749df695019c9079913c6
192
+ sqlite3 (2.9.5-arm64-darwin) sha256=d0cf444a70fc9395d513cfbcc1e6719e224aa645314e3824cb0474c721425aa2
193
+ sqlite3 (2.9.5-x86-linux-gnu) sha256=c94b96b16f17796be6fa099d15218b52e396f55690c4760faaaefa21ebab9dd5
194
+ sqlite3 (2.9.5-x86-linux-musl) sha256=063a8c13cbadfe7f29453b1706cbdf91fca4a78d244f816ff20bac4fb259f1e4
195
+ sqlite3 (2.9.5-x86_64-darwin) sha256=8e9caae38bd7ebb29cbeee3e7ab1d12dc2327d9a1b92c7fcf0dda05589627a81
196
+ sqlite3 (2.9.5-x86_64-linux-gnu) sha256=233dbcb6714148dd23bc5aeb33e8efd6eac974969564ddd5794c23d5f52b231e
197
+ sqlite3 (2.9.5-x86_64-linux-musl) sha256=e7d3a7474e8af0f96150c21abc203fbab5437206bfcdf11deab7741c0ca516f2
198
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
199
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
200
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
201
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
202
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
203
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
204
+ yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51
205
+
206
+ BUNDLED WITH
207
+ 4.0.14
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Nestor Custodio
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,241 @@
1
+ [![MIT License](https://img.shields.io/github/license/nestor-custodio/activerecord-hashlike_access)](https://tldrlegal.com/license/mit-license)
2
+
3
+
4
+ # ActiveRecord::HashlikeAccess
5
+
6
+ Pulling this module into your ActiveRecord models gives you Hash-like getters (`[]`) and setters (`[]=`) for _individual records/values_. This is not only slightly cleaner to read/write, but also encourages fetching items by either their `id` or a canonical "lookup field", minimizing the likelihood of pulling records by an unindexed field.
7
+
8
+ In simplest terms:
9
+ ```ruby
10
+ # Given a "roles" table with
11
+ # `id`, `name`, and `display_text`...
12
+
13
+
14
+ # Hash-Like Access to Records:
15
+
16
+ class Role < ApplicationRecord
17
+ extend ActiveRecord::HashlikeAccess
18
+ hashlike_access by: :name
19
+ end
20
+
21
+ role = Role.find_by name: name
22
+ # ... becomes ...
23
+ role = Role[name]
24
+
25
+
26
+ # Hash-Like Access to Values:
27
+
28
+ class Role < ApplicationRecord
29
+ extend ActiveRecord::HashlikeAccess
30
+ hashlike_access to: :display_text, by: :name
31
+ end
32
+
33
+ role_display_text = Role.find_by(name: name).display_text
34
+ # ... becomes ...
35
+ role_display_text = Role[name]
36
+
37
+ Role.find_by(name: name).update! display_text: new_display_text
38
+ # ... becomes ...
39
+ Role[name] = new_display_text
40
+
41
+ ```
42
+
43
+ Importantly, note that **this gem works on the model, not its relations**. ActiveRecord's existing bracket constructs for referencing _relations_ (i.e. resultsets) in an `Array`-like fashion will continue to work as they always have.
44
+
45
+
46
+ ## Installation
47
+
48
+ - If your project uses [Bundler](https://github.com/bundler/bundler):
49
+ - Add the following to your application's Gemfile:
50
+ ```ruby
51
+ gem 'activerecord-hashlike_access'
52
+ ```
53
+ - And then run a:
54
+ ```shell
55
+ $ bundle install
56
+ ```
57
+
58
+ - Or, you can keep things simple with a manual install:
59
+ ```shell
60
+ $ gem install activerecord-hashlike_access
61
+ ```
62
+
63
+
64
+ ## Usage
65
+
66
+ Incorporating the `ActiveRecord::HashlikeAccess` module into a model will allow you to access it by a hash-like syntax, with the entrypoints for defining _what_ you would like access to and _how_ you would like to access it being the `hashlike_access` and `hashlike_access!` methods.
67
+
68
+ These two methods are nearly identical, the only difference being that a `hashlike_access` lookup that finds no matching records will return a `nil` (similar to ActiveRecord's `find_by`), whereas a failed `hashlike_access!` lookup will instead `raise ActiveRecord::RecordNotFound` (as a `find_by!` would).
69
+
70
+ `hashlike_access`/`hashlike_access!` take two arguments:
71
+ - `to` (optional): the field (or _arity-0_ method) whose value you would like to get back for hash-like lookups; if omitted, the full record is returned
72
+ - `by` (optional): the field by which you would like to find matching records; if omitted, the model's primary key is used
73
+
74
+ If your model pulls from `ActiveRecord::HashlikeAccess` but does not explicitly call one of these methods, the default behaviour will grant you hash-like access to records by the model's primary key.
75
+
76
+
77
+ ### Hash-Like Access to _Records_
78
+
79
+ ```ruby
80
+ class YourModel < ApplicationRecord
81
+ extend ActiveRecord::HashlikeAccess
82
+
83
+ # For implicit access to records BY the primary key,
84
+ # you can omit the `hashlike_access` call altogether.
85
+
86
+ # ...
87
+
88
+
89
+ # For explicit access to records BY the primary key,
90
+ # call `hashlike_access` while omitting the `to` option:
91
+
92
+ hashlike_access by: primary_key
93
+
94
+
95
+ # For access to records BY an arbitrary "lookup field",
96
+ # give `hashlike_access` the 'by' field and no 'to' option:
97
+
98
+ hashlike_access by: :some_lookup_field_name
99
+
100
+ end
101
+ ```
102
+
103
+ Any of the above options will make hash-like access (`YourModel[some_value]`) equivalent to a `find_by` (or a `find_by!`, in the case of `hashlike_access!`) using the requested field, resulting in:
104
+ - the matching record, if one is found
105
+ - a `nil` value, in the case of `hashlike_access`
106
+ - an `ActiveRecord::RecordNotFound` error, in the case of `hashlike_access!`
107
+
108
+
109
+ ### Hash-Like Access to _Values_
110
+
111
+ ```ruby
112
+ class YourModel < ApplicationRecord
113
+ extend ActiveRecord::HashlikeAccess
114
+
115
+ # For access TO a specific value for records matched BY the primary key,
116
+ # `hashlike_access` needs the target name via 'to', but the 'by' is optional:
117
+
118
+ hashlike_access to: :field_or_method_name
119
+ # ... or ...
120
+ hashlike_access to: :field_or_method_name, by: primary_key
121
+
122
+
123
+ # For access TO a specific value BY an arbitrary "lookup field",
124
+ # give `hashlike_access` both the 'to' target and the 'by' field:
125
+
126
+ hashlike_access to: :field_or_method_name, by: :some_lookup_field_name
127
+
128
+ end
129
+ ```
130
+
131
+ Any of the above options will make hash-like access (`YourModel[some_value]`) equivalent to a `find_by`/`find_by!` followed by the requested method call. As with record lookups: if no matching record is found, expect a `nil` value or an `ActiveRecord::RecordNotFound` error, depending on whether you use `hashlike_access` or `hashlike_access!`.
132
+
133
+
134
+ ### Hash-Like Assignment to _Values_
135
+
136
+ Requesting hash-like access to a value ([see above](#hash-like-access-to-values)) also makes hash-like _assignment_ (`[]=`) available **provided that makes sense**, i.e.:
137
+
138
+ - You have hash-like access to a field and the model is not read-only:
139
+ ```ruby
140
+ class YourModel < ApplicationRecord
141
+ extend ActiveRecord::HashlikeAccess
142
+ hashlike_access to: :field_x
143
+ end
144
+
145
+ # Getting a value:
146
+ field_x = YourModel[some_key]
147
+
148
+ # Setting a value:
149
+ YourModel[some_key] = new_value_for_field_x
150
+ ```
151
+
152
+ - You have hash-like access to a method and there is _also_ a corresponding `=` method for it:
153
+ ```ruby
154
+ class YourModel < ApplicationRecord
155
+ extend ActiveRecord::HashlikeAccess
156
+ hashlike_access to: :method_x
157
+
158
+ def method_x
159
+ # ...
160
+ end
161
+
162
+ def method_x=(value)
163
+ # ...
164
+ end
165
+ end
166
+
167
+ # Getting a value:
168
+ method_x = YourModel[some_key]
169
+
170
+ # Setting a value:
171
+ YourModel[some_key] = value_to_pass_to_method_x_assignment
172
+ ```
173
+ If you attempt a hash-like value assignment and the requisite assignment method has not been defined, a `NoMethodError` will be raised.
174
+
175
+ Note hash-like value assignments are backed by an `update!` call, which will:
176
+ - run validations, possibly raising an `ActiveRecord::RecordInvalid` error
177
+ - trigger callbacks, possibly raising an `ActiveRecord::RecordNotSaved` error
178
+ - update timestamps
179
+
180
+
181
+ ### Using Non-Scalar Keys
182
+
183
+ If your model uses a composite primary key, hash-like access happens by passing all the needed values:
184
+ ```ruby
185
+ class YourModel < ApplicationRecord
186
+ extend ActiveRecord::HashlikeAccess
187
+ self.primary_key = [:primary_key_1, :primary_key_2]
188
+ end
189
+
190
+
191
+ # You need to provide as many key values
192
+ # as there are fields in the primary key:
193
+
194
+ record = YourModel[1, 2]
195
+ # ... or if you have an array:
196
+ record = YourModel[*key_values]
197
+ ```
198
+
199
+ You can also do this if you want to use multiple non-primary-key lookup fields by passing an array to the `by` option:
200
+ ```ruby
201
+ class YourModel < ApplicationRecord
202
+ extend ActiveRecord::HashlikeAccess
203
+ hashlike_access by: [:lookup_field_1, :lookup_field_2]
204
+ end
205
+
206
+
207
+ # You need to provide as many key values
208
+ # as there were fields in the "by" option:
209
+
210
+ record = YourModel[1, 2]
211
+ # ... or if you have an array:
212
+ record = YourModel[*lookup_values]
213
+ ```
214
+
215
+
216
+ ## Performance Considerations
217
+
218
+ Every instance of a hash-like lookup (`[some_key]`) is backed by a `find_by`/`find_by!` call, but ActiveRecord's query cache goes _a long way_ to reducing the performance penalty from repeated calls for the same record within a single worker.
219
+
220
+
221
+ ## Potential Gotchas
222
+
223
+ As with anything ActiveRecord-related, there are always several ways to shoot yourself in the foot if you're not careful. Here are a few items to keep in mind and hopefully prevent this:
224
+
225
+ - The hash-like access construct is essentially syntax sugar around ActiveRecord's `find_by` mechanism. For many (most?) database engines, selecting (or _including_, in the case of a multi-field key) an unindexed field for your lookups will likely result in a full-table scan with every hash-like access. Be mindful of what field(s) you're using as your key(s).
226
+
227
+ - Selecting a lookup field with non-unique values means the matching record you get back is non-deterministic _unless you take precautions to avoid this_ (e.g. by setting a default scope with an `order` on fields that yield a unique combination of values). This should go without saying, but: maybe don't use a non-unique field to try to find a specific record?
228
+
229
+
230
+ ## Contribution / Development
231
+
232
+ Bug reports and pull requests are welcome at: [https://github.com/nestor-custodio/activerecord-hashlike_access](https://github.com/nestor-custodio/activerecord-hashlike_access)
233
+
234
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
235
+
236
+ Linting is courtesy of [Rubocop](https://docs.rubocop.org/) (`rake rubocop`) and documentation is built using [YARD](https://yardoc.org/). Please ensure you have a clean bill of health from Rubocop and that any new features and/or changes to behaviour are reflected in the adjacent documentation before submitting a pull request.
237
+
238
+
239
+ ## License
240
+
241
+ `ActiveRecord::HashlikeAccess` is available as open source under the terms of the [MIT License](https://tldrlegal.com/license/mit-license).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/active_record/hashlike_access/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'activerecord-hashlike_access'
5
+ spec.version = ActiveRecord::HashlikeAccess::VERSION
6
+ spec.authors = ['Nestor Custodio']
7
+ spec.email = ['nestor@custodio.org']
8
+
9
+ spec.summary = 'Provides Hash-like access to ActiveRecord models.'
10
+ spec.homepage = 'https://github.com/nestor-custodio/activerecord-hashlike_acces'
11
+ spec.license = 'MIT'
12
+ spec.required_ruby_version = '>= 3.4'
13
+
14
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/CHANGELOG.md"
18
+ spec.metadata['rubygems_mfa_required'] = 'true'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(__dir__) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
25
+ end
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # This of course requires ActiveRecord.
32
+ #
33
+ spec.add_dependency 'activerecord', '>= 6'
34
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module HashlikeAccess
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,114 @@
1
+ require 'active_record'
2
+
3
+ module ActiveRecord
4
+ # The HashlikeAccess module simplifies querying for model records by providing
5
+ # `find_by`-equivalent access to records (or their values) via bracket syntax.
6
+ #
7
+ module HashlikeAccess
8
+ # Finds a record by the previously-configured "lookup_fields" and calls the requested "response_method" on it.
9
+ #
10
+ # @param * [Array]
11
+ # A list consisting of lookup values to match against the "lookup_fields" in search of a record.
12
+ # These are passed through, unaltered, to {.hashlike_access_lookup}.
13
+ #
14
+ # @return
15
+ # Returns either `nil` or the result of calling the "response_method" on the matched record.
16
+ #
17
+ # @raise [ActiveRecord::RecordNotFound]
18
+ #
19
+ def [](*)
20
+ hashlike_access_lookup(*)&.public_send hashlike_access_config[:response_method]
21
+ end
22
+
23
+ # Finds a record (as in {.[]}), and calls the "assignment_method" with the remaining param.
24
+ #
25
+ # @param * [Array]
26
+ # A list consisting of lookup values as well as the actual assignment value.
27
+ # All but the last of these is passed through, unaltered, to {.hashlike_access_lookup}.
28
+ #
29
+ # @return
30
+ # As with all assignment methods, this returns the assignment value given.
31
+ #
32
+ # @raise [NoMethodError, ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved]
33
+ #
34
+ def []=(*args)
35
+ value = args.pop
36
+ record = hashlike_access_lookup(*args) || return
37
+
38
+ record.public_send hashlike_access_config[:assignment_method], value
39
+ record.save!
40
+ end
41
+
42
+ private
43
+
44
+ # The primary entrypoint for defining hash-like access behaviours.
45
+ #
46
+ # @option [Symbol, String] :to
47
+ # The name of the method to call on a record once found. Defaults to `:itself`, yielding the full record.
48
+ #
49
+ # @option [Symbol, String, Array<Symbol>, Array<String>] :by
50
+ # The field name (or list of names) against which lookup values should match to yield a "found record".
51
+ # IOW, this is the list of keys that will be passed to a `find_by` equivalent when looking for a record.
52
+ #
53
+ # @option [true, false] :raise_if_not_found
54
+ # Determines whether failing to find a requested record should raise an `ActiveRecord::RecordNotFound` error.
55
+ # Defaults to `false`.
56
+ #
57
+ # @return [Hash]
58
+ # Returns the resulting hash-like access config.
59
+ #
60
+ def hashlike_access(to: :itself, by: nil, raise_if_not_found: false)
61
+ requested_config = { lookup_fields: Array.wrap(by),
62
+ response_method: to,
63
+ assignment_method: :"#{to}=",
64
+ raise_if_not_found: raise_if_not_found }
65
+
66
+ hashlike_access_config.merge! requested_config.compact_blank
67
+ end
68
+
69
+ # A convenience method that calls {.hashlike_access} with `raise_if_not_found: true`.
70
+ #
71
+ # @option [Symbol, String] :to
72
+ # The name of the method to call on a record once found. Defaults to `:itself`, yielding the full record.
73
+ #
74
+ # @option [Symbol, String, Array<Symbol>, Array<String>] :by
75
+ # The field name (or list of names) against which lookup values should match to yield a "found record".
76
+ # IOW, this is the list of keys that will be passed to a `find_by` equivalent when looking for a record.
77
+ #
78
+ # @return [Hash]
79
+ # Returns the resulting hash-like access config.
80
+ #
81
+ def hashlike_access!(to: :itself, by: nil)
82
+ hashlike_access(to:, by:, raise_if_not_found: true)
83
+ end
84
+
85
+ # Returns the current hash-like access config, with sensible defaults if {.hashlike_access} has not been called.
86
+ #
87
+ # @return [Hash]
88
+ # Returns the current hash-like access config.
89
+ #
90
+ def hashlike_access_config
91
+ @hashlike_access_config ||= { lookup_fields: Array.wrap(primary_key),
92
+ response_method: :itself,
93
+ assignment_method: nil,
94
+ raise_if_not_found: false }
95
+ end
96
+
97
+ # Finds a record using the configured "lookup_fields" matched to the provided "lookup_values".
98
+ #
99
+ # @param lookup_values [Array]
100
+ # A list consisting of lookup values to match against the "lookup_fields" in search of a record.
101
+ #
102
+ # @return [ActiveRecord::Base, nil]
103
+ # Returns the matching record or `nil` if none was found and the "raise_if_not_found" config is `false`.
104
+ #
105
+ # @raise [ActiveRecord::RecordNotFound]
106
+ #
107
+ def hashlike_access_lookup(*lookup_values)
108
+ record = find_by hashlike_access_config[:lookup_fields].zip(lookup_values).to_h
109
+ raise ActiveRecord::RecordNotFound if record.blank? && hashlike_access_config[:raise_if_not_found]
110
+
111
+ record
112
+ end
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-hashlike_access
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nestor Custodio
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: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6'
26
+ email:
27
+ - nestor@custodio.org
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - ".rspec"
33
+ - ".rubocop.yml"
34
+ - ".ruby-version"
35
+ - ".yardopts"
36
+ - CHANGELOG.md
37
+ - Gemfile
38
+ - Gemfile.lock
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - activerecord-hashlike_access.gemspec
43
+ - lib/active_record/hashlike_access.rb
44
+ - lib/active_record/hashlike_access/version.rb
45
+ homepage: https://github.com/nestor-custodio/activerecord-hashlike_acces
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ allowed_push_host: https://rubygems.org
50
+ homepage_uri: https://github.com/nestor-custodio/activerecord-hashlike_acces
51
+ changelog_uri: https://github.com/nestor-custodio/activerecord-hashlike_acces/CHANGELOG.md
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.4'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.6.9
68
+ specification_version: 4
69
+ summary: Provides Hash-like access to ActiveRecord models.
70
+ test_files: []