anchormodel 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/doc/file.README.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  File: README
8
8
 
9
- &mdash; Documentation by YARD 0.9.28
9
+ &mdash; Documentation by YARD 0.9.36
10
10
 
11
11
  </title>
12
12
 
@@ -68,11 +68,11 @@
68
68
 
69
69
  <p>Typically, a Rails application consists of three kinds of state:</p>
70
70
  <ul><li>
71
- <p>The code, which we can consider static within a given version. Code can reference to other code, e.g. <code>node.parent = other_node</code>.</p>
71
+ <p>The code, which we can consider static within a given version. Code can reference to other code, e.g. <code>node.parent = other_node</code>.</p>
72
72
  </li><li>
73
- <p>The database contents, which can fluctuate within the bounds of the DB schema. Data can reference to other data, ideally via foreign keys.</p>
73
+ <p>The database contents, which can fluctuate within the bounds of the DB schema. Data can reference to other data, ideally via foreign keys.</p>
74
74
  </li><li>
75
- <p>A mix of the two, where code needs to be specifically tailored for some kind of data. A prominent example of such a mix would for instance be user roles: roles must be hardcoded in the code because security logic is tied to them. However, as users are assigned to roles in the database, roles also need to be persisted in the database. This is where Anchormodel comes into play.</p>
75
+ <p>A mix of the two, where code needs to be specifically tailored for some kind of data. A prominent example of such a mix would for instance be user roles: roles must be hardcoded in the code because security logic is tied to them. However, as users are assigned to roles in the database, roles also need to be persisted in the database. This is where Anchormodel comes into play.</p>
76
76
  </li></ul>
77
77
 
78
78
  <h2 id="label-Alternatives+coviering+the+same+use+case">Alternatives coviering the same use case</h2>
@@ -96,6 +96,14 @@
96
96
  <p>In <code>application_record.rb</code>, add in the class body: <code>include Anchormodel::ModelMixin</code></p>
97
97
  </li></ol>
98
98
 
99
+ <h1 id="label-Generator">Generator</h1>
100
+
101
+ <p>For convenience, Anchormodel provides a Rails generator:</p>
102
+
103
+ <p><code>rails generate anchormodel Role</code></p>
104
+
105
+ <p>This will create <code>app/anchormodels/role.rb</code>.</p>
106
+
99
107
  <h1 id="label-Basic+example">Basic example</h1>
100
108
 
101
109
  <p><code>app/anchormodels/role.rb</code>:</p>
@@ -228,9 +236,9 @@
228
236
  </div></div>
229
237
 
230
238
  <div id="footer">
231
- Generated on Wed Jan 25 12:36:39 2023 by
239
+ Generated on Tue Apr 23 17:20:43 2024 by
232
240
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
233
- 0.9.28 (ruby-3.1.3).
241
+ 0.9.36 (ruby-3.2.2).
234
242
  </div>
235
243
 
236
244
  </div>
data/doc/frames.html CHANGED
@@ -2,13 +2,18 @@
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <title>Documentation by YARD 0.9.28</title>
5
+ <title>Documentation by YARD 0.9.36</title>
6
6
  </head>
7
7
  <script type="text/javascript">
