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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +14 -0
  4. data/EXAMPLES.md +408 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +43 -1
  7. data/VERSION +1 -1
  8. data/anchormodel.gemspec +3 -3
  9. data/bin/test +9 -0
  10. data/doc/Anchormodel/ActiveModelTypeValueMulti.html +204 -42
  11. data/doc/Anchormodel/ActiveModelTypeValueSingle.html +243 -54
  12. data/doc/Anchormodel/Attribute.html +60 -37
  13. data/doc/Anchormodel/ModelMixin.html +166 -16
  14. data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +82 -21
  15. data/doc/Anchormodel/SimpleFormInputs/Helpers.html +2 -11
  16. data/doc/Anchormodel/SimpleFormInputs.html +2 -11
  17. data/doc/Anchormodel/Util.html +497 -38
  18. data/doc/Anchormodel/Version.html +2 -20
  19. data/doc/Anchormodel.html +536 -129
  20. data/doc/AnchormodelCheckBoxesInput.html +22 -1
  21. data/doc/AnchormodelGenerator.html +64 -13
  22. data/doc/AnchormodelInput.html +24 -1
  23. data/doc/AnchormodelRadioButtonsInput.html +22 -1
  24. data/doc/_index.html +16 -1
  25. data/doc/class_list.html +1 -1
  26. data/doc/file.README.html +40 -2
  27. data/doc/index.html +40 -2
  28. data/doc/method_list.html +57 -17
  29. data/doc/top-level-namespace.html +1 -1
  30. data/lib/anchormodel/active_model_type_value_multi.rb +31 -5
  31. data/lib/anchormodel/active_model_type_value_single.rb +34 -6
  32. data/lib/anchormodel/attribute.rb +20 -8
  33. data/lib/anchormodel/model_mixin.rb +43 -8
  34. data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +7 -0
  35. data/lib/anchormodel/simple_form_inputs/anchormodel_input.rb +9 -0
  36. data/lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb +7 -0
  37. data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +14 -0
  38. data/lib/anchormodel/util.rb +102 -17
  39. data/lib/anchormodel.rb +109 -15
  40. data/lib/generators/anchormodel/anchormodel_generator.rb +8 -0
  41. data/test/active_record_model/user_test.rb +221 -10
  42. data/test/dummy/app/anchormodels/animal.rb +1 -0
  43. metadata +3 -1
data/doc/method_list.html CHANGED
@@ -69,6 +69,14 @@
69
69
 
70
70
 
71
71
  <li class="even ">
72
+ <div class="item">
73
+ <span class='object_link'><a href="Anchormodel/ModelMixin.html#anchormodel_attributes-instance_method" title="Anchormodel::ModelMixin#anchormodel_attributes (method)">#anchormodel_attributes</a></span>
74
+ <small>Anchormodel::ModelMixin</small>
75
+ </div>
76
+ </li>
77
+
78
+
79
+ <li class="odd ">
72
80
  <div class="item">
73
81
  <span class='object_link'><a href="Anchormodel/Attribute.html#anchormodel_class-instance_method" title="Anchormodel::Attribute#anchormodel_class (method)">#anchormodel_class</a></span>
74
82
  <small>Anchormodel::Attribute</small>
@@ -76,7 +84,7 @@
76
84
  </li>
77
85
 
78
86
 
79
- <li class="odd ">
87
+ <li class="even ">
80
88
  <div class="item">
81
89
  <span class='object_link'><a href="Anchormodel.html#as_json-instance_method" title="Anchormodel#as_json (method)">#as_json</a></span>
82
90
  <small>Anchormodel</small>
@@ -84,7 +92,7 @@
84
92
  </li>
85
93
 
86
94
 
87
- <li class="even ">
95
+ <li class="odd ">
88
96
  <div class="item">
89
97
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#attribute-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#attribute (method)">#attribute</a></span>
90
98
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -92,7 +100,7 @@
92
100
  </li>
93
101
 
94
102
 
95
- <li class="odd ">
103
+ <li class="even ">
96
104
  <div class="item">
