anchormodel 0.1.2 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +26 -24
- data/README.md +8 -0
- data/VERSION +1 -0
- data/anchormodel.gemspec +16 -31
- data/doc/Anchormodel/Attribute.html +3 -3
- data/doc/Anchormodel/ModelMixin.html +19 -284
- data/doc/Anchormodel/Version.html +4 -60
- data/doc/Anchormodel.html +5 -5
- data/doc/_index.html +25 -5
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +14 -6
- data/doc/frames.html +10 -5
- data/doc/index.html +14 -6
- data/doc/method_list.html +43 -19
- data/doc/top-level-namespace.html +4 -4
- data/lib/anchormodel/{active_model_type_value.rb → active_model_type_value_single.rb} +22 -9
- data/lib/anchormodel/model_mixin.rb +4 -82
- data/lib/anchormodel/util.rb +93 -0
- data/lib/anchormodel/version.rb +1 -7
- data/lib/anchormodel.rb +2 -1
- data/lib/generators/anchormodel/USAGE +8 -0
- data/lib/generators/anchormodel/anchormodel_generator.rb +11 -0
- data/lib/generators/anchormodel/templates/anchormodel.rb.erb +3 -0
- data/pkg/anchormodel-0.1.3.gem +0 -0
- data/test/active_record_model/user_test.rb +19 -0
- metadata +15 -9
data/doc/file.README.html
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
<title>
|
7
7
|
File: README
|
8
8
|
|
9
|
-
— Documentation by YARD 0.9.
|
9
|
+
— 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
|
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.
|
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
|
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
|
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.
|
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.
|
5
|
+
<title>Documentation by YARD 0.9.36</title>
|
6
6
|
</head>
|
7
7
|
<script type="text/javascript">
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
— Documentation by YARD 0.9.
|
9
|
+
— 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
|
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.
|
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
|
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
|
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.
|
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="
|
58
|
-
<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
|
66
|
-
<small>Anchormodel
|
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#
|
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/
|
82
|
-
<small>Anchormodel::
|
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/
|
90
|
-
<small>Anchormodel::
|
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/
|
98
|
-
<small>Anchormodel::
|
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/
|
106
|
-
<small>Anchormodel::
|
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/
|
146
|
-
<small>Anchormodel::
|
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="
|
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="
|
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/
|
186
|
-
<small>Anchormodel::
|
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
|
-
— Documentation by YARD 0.9.
|
9
|
+
— 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
|
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.
|
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::
|
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
|
-
|
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
|
-
#
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
37
|
-
|
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
|
-
# @
|
14
|
-
|
15
|
-
|
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
|
data/lib/anchormodel/version.rb
CHANGED
data/lib/anchormodel.rb
CHANGED
@@ -77,7 +77,8 @@ class Anchormodel
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
require 'anchormodel/
|
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,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
|
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
|