8
- var match = unescape(window.location.hash).match(/^#!(.+)/);
9
- var name = match ? match[1] : 'index.html';
10
- name = name.replace(/^(\w+):\/\//, '').replace(/^\/\//, '');
11
- window.top.location = name;
8
+ var mainUrl = 'index.html';
9
+ try {
10
+ var match = decodeURIComponent(window.location.hash).match(/^#!(.+)/);
11
+ var name = match ? match[1] : mainUrl;
12
+ var url = new URL(name, location.href);
13
+ window.top.location.replace(url.origin === location.origin ? name : mainUrl);
14
+ } catch (e) {
15
+ window.top.location.replace(mainUrl);
16
+ }
12
17
  </script>
13
18
  <noscript>
14
19
  <h1>Oops!</h1>
data/doc/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  File: README
8
8
 
9
- &mdash; Documentation by YARD 0.9.28
9
+ &mdash; Documentation by YARD 0.9.36
10
10
 
11
11
  </title>
12
12
 
@@ -68,11 +68,11 @@
68
68
 
69
69
  <p>Typically, a Rails application consists of three kinds of state:</p>
70
70
  <ul><li>
71
- <p>The code, which we can consider static within a given version. Code can reference to other code, e.g. <code>node.parent = other_node</code>.</p>
71
+ <p>The code, which we can consider static within a given version. Code can reference to other code, e.g. <code>node.parent = other_node</code>.</p>
72
72
  </li><li>
73
- <p>The database contents, which can fluctuate within the bounds of the DB schema. Data can reference to other data, ideally via foreign keys.</p>
73
+ <p>The database contents, which can fluctuate within the bounds of the DB schema. Data can reference to other data, ideally via foreign keys.</p>
74
74
  </li><li>
75
- <p>A mix of the two, where code needs to be specifically tailored for some kind of data. A prominent example of such a mix would for instance be user roles: roles must be hardcoded in the code because security logic is tied to them. However, as users are assigned to roles in the database, roles also need to be persisted in the database. This is where Anchormodel comes into play.</p>
75
+ <p>A mix of the two, where code needs to be specifically tailored for some kind of data. A prominent example of such a mix would for instance be user roles: roles must be hardcoded in the code because security logic is tied to them. However, as users are assigned to roles in the database, roles also need to be persisted in the database. This is where Anchormodel comes into play.</p>
76
76
  </li></ul>
77
77
 
78
78
  <h2 id="label-Alternatives+coviering+the+same+use+case">Alternatives coviering the same use case</h2>
@@ -96,6 +96,14 @@
96
96
  <p>In <code>application_record.rb</code>, add in the class body: <code>include Anchormodel::ModelMixin</code></p>
97
97
  </li></ol>
98
98
 
99
+ <h1 id="label-Generator">Generator</h1>
100
+
101
+ <p>For convenience, Anchormodel provides a Rails generator:</p>
102
+
103
+ <p><code>rails generate anchormodel Role</code></p>
104
+
105
+ <p>This will create <code>app/anchormodels/role.rb</code>.</p>
106
+
99
107
  <h1 id="label-Basic+example">Basic example</h1>
100
108
 
101
109
  <p><code>app/anchormodels/role.rb</code>:</p>
@@ -228,9 +236,9 @@
228
236
  </div></div>
229
237
 
230
238
  <div id="footer">
231
- Generated on Wed Jan 25 12:36:39 2023 by
239
+ Generated on Tue Apr 23 17:20:43 2024 by
232
240
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
233
- 0.9.28 (ruby-3.1.3).
241
+ 0.9.36 (ruby-3.2.2).
234
242
  </div>
235
243
 
236
244
  </div>
data/doc/method_list.html CHANGED
@@ -54,23 +54,23 @@
54
54
 
55
55
  <li class="even ">
56
56
  <div class="item">
57
- <span class='object_link'><a href="Anchormodel.html#all-class_method" title="Anchormodel.all (method)">all</a></span>
58
- <small>Anchormodel</small>
57
+ <span class='object_link'><a href="AnchormodelGenerator.html#add_anchormodel-instance_method" title="AnchormodelGenerator#add_anchormodel (method)">#add_anchormodel</a></span>
58
+ <small>AnchormodelGenerator</small>
59
59
  </div>
60
60
  </li>
61
61
 
62
62
 
63
63
  <li class="odd ">
64
64
  <div class="item">
65
- <span class='object_link'><a href="Anchormodel/Attribute.html#anchormodel_class-instance_method" title="Anchormodel::Attribute#anchormodel_class (method)">#anchormodel_class</a></span>
66
- <small>Anchormodel::Attribute</small>
65
+ <span class='object_link'><a href="Anchormodel.html#all-class_method" title="Anchormodel.all (method)">all</a></span>
66
+ <small>Anchormodel</small>
67
67
  </div>
68
68
  </li>
69
69
 
70
70
 
71
71
  <li class="even ">
72
72
  <div class="item">
73
- <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
73
+ <span class='object_link'><a href="Anchormodel/Attribute.html#anchormodel_class-instance_method" title="Anchormodel::Attribute#anchormodel_class (method)">#anchormodel_class</a></span>
74
74
  <small>Anchormodel::Attribute</small>
75
75
  </div>
76
76
  </li>
@@ -78,32 +78,32 @@
78
78
 
79
79
  <li class="odd ">
80
80
  <div class="item">
81
- <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>
82
- <small>Anchormodel::ModelMixin</small>
81
+ <span class='object_link'><a href="Anchormodel/Attribute.html#attribute_name-instance_method" title="Anchormodel::Attribute#attribute_name (method)">#attribute_name</a></span>
82
+ <small>Anchormodel::Attribute</small>
83
83
  </div>
84
84
  </li>
85
85
 
86
86
 
87
87
  <li class="even ">
88
88
  <div class="item">
89
- <span class='object_link'><a href="Anchormodel/ActiveModelTypeValue.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValue#cast (method)">#cast</a></span>
90
- <small>Anchormodel::ActiveModelTypeValue</small>
89
+ <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
+ <small>Anchormodel::ModelMixin</small>
91
91
  </div>
92
92
  </li>
93
93
 
94
94
 
95
95
  <li class="odd ">
96
96
  <div class="item">
97
- <span class='object_link'><a href="Anchormodel/ActiveModelTypeValue.html#changed%3F-instance_method" title="Anchormodel::ActiveModelTypeValue#changed? (method)">#changed?</a></span>
98
- <small>Anchormodel::ActiveModelTypeValue</small>
97
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#cast-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#cast (method)">#cast</a></span>
98
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
99
99
  </div>
100
100
  </li>
101
101
 
102
102
 
103
103
  <li class="even ">
104
104
  <div class="item">
105
- <span class='object_link'><a href="Anchormodel/ActiveModelTypeValue.html#deserialize-instance_method" title="Anchormodel::ActiveModelTypeValue#deserialize (method)">#deserialize</a></span>
106
- <small>Anchormodel::ActiveModelTypeValue</small>
105
+ <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
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
107
107
  </div>
108
108
  </li>
109
109
 
@@ -142,8 +142,8 @@
142
142
 
143
143
  <li class="odd ">
144
144
  <div class="item">
145
- <span class='object_link'><a href="Anchormodel/ActiveModelTypeValue.html#initialize-instance_method" title="Anchormodel::ActiveModelTypeValue#initialize (method)">#initialize</a></span>
146
- <small>Anchormodel::ActiveModelTypeValue</small>
145
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#initialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#initialize (method)">#initialize</a></span>
146
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
147
147
  </div>
148
148
  </li>
149
149
 
@@ -157,6 +157,14 @@
157
157
 
158
158
 
159
159
  <li class="odd ">
160
+ <div class="item">
161
+ <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
+ <small>Anchormodel::Util</small>
163
+ </div>
164
+ </li>
165
+
166
+
167
+ <li class="even ">
160
168
  <div class="item">
161
169
  <span class='object_link'><a href="Anchormodel.html#key-instance_method" title="Anchormodel#key (method)">#key</a></span>
162
170
  <small>Anchormodel</small>
@@ -164,7 +172,7 @@
164
172
  </li>
165
173
 
166
174
 
167
- <li class="even ">
175
+ <li class="odd ">
168
176
  <div class="item">
169
177
  <span class='object_link'><a href="Anchormodel.html#label-instance_method" title="Anchormodel#label (method)">#label</a></span>
170
178
  <small>Anchormodel</small>
@@ -172,7 +180,7 @@
172
180
  </li>
173
181
 
174
182
 
175
- <li class="odd ">
183
+ <li class="even ">
176
184
  <div class="item">
177
185
  <span class='object_link'><a href="Anchormodel/Attribute.html#optional-instance_method" title="Anchormodel::Attribute#optional (method)">#optional</a></span>
178
186
  <small>Anchormodel::Attribute</small>
@@ -180,10 +188,18 @@
180
188
  </li>
181
189
 
182
190
 
191
+ <li class="odd ">
192
+ <div class="item">
193
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serializable%3F-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serializable? (method)">#serializable?</a></span>
194
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
195
+ </div>
196
+ </li>
197
+
198
+
183
199
  <li class="even ">
184
200
  <div class="item">
185
- <span class='object_link'><a href="Anchormodel/ActiveModelTypeValue.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValue#serialize (method)">#serialize</a></span>
186
- <small>Anchormodel::ActiveModelTypeValue</small>
201
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#serialize-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#serialize (method)">#serialize</a></span>
202
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
187
203
  </div>
188
204
  </li>
189
205
 
@@ -204,6 +220,14 @@
204
220
  </li>
205
221
 
206
222
 
223
+ <li class="odd ">
224
+ <div class="item">
225
+ <span class='object_link'><a href="Anchormodel/ActiveModelTypeValueSingle.html#type-instance_method" title="Anchormodel::ActiveModelTypeValueSingle#type (method)">#type</a></span>
226
+ <small>Anchormodel::ActiveModelTypeValueSingle</small>
227
+ </div>
228
+ </li>
229
+
230
+
207
231
 
208
232
  </ul>
209
233
  </div>
@@ -6,7 +6,7 @@
6
6
  <title>
7
7
  Top Level Namespace
8
8
 
9
- &mdash; Documentation by YARD 0.9.28
9
+ &mdash; Documentation by YARD 0.9.36
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>
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>
88
88
 
89
89
 
90
90
  </p>
@@ -100,9 +100,9 @@
100
100
  </div>
101
101
 
102
102
  <div id="footer">
103
- Generated on Wed Jan 25 12:36:39 2023 by
103
+ Generated on Tue Apr 23 17:20:43 2024 by
104
104
  <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
- 0.9.28 (ruby-3.1.3).
105
+ 0.9.36 (ruby-3.2.2).
106
106
  </div>
107
107
 
108
108
  </div>
@@ -1,15 +1,22 @@
1
1
  # @see https://www.rubydoc.info/docs/rails/ActiveModel/Type/Value
2
- class Anchormodel::ActiveModelTypeValue < ActiveModel::Type::Value
2
+ class Anchormodel::ActiveModelTypeValueSingle < ActiveModel::Type::Value
3
3
  def initialize(attribute)
4
4
  super()
5
5
  @attribute = attribute
6
6
  end
7
7
 
8
+ def type
9
+ :anchormodel
10
+ end
11
+
12
+ # This converts an Anchormodel instance to string for DB
8
13
  def cast(value)
9
- serialize value
14
+ value = value.presence
15
+ return value if value.is_a?(@attribute.anchormodel_class)
16
+ return @attribute.anchormodel_class.find(value)
10
17
  end
11
18
 
12
- # Implementing this instead of cast to force key validation in any case
19
+ # This converts DB or input to an Anchormodel instance
13
20
  def serialize(value)
14
21
  value = value.presence
15
22
  return case value
@@ -27,13 +34,19 @@ class Anchormodel::ActiveModelTypeValue < ActiveModel::Type::Value
27
34
  end
28
35
  end
29
36
 
30
- def deserialize(value)
31
- value = value.presence
32
- return value if value.is_a?(@attribute.anchormodel_class)
33
- return @attribute.anchormodel_class.find(value)
37
+ def serializable?(value)
38
+ return case value
39
+ when Symbol, String
40
+ @attribute.anchormodel_class.valid_keys.exclude?(value.to_sym)
41
+ when nil, @attribute.anchormodel_class
42
+ true
43
+ else
44
+ false
45
+ end
34
46
  end
35
47
 
36
- def changed?(old_value, new_value, _new_value_before_type_cast)
37
- return deserialize(old_value) != deserialize(new_value)
48
+ def changed_in_place?(raw_old_value, value)
49
+ old_value = deserialize(raw_old_value)
50
+ old_value != value
38
51
  end
39
52
  end
@@ -9,88 +9,10 @@ module Anchormodel::ModelMixin
9
9
 
10
10
  class_methods do
11
11
  # Creates an attribute linking to an Anchormodel. The attribute should be
12
- # present in the DB and the column should be named the same as `attribute_name.`
13
- # @param attribute_name [String,Symbol] The name and database column of the attribute
14
- # @param anchormodel_class [Class] Class of the Anchormodel (omit if attribute `:foo_bar` holds a `FooBar`)
15
- # @param optional [Boolean] If true, a presence validation is added to the model.
16
- # @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
17
- # @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
18
- # @param model_scopes [Boolean] If true, the model is given an ActiveRecord::Enum style scope `MyModel.mykey` for each key in the anchormodel
19
- # @param model_methods [Boolean, NilClass] If non-nil, this mass-assigns and overrides `model_readers`, `model_writers` and `model_scopes`
20
- def belongs_to_anchormodel(attribute_name, anchormodel_class = nil, optional: false, model_readers: true,
21
- model_writers: true, model_scopes: true, model_methods: nil)
22
- anchormodel_class ||= attribute_name.to_s.classify.constantize
23
- attribute_name = attribute_name.to_sym
24
- attribute = Anchormodel::Attribute.new(self, attribute_name, anchormodel_class, optional)
25
-
26
- # Mass configurations if model_methods was specfied
27
- unless model_methods.nil?
28
- model_readers = model_methods
29
- model_writers = model_methods
30
- model_scopes = model_methods
31
- end
32
-
33
- # Register attribute
34
- self.anchormodel_attributes = anchormodel_attributes.merge({ attribute_name => attribute }).freeze
35
-
36
- # Add presence validation if required
37
- unless optional
38
- validates attribute_name, presence: true
39
- end
40
-
41
- # Make casting work
42
- # Define serializer/deserializer
43
- active_model_type_value = Anchormodel::ActiveModelTypeValue.new(attribute)
44
-
45
- # Overwrite reader to force building anchors at every retrieval
46
- define_method(attribute_name.to_s) do
47
- active_model_type_value.deserialize(read_attribute(attribute_name))
48
- end
49
-
50
- # Override writer to fail early when an invalid target value is specified
51
- define_method("#{attribute_name}=") do |new_value|
52
- write_attribute(attribute_name, active_model_type_value.serialize(new_value))
53
- end
54
-
55
- # Supply serializer and deserializer
56
- attribute attribute_name, active_model_type_value
57
-
58
- # Create ActiveRecord::Enum style reader directly in the model if asked to do so
59
- # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin? and user.guest? (returning true iff role is admin/guest)
60
- if model_readers
61
- anchormodel_class.all.each do |entry|
62
- if respond_to?(:"#{entry.key}?")
63
- fail("Anchormodel reader #{entry.key}? already defined for #{self}, add `model_readers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
64
- end
65
- define_method(:"#{entry.key}?") do
66
- public_send(attribute_name.to_s) == entry
67
- end
68
- end
69
- end
70
-
71
- # Create ActiveRecord::Enum style writer directly in the model if asked to do so
72
- # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin! and user.guest! (setting the role to admin/guest)
73
- if model_writers
74
- anchormodel_class.all.each do |entry|
75
- if respond_to?(:"#{entry.key}!")
76
- fail("Anchormodel writer #{entry.key}! already defined for #{self}, add `model_writers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
77
- end
78
- define_method(:"#{entry.key}!") do
79
- public_send(:"#{attribute_name}=", entry)
80
- end
81
- end
82
- end
83
-
84
- # Create ActiveRecord::Enum style scope directly in the model class if asked to do so
85
- # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin! and user.guest! (setting the role to admin/guest)
86
- if model_scopes
87
- anchormodel_class.all.each do |entry|
88
- if respond_to?(entry.key)
89
- fail("Anchormodel scope #{entry.key} already defined for #{self}, add `model_scopes: false` to `belongs_to_anchormodel :#{attribute_name}`.")
90
- end
91
- scope(entry.key, -> { where(attribute_name => entry.key) })
92
- end
93
- end
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
14
+ def belongs_to_anchormodel(*args, **kwargs)
15
+ Anchormodel::Util.install_methods_in_model(self, *args, **kwargs)
94
16
  end
95
17
  end
96
18
  end
@@ -0,0 +1,93 @@
1
+ # @api description
2
+ # A swiss army knife for common functionality
3
+ module Anchormodel::Util
4
+ # Installs an anchormodel attribute in a model class
5
+ # @param model_class [ActiveRecord::Base] Internal only. The model class that the attribute should be installed to.
6
+ # @param attribute_name [String,Symbol] The name and database column of the attribute
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.
9
+ # @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
+ # @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
+ # @param model_scopes [Boolean] If true, the model is given an ActiveRecord::Enum style scope `MyModel.mykey` for each key in the anchormodel
12
+ # @param model_methods [Boolean, NilClass] If non-nil, this mass-assigns and overrides `model_readers`, `model_writers` and `model_scopes`
13
+ def self.install_methods_in_model(model_class, attribute_name, anchormodel_class = nil,
14
+ optional: false,
15
+ model_readers: true,
16
+ model_writers: true,
17
+ model_scopes: true,
18
+ model_methods: nil)
19
+
20
+ anchormodel_class ||= attribute_name.to_s.classify.constantize
21
+ attribute_name = attribute_name.to_sym
22
+ attribute = Anchormodel::Attribute.new(self, attribute_name, anchormodel_class, optional)
23
+
24
+ # Mass configurations if model_methods was specfied
25
+ unless model_methods.nil?
26
+ model_readers = model_methods
27
+ model_writers = model_methods
28
+ model_scopes = model_methods
29
+ end
30
+
31
+ # Register attribute
32
+ model_class.anchormodel_attributes = model_class.anchormodel_attributes.merge({ attribute_name => attribute }).freeze
33
+
34
+ # Add presence validation if required
35
+ unless optional
36
+ model_class.validates attribute_name, presence: true
37
+ end
38
+
39
+ # Make casting work
40
+ # Define serializer/deserializer
41
+ active_model_type_value = Anchormodel::ActiveModelTypeValueSingle.new(attribute)
42
+
43
+ # Overwrite reader to force building anchors at every retrieval
44
+ model_class.define_method(attribute_name.to_s) do
45
+ active_model_type_value.deserialize(read_attribute_before_type_cast(attribute_name))
46
+ end
47
+
48
+ # Override writer to fail early when an invalid target value is specified
49
+ model_class.define_method("#{attribute_name}=") do |new_value|
50
+ write_attribute(attribute_name, active_model_type_value.serialize(new_value))
51
+ end
52
+
53
+ # Supply serializer and deserializer
54
+ model_class.attribute attribute_name, active_model_type_value
55
+
56
+ # Create ActiveRecord::Enum style reader directly in the model if asked to do so
57
+ # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin? and user.guest? (returning true iff role is admin/guest)
58
+ if model_readers
59
+ anchormodel_class.all.each do |entry|
60
+ if model_class.respond_to?(:"#{entry.key}?")
61
+ fail("Anchormodel reader #{entry.key}? already defined for #{self}, add `model_readers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
62
+ end
63
+ model_class.define_method(:"#{entry.key}?") do
64
+ public_send(attribute_name.to_s) == entry
65
+ end
66
+ end
67
+ end
68
+
69
+ # Create ActiveRecord::Enum style writer directly in the model if asked to do so
70
+ # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin! and user.guest! (setting the role to admin/guest)
71
+ if model_writers
72
+ anchormodel_class.all.each do |entry|
73
+ if model_class.respond_to?(:"#{entry.key}!")
74
+ fail("Anchormodel writer #{entry.key}! already defined for #{self}, add `model_writers: false` to `belongs_to_anchormodel :#{attribute_name}`.")
75
+ end
76
+ model_class.define_method(:"#{entry.key}!") do
77
+ public_send(:"#{attribute_name}=", entry)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Create ActiveRecord::Enum style scope directly in the model class if asked to do so
83
+ # For a model User with anchormodel Role with keys :admin and :guest, this creates user.admin! and user.guest! (setting the role to admin/guest)
84
+ if model_scopes
85
+ anchormodel_class.all.each do |entry|
86
+ if model_class.respond_to?(entry.key)
87
+ fail("Anchormodel scope #{entry.key} already defined for #{self}, add `model_scopes: false` to `belongs_to_anchormodel :#{attribute_name}`.")
88
+ end
89
+ model_class.scope(entry.key, -> { where(attribute_name => entry.key) })
90
+ end
91
+ end
92
+ end
93
+ end
@@ -1,11 +1,5 @@
1
1
  class Anchormodel
2
2
  module Version
3
- MAJOR = 0
4
- MINOR = 1
5
- PATCH = 2
6
-
7
- EDGE = false
8
-
9
- LABEL = [MAJOR, MINOR, PATCH, EDGE ? 'edge' : nil].compact.join('.')
3
+ LABEL = (Pathname.new(__FILE__).dirname.dirname.dirname / 'VERSION').read
10
4
  end
11
5
  end
data/lib/anchormodel.rb CHANGED
@@ -77,7 +77,8 @@ class Anchormodel
77
77
  end
78
78
  end
79
79
 
80
- require 'anchormodel/active_model_type_value'
80
+ require 'anchormodel/util'
81
+ require 'anchormodel/active_model_type_value_single'
81
82
  require 'anchormodel/attribute'
82
83
  require 'anchormodel/model_mixin'
83
84
  require 'anchormodel/version'
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a single Anchormodel
3
+
4
+ Example:
5
+ bin/rails generate anchormodel Color
6
+
7
+ This will create:
8
+ app/anchormodels/color.rb
@@ -0,0 +1,11 @@
1
+ class AnchormodelGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('templates', __dir__)
3
+
4
+ def add_anchormodel
5
+ fail('NAME must be present.') if name.blank?
6
+ @klass = @name.camelize
7
+ @filename = @name.underscore
8
+
9
+ template 'anchormodel.rb.erb', "app/anchormodels/#{@filename}.rb"
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ class <%= @klass %> < Anchormodel
2
+ new :foo
3
+ end
Binary file
@@ -118,4 +118,23 @@ class UserTest < Minitest::Test
118
118
  u.secondary_role = ''
119
119
  assert_nil u.secondary_role
120
120
  end
121
+
122
+ # Attempting to create a model with an invalid constant name should fail
123
+ def test_invalid_key_update
124
+ assert_raises(RuntimeError) { User.create!(role: :admin, locale: :de, preferred_locale: :invalid) }
125
+ end
126
+
127
+ # Attempting to assign an invalid constant name to a model should fail
128
+ def test_invalid_key_assignment
129
+ assert_raises(RuntimeError) { User.new(role: :invalid) }
130
+ end
131
+
132
+ # An invalid constant name into the DB should raise when reading
133
+ def test_invalid_db_read
134
+ sql = <<~SQL.squish
135
+ INSERT INTO users (role, locale, preferred_locale, created_at, updated_at) VALUES ('invalid', 'de', 'de', 'now', 'now')
136
+ SQL
137
+ ActiveRecord::Base.connection.execute(sql)
138
+ assert_raises(RuntimeError) { User.first.role }
139
+ end
121
140
  end