anchormodel 0.3.0 → 0.4.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 +4 -4
- data/.gitignore +3 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +19 -0
- data/EXAMPLES.md +408 -0
- data/Gemfile.lock +8 -8
- data/README.md +43 -1
- data/VERSION +1 -1
- data/anchormodel.gemspec +4 -4
- data/bin/test +9 -0
- data/doc/Anchormodel/ActiveModelTypeValueMulti.html +204 -42
- data/doc/Anchormodel/ActiveModelTypeValueSingle.html +245 -92
- data/doc/Anchormodel/Attribute.html +60 -37
- data/doc/Anchormodel/ModelMixin.html +166 -16
- data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +82 -21
- data/doc/Anchormodel/SimpleFormInputs/Helpers.html +2 -11
- data/doc/Anchormodel/SimpleFormInputs.html +2 -11
- data/doc/Anchormodel/Util.html +497 -38
- data/doc/Anchormodel/Version.html +2 -20
- data/doc/Anchormodel.html +536 -129
- data/doc/AnchormodelCheckBoxesInput.html +22 -1
- data/doc/AnchormodelGenerator.html +64 -13
- data/doc/AnchormodelInput.html +24 -1
- data/doc/AnchormodelRadioButtonsInput.html +22 -1
- data/doc/_index.html +16 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +40 -2
- data/doc/index.html +40 -2
- data/doc/method_list.html +57 -17
- data/doc/top-level-namespace.html +1 -1
- data/lib/anchormodel/active_model_type_value_multi.rb +31 -5
- data/lib/anchormodel/active_model_type_value_single.rb +51 -13
- data/lib/anchormodel/attribute.rb +20 -8
- data/lib/anchormodel/model_mixin.rb +43 -8
- data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +7 -0
- data/lib/anchormodel/simple_form_inputs/anchormodel_input.rb +9 -0
- data/lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb +7 -0
- data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +14 -0
- data/lib/anchormodel/util.rb +102 -17
- data/lib/anchormodel.rb +109 -15
- data/lib/generators/anchormodel/anchormodel_generator.rb +8 -0
- data/test/active_record_model/user_test.rb +256 -6
- data/test/dummy/app/anchormodels/animal.rb +1 -0
- metadata +4 -2
|
@@ -5,12 +5,14 @@ class UserTest < Minitest::Test
|
|
|
5
5
|
User.destroy_all
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
# `find` by Symbol and by String must return the same singleton instance.
|
|
8
9
|
def test_retrieval
|
|
9
10
|
assert_equal Role.find(:guest), Role.find('guest')
|
|
10
11
|
end
|
|
11
12
|
|
|
13
|
+
# `Anchormodel.all` preserves declaration order and is isolated per subclass
|
|
14
|
+
# (no cross-class registry pollution).
|
|
12
15
|
def test_collections
|
|
13
|
-
# Order must fit as well
|
|
14
16
|
assert_equal(
|
|
15
17
|
%i[guest moderator admin the_chosen_one].map { |key| Role.find(key) },
|
|
16
18
|
Role.all
|
|
@@ -21,15 +23,40 @@ class UserTest < Minitest::Test
|
|
|
21
23
|
)
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
# All three assignment forms are valid: String, Symbol, Anchormodel instance.
|
|
27
|
+
# Reader always returns the Anchormodel instance regardless of input form.
|
|
24
28
|
def test_basic_setters_and_getters
|
|
25
29
|
u = User.create!(role: 'guest', locale: 'de') # String assignment
|
|
26
30
|
assert_equal Role.find(:guest), u.role
|
|
27
31
|
assert_equal Locale.find(:de), u.locale
|
|
28
|
-
u.update!(role: :admin, locale: Locale.find(:en)) # Symbol and Anchormodel
|
|
32
|
+
u.update!(role: :admin, locale: Locale.find(:en)) # Symbol and Anchormodel assignment
|
|
29
33
|
assert_equal Role.find(:admin), u.role
|
|
30
34
|
assert_equal Locale.find(:en), u.locale
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
# `Anchormodel#==` defined class+key equality, but `hash` and `eql?` defaulted to
|
|
38
|
+
# object identity. Worked in practice only because instances are singletons via
|
|
39
|
+
# `entries_hash`. Any non-singleton copy (e.g. `dup`, Marshal round-trip, test doubles)
|
|
40
|
+
# broke `Set`/`Hash` membership and `==` comparison invariants.
|
|
41
|
+
def test_hash_and_eql_match_equality
|
|
42
|
+
admin1 = Role.find(:admin)
|
|
43
|
+
admin2 = admin1.dup
|
|
44
|
+
|
|
45
|
+
assert_equal admin1, admin2
|
|
46
|
+
assert_equal admin1.hash, admin2.hash
|
|
47
|
+
assert admin1.eql?(admin2)
|
|
48
|
+
assert admin2.eql?(admin1)
|
|
49
|
+
|
|
50
|
+
# Different keys must differ
|
|
51
|
+
refute_equal admin1.hash, Role.find(:guest).hash # rubocop:disable Rails/RefuteMethods
|
|
52
|
+
|
|
53
|
+
# Set/Hash membership now consistent with `==`
|
|
54
|
+
assert_equal 1, Set.new([admin1, admin2]).size
|
|
55
|
+
assert_equal 'a', ({ admin1 => 'a' }[admin2])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Two users with the same locale yield equal locale instances; different locales differ.
|
|
59
|
+
# Verifies that `==` works through model accessors (not just direct `find` results).
|
|
33
60
|
def test_comparison
|
|
34
61
|
bob = User.create(locale: :en)
|
|
35
62
|
alice = User.create(locale: :fr)
|
|
@@ -38,6 +65,8 @@ class UserTest < Minitest::Test
|
|
|
38
65
|
assert bob.locale != alice.locale
|
|
39
66
|
end
|
|
40
67
|
|
|
68
|
+
# Custom attributes declared on the Anchormodel subclass (e.g. `privilege_level` on Role)
|
|
69
|
+
# are accessible both standalone and through the model accessor.
|
|
41
70
|
def test_attributes
|
|
42
71
|
# Standalone
|
|
43
72
|
assert_equal 0, Role.find(:guest).privilege_level
|
|
@@ -46,6 +75,8 @@ class UserTest < Minitest::Test
|
|
|
46
75
|
assert_equal 42, u.role.privilege_level
|
|
47
76
|
end
|
|
48
77
|
|
|
78
|
+
# User-defined `<=>` on the Anchormodel subclass (via `include Comparable`) powers
|
|
79
|
+
# `<`, `>`, `==`, and `<=>` between instances.
|
|
49
80
|
def test_custom_comparison
|
|
50
81
|
assert_equal(-1, Role.find(:moderator) <=> Role.find(:admin))
|
|
51
82
|
assert_equal(1, Role.find(:moderator) <=> Role.find(:guest))
|
|
@@ -53,6 +84,8 @@ class UserTest < Minitest::Test
|
|
|
53
84
|
assert Role.find(:moderator) < Role.find(:admin)
|
|
54
85
|
end
|
|
55
86
|
|
|
87
|
+
# `belongs_to_anchormodel :col_name, AnchormodelClass` decouples the DB column name
|
|
88
|
+
# from the anchormodel class — here `:secondary_role` maps to `Role`.
|
|
56
89
|
def test_alternative_column_name
|
|
57
90
|
ben = User.create!(
|
|
58
91
|
role: Role.find(:moderator),
|
|
@@ -64,11 +97,14 @@ class UserTest < Minitest::Test
|
|
|
64
97
|
assert_equal(Locale.find(:de), ben.locale)
|
|
65
98
|
end
|
|
66
99
|
|
|
100
|
+
# `optional: true` permits NULL in the column; reader returns nil rather than raising.
|
|
67
101
|
def test_optional_attribute
|
|
68
102
|
jenny = User.create!(role: :admin, locale: :en)
|
|
69
103
|
assert_nil jenny.secondary_role
|
|
70
104
|
end
|
|
71
105
|
|
|
106
|
+
# Auto-generated per-key readers (`pia.admin?`) and writers (`pia.admin!`) installed
|
|
107
|
+
# by `belongs_to_anchormodel` for every key in the anchormodel.
|
|
72
108
|
def test_model_readers_and_writers
|
|
73
109
|
pia = User.new
|
|
74
110
|
pia.admin!
|
|
@@ -77,6 +113,8 @@ class UserTest < Minitest::Test
|
|
|
77
113
|
assert_equal Role.find(:admin), pia.role
|
|
78
114
|
end
|
|
79
115
|
|
|
116
|
+
# Auto-generated per-key class scopes (`User.admin`, `User.moderator`, etc.) installed
|
|
117
|
+
# by `belongs_to_anchormodel`.
|
|
80
118
|
def test_model_scopes
|
|
81
119
|
User.create!(role: :admin, locale: :en)
|
|
82
120
|
User.create!(role: :admin, locale: :en)
|
|
@@ -86,6 +124,180 @@ class UserTest < Minitest::Test
|
|
|
86
124
|
assert_equal 0, User.guest.count
|
|
87
125
|
end
|
|
88
126
|
|
|
127
|
+
# Regression: `where(anchormodel_col: %w[a b])` used to collapse to `IN (NULL)`
|
|
128
|
+
# because `Single#serializable?` was inverted, causing AR's `HomogeneousIn` to
|
|
129
|
+
# filter out all valid keys before binding.
|
|
130
|
+
def test_where_with_array_of_keys
|
|
131
|
+
User.create!(role: :admin, locale: :en)
|
|
132
|
+
User.create!(role: :moderator, locale: :en)
|
|
133
|
+
User.create!(role: :guest, locale: :en)
|
|
134
|
+
|
|
135
|
+
sql = User.where(role: %w[admin moderator]).to_sql
|
|
136
|
+
assert_match(/admin/, sql)
|
|
137
|
+
assert_match(/moderator/, sql)
|
|
138
|
+
refute_match(/IN \(NULL\)/i, sql) # rubocop:disable Rails/RefuteMethods
|
|
139
|
+
|
|
140
|
+
assert_equal 2, User.where(role: %w[admin moderator]).count
|
|
141
|
+
assert_equal 2, User.where(role: %i[admin moderator]).count
|
|
142
|
+
assert_equal(
|
|
143
|
+
2,
|
|
144
|
+
User.where(role: [Role.find(:admin), Role.find(:moderator)]).count
|
|
145
|
+
)
|
|
146
|
+
assert_equal 1, User.where.not(role: %w[admin moderator]).count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Bulk `where` with any unknown key raises immediately rather than silently filtering it out.
|
|
150
|
+
def test_where_with_array_of_invalid_keys_raises
|
|
151
|
+
assert_raises(RuntimeError) { User.where(role: %w[admin nope]).to_a }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Single-value anchormodel attributes must reject Array assignment with a clear RuntimeError.
|
|
155
|
+
# Regression: when Array handling was added to `Single#serialize` for the `where(col: array)`
|
|
156
|
+
# fix, it began silently accepting Array writes (e.g. `u.role = %w[admin guest]`) and only
|
|
157
|
+
# blew up later with `NoMethodError: undefined method 'to_sym' for an instance of Array`.
|
|
158
|
+
def test_single_attr_rejects_array_assignment
|
|
159
|
+
u = User.new(role: 'admin', locale: 'de')
|
|
160
|
+
assert_raises(RuntimeError) { u.role = %w[admin guest] }
|
|
161
|
+
assert_raises(RuntimeError) { User.new(role: %w[admin guest], locale: 'de') }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Direct probe on `Single#serializable?` — guards against re-introducing the inverted
|
|
165
|
+
# `exclude?` logic that silently dropped valid keys from `HomogeneousIn` binds.
|
|
166
|
+
# Single-value attributes do NOT accept Array values (those are for `belongs_to_anchormodels`),
|
|
167
|
+
# so `serializable?(array)` must return false.
|
|
168
|
+
def test_serializable_predicate
|
|
169
|
+
type = User.type_for_attribute(:role)
|
|
170
|
+
assert type.serializable?('admin')
|
|
171
|
+
assert type.serializable?(:admin)
|
|
172
|
+
assert type.serializable?(Role.find(:admin))
|
|
173
|
+
assert type.serializable?(nil)
|
|
174
|
+
refute type.serializable?(42) # rubocop:disable Rails/RefuteMethods
|
|
175
|
+
refute type.serializable?(%w[admin guest]) # rubocop:disable Rails/RefuteMethods
|
|
176
|
+
refute type.serializable?([:admin, Role.find(:guest)]) # rubocop:disable Rails/RefuteMethods
|
|
177
|
+
refute type.serializable?([42]) # rubocop:disable Rails/RefuteMethods
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# `Multi#serialize` must validate String inputs against valid keys, not pass through verbatim.
|
|
181
|
+
# Old impl returned the raw String unchecked, which corrupted DB rows and deferred errors
|
|
182
|
+
# to the next read.
|
|
183
|
+
def test_multi_serialize_validates_string_input
|
|
184
|
+
type = User.type_for_attribute(:animals)
|
|
185
|
+
assert_equal 'cat', type.serialize('cat')
|
|
186
|
+
assert_equal 'cat,dog', type.serialize('cat,dog')
|
|
187
|
+
assert_equal '', type.serialize('')
|
|
188
|
+
assert_raises(RuntimeError) { type.serialize('bogus') }
|
|
189
|
+
assert_raises(RuntimeError) { type.serialize('bogus,nonsense') }
|
|
190
|
+
assert_raises(RuntimeError) { type.serialize('cat,bogus') }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Same String-input validation must apply through normal model assignment, not just
|
|
194
|
+
# the type object directly. Verifies constructor, writer, and DB round-trip.
|
|
195
|
+
def test_multi_string_assignment_validates
|
|
196
|
+
# Constructor with valid String
|
|
197
|
+
u = User.create!(role: 'guest', locale: 'de', animals: 'cat,dog')
|
|
198
|
+
assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), u.animals)
|
|
199
|
+
assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), User.first.animals) # round-trip via DB
|
|
200
|
+
|
|
201
|
+
# Writer with valid single-key String
|
|
202
|
+
u.animals = 'horse'
|
|
203
|
+
assert_equal(Set.new([Animal.find(:horse)]), u.animals)
|
|
204
|
+
|
|
205
|
+
# Writer with invalid String raises immediately, not on next read
|
|
206
|
+
assert_raises(RuntimeError) { u.animals = 'bogus' }
|
|
207
|
+
assert_raises(RuntimeError) { u.animals = 'cat,bogus' }
|
|
208
|
+
|
|
209
|
+
# Constructor with invalid String raises immediately
|
|
210
|
+
assert_raises(RuntimeError) { User.new(role: 'guest', locale: 'de', animals: 'bogus,nonsense') }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# SQL LIKE treats `_` as a single-character wildcard. Keys containing `_` (e.g. `:big_cat`)
|
|
214
|
+
# must be escaped in the per-key scope and in the `with_any_<attr>` helper, or they
|
|
215
|
+
# cross-match arbitrary column values with one character in place of the underscore.
|
|
216
|
+
def test_multi_scope_escapes_underscore_wildcard_in_keys
|
|
217
|
+
# Raw row whose `animals` CSV does NOT contain `big_cat` but does contain a string
|
|
218
|
+
# that an unescaped LIKE pattern (`%big_cat,%`) would match: `bigXcat,foo`.
|
|
219
|
+
ActiveRecord::Base.connection.execute(<<~SQL.squish)
|
|
220
|
+
INSERT INTO users (role, locale, preferred_locale, animals, created_at, updated_at)
|
|
221
|
+
VALUES ('guest', 'de', 'de', 'bigXcat,foo', 'now', 'now')
|
|
222
|
+
SQL
|
|
223
|
+
# Baseline row that actually contains :big_cat.
|
|
224
|
+
User.create!(role: 'guest', locale: 'fr', animals: %w[big_cat])
|
|
225
|
+
|
|
226
|
+
# Both scope styles must match only the real row, not the look-alike.
|
|
227
|
+
assert_equal 1, User.big_cat.count
|
|
228
|
+
assert_equal 1, User.with_any_animals(:big_cat).count
|
|
229
|
+
assert_equal 1, User.with_all_animals(:big_cat).count
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# `where(multi_col: array)` cannot match CSV-in-column storage. Helper scopes
|
|
233
|
+
# `with_any_<attr>` (OR semantics) and `with_all_<attr>` (AND semantics) provide
|
|
234
|
+
# a working idiom for bulk-key queries.
|
|
235
|
+
def test_multi_helper_scopes_with_any_and_with_all
|
|
236
|
+
u_cat_dog = User.create!(role: 'guest', locale: 'fr', animals: %w[cat dog])
|
|
237
|
+
u_dog_horse = User.create!(role: 'guest', locale: 'it', animals: %w[dog horse])
|
|
238
|
+
u_cat = User.create!(role: 'guest', locale: 'de', animals: %w[cat])
|
|
239
|
+
|
|
240
|
+
# Plain `where(animals: array)` cannot reliably match CSV-in-column storage —
|
|
241
|
+
# it only catches rows whose entire column value equals one of the given keys.
|
|
242
|
+
# Use `with_any_<attr>` / `with_all_<attr>` instead.
|
|
243
|
+
plain_count = User.where(animals: %w[cat dog]).count
|
|
244
|
+
helper_count = User.with_any_animals(:cat, :dog).count
|
|
245
|
+
refute_equal plain_count, helper_count, # rubocop:disable Rails/RefuteMethods
|
|
246
|
+
'plain where(col: array) cannot match CSV-in-column storage; use with_any_<col>'
|
|
247
|
+
|
|
248
|
+
# `with_any_<attr>` — users that hold at least one of the given keys.
|
|
249
|
+
assert_equal 3, User.with_any_animals(:cat, :dog).count
|
|
250
|
+
assert_equal 2, User.with_any_animals(:cat).count
|
|
251
|
+
assert_equal 1, User.with_any_animals(:horse).count
|
|
252
|
+
assert_equal 0, User.with_any_animals(:rat).count
|
|
253
|
+
assert_equal 0, User.with_any_animals.count # empty arg list
|
|
254
|
+
|
|
255
|
+
# Accepts Strings, Symbols, and Anchormodel instances interchangeably.
|
|
256
|
+
assert_equal 2, User.with_any_animals('cat').count
|
|
257
|
+
assert_equal 2, User.with_any_animals(Animal.find(:cat)).count
|
|
258
|
+
assert_equal 3, User.with_any_animals([:cat, 'dog']).count # flattened
|
|
259
|
+
|
|
260
|
+
# `with_all_<attr>` — users that hold every given key.
|
|
261
|
+
assert_equal 1, User.with_all_animals(:cat, :dog).count
|
|
262
|
+
assert_equal 2, User.with_all_animals(:dog).count
|
|
263
|
+
assert_equal 0, User.with_all_animals(:cat, :dog, :rat).count
|
|
264
|
+
|
|
265
|
+
# Invalid keys raise immediately.
|
|
266
|
+
assert_raises(RuntimeError) { User.with_any_animals(:bogus).to_a }
|
|
267
|
+
assert_raises(RuntimeError) { User.with_all_animals(:cat, :bogus).to_a }
|
|
268
|
+
|
|
269
|
+
# Returned IDs check
|
|
270
|
+
assert_equal [u_cat_dog.id, u_cat.id].sort, User.with_any_animals(:cat).pluck(:id).sort
|
|
271
|
+
assert_equal [u_cat_dog.id, u_dog_horse.id].sort, User.with_any_animals(:dog).pluck(:id).sort
|
|
272
|
+
assert_equal [u_cat_dog.id], User.with_all_animals(:cat, :dog).pluck(:id)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# `Multi#cast` must tolerate nil — e.g. NULL stored in DB from a migration that did
|
|
276
|
+
# not backfill, or column with no default. Also covers `deserialize(nil)` which falls
|
|
277
|
+
# back to `cast` from `ActiveModel::Type::Value`.
|
|
278
|
+
def test_multi_cast_nil_returns_empty_set
|
|
279
|
+
type = User.type_for_attribute(:animals)
|
|
280
|
+
assert_equal Set.new, type.cast(nil)
|
|
281
|
+
assert_equal Set.new, type.deserialize(nil)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# `Multi#serializable?` must return strict Boolean (true/false), not a truthy String/Array.
|
|
285
|
+
# Old impl: `values.map { super }.compact.join(',')` etc — returned non-Boolean and was
|
|
286
|
+
# always truthy regardless of element validity.
|
|
287
|
+
def test_multi_serializable_predicate_returns_boolean
|
|
288
|
+
type = User.type_for_attribute(:animals)
|
|
289
|
+
assert_equal true, type.serializable?(%w[cat dog])
|
|
290
|
+
assert_equal true, type.serializable?([:cat, Animal.find(:dog)])
|
|
291
|
+
assert_equal true, type.serializable?(Set.new(%w[cat]))
|
|
292
|
+
assert_equal true, type.serializable?('cat,dog')
|
|
293
|
+
assert_equal true, type.serializable?(nil)
|
|
294
|
+
assert_equal false, type.serializable?(42)
|
|
295
|
+
assert_equal false, type.serializable?([42])
|
|
296
|
+
assert_equal false, type.serializable?([:cat, 42])
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Per-key readers/writers use the anchormodel's own keys (`:de`, `:fr`) even when the
|
|
300
|
+
# model attribute name differs (`preferred_locale` → `Locale`).
|
|
89
301
|
def test_model_readers_writers_with_different_class_name
|
|
90
302
|
pia = User.new(locale: :en)
|
|
91
303
|
pia.de!
|
|
@@ -95,6 +307,8 @@ class UserTest < Minitest::Test
|
|
|
95
307
|
assert_equal Locale.find(:en), pia.locale
|
|
96
308
|
end
|
|
97
309
|
|
|
310
|
+
# Per-key scopes use the anchormodel's own keys (`User.de`, `User.fr`) even when the
|
|
311
|
+
# model attribute name differs (`preferred_locale` → `Locale`).
|
|
98
312
|
def test_model_scopes_with_different_class_name
|
|
99
313
|
User.create!(role: :admin, locale: :en, preferred_locale: :de)
|
|
100
314
|
User.create!(role: :admin, locale: :en, preferred_locale: :de)
|
|
@@ -104,6 +318,8 @@ class UserTest < Minitest::Test
|
|
|
104
318
|
assert_equal 0, User.en.count
|
|
105
319
|
end
|
|
106
320
|
|
|
321
|
+
# Empty String is treated as nil — supports default Rails form submissions that send
|
|
322
|
+
# `""` for unset selects on optional attributes.
|
|
107
323
|
def test_rails_blank_assignment
|
|
108
324
|
u = User.new(role: :admin, secondary_role: :admin, locale: :en, preferred_locale: :en)
|
|
109
325
|
u.secondary_role = ''
|
|
@@ -114,26 +330,53 @@ class UserTest < Minitest::Test
|
|
|
114
330
|
# Testing failures
|
|
115
331
|
###---
|
|
116
332
|
|
|
333
|
+
# A required (non-optional) anchormodel attribute that is unset triggers Rails presence
|
|
334
|
+
# validation on save.
|
|
117
335
|
def test_presence_validation
|
|
118
336
|
valentine = User.new
|
|
119
337
|
assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
|
|
120
338
|
end
|
|
121
339
|
|
|
340
|
+
# `find` with an unregistered key raises `Anchormodel::InvalidKey`.
|
|
122
341
|
def test_missing_key
|
|
123
|
-
assert_raises { Role.find(:does_not_exist) }
|
|
342
|
+
assert_raises(Anchormodel::InvalidKey) { Role.find(:does_not_exist) }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Callers want to `rescue` invalid-key errors specifically without matching exception
|
|
346
|
+
# message strings. `Anchormodel::InvalidKey` is the dedicated class, raised from every
|
|
347
|
+
# entry point that could encounter an unknown key.
|
|
348
|
+
def test_invalid_key_error_class
|
|
349
|
+
# `find` with unknown key
|
|
350
|
+
assert_raises(Anchormodel::InvalidKey) { Role.find(:nope) }
|
|
351
|
+
|
|
352
|
+
# Assignment with unknown key
|
|
353
|
+
u = User.new(locale: 'de')
|
|
354
|
+
assert_raises(Anchormodel::InvalidKey) { u.role = :nope }
|
|
355
|
+
|
|
356
|
+
# Bulk where with mixed valid/invalid array
|
|
357
|
+
assert_raises(Anchormodel::InvalidKey) { User.where(role: %w[admin nope]).to_a }
|
|
358
|
+
|
|
359
|
+
# Multi attribute assignment with unknown key
|
|
360
|
+
assert_raises(Anchormodel::InvalidKey) { User.new(role: 'admin', locale: 'de', animals: %w[cat nope]) }
|
|
361
|
+
|
|
362
|
+
# Helper scope with unknown key
|
|
363
|
+
assert_raises(Anchormodel::InvalidKey) { User.with_any_animals(:nope).to_a }
|
|
364
|
+
|
|
365
|
+
# InvalidKey must inherit from StandardError so generic `rescue` catches it
|
|
366
|
+
assert_operator Anchormodel::InvalidKey, :<, StandardError
|
|
124
367
|
end
|
|
125
368
|
|
|
126
|
-
# Attempting to create a model with an invalid
|
|
369
|
+
# Attempting to create a model with an invalid anchormodel key should fail.
|
|
127
370
|
def test_invalid_key_update
|
|
128
371
|
assert_raises(RuntimeError) { User.create!(role: :admin, locale: :de, preferred_locale: :invalid) }
|
|
129
372
|
end
|
|
130
373
|
|
|
131
|
-
# Attempting to assign an invalid
|
|
374
|
+
# Attempting to assign an invalid anchormodel key to a model attribute should fail.
|
|
132
375
|
def test_invalid_key_assignment
|
|
133
376
|
assert_raises(RuntimeError) { User.new(role: :invalid) }
|
|
134
377
|
end
|
|
135
378
|
|
|
136
|
-
# An invalid
|
|
379
|
+
# An invalid anchormodel key sneaked into the DB raises on read (via `cast` → `find`).
|
|
137
380
|
def test_invalid_db_read
|
|
138
381
|
sql = <<~SQL.squish
|
|
139
382
|
INSERT INTO users (role, locale, preferred_locale, created_at, updated_at) VALUES ('invalid', 'de', 'de', 'now', 'now')
|
|
@@ -146,6 +389,8 @@ class UserTest < Minitest::Test
|
|
|
146
389
|
# Testing multiple anchormodel associations
|
|
147
390
|
###---
|
|
148
391
|
|
|
392
|
+
# Multi attribute full lifecycle: empty default, `<<`, `add` with String/Symbol/instance,
|
|
393
|
+
# `delete` with String/Symbol/instance, mass `=`, and `clear`.
|
|
149
394
|
def test_multi_basics
|
|
150
395
|
u = User.create!(role: 'guest', locale: 'de')
|
|
151
396
|
assert_equal(Set.new, u.animals)
|
|
@@ -171,6 +416,7 @@ class UserTest < Minitest::Test
|
|
|
171
416
|
assert_equal(false, u.animals.any?)
|
|
172
417
|
end
|
|
173
418
|
|
|
419
|
+
# Round-trip through the database preserves the Set of anchormodels (write CSV → read CSV → Set).
|
|
174
420
|
def test_multi_save_load
|
|
175
421
|
u = User.create!(role: 'guest', locale: 'de')
|
|
176
422
|
u.animals = %i[cat dog]
|
|
@@ -179,6 +425,8 @@ class UserTest < Minitest::Test
|
|
|
179
425
|
assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), freshly_loaded_u.animals)
|
|
180
426
|
end
|
|
181
427
|
|
|
428
|
+
# Per-key readers (`u.cat?`) and writers (`u.cat!`) work for multi attributes.
|
|
429
|
+
# `cat!` is idempotent — Set-based storage prevents duplicates.
|
|
182
430
|
def test_multi_model_readers_and_writers
|
|
183
431
|
u = User.create!(role: 'guest', locale: 'de')
|
|
184
432
|
u.cat!
|
|
@@ -191,6 +439,8 @@ class UserTest < Minitest::Test
|
|
|
191
439
|
assert_equal(false, u.horse?)
|
|
192
440
|
end
|
|
193
441
|
|
|
442
|
+
# Per-key scopes (`User.cat`, `User.dog`) work for multi attributes via the
|
|
443
|
+
# CSV-contains LIKE predicate (`Anchormodel::Util.csv_contains_like`).
|
|
194
444
|
def test_multi_model_scopes
|
|
195
445
|
u = User.create!(role: 'guest', locale: 'fr', animals: %w[dog cat])
|
|
196
446
|
v = User.create!(role: 'guest', locale: 'it', animals: %w[dog horse])
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: anchormodel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Kalbermatter
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- ".ruby-version"
|
|
158
158
|
- ".yardopts"
|
|
159
159
|
- CHANGELOG.md
|
|
160
|
+
- EXAMPLES.md
|
|
160
161
|
- Gemfile
|
|
161
162
|
- Gemfile.lock
|
|
162
163
|
- LICENSE
|
|
@@ -165,6 +166,7 @@ files:
|
|
|
165
166
|
- VERSION
|
|
166
167
|
- anchormodel.gemspec
|
|
167
168
|
- bin/rails
|
|
169
|
+
- bin/test
|
|
168
170
|
- doc/Anchormodel.html
|
|
169
171
|
- doc/Anchormodel/ActiveModelTypeValue.html
|
|
170
172
|
- doc/Anchormodel/ActiveModelTypeValueMulti.html
|
|
@@ -263,7 +265,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
263
265
|
- !ruby/object:Gem::Version
|
|
264
266
|
version: '0'
|
|
265
267
|
requirements: []
|
|
266
|
-
rubygems_version: 4.0.
|
|
268
|
+
rubygems_version: 4.0.11
|
|
267
269
|
specification_version: 4
|
|
268
270
|
summary: Bringing object-oriented programming to Rails enums
|
|
269
271
|
test_files: []
|