97
105
  <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
98
106
  <small>Anchormodel::Attribute</small>
@@ -100,7 +108,7 @@
100
108
  </li>
101
109
 
102
110
 
103
- <li class="even ">
111
+ <li class="odd ">
104
112
  <div class="item">
105
113
  <span class='object_link'><a href="Anchormodel/ModelMixin.html#belongs_to_anchormodel-class_method" title="Anchormodel::ModelMixin.belongs_to_anchormodel (method)">belongs_to_anchormodel</a></span>
106
114
  <small>Anchormodel::ModelMixin</small>
@@ -108,7 +116,7 @@
108
116
  </li>
109
117
 
110
118
 
111
- <li class="odd ">
119
+ <li class="even ">
112
120
  <div class="item">
113
121
  <span class='object_link'><a href="Anchormodel/ModelMixin.html#belongs_to_anchormodels-class_method" title="Anchormodel::ModelMixin.belongs_to_anchormodels (method)">belongs_to_anchormodels</a></span>
114
122
  <small>Anchormodel::ModelMixin</small>
@@ -116,7 +124,7 @@
116
124
  </li>
117
125
 
118
126
 
119
- <li class="even ">
127
+ <li class="odd ">
120
128
  <div class="item">
121
129
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#cast (method)">#cast</a></span>
122
130
  <small>Anchormodel::ActiveModelTypeValueMulti</small>
@@ -124,7 +132,7 @@
124
132
  </li>
125
133
 
126
134
 
127
- <li class="odd ">
135
+ <li class="even ">
128
136
  <div class="item">
129
137
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#cast (method)">#cast</a></span>
130
138
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -132,7 +140,7 @@
132
140
  </li>
133
141
 
134
142
 
135
- <li class="even ">
143
+ <li class="odd ">
136
144
  <div class="item">
137
145
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#changed_in_place%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#changed_in_place? (method)">#changed_in_place?</a></span>
138
146
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -140,7 +148,23 @@
140
148
  </li>
141
149
 
142
150
 
151
+ <li class="even ">
152
+ <div class="item">
153
+ <span class='object_link'><a href="Anchormodel/Util.html#csv_contains_like-class_method" title="Anchormodel::Util.csv_contains_like (method)">csv_contains_like</a></span>
154
+ <small>Anchormodel::Util</small>
155
+ </div>
156
+ </li>
157
+
158
+
143
159
  <li class="odd ">
160
+ <div class="item">
161
+ <span class='object_link'><a href="Anchormodel/Util.html#escape_like-class_method" title="Anchormodel::Util.escape_like (method)">escape_like</a></span>
162
+ <small>Anchormodel::Util</small>
163
+ </div>
164
+ </li>
165
+
166
+
167
+ <li class="even ">
144
168
  <div class="item">
145
169
  <span class='object_link'><a href="Anchormodel.html#find-class_method" title="Anchormodel.find (method)">find</a></span>
146
170
  <small>Anchormodel</small>
@@ -148,7 +172,7 @@
148
172
  </li>
149
173
 
150
174
 
151
- <li class="even ">
175
+ <li class="odd ">
152
176
  <div class="item">
153
177
  <span class='object_link'><a href="Anchormodel.html#first-class_method" title="Anchormodel.first (method)">first</a></span>
154
178
  <small>Anchormodel</small>
@@ -156,7 +180,7 @@
156
180
  </li>
157
181
 
158
182
 
159
- <li class="odd ">
183
+ <li class="even ">
160
184
  <div class="item">
161
185
  <span class='object_link'><a href="Anchormodel.html#form_collection-class_method" title="Anchormodel.form_collection (method)">form_collection</a></span>
162
186
  <small>Anchormodel</small>
@@ -164,6 +188,14 @@
164
188
  </li>
165
189
 
166
190
 
191
+ <li class="odd ">
192
+ <div class="item">
193
+ <span class='object_link'><a href="Anchormodel.html#hash-instance_method" title="Anchormodel#hash (method)">#hash</a></span>
194
+ <small>Anchormodel</small>
195
+ </div>
196
+ </li>
197
+
198
+
167
199
  <li class="even ">
