anchormodel 0.1.4 → 0.2.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/.gitignore +2 -1
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +114 -3
  6. data/VERSION +1 -1
  7. data/anchormodel.gemspec +5 -5
  8. data/doc/Anchormodel/ActiveModelTypeValueSingle.html +697 -0
  9. data/doc/Anchormodel/Attribute.html +109 -9
  10. data/doc/Anchormodel/ModelMixin.html +75 -3
  11. data/doc/Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html +269 -0
  12. data/doc/Anchormodel/SimpleFormInputs/Helpers.html +124 -0
  13. data/doc/Anchormodel/SimpleFormInputs.html +124 -0
  14. data/doc/Anchormodel/Util.html +612 -0
  15. data/doc/Anchormodel/Version.html +3 -3
  16. data/doc/Anchormodel.html +99 -34
  17. data/doc/AnchormodelGenerator.html +201 -0
  18. data/doc/AnchormodelInput.html +140 -0
  19. data/doc/AnchormodelRadioButtonsInput.html +140 -0
  20. data/doc/_index.html +63 -4
  21. data/doc/class_list.html +1 -1
  22. data/doc/file.README.html +109 -6
  23. data/doc/frames.html +5 -10
  24. data/doc/index.html +109 -6
  25. data/doc/method_list.html +70 -6
  26. data/doc/top-level-namespace.html +4 -4
  27. data/lib/anchormodel/active_model_type_value_multi.rb +31 -0
  28. data/lib/anchormodel/active_model_type_value_single.rb +4 -2
  29. data/lib/anchormodel/attribute.rb +7 -1
  30. data/lib/anchormodel/model_mixin.rb +7 -0
  31. data/lib/anchormodel/simple_form_inputs/anchormodel_check_boxes_input.rb +23 -0
  32. data/lib/anchormodel/simple_form_inputs/anchormodel_input.rb +21 -0
  33. data/lib/anchormodel/simple_form_inputs/anchormodel_radio_buttons_input.rb +23 -0
  34. data/lib/anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common.rb +54 -0
  35. data/lib/anchormodel/util.rb +57 -7
  36. data/lib/anchormodel.rb +10 -0
  37. data/test/active_record_model/user_test.rb +73 -9
  38. data/test/dummy/app/anchormodels/animal.rb +6 -0
  39. data/test/dummy/app/models/user.rb +1 -0
  40. data/test/dummy/db/migrate/20240425182000_add_animals_to_users.rb +5 -0
  41. data/test/dummy/db/schema.rb +2 -1
  42. metadata +18 -4
  43. data/pkg/anchormodel-0.1.3.gem +0 -0
data/doc/method_list.html CHANGED
@@ -77,6 +77,14 @@
77
77
 
78
78
 
79
79
  <li class="odd ">
80
+ <div class="item">
81
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#attribute-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#attribute (method)">#attribute</a></span>
82
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
83
+ </div>
84
+ </li>
85
+
86
+
87
+ <li class="even ">
80
88
  <div class="item">
81
89
  <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
82
90
  <small>Anchormodel::Attribute</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/ModelMixin.html#belongs_to_anchormodel-class_method" title="Anchormodel::ModelMixin.belongs_to_anchormodel (method)">belongs_to_anchormodel</a></span>
90
98
  <small>Anchormodel::ModelMixin</small>
@@ -92,7 +100,23 @@
92
100
  </li>
93
101
 
94
102
 
103
+ <li class="even ">
104
+ <div class="item">
105
+ <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>
106
+ <small>Anchormodel::ModelMixin</small>
107
+ </div>
108
+ </li>
109
+
110
+
95
111
  <li class="odd ">
112
+ <div class="item">
113
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#cast (method)">#cast</a></span>
114
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
115
+ </div>
116
+ </li>
117
+
118
+
119
+ <li class="even ">
96
120
  <div class="item">
97
121
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#cast (method)">#cast</a></span>
98
122
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -100,7 +124,7 @@
100
124
  </li>
101
125
 
102
126
 
103
- <li class="even ">
127
+ <li class="odd ">
104
128
  <div class="item">
105
129
  <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>
106
130
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -108,7 +132,7 @@
108
132
  </li>
109
133
 
110
134
 
