motion_model 0.2.8 → 0.3.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.
- data/CHANGELOG +23 -4
- data/LICENSE +20 -0
- data/README.md +44 -5
- data/lib/motion_model/ext.rb +15 -19
- data/lib/motion_model/input_helpers.rb +40 -37
- data/lib/motion_model/model/column.rb +12 -7
- data/lib/motion_model/model/model.rb +313 -248
- data/lib/motion_model/model/persistence.rb +1 -1
- data/lib/motion_model/version.rb +1 -1
- data/spec/cascading_delete_spec.rb +100 -0
- data/spec/model_spec.rb +75 -61
- data/spec/relation_spec.rb +32 -25
- metadata +5 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
2012-12-07: Added MIT license file.
|
2
|
+
InputHelpers: Whitespace cleanup. Fixed keyboard show/hide to scroll to correct position.
|
3
|
+
MotionModel::Column: Whitespace cleanup, added code to support cascading delete (:dependent => :destroy)
|
4
|
+
MotionModel::Model: Whitespace cleanup, added code to support cascading destroy, destroy_all, and cascading if specified.
|
5
|
+
relation_spec.rb: removed delete tests into cascading_delete_spec.rb
|
6
|
+
|
7
|
+
2012-12-06: Work on has_many to add cascading delete
|
8
|
+
|
9
|
+
MotionModel: POTENTIAL CODE-BREAKING CHANGE. has_many now takes two arguments
|
10
|
+
only. Previously, it would allow a list of symbols or strings,
|
11
|
+
now it conforms more to the Rails way of one call per relation.
|
12
|
+
E.g.:
|
13
|
+
|
14
|
+
has_many :pets
|
15
|
+
|
16
|
+
-or-
|
17
|
+
|
18
|
+
has_many :pets, :delete => :destroy # cascade delete.
|
19
|
+
|
1
20
|
2012-10-14: Primary New Feature: Notifications
|
2
21
|
|
3
22
|
MotionModel: Added bulk update, which suppresses notifications and added it to delete_all.
|
@@ -17,15 +36,15 @@ to the column metadata.
|
|
17
36
|
|
18
37
|
* Default values have been added to fill in values
|
19
38
|
if not specified in new or create.
|
20
|
-
|
39
|
+
|
21
40
|
2012-09-06: Added block-style finders. Added delete method.
|
22
41
|
|
23
42
|
2012-09-07: IMPORTANT! PLEASE READ! Two new methods were added
|
24
43
|
to MotionModel to support persistence:
|
25
|
-
|
44
|
+
|
26
45
|
Task#serialize_to_file(file_name)
|
27
46
|
Task.deserialize_from_file(file_name)
|
28
|
-
|
47
|
+
|
29
48
|
Note that serialize operates on an instance and
|
30
49
|
deserialize is a class method that creates an
|
31
|
-
instance.
|
50
|
+
instance.
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Steve Ross
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](https://codeclimate.com/github/sxross/MotionModel)
|
2
|
+
|
1
3
|
MotionModel -- Simple Model, Validation, and Input Mixins for RubyMotion
|
2
4
|
================
|
3
5
|
|
@@ -26,6 +28,9 @@ are:
|
|
26
28
|
helpers are certainly not the focus of this release, but
|
27
29
|
I am using these in an app to create Apple-like input forms in
|
28
30
|
static tables.
|
31
|
+
|
32
|
+
MotionModel is MIT licensed, which means you can pretty much do whatever
|
33
|
+
you like with it. See the LICENSE file in this project.
|
29
34
|
|
30
35
|
Getting Going
|
31
36
|
================
|
@@ -237,11 +242,45 @@ Using MotionModel
|
|
237
242
|
Assignee.first.task.name # => "Walk the Dog"
|
238
243
|
```
|
239
244
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
245
|
+
There are four ways to delete objects from your data store:
|
246
|
+
|
247
|
+
* `object.delete #` just deletes the object and ignores all relations
|
248
|
+
* `object.destroy #` deletes the object and honors any cascading declarations
|
249
|
+
* `Class.delete_all #` just deletes all objects of this class and ignores all relations
|
250
|
+
* `Class.destroy_all #` deletes all objects of this class and honors any cascading declarations
|
244
251
|
|
252
|
+
The key to how the `destroy` variants work in how the relation is declared. You can declare:
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
class Task
|
256
|
+
include MotionModel::Model
|
257
|
+
columns :name => :string
|
258
|
+
has_many :assignees
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
and `assignees` will *not be considered* when deleting `Task`s. However, by modifying the `has_many`,
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
has_many :assignees, :dependent => :destroy
|
266
|
+
```
|
267
|
+
|
268
|
+
When you `destroy` an object, all of the objects related to it, and only those related
|
269
|
+
to that object, are also destroyed. So, if you call `task.destroy` and there are 5
|
270
|
+
`assignees` related to that task, they will also be destroyed. Any other `assignees`
|
271
|
+
are left untouched.
|
272
|
+
|
273
|
+
You can also specify:
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
has_many :assignees, :dependent => :delete
|
277
|
+
```
|
278
|
+
|
279
|
+
The difference here is that the cascade stops as the `assignees` are deleted so anything
|
280
|
+
related to the assignees remains intact.
|
281
|
+
|
282
|
+
Note: This syntax is modeled on the Rails `:dependent => :destroy` options in `ActiveRecord`.
|
283
|
+
|
245
284
|
Notifications
|
246
285
|
-------------
|
247
286
|
|
@@ -405,4 +444,4 @@ specs.
|
|
405
444
|
|
406
445
|
Really, for a bug report, even a failing spec or some proposed code is fine. I really want to make
|
407
446
|
this a decent tool for RubyMotion developers who need a straightforward data
|
408
|
-
modeling and persistence framework.
|
447
|
+
modeling and persistence framework.
|
data/lib/motion_model/ext.rb
CHANGED
@@ -52,11 +52,11 @@ end
|
|
52
52
|
# words. It is very much based on the Rails
|
53
53
|
# ActiveSupport implementation or Inflector
|
54
54
|
class Inflector
|
55
|
-
def self.instance
|
55
|
+
def self.instance #nodoc
|
56
56
|
@__instance__ ||= new
|
57
57
|
end
|
58
58
|
|
59
|
-
def initialize
|
59
|
+
def initialize #nodoc
|
60
60
|
reset
|
61
61
|
end
|
62
62
|
|
@@ -123,32 +123,28 @@ class Inflector
|
|
123
123
|
false
|
124
124
|
end
|
125
125
|
|
126
|
-
def
|
126
|
+
def inflect(word, direction) #nodoc
|
127
127
|
return word if uncountable?(word)
|
128
|
-
|
128
|
+
|
129
|
+
subject = word.dup
|
129
130
|
|
130
131
|
@irregulars.each do |rule|
|
131
|
-
return
|
132
|
+
return subject if subject.gsub!(rule.first, rule.last)
|
132
133
|
end
|
133
134
|
|
134
|
-
@singulars
|
135
|
-
|
135
|
+
sense_group = direction == :singularize ? @singulars : @plurals
|
136
|
+
sense_group.each do |rule|
|
137
|
+
return subject if subject.gsub!(rule.first, rule.last)
|
136
138
|
end
|
137
|
-
|
139
|
+
subject
|
138
140
|
end
|
139
141
|
|
140
|
-
def
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
@irregulars.each do |rule|
|
145
|
-
return singular if singular.gsub!(rule.first, rule.last)
|
146
|
-
end
|
142
|
+
def singularize(word)
|
143
|
+
inflect word, :singularize
|
144
|
+
end
|
147
145
|
|
148
|
-
|
149
|
-
|
150
|
-
end
|
151
|
-
singular
|
146
|
+
def pluralize(word)
|
147
|
+
inflect word, :pluralize
|
152
148
|
end
|
153
149
|
end
|
154
150
|
|
@@ -1,24 +1,24 @@
|
|
1
1
|
module MotionModel
|
2
2
|
module InputHelpers
|
3
3
|
class ModelNotSetError < RuntimeError; end
|
4
|
-
|
4
|
+
|
5
5
|
# FieldBindingMap contains a simple label to model
|
6
6
|
# field binding, and is decorated by a tag to be
|
7
7
|
# used on the UI control.
|
8
8
|
class FieldBindingMap
|
9
9
|
attr_accessor :label, :name, :tag
|
10
|
-
|
10
|
+
|
11
11
|
def initialize(options = {})
|
12
12
|
@name = options[:name]
|
13
13
|
@label = options[:label]
|
14
14
|
end
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def self.included(base)
|
18
18
|
base.extend(ClassMethods)
|
19
19
|
base.instance_variable_set('@binding_data', [])
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
module ClassMethods
|
23
23
|
# +field+ is a declarative macro that specifies
|
24
24
|
# the field name (i.e., the model field name)
|
@@ -41,15 +41,15 @@ module MotionModel
|
|
41
41
|
@binding_data << FieldBindingMap.new(:label => label, :name => field)
|
42
42
|
end
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
# +model+ is a mandatory method in which you
|
46
46
|
# specify the instance of the model to which
|
47
47
|
# your fields are bound.
|
48
|
-
|
48
|
+
|
49
49
|
def model(model_instance)
|
50
50
|
@model = model_instance
|
51
51
|
end
|
52
|
-
|
52
|
+
|
53
53
|
# +field_count+ specifies how many fields have
|
54
54
|
# been bound.
|
55
55
|
#
|
@@ -66,10 +66,10 @@ module MotionModel
|
|
66
66
|
# +field_at+ retrieves the field at a given index.
|
67
67
|
#
|
68
68
|
# Usage:
|
69
|
-
#
|
69
|
+
#
|
70
70
|
# field = field_at(indexPath.row)
|
71
71
|
# label_view = subview(UILabel, :label_frame, text: field.label)
|
72
|
-
|
72
|
+
|
73
73
|
def field_at(index)
|
74
74
|
data = self.class.instance_variable_get('@binding_data'.to_sym)
|
75
75
|
data[index].tag = index + 1
|
@@ -86,7 +86,7 @@ module MotionModel
|
|
86
86
|
def value_at(field)
|
87
87
|
@model.send(field.name)
|
88
88
|
end
|
89
|
-
|
89
|
+
|
90
90
|
# +fields+ is the iterator for all fields
|
91
91
|
# mapped for this class.
|
92
92
|
#
|
@@ -95,11 +95,11 @@ module MotionModel
|
|
95
95
|
# fields do |field|
|
96
96
|
# do_something_with field.label, field.value
|
97
97
|
# end
|
98
|
-
|
98
|
+
|
99
99
|
def fields
|
100
100
|
self.class.instance_variable_get('@binding_data'.to_sym).each{|datum| yield datum}
|
101
101
|
end
|
102
|
-
|
102
|
+
|
103
103
|
# +bind+ fetches all mapped fields from
|
104
104
|
# any subview of the current +UIView+
|
105
105
|
# and transfers the contents to the
|
@@ -107,7 +107,7 @@ module MotionModel
|
|
107
107
|
# specified by the +model+ method.
|
108
108
|
def bind
|
109
109
|
raise ModelNotSetError.new("You must set the model before binding it.") unless @model
|
110
|
-
|
110
|
+
|
111
111
|
fields do |field|
|
112
112
|
view_obj = self.view.viewWithTag(field.tag)
|
113
113
|
@model.send("#{field.name}=".to_sym, view_obj.text) if view_obj.respond_to?(:text)
|
@@ -149,7 +149,6 @@ module MotionModel
|
|
149
149
|
animationCurve = notification.userInfo.valueForKey(UIKeyboardAnimationCurveUserInfoKey)
|
150
150
|
animationDuration = notification.userInfo.valueForKey(UIKeyboardAnimationDurationUserInfoKey)
|
151
151
|
keyboardEndRect = notification.userInfo.valueForKey(UIKeyboardFrameEndUserInfoKey)
|
152
|
-
|
153
152
|
keyboardEndRect = view.convertRect(keyboardEndRect.CGRectValue, fromView:App.delegate.window)
|
154
153
|
|
155
154
|
UIView.beginAnimations "changeTableViewContentInset", context:nil
|
@@ -158,28 +157,29 @@ module MotionModel
|
|
158
157
|
|
159
158
|
intersectionOfKeyboardRectAndWindowRect = CGRectIntersection(App.delegate.window.frame, keyboardEndRect)
|
160
159
|
bottomInset = intersectionOfKeyboardRectAndWindowRect.size.height;
|
161
|
-
|
160
|
+
|
162
161
|
@table.contentInset = UIEdgeInsetsMake(0, 0, bottomInset, 0)
|
163
162
|
|
163
|
+
UIView.commitAnimations
|
164
|
+
|
165
|
+
|
166
|
+
@table.scrollToRowAtIndexPath(owner_cell_index_path,
|
167
|
+
atScrollPosition:UITableViewScrollPositionMiddle,
|
168
|
+
animated: true)
|
169
|
+
end
|
170
|
+
|
171
|
+
def owner_cell_index_path
|
164
172
|
# Find active cell
|
165
173
|
indexPathOfOwnerCell = nil
|
166
174
|
numberOfCells = @table.dataSource.tableView(@table, numberOfRowsInSection:0)
|
167
175
|
0.upto(numberOfCells) do |index|
|
168
176
|
indexPath = NSIndexPath.indexPathForRow(index, inSection:0)
|
169
177
|
cell = @table.cellForRowAtIndexPath(indexPath)
|
170
|
-
if
|
171
|
-
indexPathOfOwnerCell = indexPath
|
172
|
-
break
|
173
|
-
end
|
178
|
+
return indexPath if find_first_responder(cell)
|
174
179
|
end
|
175
180
|
|
176
|
-
|
177
|
-
|
178
|
-
if indexPathOfOwnerCell
|
179
|
-
@table.scrollToRowAtIndexPath(indexPathOfOwnerCell,
|
180
|
-
atScrollPosition:UITableViewScrollPositionMiddle,
|
181
|
-
animated: true)
|
182
|
-
end
|
181
|
+
# By default use the first section, first row.
|
182
|
+
NSIndexPath.indexPathForRow 0, inSection: 0
|
183
183
|
end
|
184
184
|
|
185
185
|
# Undo all the rejiggering when the keyboard slides
|
@@ -194,25 +194,28 @@ module MotionModel
|
|
194
194
|
if UIEdgeInsetsEqualToEdgeInsets(@table.contentInset, UIEdgeInsetsZero)
|
195
195
|
return
|
196
196
|
end
|
197
|
-
|
197
|
+
|
198
198
|
animationCurve = notification.userInfo.valueForKey(UIKeyboardAnimationCurveUserInfoKey)
|
199
199
|
animationDuration = notification.userInfo.valueForKey(UIKeyboardAnimationDurationUserInfoKey)
|
200
|
-
|
200
|
+
|
201
201
|
UIView.beginAnimations("changeTableViewContentInset", context:nil)
|
202
202
|
UIView.setAnimationDuration(animationDuration)
|
203
203
|
UIView.setAnimationCurve(animationCurve)
|
204
|
-
|
204
|
+
|
205
205
|
@table.contentInset = UIEdgeInsetsZero;
|
206
|
-
|
207
|
-
UIView.commitAnimations
|
206
|
+
|
207
|
+
UIView.commitAnimations
|
208
208
|
end
|
209
|
-
|
210
|
-
def
|
211
|
-
return
|
212
|
-
|
213
|
-
|
209
|
+
|
210
|
+
def find_first_responder(parent)
|
211
|
+
return parent if parent.isFirstResponder
|
212
|
+
|
213
|
+
parent.subviews.each do |subview|
|
214
|
+
first_responder = find_first_responder(subview)
|
215
|
+
return first_responder if first_responder
|
214
216
|
end
|
215
|
-
|
217
|
+
|
218
|
+
return false
|
216
219
|
end
|
217
220
|
end
|
218
221
|
end
|
@@ -4,20 +4,25 @@ module MotionModel
|
|
4
4
|
attr_accessor :name
|
5
5
|
attr_accessor :type
|
6
6
|
attr_accessor :default
|
7
|
+
attr_accessor :destroy
|
7
8
|
|
8
|
-
def initialize(name = nil, type = nil,
|
9
|
+
def initialize(name = nil, type = nil, options = {})
|
9
10
|
@name = name
|
10
11
|
@type = type
|
11
|
-
@default = default
|
12
|
+
@default = options[:default]
|
13
|
+
@destroy = options[:dependent]
|
12
14
|
end
|
13
|
-
|
14
|
-
|
15
|
+
|
16
|
+
# REVIEW: Dead code?
|
17
|
+
def add_attr(name, type, options)
|
15
18
|
@name = name
|
16
19
|
@type = type
|
17
|
-
@default = default
|
20
|
+
@default = options[:default]
|
21
|
+
@destroy = options[:dependent]
|
18
22
|
end
|
23
|
+
|
19
24
|
alias_method :add_attribute, :add_attr
|
20
|
-
|
25
|
+
|
21
26
|
def classify
|
22
27
|
case @type
|
23
28
|
when :belongs_to
|
@@ -30,4 +35,4 @@ module MotionModel
|
|
30
35
|
end
|
31
36
|
end
|
32
37
|
end
|
33
|
-
end
|
38
|
+
end
|
@@ -28,7 +28,7 @@
|
|
28
28
|
# * :float
|
29
29
|
#
|
30
30
|
# Assuming you have a bunch of tasks in your data store, you can do this:
|
31
|
-
#
|
31
|
+
#
|
32
32
|
# tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week).order(:due_date)
|
33
33
|
#
|
34
34
|
# Partial queries are supported so you can do:
|
@@ -36,13 +36,15 @@
|
|
36
36
|
# tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week)
|
37
37
|
# ordered_tasks_this_week = tasks_this_week.order(:due_date)
|
38
38
|
#
|
39
|
-
|
39
|
+
|
40
40
|
module MotionModel
|
41
41
|
class PersistFileError < Exception; end
|
42
|
-
|
42
|
+
class RelationIsNilError < Exception; end
|
43
|
+
|
43
44
|
module Model
|
44
45
|
def self.included(base)
|
45
|
-
base.extend(
|
46
|
+
base.extend(PrivateClassMethods)
|
47
|
+
base.extend(PublicClassMethods)
|
46
48
|
base.instance_variable_set("@_columns", []) # Columns in model
|
47
49
|
base.instance_variable_set("@_column_hashes", {}) # Hashes to for quick column lookup
|
48
50
|
base.instance_variable_set("@_relations", {}) # relations
|
@@ -50,8 +52,8 @@ module MotionModel
|
|
50
52
|
base.instance_variable_set("@_next_id", 1) # Next assignable id
|
51
53
|
base.instance_variable_set("@_issue_notifications", true) # Next assignable id
|
52
54
|
end
|
53
|
-
|
54
|
-
module
|
55
|
+
|
56
|
+
module PublicClassMethods
|
55
57
|
# Use to do bulk insertion, updating, or deleting without
|
56
58
|
# making repeated calls to a delegate. E.g., when syncing
|
57
59
|
# with an external data source.
|
@@ -60,55 +62,6 @@ module MotionModel
|
|
60
62
|
class_eval &block
|
61
63
|
@_issue_notifications = true
|
62
64
|
end
|
63
|
-
|
64
|
-
def issue_notification(object, info) #nodoc
|
65
|
-
if @_issue_notifications == true && !object.nil?
|
66
|
-
NSNotificationCenter.defaultCenter.postNotificationName('MotionModelDataDidChangeNotification', object: object, userInfo: info)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def define_accessor_methods(name)
|
71
|
-
define_method(name.to_sym) {
|
72
|
-
@data[name]
|
73
|
-
}
|
74
|
-
define_method("#{name}=".to_sym) { |value|
|
75
|
-
@data[name] = cast_to_type(name, value)
|
76
|
-
}
|
77
|
-
end
|
78
|
-
|
79
|
-
def define_belongs_to_methods(name)
|
80
|
-
define_method(name) {
|
81
|
-
col = column_named(name)
|
82
|
-
parent_id = @data[self.class.belongs_to_id(col.name)]
|
83
|
-
col.classify.find(parent_id)
|
84
|
-
}
|
85
|
-
define_method("#{name}=") { |value|
|
86
|
-
col = column_named(name)
|
87
|
-
parent_id = self.class.belongs_to_id(col.name)
|
88
|
-
@data[parent_id.to_sym] = value.to_i
|
89
|
-
}
|
90
|
-
end
|
91
|
-
|
92
|
-
def define_has_many_methods(name)
|
93
|
-
define_method(name) {
|
94
|
-
relation_for(name)
|
95
|
-
}
|
96
|
-
end
|
97
|
-
|
98
|
-
def add_field(name, type, default = nil) #nodoc
|
99
|
-
col = Column.new(name, type, default)
|
100
|
-
@_columns.push col
|
101
|
-
@_column_hashes[col.name.to_sym] = col
|
102
|
-
|
103
|
-
case type
|
104
|
-
when :has_many
|
105
|
-
define_has_many_methods(name)
|
106
|
-
when :belongs_to
|
107
|
-
define_belongs_to_methods(name)
|
108
|
-
else
|
109
|
-
define_accessor_methods(name)
|
110
|
-
end
|
111
|
-
end
|
112
65
|
|
113
66
|
# Macro to define names and types of columns. It can be used in one of
|
114
67
|
# two forms:
|
@@ -120,41 +73,30 @@ module MotionModel
|
|
120
73
|
# Pass a hash of hashes and you can specify defaults such as:
|
121
74
|
#
|
122
75
|
# columns :name => {:type => :string, :default => 'Joe Bob'}, :age => :integer
|
123
|
-
#
|
76
|
+
#
|
124
77
|
# Pass an array, and you create column names, all of which have type +:string+.
|
125
|
-
#
|
78
|
+
#
|
126
79
|
# columns :name, :age, :hobby
|
127
|
-
|
80
|
+
|
128
81
|
def columns(*fields)
|
129
82
|
return @_columns.map{|c| c.name} if fields.empty?
|
130
83
|
|
131
|
-
col = Column.new
|
132
|
-
|
84
|
+
# col = Column.new # REVIEW: Dead code?
|
85
|
+
|
133
86
|
case fields.first
|
134
87
|
when Hash
|
135
|
-
fields
|
136
|
-
|
137
|
-
|
138
|
-
case options
|
139
|
-
when Symbol, String
|
140
|
-
add_field(name, options)
|
141
|
-
when Hash
|
142
|
-
add_field(name, options[:type], options[:default])
|
143
|
-
else
|
144
|
-
raise ArgumentError.new("arguments to fields must be a symbol, a hash, or a hash of hashes.")
|
145
|
-
end
|
146
|
-
end
|
88
|
+
column_from_hash fields
|
89
|
+
when String, Symbol
|
90
|
+
column_from_string_or_sym fields
|
147
91
|
else
|
148
|
-
|
149
|
-
add_field(name, :string)
|
150
|
-
end
|
92
|
+
raise ArgumentError.new("arguments to `columns' must be a symbol, a hash, or a hash of hashes -- was #{fields.first}.")
|
151
93
|
end
|
152
94
|
|
153
95
|
unless self.respond_to?(:id)
|
154
96
|
add_field(:id, :integer)
|
155
97
|
end
|
156
98
|
end
|
157
|
-
|
99
|
+
|
158
100
|
# Use at class level, as follows:
|
159
101
|
#
|
160
102
|
# class Task
|
@@ -174,15 +116,14 @@ module MotionModel
|
|
174
116
|
#
|
175
117
|
# This must be used with a belongs_to macro in the related model class
|
176
118
|
# if you want to be able to access the inverse relation.
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
end
|
119
|
+
|
120
|
+
def has_many(relation, options = {})
|
121
|
+
raise ArgumentError.new("arguments to has_many must be a symbol or string.") unless [Symbol, String].include? relation.class
|
122
|
+
add_field relation, :has_many, options # Relation must be plural
|
182
123
|
end
|
183
|
-
|
184
|
-
def
|
185
|
-
(relation.to_s.underscore + '_id').to_sym
|
124
|
+
|
125
|
+
def generate_belongs_to_id(relation)
|
126
|
+
(relation.to_s.singularize.underscore + '_id').to_sym
|
186
127
|
end
|
187
128
|
|
188
129
|
# Use at class level, as follows
|
@@ -198,54 +139,24 @@ module MotionModel
|
|
198
139
|
# Assignee.find(:assignee_name).like('smith').first.task
|
199
140
|
def belongs_to(relation)
|
200
141
|
add_field relation, :belongs_to
|
201
|
-
add_field
|
202
|
-
end
|
203
|
-
|
204
|
-
# Returns a column denoted by +name+
|
205
|
-
def column_named(name)
|
206
|
-
@_column_hashes[name.to_sym]
|
207
|
-
end
|
208
|
-
|
209
|
-
# Returns next available id
|
210
|
-
def next_id #nodoc
|
211
|
-
@_next_id
|
212
|
-
end
|
213
|
-
|
214
|
-
# Sets next available id
|
215
|
-
def next_id=(value)
|
216
|
-
@_next_id = value
|
217
|
-
end
|
218
|
-
|
219
|
-
# Increments next available id
|
220
|
-
def increment_id #nodoc
|
221
|
-
@_next_id += 1
|
142
|
+
add_field generate_belongs_to_id(relation), :belongs_to_id # a relation is singular.
|
222
143
|
end
|
223
144
|
|
224
145
|
# Returns true if a column exists on this model, otherwise false.
|
225
146
|
def column?(column)
|
226
147
|
respond_to?(column)
|
227
148
|
end
|
228
|
-
|
149
|
+
|
229
150
|
# Returns type of this column.
|
230
151
|
def type(column)
|
231
152
|
column_named(column).type || nil
|
232
153
|
end
|
233
|
-
|
154
|
+
|
234
155
|
# returns default value for this column or nil.
|
235
156
|
def default(column)
|
236
157
|
column_named(column).default || nil
|
237
158
|
end
|
238
|
-
|
239
|
-
def has_relation?(col)
|
240
|
-
col = case col
|
241
|
-
when MotionModel::Model::Column
|
242
|
-
column_named(col.name)
|
243
|
-
else
|
244
|
-
column_named(col)
|
245
|
-
end
|
246
|
-
col.type == :has_many || col.type == :belongs_to
|
247
|
-
end
|
248
|
-
|
159
|
+
|
249
160
|
# Creates an object and saves it. E.g.:
|
250
161
|
#
|
251
162
|
# @bob = Person.create(:name => 'Bob', :hobby => 'Bird Watching')
|
@@ -255,20 +166,22 @@ module MotionModel
|
|
255
166
|
row = self.new(options)
|
256
167
|
row.before_create if row.respond_to?(:before_create)
|
257
168
|
row.before_save if row.respond_to?(:before_save)
|
258
|
-
|
169
|
+
|
259
170
|
# TODO: Check for Validatable and if it's
|
260
171
|
# present, check valid? before saving.
|
261
172
|
|
262
173
|
row.save
|
263
174
|
row
|
264
175
|
end
|
265
|
-
|
176
|
+
|
266
177
|
def length
|
267
178
|
@collection.length
|
268
179
|
end
|
269
180
|
alias_method :count, :length
|
270
181
|
|
271
|
-
#
|
182
|
+
# Deletes all rows in the model -- no hooks are called and
|
183
|
+
# deletes are not cascading so this does not affected related
|
184
|
+
# data.
|
272
185
|
def delete_all
|
273
186
|
# Do each delete so any on_delete and
|
274
187
|
# cascades are called, then empty the
|
@@ -278,7 +191,19 @@ module MotionModel
|
|
278
191
|
end
|
279
192
|
@collection = []
|
280
193
|
@_next_id = 1
|
281
|
-
|
194
|
+
end
|
195
|
+
|
196
|
+
# Destroys all rows in the model -- before_delete and after_delete
|
197
|
+
# hooks are called and deletes are not cascading if declared with
|
198
|
+
# :delete => destroy in the has_many macro.
|
199
|
+
def destroy_all
|
200
|
+
ids = self.all.map{|item| item.id}
|
201
|
+
bulk_update do
|
202
|
+
ids.each do |item|
|
203
|
+
find(item).destroy
|
204
|
+
end
|
205
|
+
end
|
206
|
+
# Note collection is not emptied, and next_id is not reset.
|
282
207
|
end
|
283
208
|
|
284
209
|
# Finds row(s) within the data store. E.g.,
|
@@ -295,226 +220,366 @@ module MotionModel
|
|
295
220
|
end.compact
|
296
221
|
return FinderQuery.new(matches)
|
297
222
|
end
|
298
|
-
|
223
|
+
|
299
224
|
unless args[0].is_a?(Symbol) || args[0].is_a?(String)
|
300
|
-
|
225
|
+
target_id = args[0].to_i
|
226
|
+
return @collection.select{|element| element.id == target_id}.first
|
301
227
|
end
|
302
|
-
|
228
|
+
|
303
229
|
FinderQuery.new(args[0].to_sym, @collection)
|
304
230
|
end
|
305
231
|
alias_method :where, :find
|
306
|
-
|
232
|
+
|
307
233
|
# Retrieves first row of query
|
308
234
|
def first
|
309
235
|
@collection.first
|
310
236
|
end
|
311
|
-
|
237
|
+
|
312
238
|
# Retrieves last row of query
|
313
239
|
def last
|
314
240
|
@collection.last
|
315
241
|
end
|
316
|
-
|
242
|
+
|
317
243
|
# Returns query result as an array
|
318
244
|
def all
|
319
245
|
@collection
|
320
246
|
end
|
321
|
-
|
247
|
+
|
322
248
|
def order(field_name = nil, &block)
|
323
249
|
FinderQuery.new(@collection).order(field_name, &block)
|
324
250
|
end
|
325
|
-
|
251
|
+
|
326
252
|
def each(&block)
|
327
253
|
raise ArgumentError.new("each requires a block") unless block_given?
|
328
254
|
@collection.each{|item| yield item}
|
329
|
-
end
|
330
|
-
|
255
|
+
end
|
256
|
+
|
331
257
|
def empty?
|
332
258
|
@collection.empty?
|
333
259
|
end
|
334
260
|
end
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
#
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
261
|
+
|
262
|
+
module PrivateClassMethods
|
263
|
+
# This populates a column from something like:
|
264
|
+
#
|
265
|
+
# columns :name => :string, :age => :integer
|
266
|
+
#
|
267
|
+
# or
|
268
|
+
#
|
269
|
+
# columns :name => {:type => :string, :default => 'Joe Bob'}, :age => :integer
|
270
|
+
|
271
|
+
def column_from_hash(hash) #nodoc
|
272
|
+
hash.first.each_pair do |name, options|
|
273
|
+
raise ArgumentError.new("you cannot use `description' as a column name because of a conflict with Cocoa.") if name.to_s == 'description'
|
274
|
+
|
275
|
+
case options
|
276
|
+
when Symbol, String
|
277
|
+
add_field(name, options)
|
278
|
+
when Hash
|
279
|
+
add_field(name, options[:type], :default => options[:default])
|
280
|
+
else
|
281
|
+
raise ArgumentError.new("arguments to `columns' must be a symbol, a hash, or a hash of hashes.")
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# This populates a column from something like:
|
287
|
+
#
|
288
|
+
# columns :name, :age, :hobby
|
289
|
+
|
290
|
+
def column_from_string_or_sym(string) #nodoc
|
291
|
+
string.each do |name|
|
292
|
+
add_field(name.to_sym, :string)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def issue_notification(object, info) #nodoc
|
297
|
+
if @_issue_notifications == true && !object.nil?
|
298
|
+
NSNotificationCenter.defaultCenter.postNotificationName('MotionModelDataDidChangeNotification', object: object, userInfo: info)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
def define_accessor_methods(name) #nodoc
|
303
|
+
define_method(name.to_sym) {
|
304
|
+
@data[name]
|
305
|
+
}
|
306
|
+
define_method("#{name}=".to_sym) { |value|
|
307
|
+
@data[name] = cast_to_type(name, value)
|
308
|
+
@dirty = true
|
309
|
+
}
|
350
310
|
end
|
351
|
-
|
311
|
+
|
312
|
+
def define_belongs_to_methods(name) #nodoc
|
313
|
+
define_method(name) {
|
314
|
+
col = column_named(name)
|
315
|
+
parent_id = @data[self.class.generate_belongs_to_id(col.name)]
|
316
|
+
col.classify.find(parent_id)
|
317
|
+
}
|
318
|
+
define_method("#{name}=") { |value|
|
319
|
+
col = column_named(name)
|
320
|
+
parent_id = self.class.generate_belongs_to_id(col.name)
|
321
|
+
@data[parent_id.to_sym] = value.to_i
|
322
|
+
}
|
323
|
+
end
|
324
|
+
|
325
|
+
def define_has_many_methods(name) #nodoc
|
326
|
+
define_method(name) {
|
327
|
+
relation_for(name)
|
328
|
+
}
|
329
|
+
end
|
330
|
+
|
331
|
+
def add_field(name, type, options = {:default => nil}) #nodoc
|
332
|
+
col = Column.new(name, type, options)
|
333
|
+
|
334
|
+
@_columns.push col
|
335
|
+
@_column_hashes[col.name.to_sym] = col
|
336
|
+
|
337
|
+
case type
|
338
|
+
when :has_many then define_has_many_methods(name)
|
339
|
+
when :belongs_to then define_belongs_to_methods(name)
|
340
|
+
else
|
341
|
+
define_accessor_methods(name)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# Returns a column denoted by +name+
|
346
|
+
def column_named(name) #nodoc
|
347
|
+
@_column_hashes[name.to_sym]
|
348
|
+
end
|
349
|
+
|
350
|
+
# Returns next available id
|
351
|
+
def next_id #nodoc
|
352
|
+
@_next_id
|
353
|
+
end
|
354
|
+
|
355
|
+
# Sets next available id
|
356
|
+
def next_id=(value) #nodoc
|
357
|
+
@_next_id = value
|
358
|
+
end
|
359
|
+
|
360
|
+
# Increments next available id
|
361
|
+
def increment_id #nodoc
|
362
|
+
@_next_id += 1
|
363
|
+
end
|
364
|
+
|
365
|
+
def has_relation?(col) #nodoc
|
366
|
+
return false if col.nil?
|
367
|
+
|
368
|
+
col = case col
|
369
|
+
when MotionModel::Model::Column
|
370
|
+
column_named(col.name)
|
371
|
+
else
|
372
|
+
column_named(col)
|
373
|
+
end
|
374
|
+
col.type == :has_many || col.type == :belongs_to
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
379
|
+
def initialize(options = {})
|
380
|
+
@data ||= {}
|
381
|
+
|
382
|
+
assign_id options
|
352
383
|
|
353
384
|
columns.each do |col|
|
354
|
-
unless
|
355
|
-
|
356
|
-
cast_value = cast_to_type(col, options[col])
|
357
|
-
@data[col] = cast_value
|
385
|
+
unless relation_column?(col) # all data columns
|
386
|
+
initialize_data_columns col, options
|
358
387
|
else
|
359
|
-
if column_named(col).type == :belongs_to_id
|
360
|
-
@data[col] = options[col]
|
361
|
-
end
|
388
|
+
@data[col] = options[col] if column_named(col).type == :belongs_to_id
|
362
389
|
end
|
363
390
|
end
|
364
|
-
|
365
|
-
dirty = true
|
391
|
+
|
392
|
+
@dirty = true
|
366
393
|
end
|
367
394
|
|
395
|
+
# Default to_i implementation returns value of id column, much as
|
396
|
+
# in Rails.
|
397
|
+
|
368
398
|
def to_i
|
369
399
|
@data[:id].to_i
|
370
400
|
end
|
371
401
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
return_value = arg
|
376
|
-
|
377
|
-
case type(column_name)
|
378
|
-
when :string
|
379
|
-
return_value = arg.to_s
|
380
|
-
when :int, :integer, :belongs_to_id
|
381
|
-
return_value = arg.is_a?(Integer) ? arg : arg.to_i
|
382
|
-
when :float, :double
|
383
|
-
return_value = arg.is_a?(Float) ? arg : arg.to_f
|
384
|
-
when :date
|
385
|
-
return arg if arg.is_a?(NSDate)
|
386
|
-
return_value = NSDate.dateWithNaturalLanguageString(arg, locale:NSUserDefaults.standardUserDefaults.dictionaryRepresentation)
|
387
|
-
else
|
388
|
-
raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
389
|
-
end
|
390
|
-
return_value
|
391
|
-
end
|
392
|
-
|
402
|
+
# Default to_s implementation returns a list of columns and values
|
403
|
+
# separated by newlines.
|
393
404
|
def to_s
|
394
405
|
columns.each{|c| "#{c}: #{self.send(c)}\n"}
|
395
406
|
end
|
396
|
-
|
407
|
+
|
408
|
+
# Save current object. Speaking from the context of relational
|
409
|
+
# databases, this inserts a row if it's a new one, or updates
|
410
|
+
# in place if not.
|
397
411
|
def save
|
398
|
-
collection = self.class.instance_variable_get('@collection')
|
399
412
|
@dirty = false
|
400
|
-
|
413
|
+
|
401
414
|
# Existing object implies update in place
|
402
415
|
# TODO: Optimize location of existing id
|
403
416
|
action = 'add'
|
404
417
|
if obj = collection.find{|o| o.id == @data[:id]}
|
405
|
-
|
418
|
+
obj = self
|
406
419
|
action = 'update'
|
407
420
|
else
|
408
421
|
collection << self
|
409
422
|
end
|
410
423
|
self.class.issue_notification(self, :action => action)
|
411
424
|
end
|
412
|
-
|
425
|
+
|
426
|
+
# Deletes the current object. The object can still be used.
|
427
|
+
|
413
428
|
def delete
|
414
|
-
collection = self.class.instance_variable_get('@collection')
|
415
|
-
|
416
429
|
target_index = collection.index{|item| item.id == self.id}
|
417
430
|
collection.delete_at(target_index)
|
418
431
|
self.class.issue_notification(self, :action => 'delete')
|
419
432
|
end
|
420
433
|
|
434
|
+
# Destroys the current object. The difference between delete
|
435
|
+
# and destroy is that destroy calls <tt>before_delete</tt>
|
436
|
+
# and <tt>after_delete</tt> hooks. As well, it will cascade
|
437
|
+
# into related objects, deleting them if they are related
|
438
|
+
# using <tt>:delete => :destroy</tt> in the <tt>has_many</tt>
|
439
|
+
# declaration
|
440
|
+
def destroy
|
441
|
+
before_delete if respond_to? :before_delete
|
442
|
+
has_many_columns.each do |col|
|
443
|
+
delete_candidates = self.send(col.name)
|
444
|
+
|
445
|
+
delete_candidates.each do |candidate|
|
446
|
+
candidate.delete if col.destroy == :delete
|
447
|
+
candidate.destroy if col.destroy == :destroy
|
448
|
+
end
|
449
|
+
end
|
450
|
+
delete
|
451
|
+
after_delete if respond_to? :after_delete
|
452
|
+
end
|
453
|
+
|
454
|
+
# Undelete does pretty much as its name implies. However,
|
455
|
+
# the natural sort order is not preserved. IMPORTANT: If
|
456
|
+
# you are trying to undo a cascading delete, this will not
|
457
|
+
# work. It only undeletes the object you still own.
|
458
|
+
|
459
|
+
def undelete
|
460
|
+
collection << self
|
461
|
+
self.class.issue_notification(self, :action => 'add')
|
462
|
+
end
|
463
|
+
|
464
|
+
# Count of objects in the current collection
|
421
465
|
def length
|
422
|
-
|
466
|
+
collection.length
|
423
467
|
end
|
424
|
-
|
425
468
|
alias_method :count, :length
|
426
|
-
|
427
|
-
|
428
|
-
|
469
|
+
|
470
|
+
# True if the column exists, otherwise false
|
471
|
+
def column?(column_name)
|
472
|
+
self.class.column?(column_name.to_sym)
|
429
473
|
end
|
430
474
|
|
475
|
+
# Returns list of column names as an array
|
431
476
|
def columns
|
432
477
|
self.class.columns
|
433
478
|
end
|
434
479
|
|
435
|
-
|
436
|
-
|
480
|
+
# Type of a given column
|
481
|
+
def type(column_name)
|
482
|
+
self.class.type(column_name)
|
437
483
|
end
|
438
484
|
|
439
|
-
|
440
|
-
|
441
|
-
end
|
442
|
-
|
443
|
-
# Modify respond_to? to add model's attributes.
|
485
|
+
# True if this object responds to the method or
|
486
|
+
# property, otherwise false.
|
444
487
|
alias_method :old_respond_to?, :respond_to?
|
445
488
|
def respond_to?(method)
|
446
489
|
column_named(method) || old_respond_to?(method)
|
447
490
|
end
|
448
|
-
|
491
|
+
|
449
492
|
def dirty?
|
450
|
-
@dirty
|
493
|
+
@dirty
|
451
494
|
end
|
452
|
-
|
453
|
-
|
454
|
-
|
495
|
+
|
496
|
+
|
497
|
+
private
|
498
|
+
|
499
|
+
def assign_id(options) #nodoc
|
500
|
+
unless options[:id]
|
501
|
+
options[:id] = self.class.next_id
|
502
|
+
else
|
503
|
+
self.class.next_id = [options[:id].to_i, self.class.next_id].max
|
504
|
+
end
|
505
|
+
self.class.increment_id
|
506
|
+
end
|
507
|
+
|
508
|
+
def relation_column?(column) #nodoc
|
509
|
+
[:belongs_to, :belongs_to_id, :has_many].include? column_named(column).type
|
510
|
+
end
|
511
|
+
|
512
|
+
def initialize_data_columns(column, options) #nodoc
|
513
|
+
options[column] ||= self.class.default(column)
|
514
|
+
cast_value = cast_to_type(column, options[column])
|
515
|
+
@data[column] = cast_value
|
516
|
+
end
|
517
|
+
|
518
|
+
def cast_to_type(column_name, arg) #nodoc
|
519
|
+
return nil if arg.nil?
|
520
|
+
|
521
|
+
return case type(column_name)
|
522
|
+
when :string then arg.to_s
|
523
|
+
when :int, :integer, :belongs_to_id
|
524
|
+
arg.is_a?(Integer) ? arg : arg.to_i
|
525
|
+
when :float, :double
|
526
|
+
arg.is_a?(Float) ? arg : arg.to_f
|
527
|
+
when :date
|
528
|
+
arg.is_a?(NSDate) ? arg : NSDate.dateWithNaturalLanguageString(arg, locale:NSUserDefaults.standardUserDefaults.dictionaryRepresentation)
|
529
|
+
else
|
530
|
+
raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def collection #nodoc
|
535
|
+
self.class.instance_variable_get('@collection')
|
536
|
+
end
|
537
|
+
|
538
|
+
def column_named(name) #nodoc
|
539
|
+
self.class.column_named(name.to_sym)
|
540
|
+
end
|
541
|
+
|
542
|
+
def has_many_columns
|
543
|
+
columns.map{|col| column_named(col)}.select{|col| col.type == :has_many}
|
544
|
+
end
|
545
|
+
|
546
|
+
def generate_belongs_to_id(class_or_column) # nodoc
|
547
|
+
self.class.generate_belongs_to_id(self.class)
|
548
|
+
end
|
549
|
+
|
550
|
+
def relation_for(col) # nodoc
|
455
551
|
col = column_named(col)
|
552
|
+
related_klass = col.classify
|
553
|
+
|
456
554
|
case col.type
|
457
555
|
when :belongs_to
|
458
|
-
|
459
|
-
@data.send( # and look inside it to find the
|
460
|
-
:[], :id # parent element that the current
|
461
|
-
) # object belongs to.
|
462
|
-
)
|
463
|
-
result
|
556
|
+
related_klass.find(@data[:id])
|
464
557
|
when :has_many
|
465
|
-
|
466
|
-
|
467
|
-
# For has_many to work, the finder query needs the
|
468
|
-
# actual object, and the class of the relation
|
469
|
-
result = col.classify.find(belongs_to_id).belongs_to(self, col.classify).eq( # find all elements of belongs_to
|
470
|
-
@data.send(:[], :id) # class that have the ID of this element Task.find(:assignee_id).eq(3)
|
471
|
-
)
|
472
|
-
result
|
558
|
+
related_klass.find(generate_belongs_to_id(self.class)).belongs_to(self, related_klass).eq(@data[:id])
|
473
559
|
else
|
474
560
|
nil
|
475
561
|
end
|
476
562
|
end
|
477
|
-
|
563
|
+
|
564
|
+
|
478
565
|
# Handle attribute retrieval
|
479
|
-
#
|
566
|
+
#
|
480
567
|
# Gets and sets work as expected, and type casting occurs
|
481
568
|
# For example:
|
482
|
-
#
|
569
|
+
#
|
483
570
|
# Task.date = '2012-09-15'
|
484
|
-
#
|
571
|
+
#
|
485
572
|
# This creates a real Date object in the data store.
|
486
|
-
#
|
573
|
+
#
|
487
574
|
# date = Task.date
|
488
|
-
#
|
575
|
+
#
|
489
576
|
# Date is a real date object.
|
490
|
-
def method_missing(method, *args, &block)
|
491
|
-
|
492
|
-
|
493
|
-
raise NoMethodError.new("nil column #{method} accessed from #{caller[1]}.") if col.nil?
|
494
|
-
|
495
|
-
unless col.type == :belongs_to_id
|
496
|
-
Debug.error "method missing for #{base_method}"
|
497
|
-
has_relation = relation_for(col) if self.class.has_relation?(col)
|
498
|
-
return has_relation if has_relation
|
499
|
-
end
|
500
|
-
|
501
|
-
unless col.nil?
|
502
|
-
if method.to_s.include?('=')
|
503
|
-
@dirty = true
|
504
|
-
return @data[base_method] = col.type == :belongs_to_id ? args[0] : self.cast_to_type(base_method, args[0])
|
505
|
-
else
|
506
|
-
return @data[base_method]
|
507
|
-
end
|
577
|
+
def method_missing(method, *args, &block) #nodoc
|
578
|
+
if self.respond_to? method
|
579
|
+
return method(args, &block)
|
508
580
|
else
|
509
|
-
|
510
|
-
method: #{method}
|
511
|
-
args: #{args.inspect}
|
512
|
-
in: #{self.class.name}
|
513
|
-
ERRORINFO
|
514
|
-
)
|
515
|
-
raise exception
|
581
|
+
raise NoMethodError.new("nil column #{self.class}##{method} accessed from #{caller[1]}.")
|
516
582
|
end
|
517
583
|
end
|
518
|
-
|
519
584
|
end
|
520
585
|
end
|