168
200
  <div class="item">
169
201
  <span class='object_link'><a href="Anchormodel.html#index-instance_method" title="Anchormodel#index (method)">#index</a></span>
@@ -245,6 +277,14 @@
245
277
 
246
278
 
247
279
  <li class="even ">
280
+ <div class="item">
281
+ <span class='object_link'><a href="Anchormodel/Util.html#normalize_anchormodel_keys-class_method" title="Anchormodel::Util.normalize_anchormodel_keys (method)">normalize_anchormodel_keys</a></span>
282
+ <small>Anchormodel::Util</small>
283
+ </div>
284
+ </li>
285
+
286
+
287
+ <li class="odd ">
248
288
  <div class="item">
249
289
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
250
290
  <small>Anchormodel::Attribute</small>
@@ -252,7 +292,7 @@
252
292
  </li>
253
293
 
254
294
 
255
- <li class="odd ">
295
+ <li class="even ">
256
296
  <div class="item">
257
297
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serializable? (method)">#serializable?</a></span>
258
298
  <small>Anchormodel::ActiveModelTypeValueMulti</small>
@@ -260,7 +300,7 @@
260
300
  </li>
261
301
 
262
302
 
263
- <li class="even ">
303
+ <li class="odd ">
264
304
  <div class="item">
265
305
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
266
306
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -268,7 +308,7 @@
268
308
  </li>
269
309
 
270
310
 
271
- <li class="odd ">
311
+ <li class="even ">
272
312
  <div class="item">
273
313
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serialize (method)">#serialize</a></span>
274
314
  <small>Anchormodel::ActiveModelTypeValueMulti</small>
@@ -276,7 +316,7 @@
276
316
  </li>
277
317
 
278
318
 
279
- <li class="even ">
319
+ <li class="odd ">
280
320
  <div class="item">
281
321
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
282
322
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -284,7 +324,7 @@
284
324
  </li>
285
325
 
286
326
 
287
- <li class="odd ">
327
+ <li class="even ">
288
328
  <div class="item">
289
329
  <span class='object_link'><a href="Anchormodel.html#setup!-class_method" title="Anchormodel.setup! (method)">setup!</a></span>
290
330
  <small>Anchormodel</small>
@@ -292,7 +332,7 @@
292
332
  </li>
293
333
 
294
334
 
295
- <li class="even ">
335
+ <li class="odd ">
296
336
  <div class="item">
297
337
  <span class='object_link'><a href="Anchormodel.html#to_s-instance_method" title="Anchormodel#to_s (method)">#to_s</a></span>
298
338
  <small>Anchormodel</small>
@@ -300,7 +340,7 @@
300
340
  </li>
301
341
 
302
342
 
303
- <li class="odd ">
343
+ <li class="even ">
304
344
  <div class="item">
305
345
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#type-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#type (method)">#type</a></span>
306
346
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -100,7 +100,7 @@
100
100
  </div>
101
101
 
102
102
  <div id="footer">
103
- Generated on Wed May 13 11:46:10 2026 by
103
+ Generated on Wed May 13 15:48:24 2026 by
104
104
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
105
  0.9.28 (ruby-3.3.5).
106
106
  </div>
@@ -1,16 +1,36 @@
1
+ # ActiveModel type adapter for collection-valued anchormodel attributes (`belongs_to_anchormodels`).
2
+ #
3
+ # Translates between an in-memory `Set<Anchormodel>` and a CSV `String` stored in a single
4
+ # DB column. Inherits scalar handling from {Anchormodel::ActiveModelTypeValueSingle} and
5
+ # overrides the collection-aware methods.
6
+ #
7
+ # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value Rails type interface
1
8
  class Anchormodel::ActiveModelTypeValueMulti < Anchormodel::ActiveModelTypeValueSingle