111
- <li class="odd ">
135
+ <li class="even ">
112
136
  <div class="item">
113
137
  <span class='object_link'><a href="Anchormodel.html#find-class_method" title="Anchormodel.find (method)">find</a></span>
114
138
  <small>Anchormodel</small>
@@ -116,6 +140,14 @@
116
140
  </li>
117
141
 
118
142
 
143
+ <li class="odd ">
144
+ <div class="item">
145
+ <span class='object_link'><a href="Anchormodel.html#form_collection-class_method" title="Anchormodel.form_collection (method)">form_collection</a></span>
146
+ <small>Anchormodel</small>
147
+ </div>
148
+ </li>
149
+
150
+
119
151
  <li class="even ">
120
152
  <div class="item">
121
153
  <span class='object_link'><a href="Anchormodel.html#index-instance_method" title="Anchormodel#index (method)">#index</a></span>
@@ -149,6 +181,14 @@
149
181
 
150
182
 
151
183
  <li class="even ">
184
+ <div class="item">
185
+ <span class='object_link'><a href="Anchormodel/SimpleFormInputs/Helpers/AnchormodelInputsCommon.html#input-instance_method" title="Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon#input (method)">#input</a></span>
186
+ <small>Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon</small>
187
+ </div>
188
+ </li>
189
+
190
+
191
+ <li class="odd ">
152
192
  <div class="item">
153
193
  <span class='object_link'><a href="Anchormodel.html#inspect-instance_method" title="Anchormodel#inspect (method)">#inspect</a></span>
154
194
  <small>Anchormodel</small>
@@ -156,7 +196,7 @@
156
196
  </li>
157
197
 
158
198
 
159
- <li class="odd ">
199
+ <li class="even ">
160
200
  <div class="item">
161
201
  <span class='object_link'><a href="Anchormodel/Util.html#install_methods_in_model-class_method" title="Anchormodel::Util.install_methods_in_model (method)">install_methods_in_model</a></span>
162
202
  <small>Anchormodel::Util</small>
@@ -164,7 +204,7 @@
164
204
  </li>
165
205
 
166
206
 
167
- <li class="even ">
207
+ <li class="odd ">
168
208
  <div class="item">
169
209
  <span class='object_link'><a href="Anchormodel.html#key-instance_method" title="Anchormodel#key (method)">#key</a></span>
170
210
  <small>Anchormodel</small>
@@ -172,7 +212,7 @@
172
212
  </li>
173
213
 
174
214
 
175
- <li class="odd ">
215
+ <li class="even ">
176
216
  <div class="item">
177
217
  <span class='object_link'><a href="Anchormodel.html#label-instance_method" title="Anchormodel#label (method)">#label</a></span>
178
218
  <small>Anchormodel</small>
@@ -180,6 +220,14 @@
180
220
  </li>
181
221
 
182
222
 
223
+ <li class="odd ">
224
+ <div class="item">
225
+ <span class='object_link'><a href="Anchormodel/Attribute.html#multiple%3F-instance_method" title="Anchormodel::Attribute#multiple? (method)">#multiple?</a></span>
226
+ <small>Anchormodel::Attribute</small>
227
+ </div>
228
+ </li>
229
+
230
+
183
231
  <li class="even ">
184
232
  <div class="item">
185
233
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
@@ -189,6 +237,14 @@
189
237
 
190
238
 
191
239
  <li class="odd ">
240
+ <div class="item">
241
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serializable? (method)">#serializable?</a></span>
242
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
243
+ </div>
244
+ </li>
245
+
246
+
247
+ <li class="even ">
192
248
  <div class="item">
193
249
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
194
250
  <small>Anchormodel::ActiveModelTypeValueSingle</small>
@@ -196,6 +252,14 @@
196
252
  </li>
197
253
 
198
254
 
255
+ <li class="odd ">
256
+ <div class="item">
257
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueMulti.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueMulti#serialize (method)">#serialize</a></span>
258
+ <small>Anchormodel::ActiveModelTypeValueMulti</small>
259
+ </div>
260
+ </li>
261
+
262
+
199
263
  <li class="even ">
200
264
  <div class="item">
201
265
  <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.9.36
9
+ &mdash; Documentation by YARD 0.9.34
10
10
 
