anchormodel 0.3.1 → 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/.ruby-version +1 -1
- data/CHANGELOG.md +14 -0
- data/EXAMPLES.md +408 -0
- data/Gemfile.lock +1 -1
- data/README.md +43 -1
- data/VERSION +1 -1
- data/anchormodel.gemspec +3 -3
- data/bin/test +9 -0
- data/doc/Anchormodel/ActiveModelTypeValueMulti.html +204 -42
- data/doc/Anchormodel/ActiveModelTypeValueSingle.html +243 -54
- 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 +34 -6
- 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 +221 -10
- data/test/dummy/app/anchormodels/animal.rb +1 -0
- metadata +3 -1
|
@@ -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)
|
|
@@ -87,7 +125,8 @@ class UserTest < Minitest::Test
|
|
|
87
125
|
end
|
|
88
126
|
|
|
89
127
|
# Regression: `where(anchormodel_col: %w[a b])` used to collapse to `IN (NULL)`
|
|
90
|
-
# because Single#
|
|
128
|
+
# because `Single#serializable?` was inverted, causing AR's `HomogeneousIn` to
|
|
129
|
+
# filter out all valid keys before binding.
|
|
91
130
|
def test_where_with_array_of_keys
|
|
92
131
|
User.create!(role: :admin, locale: :en)
|
|
93
132
|
User.create!(role: :moderator, locale: :en)
|
|
@@ -107,24 +146,158 @@ class UserTest < Minitest::Test
|
|
|
107
146
|
assert_equal 1, User.where.not(role: %w[admin moderator]).count
|
|
108
147
|
end
|
|
109
148
|
|
|
149
|
+
# Bulk `where` with any unknown key raises immediately rather than silently filtering it out.
|
|
110
150
|
def test_where_with_array_of_invalid_keys_raises
|
|
111
151
|
assert_raises(RuntimeError) { User.where(role: %w[admin nope]).to_a }
|
|
112
152
|
end
|
|
113
153
|
|
|
114
|
-
#
|
|
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
|
|
115
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.
|
|
116
168
|
def test_serializable_predicate
|
|
117
169
|
type = User.type_for_attribute(:role)
|
|
118
170
|
assert type.serializable?('admin')
|
|
119
171
|
assert type.serializable?(:admin)
|
|
120
172
|
assert type.serializable?(Role.find(:admin))
|
|
121
173
|
assert type.serializable?(nil)
|
|
122
|
-
assert type.serializable?(%w[admin guest])
|
|
123
|
-
assert type.serializable?([:admin, Role.find(:guest)])
|
|
124
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
|
|
125
177
|
refute type.serializable?([42]) # rubocop:disable Rails/RefuteMethods
|
|
126
178
|
end
|
|
127
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`).
|
|
128
301
|
def test_model_readers_writers_with_different_class_name
|
|
129
302
|
pia = User.new(locale: :en)
|
|
130
303
|
pia.de!
|
|
@@ -134,6 +307,8 @@ class UserTest < Minitest::Test
|
|
|
134
307
|
assert_equal Locale.find(:en), pia.locale
|
|
135
308
|
end
|
|
136
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`).
|
|
137
312
|
def test_model_scopes_with_different_class_name
|
|
138
313
|
User.create!(role: :admin, locale: :en, preferred_locale: :de)
|
|
139
314
|
User.create!(role: :admin, locale: :en, preferred_locale: :de)
|
|
@@ -143,6 +318,8 @@ class UserTest < Minitest::Test
|
|
|
143
318
|
assert_equal 0, User.en.count
|
|
144
319
|
end
|
|
145
320
|
|
|
321
|
+
# Empty String is treated as nil — supports default Rails form submissions that send
|
|
322
|
+
# `""` for unset selects on optional attributes.
|
|
146
323
|
def test_rails_blank_assignment
|
|
147
324
|
u = User.new(role: :admin, secondary_role: :admin, locale: :en, preferred_locale: :en)
|
|
148
325
|
u.secondary_role = ''
|
|
@@ -153,26 +330,53 @@ class UserTest < Minitest::Test
|
|
|
153
330
|
# Testing failures
|
|
154
331
|
###---
|
|
155
332
|
|
|
333
|
+
# A required (non-optional) anchormodel attribute that is unset triggers Rails presence
|
|
334
|
+
# validation on save.
|
|
156
335
|
def test_presence_validation
|
|
157
336
|
valentine = User.new
|
|
158
337
|
assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
|
|
159
338
|
end
|
|
160
339
|
|
|
340
|
+
# `find` with an unregistered key raises `Anchormodel::InvalidKey`.
|
|
161
341
|
def test_missing_key
|
|
162
|
-
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
|
|
163
367
|
end
|
|
164
368
|
|
|
165
|
-
# Attempting to create a model with an invalid
|
|
369
|
+
# Attempting to create a model with an invalid anchormodel key should fail.
|
|
166
370
|
def test_invalid_key_update
|
|
167
371
|
assert_raises(RuntimeError) { User.create!(role: :admin, locale: :de, preferred_locale: :invalid) }
|
|
168
372
|
end
|
|
169
373
|
|
|
170
|
-
# Attempting to assign an invalid
|
|
374
|
+
# Attempting to assign an invalid anchormodel key to a model attribute should fail.
|
|
171
375
|
def test_invalid_key_assignment
|
|
172
376
|
assert_raises(RuntimeError) { User.new(role: :invalid) }
|
|
173
377
|
end
|
|
174
378
|
|
|
175
|
-
# An invalid
|
|
379
|
+
# An invalid anchormodel key sneaked into the DB raises on read (via `cast` → `find`).
|
|
176
380
|
def test_invalid_db_read
|
|
177
381
|
sql = <<~SQL.squish
|
|
178
382
|
INSERT INTO users (role, locale, preferred_locale, created_at, updated_at) VALUES ('invalid', 'de', 'de', 'now', 'now')
|
|
@@ -185,6 +389,8 @@ class UserTest < Minitest::Test
|
|
|
185
389
|
# Testing multiple anchormodel associations
|
|
186
390
|
###---
|
|
187
391
|
|
|
392
|
+
# Multi attribute full lifecycle: empty default, `<<`, `add` with String/Symbol/instance,
|
|
393
|
+
# `delete` with String/Symbol/instance, mass `=`, and `clear`.
|
|
188
394
|
def test_multi_basics
|
|
189
395
|
u = User.create!(role: 'guest', locale: 'de')
|
|
190
396
|
assert_equal(Set.new, u.animals)
|
|
@@ -210,6 +416,7 @@ class UserTest < Minitest::Test
|
|
|
210
416
|
assert_equal(false, u.animals.any?)
|
|
211
417
|
end
|
|
212
418
|
|
|
419
|
+
# Round-trip through the database preserves the Set of anchormodels (write CSV → read CSV → Set).
|
|
213
420
|
def test_multi_save_load
|
|
214
421
|
u = User.create!(role: 'guest', locale: 'de')
|
|
215
422
|
u.animals = %i[cat dog]
|
|
@@ -218,6 +425,8 @@ class UserTest < Minitest::Test
|
|
|
218
425
|
assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), freshly_loaded_u.animals)
|
|
219
426
|
end
|
|
220
427
|
|
|
428
|
+
# Per-key readers (`u.cat?`) and writers (`u.cat!`) work for multi attributes.
|
|
429
|
+
# `cat!` is idempotent — Set-based storage prevents duplicates.
|
|
221
430
|
def test_multi_model_readers_and_writers
|
|
222
431
|
u = User.create!(role: 'guest', locale: 'de')
|
|
223
432
|
u.cat!
|
|
@@ -230,6 +439,8 @@ class UserTest < Minitest::Test
|
|
|
230
439
|
assert_equal(false, u.horse?)
|
|
231
440
|
end
|
|
232
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`).
|
|
233
444
|
def test_multi_model_scopes
|
|
234
445
|
u = User.create!(role: 'guest', locale: 'fr', animals: %w[dog cat])
|
|
235
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
|