2
- # This converts DB or input to an Anchormodel instance
9
+ # Splits the stored CSV and casts each entry into an Anchormodel instance.
10
+ #
11
+ # @param values [String,nil] CSV string from the DB, or `nil` (NULL).
12
+ # @return [Set<Anchormodel>] Set of Anchormodel instances. Empty when `values` is nil or `""`.
13
+ # @raise [Anchormodel::InvalidKey] if any CSV entry is not a registered key.
3
14
  def cast(values)
15
+ return Set.new if values.nil?
4
16
  return values.split(',').map { |value| super(value) }.compact.to_set
5
17
  end
6
18
 
7
- # This converts an Anchormodel instance to string for DB
19
+ # Serializes a Set/Array of anchormodel-shaped values into a CSV `String` for the DB.
20
+ # Validates every entry and raises immediately on invalid keys (rather than deferring
21
+ # the error to the next read).
22
+ #
23
+ # @param values [Enumerable,String,nil] Collection of anchormodel-shaped values, a
24
+ # pre-formed CSV `String`, or `nil`.
25
+ # @return [String] CSV of validated keys, or `""` for nil / empty collection.
26
+ # @raise [Anchormodel::InvalidKey] if any element is an unknown key.
27
+ # @raise [RuntimeError] if `values` is not an Enumerable, String, or `nil`.
8
28
  def serialize(values)
9
29
  return case values
10
30
  when Enumerable
11
31
  values.map { |value| super(value) }.compact.join(',')
12
32
  when String
13
- values
33
+ values.split(',').map { |value| super(value) }.compact.join(',')
14
34
  when nil
15
35
  ''
16
36
  else
@@ -18,12 +38,18 @@ class Anchormodel::ActiveModelTypeValueMulti < Anchormodel::ActiveModelTypeValue
18
38
  end
19
39
  end
20
40
 
41
+ # Reports whether {#serialize} would accept `values`. Returns strict Boolean.
42
+ #
43
+ # @param values [Object]
44
+ # @return [Boolean]
21
45
  def serializable?(values)
22
46
  return case values
23
47
  when Enumerable
24
- values.map { |value| super(value) }.compact.join(',')
48
+ values.all? { |value| super(value) }
25
49
  when String
26
- values.split(',').map { |value| super(value) }.compact
50
+ values.split(',').all? { |value| super(value) }
51
+ when nil
52
+ true
27
53
  else
28
54
  false
29
55
  end
@@ -1,34 +1,62 @@
1
- # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value
1
+ # ActiveModel type adapter for single-value anchormodel attributes.
2
+ #
3
+ # Translates between the in-memory Ruby form (an `Anchormodel` instance or `nil`) and
4
+ # the on-disk form (a String key in the DB column). Registered with AR via
5
+ # `model_class.attribute(name, ActiveModelTypeValueSingle.new(attribute))` by
6
+ # {Anchormodel::Util.install_methods_in_model}.
7
+ #
8
+ # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value Rails type interface
2
9
  class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
10
+ # @!attribute [r] attribute
11
+ # @return [Anchormodel::Attribute] Metadata about the column this type is bound to.
3
12
  attr_reader :attribute
4
13
 
14
+ # @param attribute [Anchormodel::Attribute]
5
15
  def initialize(attribute)
6
16
  super()
7
17
  @attribute = attribute
8
18
  end
9
19
 
20
+ # @return [Symbol] `:anchormodel` — the type identifier.
10
21
  def type
11
22
  :anchormodel
12
23
  end
13
24
 
14
- # This converts DB or input to an Anchormodel instance
25
+ # Coerces an input value into an Anchormodel instance (or nil). Used by Rails when
26
+ # reading from the DB, when assigning via mass-assignment, and for dirty-tracking.
27
+ #
28
+ # @param value [String,Symbol,Anchormodel,nil] DB value, user input, or already-cast instance.
29
+ # @return [Anchormodel,nil] The matching Anchormodel instance, or `nil` for blank input.
30
+ # @raise [Anchormodel::InvalidKey] if a String/Symbol is not a registered key.
15
31
  def cast(value)