11
11
  </title>
12
12
 
@@ -84,7 +84,7 @@
84
84
 
85
85
 
86
86
 
87
- <strong class="classes">Classes:</strong> <span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span>, <span class='object_link'><a href="AnchormodelGenerator.html" title="AnchormodelGenerator (class)">AnchormodelGenerator</a></span>
87
+ <strong class="classes">Classes:</strong> <span class='object_link'><a href="Anchormodel.html" title="Anchormodel (class)">Anchormodel</a></span>, <span class='object_link'><a href="AnchormodelCheckBoxesInput.html" title="AnchormodelCheckBoxesInput (class)">AnchormodelCheckBoxesInput</a></span>, <span class='object_link'><a href="AnchormodelGenerator.html" title="AnchormodelGenerator (class)">AnchormodelGenerator</a></span>, <span class='object_link'><a href="AnchormodelInput.html" title="AnchormodelInput (class)">AnchormodelInput</a></span>, <span class='object_link'><a href="AnchormodelRadioButtonsInput.html" title="AnchormodelRadioButtonsInput (class)">AnchormodelRadioButtonsInput</a></span>
88
88
 
89
89
 
90
90
  </p>
@@ -100,9 +100,9 @@
100
100
  </div>
101
101
 
102
102
  <div id="footer">
103
- Generated on Tue Apr 23 17:20:43 2024 by
103
+ Generated on Sat Apr 27 10:18:33 2024 by
104
104
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
- 0.9.36 (ruby-3.2.2).
105
+ 0.9.34 (ruby-3.2.2).
106
106
  </div>
107
107
 
108
108
  </div>
@@ -0,0 +1,31 @@
1
+ class Anchormodel::ActiveModelTypeValueMulti < Anchormodel::ActiveModelTypeValueSingle
2
+ # This converts DB or input to an Anchormodel instance
3
+ def cast(values)
4
+ return values.split(',').map { |value| super(value) }.compact.to_set
5
+ end
6
+
7
+ # This converts an Anchormodel instance to string for DB
8
+ def serialize(values)
9
+ return case values
10
+ when Enumerable
11
+ values.map { |value| super(value) }.compact.join(',')
12
+ when String
13
+ values
14
+ when nil
15
+ ''
16
+ else
17
+ fail "Attempt to set #{@attribute.attribute_name} to unsupported type #{values.class}"
18
+ end
19
+ end
20
+
21
+ def serializable?(values)
22
+ return case values
23
+ when Enumerable
24
+ values.map { |value| super(value) }.compact.join(',')
25
+ when String
26
+ values.split(',').map { |value| super(value) }.compact
27
+ else
28
+ false
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,7 @@
1
1
  # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value
2
2
  class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
3
+ attr_reader :attribute
4
+
3
5
  def initialize(attribute)
4
6
  super()
5
7
  @attribute = attribute
@@ -9,14 +11,14 @@ class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
9
11
  :anchormodel
10
12
  end
11
13
 
12
- # This converts an Anchormodel instance to string for DB
14
+ # This converts DB or input to an Anchormodel instance
13
15
  def cast(value)
14
16
  value = value.presence
15
17
  return value if value.is_a?(@attribute.anchormodel_class)
16
18
  return @attribute.anchormodel_class.find(value)
17
19
  end
18
20
 
19
- # This converts DB or input to an Anchormodel instance
21
+ # This converts an Anchormodel instance to string for DB
20
22
  def serialize(value)
21
23
  value = value.presence
22
24
  return case value
@@ -10,10 +10,16 @@ class Anchormodel::Attribute
10
10
  # @param attribute_name [String,Symbol] The name and database column of the attribute
11
11
  # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds an `FooBar`)
12
12
  # @param optional [Boolean] If true, a presence validation is added to the model.
13
- def initialize(model_class, attribute_name, anchormodel_class = nil, optional = false)
13
+ # @param multiple [Boolean] If true, this attribute holds multiple anchormodels.
14
+ def initialize(model_class, attribute_name, anchormodel_class = nil, optional = false, multiple = false)
14
15
  @model_class = model_class
15
16
  @attribute_name = attribute_name.to_sym
16
17
  @anchormodel_class = anchormodel_class
