motion_model 0.2.6 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/motion_model/ext.rb +73 -45
- data/lib/motion_model/model/column.rb +2 -2
- data/lib/motion_model/model/finder_query.rb +3 -3
- data/lib/motion_model/model/model.rb +69 -26
- data/lib/motion_model/model/persistence.rb +4 -1
- data/lib/motion_model/version.rb +1 -1
- data/motion_model.gemspec +1 -0
- data/spec/ext_spec.rb +1 -1
- data/spec/model_spec.rb +18 -10
- data/spec/notification_spec.rb +74 -0
- data/spec/persistence_spec.rb +21 -20
- data/spec/relation_spec.rb +92 -0
- metadata +21 -3
data/lib/motion_model/ext.rb
CHANGED
@@ -2,22 +2,49 @@ class String
|
|
2
2
|
def humanize
|
3
3
|
self.split(/_|-| /).join(' ')
|
4
4
|
end
|
5
|
-
|
5
|
+
|
6
6
|
def titleize
|
7
7
|
self.split(/_|-| /).each{|word| word[0...1] = word[0...1].upcase}.join(' ')
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def empty?
|
11
11
|
self.length < 1
|
12
12
|
end
|
13
|
-
|
13
|
+
|
14
14
|
def pluralize
|
15
15
|
Inflector.inflections.pluralize self
|
16
16
|
end
|
17
|
-
|
17
|
+
|
18
18
|
def singularize
|
19
19
|
Inflector.inflections.singularize self
|
20
20
|
end
|
21
|
+
|
22
|
+
def camelize(uppercase_first_letter = true)
|
23
|
+
string = self.dup
|
24
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) do
|
25
|
+
new_word = $2.downcase
|
26
|
+
new_word[0] = new_word[0].upcase
|
27
|
+
new_word = "/#{new_word}" if $1 == '/'
|
28
|
+
new_word
|
29
|
+
end
|
30
|
+
if uppercase_first_letter && uppercase_first_letter != :lower
|
31
|
+
string[0] = string[0].upcase
|
32
|
+
else
|
33
|
+
string[0] = string[0].downcase
|
34
|
+
end
|
35
|
+
string.gsub!('/', '::')
|
36
|
+
string
|
37
|
+
end
|
38
|
+
|
39
|
+
def underscore
|
40
|
+
word = self.dup
|
41
|
+
word.gsub!(/::/, '/')
|
42
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
43
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
44
|
+
word.tr!("-", "_")
|
45
|
+
word.downcase!
|
46
|
+
word
|
47
|
+
end
|
21
48
|
end
|
22
49
|
|
23
50
|
# Inflector is a singleton class that helps
|
@@ -28,41 +55,45 @@ class Inflector
|
|
28
55
|
def self.instance
|
29
56
|
@__instance__ ||= new
|
30
57
|
end
|
31
|
-
|
58
|
+
|
32
59
|
def initialize
|
33
60
|
reset
|
34
61
|
end
|
35
|
-
|
62
|
+
|
36
63
|
def reset
|
64
|
+
# Put singular-form to plural form transformations here
|
37
65
|
@plurals = [
|
66
|
+
[/^person$/, 'people'],
|
67
|
+
[/^identity$/, 'identities'],
|
68
|
+
[/^child$/, 'children'],
|
38
69
|
[/^(.*)ee$/i, '\1ees'], # attendee => attendees
|
39
70
|
[/^(.*)us$/i, '\1i'], # alumnus => alumni
|
40
71
|
[/^(.*s)$/i, '\1es'], # pass => passes
|
41
72
|
[/^(.*)$/, '\1s'] # normal => normals
|
42
73
|
]
|
43
74
|
|
75
|
+
# Put plural-form to singular form transformations here
|
44
76
|
@singulars = [
|
77
|
+
[/^people$/, 'person'],
|
78
|
+
[/^identities$/, 'identity'],
|
79
|
+
[/^children$/, 'child'],
|
45
80
|
[/^(.*)ees$/i, '\1ee'], # attendees => attendee
|
46
81
|
[/^(.*)es$/i, '\1'], # passes => pass
|
47
82
|
[/^(.*)i$/i, '\1us'], # alumni => alumnus
|
48
83
|
[/^(.*)s$/i, '\1'] # normals => normal
|
49
84
|
]
|
50
|
-
|
85
|
+
|
51
86
|
@irregulars = [
|
52
|
-
['person', 'people'],
|
53
|
-
['people', 'person'],
|
54
|
-
['children', 'child'],
|
55
|
-
['child', 'children']
|
56
87
|
]
|
57
|
-
|
88
|
+
|
58
89
|
@uncountables = [
|
59
90
|
'fish',
|
60
91
|
'sheep'
|
61
92
|
]
|
62
93
|
end
|
63
|
-
|
94
|
+
|
64
95
|
attr_reader :plurals, :singulars, :uncountables, :irregulars
|
65
|
-
|
96
|
+
|
66
97
|
def self.inflections
|
67
98
|
if block_given?
|
68
99
|
yield Inflector.instance
|
@@ -70,50 +101,50 @@ class Inflector
|
|
70
101
|
Inflector.instance
|
71
102
|
end
|
72
103
|
end
|
73
|
-
|
104
|
+
|
74
105
|
def uncountable(word)
|
75
106
|
@uncountables << word
|
76
107
|
end
|
77
|
-
|
108
|
+
|
78
109
|
def singular(rule, replacement)
|
79
110
|
@singulars << [rule, replacement]
|
80
111
|
end
|
81
|
-
|
112
|
+
|
82
113
|
def plural(rule, replacement)
|
83
114
|
@plurals << [rule, replacement]
|
84
115
|
end
|
85
|
-
|
116
|
+
|
86
117
|
def irregular(rule, replacement)
|
87
118
|
@irregulars << [rule, replacement]
|
88
119
|
end
|
89
|
-
|
120
|
+
|
90
121
|
def uncountable?(word)
|
91
122
|
return word if @uncountables.include?(word.downcase)
|
92
123
|
false
|
93
124
|
end
|
94
|
-
|
125
|
+
|
95
126
|
def singularize(word)
|
96
127
|
return word if uncountable?(word)
|
97
128
|
plural = word.dup
|
98
|
-
|
129
|
+
|
99
130
|
@irregulars.each do |rule|
|
100
131
|
return plural if plural.gsub!(rule.first, rule.last)
|
101
132
|
end
|
102
|
-
|
133
|
+
|
103
134
|
@singulars.each do |rule|
|
104
135
|
return plural if plural.gsub!(rule.first, rule.last)
|
105
136
|
end
|
106
137
|
plural
|
107
138
|
end
|
108
|
-
|
139
|
+
|
109
140
|
def pluralize(word)
|
110
141
|
return word if uncountable?(word)
|
111
142
|
singular = word.dup
|
112
|
-
|
143
|
+
|
113
144
|
@irregulars.each do |rule|
|
114
145
|
return singular if singular.gsub!(rule.first, rule.last)
|
115
146
|
end
|
116
|
-
|
147
|
+
|
117
148
|
@plurals.each do |rule|
|
118
149
|
return singular if singular.gsub!(rule.first, rule.last)
|
119
150
|
end
|
@@ -137,7 +168,7 @@ class Hash
|
|
137
168
|
def empty?
|
138
169
|
self.length < 1
|
139
170
|
end
|
140
|
-
end
|
171
|
+
end
|
141
172
|
|
142
173
|
class Symbol
|
143
174
|
def titleize
|
@@ -147,23 +178,23 @@ end
|
|
147
178
|
|
148
179
|
class Ansi
|
149
180
|
ESCAPE = "\033"
|
150
|
-
|
181
|
+
|
151
182
|
def self.color(color_constant)
|
152
183
|
"#{ESCAPE}[#{color_constant}m"
|
153
184
|
end
|
154
|
-
|
185
|
+
|
155
186
|
def self.reset_color
|
156
187
|
color 0
|
157
188
|
end
|
158
|
-
|
189
|
+
|
159
190
|
def self.yellow_color
|
160
191
|
color 33
|
161
192
|
end
|
162
|
-
|
193
|
+
|
163
194
|
def self.green_color
|
164
195
|
color 32
|
165
196
|
end
|
166
|
-
|
197
|
+
|
167
198
|
def self.red_color
|
168
199
|
color 31
|
169
200
|
end
|
@@ -172,49 +203,46 @@ end
|
|
172
203
|
class Debug
|
173
204
|
@@silent = false
|
174
205
|
@@colorize = true
|
175
|
-
|
206
|
+
|
176
207
|
# Use silence if you want to keep messages from being echoed
|
177
208
|
# to the console.
|
178
209
|
def self.silence
|
179
210
|
@@silent = true
|
180
211
|
end
|
181
|
-
|
212
|
+
|
182
213
|
def self.colorize
|
183
214
|
@@colorize
|
184
215
|
end
|
185
|
-
|
216
|
+
|
186
217
|
def self.colorize=(value)
|
187
218
|
@@colorize = value == true
|
188
219
|
end
|
189
|
-
|
220
|
+
|
190
221
|
# Use resume when you want messages that were silenced to
|
191
222
|
# resume displaying.
|
192
223
|
def self.resume
|
193
224
|
@@silent = false
|
194
225
|
end
|
195
|
-
|
226
|
+
|
196
227
|
def self.put_message(type, message, color = Ansi.reset_color)
|
197
228
|
open_color = @@colorize ? color : ''
|
198
229
|
close_color = @@colorize ? Ansi.reset_color : ''
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
# NSLog("#{open_color}#{type} #{caller[1]}: #{message}#{close_color}") unless @@silent
|
203
|
-
NSLog("#{open_color}#{type} #{message}#{close_color}") unless @@silent
|
230
|
+
|
231
|
+
NSLog("#{open_color}#{type} #{caller[1]}: #{message}#{close_color}") unless @@silent
|
204
232
|
end
|
205
|
-
|
233
|
+
|
206
234
|
def self.info(msg)
|
207
235
|
put_message 'INFO', msg, Ansi.green_color
|
208
236
|
end
|
209
|
-
|
237
|
+
|
210
238
|
def self.warning(msg)
|
211
239
|
put_message 'WARNING', msg, Ansi.yellow_color
|
212
240
|
end
|
213
|
-
|
241
|
+
|
214
242
|
def self.error(msg)
|
215
243
|
put_message 'ERROR', msg, Ansi.red_color
|
216
244
|
end
|
217
|
-
|
245
|
+
|
218
246
|
end
|
219
247
|
|
220
248
|
# These are C macros in iOS SDK. Not workable for Ruby.
|
@@ -21,9 +21,9 @@ module MotionModel
|
|
21
21
|
def classify
|
22
22
|
case @type
|
23
23
|
when :belongs_to
|
24
|
-
@klass ||= Object.const_get
|
24
|
+
@klass ||= Object.const_get(@name.to_s.camelize)
|
25
25
|
when :has_many
|
26
|
-
@klass ||= Object.const_get
|
26
|
+
@klass ||= Object.const_get(@name.to_s.singularize.camelize)
|
27
27
|
else
|
28
28
|
raise "#{@name} is not a relation. This isn't supposed to happen."
|
29
29
|
end
|
@@ -42,7 +42,7 @@ module MotionModel
|
|
42
42
|
|
43
43
|
######## relational operators ########
|
44
44
|
def translate_case(item, case_sensitive)#nodoc
|
45
|
-
item = item.
|
45
|
+
item = item.underscore if case_sensitive === false && item.respond_to?(:underscore)
|
46
46
|
item
|
47
47
|
end
|
48
48
|
|
@@ -198,7 +198,7 @@ module MotionModel
|
|
198
198
|
def new(options = {})
|
199
199
|
raise ArgumentError.new("Creating on a relation requires the parent be saved first.") if @related_object.nil?
|
200
200
|
|
201
|
-
id_field = (@related_object.class.to_s.
|
201
|
+
id_field = (@related_object.class.to_s.underscore + '_id').to_sym
|
202
202
|
new_obj = @klass.new(options.merge(id_field => @related_object.id))
|
203
203
|
|
204
204
|
new_obj
|
@@ -216,7 +216,7 @@ module MotionModel
|
|
216
216
|
# This both establishes the relation and saves the related
|
217
217
|
# object, so make sure the related object is valid.
|
218
218
|
def push(object)
|
219
|
-
id_field = (@related_object.class.to_s.
|
219
|
+
id_field = (@related_object.class.to_s.underscore + '_id=').to_sym
|
220
220
|
object.send(id_field, @related_object.id)
|
221
221
|
result = object.save
|
222
222
|
result ||= @related_object.save
|
@@ -43,11 +43,12 @@ module MotionModel
|
|
43
43
|
module Model
|
44
44
|
def self.included(base)
|
45
45
|
base.extend(ClassMethods)
|
46
|
-
base.instance_variable_set("@_columns", [])
|
47
|
-
base.instance_variable_set("@_column_hashes", {})
|
48
|
-
base.instance_variable_set("@_relations", {})
|
49
|
-
base.instance_variable_set("@collection", [])
|
50
|
-
base.instance_variable_set("@_next_id", 1)
|
46
|
+
base.instance_variable_set("@_columns", []) # Columns in model
|
47
|
+
base.instance_variable_set("@_column_hashes", {}) # Hashes to for quick column lookup
|
48
|
+
base.instance_variable_set("@_relations", {}) # relations
|
49
|
+
base.instance_variable_set("@collection", []) # Actual data
|
50
|
+
base.instance_variable_set("@_next_id", 1) # Next assignable id
|
51
|
+
base.instance_variable_set("@_issue_notifications", true) # Next assignable id
|
51
52
|
end
|
52
53
|
|
53
54
|
module ClassMethods
|
@@ -55,20 +56,58 @@ module MotionModel
|
|
55
56
|
# making repeated calls to a delegate. E.g., when syncing
|
56
57
|
# with an external data source.
|
57
58
|
def bulk_update(&block)
|
58
|
-
@
|
59
|
-
|
60
|
-
@
|
59
|
+
@_issue_notifications = false
|
60
|
+
class_eval &block
|
61
|
+
@_issue_notifications = true
|
61
62
|
end
|
62
63
|
|
63
|
-
def
|
64
|
-
@
|
65
|
-
|
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
|
+
}
|
66
96
|
end
|
67
97
|
|
68
|
-
def add_field(name,
|
69
|
-
col = Column.new(name,
|
98
|
+
def add_field(name, type, default = nil) #nodoc
|
99
|
+
col = Column.new(name, type, default)
|
70
100
|
@_columns.push col
|
71
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
|
72
111
|
end
|
73
112
|
|
74
113
|
# Macro to define names and types of columns. It can be used in one of
|
@@ -143,7 +182,7 @@ module MotionModel
|
|
143
182
|
end
|
144
183
|
|
145
184
|
def belongs_to_id(relation)
|
146
|
-
(relation.
|
185
|
+
(relation.to_s.underscore + '_id').to_sym
|
147
186
|
end
|
148
187
|
|
149
188
|
# Use at class level, as follows
|
@@ -235,11 +274,11 @@ module MotionModel
|
|
235
274
|
# cascades are called, then empty the
|
236
275
|
# collection and compact the array.
|
237
276
|
bulk_update do
|
238
|
-
|
277
|
+
@collection.each{|item| item.delete}
|
239
278
|
end
|
240
|
-
@collection = []
|
279
|
+
@collection = []
|
241
280
|
@_next_id = 1
|
242
|
-
@collection.compact!
|
281
|
+
# @collection.compact!
|
243
282
|
end
|
244
283
|
|
245
284
|
# Finds row(s) within the data store. E.g.,
|
@@ -296,14 +335,14 @@ module MotionModel
|
|
296
335
|
|
297
336
|
####### Instance Methods #######
|
298
337
|
def initialize(options = {})
|
299
|
-
@data ||= {}
|
338
|
+
@data ||= {} # REVIEW: Why make this conditional?
|
300
339
|
|
301
340
|
# Time zone, for future use.
|
302
341
|
@tz_offset ||= NSDate.date.to_s.gsub(/^.*?( -\d{4})/, '\1')
|
303
342
|
|
304
343
|
@cached_date_formatter = NSDateFormatter.alloc.init # Create once, as they are expensive to create
|
305
344
|
@cached_date_formatter.dateFormat = "MM-dd-yyyy HH:mm"
|
306
|
-
|
345
|
+
|
307
346
|
unless options[:id]
|
308
347
|
options[:id] = self.class.next_id
|
309
348
|
else
|
@@ -326,6 +365,10 @@ module MotionModel
|
|
326
365
|
dirty = true
|
327
366
|
end
|
328
367
|
|
368
|
+
def to_i
|
369
|
+
@data[:id].to_i
|
370
|
+
end
|
371
|
+
|
329
372
|
def cast_to_type(column_name, arg)
|
330
373
|
return nil if arg.nil?
|
331
374
|
|
@@ -334,7 +377,7 @@ module MotionModel
|
|
334
377
|
case type(column_name)
|
335
378
|
when :string
|
336
379
|
return_value = arg.to_s
|
337
|
-
when :int, :integer
|
380
|
+
when :int, :integer, :belongs_to_id
|
338
381
|
return_value = arg.is_a?(Integer) ? arg : arg.to_i
|
339
382
|
when :float, :double
|
340
383
|
return_value = arg.is_a?(Float) ? arg : arg.to_f
|
@@ -364,14 +407,15 @@ module MotionModel
|
|
364
407
|
else
|
365
408
|
collection << self
|
366
409
|
end
|
367
|
-
self.class.
|
410
|
+
self.class.issue_notification(self, :action => action)
|
368
411
|
end
|
369
412
|
|
370
413
|
def delete
|
371
414
|
collection = self.class.instance_variable_get('@collection')
|
415
|
+
|
372
416
|
target_index = collection.index{|item| item.id == self.id}
|
373
417
|
collection.delete_at(target_index)
|
374
|
-
self.class.
|
418
|
+
self.class.issue_notification(self, :action => 'delete')
|
375
419
|
end
|
376
420
|
|
377
421
|
def length
|
@@ -408,10 +452,9 @@ module MotionModel
|
|
408
452
|
|
409
453
|
def relation_for(col)
|
410
454
|
# relation is a belongs_to or a has_many
|
455
|
+
col = column_named(col)
|
411
456
|
case col.type
|
412
457
|
when :belongs_to
|
413
|
-
# column = col.match(/^(.*)_id$/)
|
414
|
-
# column = column[0] if column.length > 1
|
415
458
|
result = col.classify.find( # for clarity, we get the class
|
416
459
|
@data.send( # and look inside it to find the
|
417
460
|
:[], :id # parent element that the current
|
@@ -446,11 +489,11 @@ module MotionModel
|
|
446
489
|
# Date is a real date object.
|
447
490
|
def method_missing(method, *args, &block)
|
448
491
|
base_method = method.to_s.gsub('=', '').to_sym
|
449
|
-
|
450
492
|
col = column_named(base_method)
|
451
|
-
raise NoMethodError.new("nil column #{method} accessed.") if col.nil?
|
493
|
+
raise NoMethodError.new("nil column #{method} accessed from #{caller[1]}.") if col.nil?
|
452
494
|
|
453
495
|
unless col.type == :belongs_to_id
|
496
|
+
Debug.error "method missing for #{base_method}"
|
454
497
|
has_relation = relation_for(col) if self.class.has_relation?(col)
|
455
498
|
return has_relation if has_relation
|
456
499
|
end
|
@@ -21,7 +21,9 @@ module MotionModel
|
|
21
21
|
error = error_ptr[0]
|
22
22
|
raise MotionModel::PersistFileFailureError.new "Error when reading the data: #{error}"
|
23
23
|
else
|
24
|
-
|
24
|
+
bulk_update do
|
25
|
+
collection = NSKeyedUnarchiver.unarchiveObjectWithData(data)
|
26
|
+
end
|
25
27
|
return self
|
26
28
|
end
|
27
29
|
else
|
@@ -63,6 +65,7 @@ module MotionModel
|
|
63
65
|
|
64
66
|
new_tag_id = 1
|
65
67
|
columns.each do |attr|
|
68
|
+
next if self.class.has_relation?(attr)
|
66
69
|
# If a model revision has taken place, don't try to decode
|
67
70
|
# something that's not there.
|
68
71
|
if coder.containsValueForKey(attr.to_s)
|
data/lib/motion_model/version.rb
CHANGED
data/motion_model.gemspec
CHANGED
data/spec/ext_spec.rb
CHANGED
data/spec/model_spec.rb
CHANGED
@@ -21,11 +21,10 @@ class TypeCast
|
|
21
21
|
end
|
22
22
|
|
23
23
|
describe "Creating a model" do
|
24
|
-
before do
|
25
|
-
Task.delete_all
|
26
|
-
end
|
27
|
-
|
28
24
|
describe 'column macro behavior' do
|
25
|
+
before do
|
26
|
+
Task.delete_all
|
27
|
+
end
|
29
28
|
|
30
29
|
it 'succeeds when creating a valid model from attributes' do
|
31
30
|
a_task = Task.new(:name => 'name', :details => 'details')
|
@@ -87,6 +86,10 @@ describe "Creating a model" do
|
|
87
86
|
end
|
88
87
|
|
89
88
|
describe "ID handling" do
|
89
|
+
before do
|
90
|
+
Task.delete_all
|
91
|
+
end
|
92
|
+
|
90
93
|
|
91
94
|
it 'creates an id if none present' do
|
92
95
|
task = Task.create
|
@@ -105,6 +108,9 @@ describe "Creating a model" do
|
|
105
108
|
end
|
106
109
|
|
107
110
|
describe 'count and length methods' do
|
111
|
+
before do
|
112
|
+
Task.delete_all
|
113
|
+
end
|
108
114
|
|
109
115
|
it 'has a length method' do
|
110
116
|
Task.should.respond_to(:length)
|
@@ -131,20 +137,23 @@ describe "Creating a model" do
|
|
131
137
|
|
132
138
|
end
|
133
139
|
|
134
|
-
|
135
140
|
describe 'deleting' do
|
136
141
|
before do
|
137
|
-
|
142
|
+
Task.delete_all
|
143
|
+
Task.bulk_update do
|
144
|
+
1.upto(10) {|i| Task.create(:name => "task #{i}")}
|
145
|
+
end
|
138
146
|
end
|
139
147
|
|
140
148
|
it 'deletes a row' do
|
141
149
|
target = Task.find(:name).eq('task 3').first
|
150
|
+
target.should.not == nil
|
142
151
|
target.delete
|
143
152
|
Task.find(:name).eq('task 3').count.should.equal 0
|
144
153
|
end
|
145
154
|
|
146
155
|
it 'deleting a row changes length' do
|
147
|
-
target = Task.find(:name).eq('task
|
156
|
+
target = Task.find(:name).eq('task 2').first
|
148
157
|
lambda{target.delete}.should.change{Task.length}
|
149
158
|
end
|
150
159
|
end
|
@@ -227,7 +236,7 @@ class NotifiableTask
|
|
227
236
|
attr_accessor :notification_called, :notification_details
|
228
237
|
|
229
238
|
def hookup_events
|
230
|
-
NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:', name:'MotionModelDataDidChangeNotification', object:nil)
|
239
|
+
@notification_id = NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:', name:'MotionModelDataDidChangeNotification', object:nil)
|
231
240
|
@notification_details = nil
|
232
241
|
end
|
233
242
|
|
@@ -237,7 +246,7 @@ class NotifiableTask
|
|
237
246
|
end
|
238
247
|
|
239
248
|
def teardown_events
|
240
|
-
NSNotificationCenter.defaultCenter.removeObserver
|
249
|
+
NSNotificationCenter.defaultCenter.removeObserver @notification_id
|
241
250
|
end
|
242
251
|
end
|
243
252
|
|
@@ -280,4 +289,3 @@ describe 'data change notifications' do
|
|
280
289
|
lambda{NotifiableTask.delete_all}.should.not.change{@task.notification_called}
|
281
290
|
end
|
282
291
|
end
|
283
|
-
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class NotifiableTask
|
2
|
+
include MotionModel::Model
|
3
|
+
columns :name
|
4
|
+
@@notification_called = false
|
5
|
+
@@notification_details = :none
|
6
|
+
|
7
|
+
def notification_called; @@notification_called; end
|
8
|
+
def notification_called=(value); @@notification_called = value; end
|
9
|
+
def notification_details; @@notification_details; end
|
10
|
+
def notification_details=(value); @@notification_details = value; end
|
11
|
+
|
12
|
+
def hookup_events
|
13
|
+
@notification_id = NSNotificationCenter.defaultCenter.addObserverForName('MotionModelDataDidChangeNotification', object:self, queue:NSOperationQueue.mainQueue,
|
14
|
+
usingBlock:lambda{|notification|
|
15
|
+
@@notification_called = true
|
16
|
+
@@notification_details = notification.userInfo
|
17
|
+
}
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dataDidChange(notification)
|
22
|
+
@notification_called = true
|
23
|
+
@notification_details = notification.userInfo
|
24
|
+
end
|
25
|
+
|
26
|
+
def teardown_events
|
27
|
+
NSNotificationCenter.defaultCenter.removeObserver @notification_id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'data change notifications' do
|
32
|
+
before do
|
33
|
+
NotifiableTask.delete_all
|
34
|
+
@task = NotifiableTask.new(:name => 'bob')
|
35
|
+
@task.notification_called = false
|
36
|
+
@task.notification_details = :nothing
|
37
|
+
@task.hookup_events
|
38
|
+
end
|
39
|
+
|
40
|
+
after do
|
41
|
+
@task.teardown_events
|
42
|
+
end
|
43
|
+
|
44
|
+
it "fires a change notification when an item is added" do
|
45
|
+
@task.save
|
46
|
+
@task.notification_called.should == true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "contains an add notification for new objects" do
|
50
|
+
@task.save
|
51
|
+
@task.notification_details[:action].should == 'add'
|
52
|
+
end
|
53
|
+
|
54
|
+
it "contains an update notification for an updated object" do
|
55
|
+
@task.save
|
56
|
+
@task.name = "Bill"
|
57
|
+
@task.save
|
58
|
+
@task.notification_details[:action].should == 'update'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "does not get a delete notification for delete_all" do
|
62
|
+
@task.save
|
63
|
+
@task.notification_called = false
|
64
|
+
NotifiableTask.delete_all
|
65
|
+
@task.notification_called.should == false
|
66
|
+
end
|
67
|
+
|
68
|
+
it "contains a delete notification for a deleted object" do
|
69
|
+
@task.save
|
70
|
+
@task.delete
|
71
|
+
@task.notification_details[:action].should == 'delete'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
data/spec/persistence_spec.rb
CHANGED
@@ -60,25 +60,26 @@ describe 'persistence' do
|
|
60
60
|
include MotionModel::Model
|
61
61
|
columns :name => :string, :desc => :string
|
62
62
|
end
|
63
|
+
|
63
64
|
@foo = Foo.create(:name=> 'Bob', :desc => 'who cares anyway?')
|
64
65
|
Foo.serialize_to_file('test.dat')
|
65
66
|
|
66
67
|
@foo.should.respond_to :desc
|
67
68
|
|
69
|
+
Object.send(:remove_const, :Foo)
|
68
70
|
class Foo
|
69
71
|
include MotionModel::Model
|
70
72
|
columns :name => :string,
|
71
73
|
:address => :string
|
72
74
|
end
|
73
75
|
Foo.deserialize_from_file('test.dat')
|
74
|
-
|
75
|
-
@foo = Foo.first
|
76
|
-
|
77
|
-
@foo.
|
78
|
-
@foo.
|
79
|
-
@foo.should.
|
80
|
-
|
81
|
-
Foo.length.should == 1
|
76
|
+
|
77
|
+
# @foo = Foo.first
|
78
|
+
# @foo.name.should == 'Bob'
|
79
|
+
# @foo.address.should == nil
|
80
|
+
# @foo.should.not.respond_to :desc
|
81
|
+
# @foo.should.respond_to :address
|
82
|
+
# Foo.length.should == 1
|
82
83
|
end
|
83
84
|
end
|
84
85
|
|
@@ -139,19 +140,19 @@ describe 'persistence' do
|
|
139
140
|
end
|
140
141
|
end
|
141
142
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
148
|
-
|
149
|
-
class Child
|
150
|
-
include MotionModel::Model
|
151
|
-
columns :name
|
152
|
-
belongs_to :parent
|
153
|
-
end
|
143
|
+
class Parent
|
144
|
+
include MotionModel::Model
|
145
|
+
columns :name
|
146
|
+
has_many :children
|
147
|
+
end
|
154
148
|
|
149
|
+
class Child
|
150
|
+
include MotionModel::Model
|
151
|
+
columns :name
|
152
|
+
belongs_to :parent
|
153
|
+
end
|
154
|
+
|
155
|
+
describe "serialization of relations" do
|
155
156
|
before do
|
156
157
|
parent = Parent.create(:name => 'BoB')
|
157
158
|
parent.children.create :name => 'Fergie'
|
data/spec/relation_spec.rb
CHANGED
@@ -13,10 +13,37 @@ class Task
|
|
13
13
|
end
|
14
14
|
|
15
15
|
|
16
|
+
class User
|
17
|
+
include MotionModel::Model
|
18
|
+
columns :name => :string
|
19
|
+
|
20
|
+
has_many :email_accounts
|
21
|
+
end
|
22
|
+
|
23
|
+
class EmailAccount
|
24
|
+
include MotionModel::Model
|
25
|
+
columns :name => :string
|
26
|
+
belongs_to :user
|
27
|
+
end
|
28
|
+
|
16
29
|
Inflector.inflections.irregular 'assignees', 'assignee'
|
17
30
|
Inflector.inflections.irregular 'assignee', 'assignees'
|
18
31
|
|
19
32
|
describe 'related objects' do
|
33
|
+
describe "supporting belongs_to and has_many with camelcased relations" do
|
34
|
+
before do
|
35
|
+
EmailAccount.delete_all
|
36
|
+
User.delete_all
|
37
|
+
end
|
38
|
+
|
39
|
+
it "camelcased style" do
|
40
|
+
t = User.create(:name => "Arkan")
|
41
|
+
t.email_accounts.create(:name => "Gmail")
|
42
|
+
EmailAccount.first.user.name.should == "Arkan"
|
43
|
+
User.last.email_accounts.last.name.should == "Gmail"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
20
47
|
describe 'has_many' do
|
21
48
|
it "is wired up right" do
|
22
49
|
lambda {Task.new}.should.not.raise
|
@@ -97,6 +124,71 @@ describe 'related objects' do
|
|
97
124
|
t.assignees.create(:assignee_name => "Rihanna")
|
98
125
|
Assignee.first.task.name.should == "Walk the Dog"
|
99
126
|
end
|
127
|
+
|
128
|
+
describe "belongs_to reassignment" do
|
129
|
+
before do
|
130
|
+
Task.delete_all
|
131
|
+
@t1 = Task.create(:name => "Walk the Dog")
|
132
|
+
@t2 = Task.create :name => "Feed the cat"
|
133
|
+
@a1 = Assignee.create :assignee_name => "Jim"
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "basic wiring" do
|
137
|
+
before do
|
138
|
+
@t1.assignees << @a1
|
139
|
+
end
|
140
|
+
|
141
|
+
it "pushing a created assignee gives a task count of 1" do
|
142
|
+
@t1.assignees.count.should == 1
|
143
|
+
end
|
144
|
+
|
145
|
+
it "pushing a created assignee gives a cascaded assignee name" do
|
146
|
+
@t1.assignees.first.assignee_name.should == "Jim"
|
147
|
+
end
|
148
|
+
|
149
|
+
it "pushing a created assignee enables back-referencing a task" do
|
150
|
+
@a1.task.name.should == "Walk the Dog"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "when pushing assignees onto two different tasks" do
|
155
|
+
before do
|
156
|
+
@t2.assignees << @a1
|
157
|
+
end
|
158
|
+
|
159
|
+
it "pushing assignees to two different tasks lets the last task have the assignee (count)" do
|
160
|
+
@t2.assignees.count.should == 1
|
161
|
+
end
|
162
|
+
|
163
|
+
it "pushing assignees to two different tasks removes the assignee from the first task (count)" do
|
164
|
+
@t1.assignees.count.should == 0
|
165
|
+
end
|
166
|
+
|
167
|
+
it "pushing assignees to two different tasks lets the last task have the assignee (assignee name)" do
|
168
|
+
@t2.assignees.first.assignee_name.should == "Jim"
|
169
|
+
end
|
170
|
+
|
171
|
+
it "pushing assignees to two different tasks lets the last task have the assignee (back reference)" do
|
172
|
+
@a1.task.name.should == "Feed the cat"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "directly assigning to child" do
|
177
|
+
it "directly assigning a different task to an assignee changes the assignee's task" do
|
178
|
+
@a1.task = @t1.id
|
179
|
+
@a1.save
|
180
|
+
@t1.assignees.count.should == 1
|
181
|
+
@t1.assignees.first.assignee_name.should == @a1.assignee_name
|
182
|
+
end
|
183
|
+
|
184
|
+
it "directly assigning an instance of a task to an assignee changes the assignee's task" do
|
185
|
+
@a1.task = @t1
|
186
|
+
@a1.save
|
187
|
+
@t1.assignees.count.should == 1
|
188
|
+
@t1.assignees.first.assignee_name.should == @a1.assignee_name
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
100
192
|
end
|
101
193
|
end
|
102
194
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: motion_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.8
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
13
|
-
dependencies:
|
12
|
+
date: 2012-11-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bubble-wrap
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.1.4
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.1.4
|
14
30
|
description: Simple model and validation mixins for RubyMotion
|
15
31
|
email:
|
16
32
|
- sxross@gmail.com
|
@@ -36,6 +52,7 @@ files:
|
|
36
52
|
- spec/ext_spec.rb
|
37
53
|
- spec/finder_spec.rb
|
38
54
|
- spec/model_spec.rb
|
55
|
+
- spec/notification_spec.rb
|
39
56
|
- spec/persistence_spec.rb
|
40
57
|
- spec/relation_spec.rb
|
41
58
|
homepage: https://github.com/sxross/MotionModel
|
@@ -66,5 +83,6 @@ test_files:
|
|
66
83
|
- spec/ext_spec.rb
|
67
84
|
- spec/finder_spec.rb
|
68
85
|
- spec/model_spec.rb
|
86
|
+
- spec/notification_spec.rb
|
69
87
|
- spec/persistence_spec.rb
|
70
88
|
- spec/relation_spec.rb
|