16
32
  value = value.presence
17
33
  return value if value.is_a?(@attribute.anchormodel_class)
18
34
  return @attribute.anchormodel_class.find(value)
19
35
  end
20
36
 
21
- # This converts an Anchormodel instance to string for DB
37
+ # Converts an Anchormodel-shaped value into the String key stored in the DB.
38
+ #
39
+ # @param value [String,Symbol,Anchormodel,nil]
40
+ # @return [String,nil] The key as a String, or `nil` for blank input.
41
+ # @raise [Anchormodel::InvalidKey] for unknown String/Symbol keys.
42
+ # @raise [RuntimeError] for any other input type (e.g. Array, Integer).
22
43
  def serialize(value)
23
- return value.map { |v| serialize_scalar(v) } if value.is_a?(Array)
24
44
  serialize_scalar(value)
25
45
  end
26
46
 
47
+ # Reports whether `value` is a shape that {#serialize} would accept. Used by AR's
48
+ # `Arel::Nodes::HomogeneousIn` to gate which array elements get bound into IN clauses.
49
+ # Returns true for any String/Symbol/`nil`/Anchormodel instance — actual key validation
50
+ # is deferred to {#serialize}.
51
+ #
52
+ # @param value [Object]
53
+ # @return [Boolean]
27
54
  def serializable?(value)
28
- return value.all? { |v| scalar_serializable?(v) } if value.is_a?(Array)
29
55
  scalar_serializable?(value)
30
56
  end
31
57
 
58
+ # Used by AR's dirty tracking to detect in-place mutation.
59
+ # @api private
32
60
  def changed_in_place?(raw_old_value, value)
33
61
  old_value = deserialize(raw_old_value)
34
62
  old_value != value
@@ -41,7 +69,7 @@ class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
41
69
  return case value
42
70
  when Symbol, String
43
71
  unless @attribute.anchormodel_class.valid_keys.include?(value.to_sym)
44
- fail("Attempt to set #{@attribute.attribute_name} to unsupported key #{value.inspect}.")
72
+ raise(Anchormodel::InvalidKey, "Attempt to set #{@attribute.attribute_name} to unsupported key #{value.inspect}.")
45
73
  end
46
74
  value.to_s
47
75
  when @attribute.anchormodel_class
@@ -1,16 +1,27 @@
1
- # @api description
2
- # This class holds all information related to a Rails model pointing to an Anchormodel.
3
- # It is instanciated when {Anchormodel::ModelMixin#belongs_to_anchormodel} is used.
1
+ # Metadata about an anchormodel attribute installed on a Rails model.
2
+ #
3
+ # One instance is created per call to {Anchormodel::ModelMixin#belongs_to_anchormodel}
4
+ # or {Anchormodel::ModelMixin#belongs_to_anchormodels} and stored in the model class's
5
+ # `anchormodel_attributes` hash. Used internally by the AR type casters and by SimpleForm
6
+ # inputs to discover what an attribute points to.
4
7
  class Anchormodel::Attribute
8
+ # @!attribute [r] attribute_name
9
+ # @return [Symbol] The model attribute / DB column name.
5
10
  attr_reader :attribute_name
11
+
12
+ # @!attribute [r] anchormodel_class
13
+ # @return [Class] The Anchormodel subclass this attribute references.
6
14
  attr_reader :anchormodel_class
15
+
16
+ # @!attribute [r] optional
17
+ # @return [Boolean] Whether the attribute may be `nil` (no presence validation added).
7
18
  attr_reader :optional
8
19
 
