motion_model 0.2.6 → 0.2.8
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/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
|