17
18
  @optional = optional
19
+ @multiple = multiple
20
+ end
21
+
22
+ def multiple?
23
+ @multiple
18
24
  end
19
25
  end
@@ -14,5 +14,12 @@ module Anchormodel::ModelMixin
14
14
  def belongs_to_anchormodel(*args, **kwargs)
15
15
  Anchormodel::Util.install_methods_in_model(self, *args, **kwargs)
16
16
  end
17
+
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
21
+ def belongs_to_anchormodels(*args, **kwargs)
22
+ Anchormodel::Util.install_methods_in_model(self, *args, **kwargs, multiple: true)
23
+ end
17
24
  end
18
25
  end
@@ -0,0 +1,23 @@
1
+ unless defined? SimpleForm
2
+ begin
3
+ require 'simple_form'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
8
+ end
9
+ if defined? SimpleForm
10
+ class AnchormodelCheckBoxesInput < SimpleForm::Inputs::CollectionCheckBoxesInput
11
+ include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
+
13
+ private
14
+
15
+ def sf_selection_key
16
+ :checked
17
+ end
18
+
19
+ def before_render_input
20
+ @input_type = :check_boxes
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ unless defined? SimpleForm
2
+ begin
3
+ require 'simple_form'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
8
+ end
9
+ if defined? SimpleForm
10
+ class AnchormodelInput < SimpleForm::Inputs::CollectionSelectInput
11
+ include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
+
13
+ private
14
+
15
+ def sf_selection_key
16
+ :selected
17
+ end
18
+
19
+ def before_render_input; end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ unless defined? SimpleForm
2
+ begin
3
+ require 'simple_form'
4
+ rescue LoadError
5
+ nil
6
+ end
7
+
8
+ end
9
+ if defined? SimpleForm
10
+ class AnchormodelRadioButtonsInput < SimpleForm::Inputs::CollectionRadioButtonsInput
11
+ include Anchormodel::SimpleFormInputs::Helpers::AnchormodelInputsCommon
12
+
13
+ private
14
+
15
+ def sf_selection_key
16
+ :checked
17
+ end
18
+
19
+ def before_render_input
20
+ @input_type = :radio_buttons
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ class Anchormodel
2
+ module SimpleFormInputs
3
+ module Helpers
4
+ module AnchormodelInputsCommon
5
+ def input(wrapper_options = nil)
6
+ unless object.respond_to?(:anchormodel_attributes)
7
+ fail("The form field object does not appear to respond to `anchormodel_attributes`, \
8
+ did you `include Anchormodel::ModelMixin` in your `application_record.rb`? Affected object: #{object.inspect}")
9
+ end
10
+
11
+ am_attr = object.anchormodel_attributes[@attribute_name]
12
+ unless am_attr
13
+ fail("#{@attribute_name.inspect} does not look like an Anchormodel attribute, is `belongs_to_anchormodel` called in your model? \
14
+ Affected object: #{object.inspect}")
15
+ end
16
+ am_class = am_attr.anchormodel_class
17
+ options[:multiple] = true if am_attr.multiple? && options[:multiple] != false # allow overriding
18
+
19
+ # Attempt to read selected key from html input options "value", as the caller might not know that this is a select.
20
+ selected_key = input_options[:value]
21
+ if selected_key.blank? && object
22
+ # No selected key override present and a model is present, use the model to find out what to select.
23
+ selected_am = object.send(@attribute_name)
24
+ if am_attr.multiple?
25
+ selected_key = selected_am&.map(&:key)&.map(&:to_s) || []
26
+ else
27
+ selected_key = (selected_am&.key || am_class.all.first).to_s
28
+ end
29
+ end
30
+
31
+ options.deep_merge!(
32
+ label_method: :first,
33
+ value_method: :second,
34
+ sf_selection_key => selected_key,
35
+ include_blank: !am_attr.multiple? && am_attr.optional
36
+ )
37
+
38
+ @collection = collect(am_class.all)
39
+
40
+ before_render_input
41
+
42
+ super
43
+ end
44
+
45
+ private
46
+
47
+ # Takes an array of objects implementing the methods `label` and `key` and returns an array suitable for simple_form select fields.
48
+ def collect(flat_array)
49
+ return flat_array.map { |entry| [entry.label, entry.key] }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -5,21 +5,24 @@ module Anchormodel::Util
5
5
  # @param model_class [ActiveRecord::Base] Internal only. The model class that the attribute should be installed to.
6
6
  # @param attribute_name [String,Symbol] The name and database column of the attribute
7
7
  # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds a `FooBar`)
8
- # @param optional [Boolean] If false, a presence validation is added to the model.
8
+ # @param optional [Boolean] If false, a presence validation is added to the model. Forced to true if multiple is true.
9
+ # @param multiple [Boolean] Internal only. Distinguishes between `belongs_to_anchormodel` and `belongs_to_anchormodels`.
9
10
  # @param model_readers [Boolean] If true, the model is given an ActiveRecord::Enum style method `my_model.my_key?` reader for each key in the anchormodel
10
11
  # @param model_writers [Boolean] If true, the model is given an ActiveRecord::Enum style method `my_model.my_key!` writer for each key in the anchormodel
11
12
  # @param model_scopes [Boolean] If true, the model is given an ActiveRecord::Enum style scope `MyModel.mykey` for each key in the anchormodel
12
13
  # @param model_methods [Boolean, NilClass] If non-nil, this mass-assigns and overrides `model_readers`, `model_writers` and `model_scopes`
13
14
  def self.install_methods_in_model(model_class, attribute_name, anchormodel_class = nil,
14
15
  optional: false,
16
+ multiple: false,
15
17
  model_readers: true,
16
18
  model_writers: true,
17
19
  model_scopes: true,
18
20
  model_methods: nil)
19
21
 
22
+ optional = true if multiple
20
23
  anchormodel_class ||= attribute_name.to_s.classify.constantize
21
24
  attribute_name = attribute_name.to_sym
22
- attribute = Anchormodel::Attribute.new(self, attribute_name, anchormodel_class, optional)
25
+ attribute = Anchormodel::Attribute.new(model_class, attribute_name, anchormodel_class, optional, multiple)
23
26
 
24
27
  # Mass configurations if model_methods was specfied
25
28
  unless model_methods.nil?
@@ -38,11 +41,43 @@ module Anchormodel::Util
38
41
 
39
42
  # Make casting work
40
43
  # Define serializer/deserializer
41
- active_model_type_value = Anchormodel::ActiveModelTypeValueSingle.new(attribute)
44
+ active_model_type_value = (multiple ? Anchormodel::ActiveModelTypeValueMulti : Anchormodel::ActiveModelTypeValueSingle).new(attribute)
42
45
 
43
46
  # Overwrite reader to force building anchors at every retrieval
44
47
  model_class.define_method(attribute_name.to_s) do
45
- active_model_type_value.deserialize(read_attribute_before_type_cast(attribute_name))
48
+ result = active_model_type_value.deserialize(read_attribute_before_type_cast(attribute_name))
49
+
50
+ # If this attribute holds multiple anchormodels (`belongs_to_anchormodels`), patch the Array before returning in order to implement collection modifiers:
51
+ if multiple
52
+ model = self # fetching the model in order to pass it to the implementation of the following via reflection to make it available for storage
53
+ single_tv = Anchormodel::ActiveModelTypeValueSingle.new(attribute) # Used for casting inputs to properly compare them to the set.
54
+
55
+ # Adding
56
+ result.define_singleton_method('add') do |value_to_add|
57
+ super(single_tv.cast(value_to_add))
58
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
59
+ next self
60
+ end
61
+ result.define_singleton_method('<<') { |value_to_add| add(single_tv.cast(value_to_add)) }
62
+
63
+ # Deleting
64
+ result.define_singleton_method('delete') do |value_to_delete|
65
+ super(single_tv.cast(value_to_delete))
66
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
67
+ next self
68
+ end
69
+
70
+ # Clearing
71
+ result.define_singleton_method('clear') do
72
+ super()
73
+ model.write_attribute(attribute_name, active_model_type_value.serialize(self))
74
+ next self
75
+ end
76
+
77
+ # In the future, further methods could be supported. e.g. delete_if, subtract etc.
78
+ end
79
+
80
+ return result
46
81
  end
47
82
 
48
83
  # Override writer to fail early when an invalid target value is specified
@@ -61,7 +96,11 @@ module Anchormodel::Util
61
96
  fail("Anchormodel reader #{entry.key}? already defined for #{self}, add `model_readers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
62
97
  end
63
98
  model_class.define_method(:"#{entry.key}?") do
64
- public_send(attribute_name.to_s) == entry
99
+ if multiple
100
+ public_send(attribute_name.to_s).include?(entry)
101
+ else
102
+ public_send(attribute_name.to_s) == entry
103
+ end
65
104
  end
66
105
  end
67
106
  end
@@ -74,7 +113,11 @@ module Anchormodel::Util
74
113
  fail("Anchormodel writer #{entry.key}! already defined for #{self}, add `model_writers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
75
114
  end
76
115
  model_class.define_method(:"#{entry.key}!") do
77
- public_send(:"#{attribute_name}=", entry)
116
+ if multiple
117
+ public_send(attribute_name.to_s) << entry
118
+ else
119
+ public_send(:"#{attribute_name}=", entry)
120
+ end
78
121
  end
79
122
  end
80
123
  end
@@ -86,7 +129,14 @@ module Anchormodel::Util
86
129
  if model_class.respond_to?(entry.key)
87
130
  fail("Anchormodel scope #{entry.key} already defined for #{self}, add `model_scopes: false` to `belongs_to_anchormodel :#{attribute_name}`.")
88
131
  end
89
- model_class.scope(entry.key, -> { where(attribute_name => entry.key) })
132
+ if multiple
133
+ model_class.scope(entry.key, lambda {
134
+ where("#{attribute_name} LIKE ? OR #{attribute_name} LIKE ? OR #{attribute_name} LIKE ? OR #{attribute_name} LIKE ?",
135
+ "%#{entry.key},%", "%#{entry.key}", "#{entry.key},%", entry.key.to_s)
136
+ })
137
+ else
138
+ model_class.scope(entry.key, -> { where(attribute_name => entry.key) })
139
+ end
90
140
  end
91
141
  end
92
142
  end
data/lib/anchormodel.rb CHANGED
@@ -25,6 +25,11 @@ class Anchormodel
25
25
  entries_list
26
26
  end
27
27
 
28
+ # Returns an array of tuples [label, key] suitable for passing as a collection to some form input helpers
29
+ def self.form_collection
30
+ entries_list.map { |el| [el.label, el.key.to_s] }
31
+ end
32
+
28
33
  # Retrieves a particular value given the key. Fails if not found.
29
34
  # @param key [String,Symbol] The key of the value that should be retrieved.
30
35
  def self.find(key)
@@ -79,6 +84,11 @@ end
79
84
 
80
85
  require 'anchormodel/util'
81
86
  require 'anchormodel/active_model_type_value_single'
87
+ require 'anchormodel/active_model_type_value_multi'
82
88
  require 'anchormodel/attribute'
83
89
  require 'anchormodel/model_mixin'
84
90
  require 'anchormodel/version'
91
+ require 'anchormodel/simple_form_inputs/helpers/anchormodel_inputs_common'
92
+ require 'anchormodel/simple_form_inputs/anchormodel_input'
93
+ require 'anchormodel/simple_form_inputs/anchormodel_radio_buttons_input'
94
+ require 'anchormodel/simple_form_inputs/anchormodel_check_boxes_input'
@@ -21,10 +21,6 @@ class UserTest < Minitest::Test
21
21
  )
22
22
  end
23
23
 
24
- def test_missing_key
25
- assert_raises { Role.find(:does_not_exist) }
26
- end
27
-
28
24
  def test_basic_setters_and_getters
29
25
  u = User.create!(role: 'guest', locale: 'de') # String assignment
30
26
  assert_equal Role.find(:guest), u.role
@@ -57,11 +53,6 @@ class UserTest < Minitest::Test
57
53
  assert Role.find(:moderator) < Role.find(:admin)
58
54
  end
59
55
 
60
- def test_presence_validation
61
- valentine = User.new
62
- assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
63
- end
64
-
65
56
  def test_alternative_column_name
66
57
  ben = User.create!(
67
58
  role: Role.find(:moderator),
@@ -119,6 +110,19 @@ class UserTest < Minitest::Test
119
110
  assert_nil u.secondary_role
120
111
  end
121
112
 
113
+ ###---
114
+ # Testing failures
115
+ ###---
116
+
117
+ def test_presence_validation
118
+ valentine = User.new
119
+ assert_raises(ActiveRecord::RecordInvalid) { valentine.save! }
120
+ end
121
+
122
+ def test_missing_key
123
+ assert_raises { Role.find(:does_not_exist) }
124
+ end
125
+
122
126
  # Attempting to create a model with an invalid constant name should fail
123
127
  def test_invalid_key_update
124
128
  assert_raises(RuntimeError) { User.create!(role: :admin, locale: :de, preferred_locale: :invalid) }
@@ -137,4 +141,64 @@ class UserTest < Minitest::Test
137
141
  ActiveRecord::Base.connection.execute(sql)
138
142
  assert_raises(RuntimeError) { User.first.role }
139
143
  end
144
+
145
+ ###---
146
+ # Testing multiple anchormodel associations
147
+ ###---
148
+
149
+ def test_multi_basics
150
+ u = User.create!(role: 'guest', locale: 'de')
151
+ assert_equal(Set.new, u.animals)
152
+ # Adding
153
+ u.animals << 'cat'
154
+ assert_equal(Set.new([Animal.find(:cat)]), u.animals)
155
+ u.animals.add :dog
156
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), u.animals)
157
+ u.animals.add Animal.find(:horse)
158
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog), Animal.find(:horse)]), u.animals)
159
+ # Removing
160
+ u.animals.delete 'cat'
161
+ assert_equal(Set.new([Animal.find(:dog), Animal.find(:horse)]), u.animals)
162
+ u.animals.delete :dog
163
+ assert_equal(Set.new([Animal.find(:horse)]), u.animals)
164
+ u.animals.delete Animal.find(:horse)
165
+ assert_equal(false, u.animals.any?)
166
+ # Setting
167
+ u.animals = %i[cat dog]
168
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), u.animals)
169
+ # Clearing
170
+ u.animals.clear
171
+ assert_equal(false, u.animals.any?)
172
+ end
173
+
174
+ def test_multi_save_load
175
+ u = User.create!(role: 'guest', locale: 'de')
176
+ u.animals = %i[cat dog]
177
+ u.save!
178
+ freshly_loaded_u = User.first
179
+ assert_equal(Set.new([Animal.find(:cat), Animal.find(:dog)]), freshly_loaded_u.animals)
180
+ end
181
+
182
+ def test_multi_model_readers_and_writers
183
+ u = User.create!(role: 'guest', locale: 'de')
184
+ u.cat!
185
+ u.cat!
186
+ assert_equal(Set.new([Animal.find(:cat)]), u.animals) # tolerate no duplicate cat
187
+ assert_equal(true, u.cat?)
188
+ u.dog!
189
+ assert_equal(true, u.cat?)
190
+ assert_equal(true, u.dog?)
191
+ assert_equal(false, u.horse?)
192
+ end
193
+
194
+ def test_multi_model_scopes
195
+ u = User.create!(role: 'guest', locale: 'fr', animals: %w[dog cat])
196
+ v = User.create!(role: 'guest', locale: 'it', animals: %w[dog horse])
197
+ assert_equal(0, User.rat.count)
198
+ assert_equal(1, User.cat.count)
199
+ assert_equal(1, User.horse.count)
200
+ assert_equal(2, User.dog.count)
201
+ assert_equal(u.id, User.cat.first.id)
202
+ assert_equal(v.id, User.horse.first.id)
203
+ end
140
204
  end
@@ -0,0 +1,6 @@
1
+ class Animal < Anchormodel
2
+ new :cat
3
+ new :dog
4
+ new :horse
5
+ new :rat
6
+ end
@@ -3,4 +3,5 @@ class User < ApplicationRecord
3
3
  belongs_to_anchormodel :secondary_role, Role, optional: true, model_readers: false, model_writers: false, model_scopes: false
4
4
  belongs_to_anchormodel :locale, model_methods: false
5
5
  belongs_to_anchormodel :preferred_locale, Locale
6
+ belongs_to_anchormodels :animals
6
7
  end