9
- # @param model_class [ActiveRecord::Base] The Rails model where {Anchormodel::ModelMixin#belongs_to_anchormodel} is used
10
- # @param attribute_name [String,Symbol] The name and database column of the attribute
11
- # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds an `FooBar`)
12
- # @param optional [Boolean] If true, a presence validation is added to the model.
13
- # @param multiple [Boolean] If true, this attribute holds multiple anchormodels.
20
+ # @param model_class [Class] The ActiveRecord model class on which {Anchormodel::ModelMixin#belongs_to_anchormodel} is called.
21
+ # @param attribute_name [String,Symbol] The model attribute / DB column name.
22
+ # @param anchormodel_class [Class,nil] The Anchormodel subclass. Omit if attribute `:foo_bar` references `FooBar`.
23
+ # @param optional [Boolean] If true, no presence validation is added.
24
+ # @param multiple [Boolean] If true, this attribute holds a Set of anchormodels (CSV in column).
14
25
  def initialize(model_class, attribute_name, anchormodel_class = nil, optional = false, multiple = false)
15
26
  @model_class = model_class
16
27
  @attribute_name = attribute_name.to_sym
@@ -19,6 +30,7 @@ class Anchormodel::Attribute
19
30
  @multiple = multiple
20
31
  end
21
32
 
33
+ # @return [Boolean] true for `belongs_to_anchormodels` (collection), false for `belongs_to_anchormodel` (single).
22
34
  def multiple?
23
35
  @multiple
24
36
  end
@@ -1,23 +1,58 @@
1
- # @api description
2
- # All Rails models making use of #belongs_to_anchormodel must include this mixin. Typically, it is included in `application_record.rb`.
1
+ # Include this mixin in every Rails model that uses anchormodel attributes. The common
2
+ # pattern is to include it once in `application_record.rb` so all descendant models pick
3
+ # it up automatically.
4
+ #
5
+ # Adds two class-level macros:
6
+ # - {ClassMethods#belongs_to_anchormodel} for single-value attributes
7
+ # - {ClassMethods#belongs_to_anchormodels} for collection-valued attributes
8
+ #
9
+ # @example Install in ApplicationRecord
10
+ # class ApplicationRecord < ActiveRecord::Base
11
+ # include Anchormodel::ModelMixin
12
+ # end
3
13
  module Anchormodel::ModelMixin
4
14
  extend ActiveSupport::Concern
5
15
 
6
16
  included do
17
+ # @!attribute [rw] anchormodel_attributes
18
+ # @return [Hash{Symbol => Anchormodel::Attribute}] All anchormodel attributes
19
+ # declared on this model class, keyed by attribute name.
7
20
  class_attribute :anchormodel_attributes, default: {}.freeze
8
21
  end
9
22
 
10
23
  class_methods do
11
- # Creates an attribute linking to an Anchormodel. The attribute should be
12
- # present in the DB and the column should be of type String and named the same as `attribute_name`.
13
- # @see Anchormodel::Util#install_methods_in_model Parameters
24
+ # Declares a single-value anchormodel attribute.
25
+ #
26
+ # The DB table must have a String column with the same name as `attribute_name`.
27
+ # @see Anchormodel::Util.install_methods_in_model Full parameter documentation.
28
+ # @example Basic usage
29
+ # class User < ApplicationRecord
30
+ # belongs_to_anchormodel :role # `Role` inferred from attribute name
31
+ # belongs_to_anchormodel :role, optional: true
32
+ # belongs_to_anchormodel :favorite_color, Color # explicit anchormodel class
33
+ # end
14
34
  def belongs_to_anchormodel(*args, **kwargs)
15
35
  Anchormodel::Util.install_methods_in_model(self, *args, **kwargs)
16
36
  end
17
37
 
18
- # Creates an attribute linking to an array of Anchormodels. The attribute should be
19
- # present in the DB and the column should be named the singular of `attribute_name`.
20
- # @see Anchormodel::Util#install_methods_in_model Parameters
38
+ # Declares a collection-valued anchormodel attribute.
39
+ #
40
+ # The DB table must have a String column with the same name as `attribute_name`.
41
+ # Multiple keys are stored as a comma-separated string. Reading the attribute returns
42
+ # a `Set` of Anchormodel instances. `optional: true` is forced (an empty collection
43
+ # is a valid value).
44
+ #
45
+ # @see Anchormodel::Util.install_methods_in_model Full parameter documentation.
46
+ # @example Basic usage
47
+ # class User < ApplicationRecord
48
+ # belongs_to_anchormodels :animals
49
+ # end
50
+ #
51
+ # user = User.create!(animals: %i[cat dog])
52
+ # user.animals # => #<Set: {#<Animal<cat>>, #<Animal<dog>>}>
53
+ # user.cat? # => true
54
+ # user.animals << :horse
55
+ # User.with_any_animals(:cat, :horse) # => relation matching the user above
21
56
  def belongs_to_anchormodels(*args, **kwargs)
22
57
  Anchormodel::Util.install_methods_in_model(self, *args, **kwargs, multiple: true)
23
58
  end
@@ -7,6 +7,13 @@ unless defined? SimpleForm
7
7
 
8
8
  end
9
9
  if defined? SimpleForm
10
+ # SimpleForm input for a collection-valued anchormodel attribute (`belongs_to_anchormodels`),
11
+ # rendered as check boxes (one per anchormodel key).
12
+ #
13
+ # @example
14
+ # <%= simple_form_for user do |f| %>
15
+ # <%= f.input :animals, as: :anchormodel_check_boxes %>
16
+ # <% end %>
10
17
  class AnchormodelCheckBoxesInput < SimpleForm::Inputs::CollectionCheckBoxesInput
11
18
  include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
19
 
@@ -7,6 +7,15 @@ unless defined? SimpleForm
7
7
 
8
8
  end
9
9
  if defined? SimpleForm
10
+ # SimpleForm input for an anchormodel attribute. Renders a `<select>` collection
11
+ # whose options are the entries of the bound anchormodel.
12
+ #
13
+ # Auto-detected by SimpleForm because the attribute's AR type is `:anchormodel`.
14
+ #
15
+ # @example
16
+ # <%= simple_form_for user do |f| %>
17
+ # <%= f.input :role %>
18
+ # <% end %>
10
19
  class AnchormodelInput < SimpleForm::Inputs::CollectionSelectInput
11
20
  include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
21
 
@@ -7,6 +7,13 @@ unless defined? SimpleForm
7
7
 
8
8
  end
9
9
  if defined? SimpleForm
10
+ # SimpleForm input for a single-value anchormodel attribute, rendered as radio buttons.
11
+ # Unsuitable for collection attributes — use {AnchormodelCheckBoxesInput} for those.
12
+ #
13
+ # @example
14
+ # <%= simple_form_for user do |f| %>
15
+ # <%= f.input :role, as: :anchormodel_radio_buttons %>
16
+ # <% end %>
10
17
  class AnchormodelRadioButtonsInput < SimpleForm::Inputs::CollectionRadioButtonsInput
11
18
  include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
19
 
@@ -1,7 +1,21 @@
1
1
  class Anchormodel
2
2
  module SimpleFormInputs
3
3
  module Helpers
4
+ # Shared logic for all anchormodel SimpleForm inputs ({AnchormodelInput},
5
+ # {AnchormodelRadioButtonsInput}, {AnchormodelCheckBoxesInput}). Resolves the
6
+ # anchormodel class from the bound object, builds the collection of label/key
7
+ # tuples, and computes the currently-selected key(s).
8
+ #
9
+ # Callers can supply a custom `:collection` (either an array of anchormodels
10
+ # or an array of `[label, key]` tuples) to override the default of `am_class.all`.
11
+ # Callers without a bound `object` must pass `:anchormodel_attribute` explicitly.
4
12
  module AnchormodelInputsCommon
13
+ # Resolves the anchormodel attribute, builds the collection, and delegates to
14
+ # the underlying SimpleForm input class via `super`.
15
+ # @param wrapper_options [Hash,nil]
16
+ # @return [String] Rendered HTML.
17
+ # @raise [RuntimeError] if the bound object does not include {Anchormodel::ModelMixin},
18
+ # or if the attribute does not look like an anchormodel attribute.
5
19
  def input(wrapper_options = nil)
6
20
  if object.present?
7
21
  unless object.respond_to?(:anchormodel_attributes)