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.
@@ -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
- ### It appears that RubyMotion does not support caller backtrace yet.
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 @name.to_s.downcase.capitalize
24
+ @klass ||= Object.const_get(@name.to_s.camelize)
25
25
  when :has_many
26
- @klass ||= Object.const_get @name.to_s.downcase.singularize.capitalize
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.downcase if case_sensitive === false && item.respond_to?(:downcase)
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.downcase + '_id').to_sym
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.downcase + '_id=').to_sym
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", []) # 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
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
- @call_delegate_method = false
59
- yield
60
- @call_delegate_method = true
59
+ @_issue_notifications = false
60
+ class_eval &block
61
+ @_issue_notifications = true
61
62
  end
62
63
 
63
- def call_delegate(object, info) #nodoc
64
- @call_delegate_method = true if @call_delegate_method.nil?
65
- NSNotificationCenter.defaultCenter.postNotificationName('MotionModelDataDidChangeNotification', object: object, userInfo: info) if @call_delegate_method && !object.nil?
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, options, default = nil) #nodoc
69
- col = Column.new(name, options, default)
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.downcase + '_id').to_sym
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
- self.each{|item| item.delete}
277
+ @collection.each{|item| item.delete}
239
278
  end
240
- @collection = [] # TODO: Handle cascading or let GC take care of it.
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.call_delegate(self, :action => action)
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.call_delegate(self, :action => 'delete')
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
- collection = NSKeyedUnarchiver.unarchiveObjectWithData(data)
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)
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.2.6"
2
+ VERSION = "0.2.8"
3
3
  end
data/motion_model.gemspec CHANGED
@@ -12,5 +12,6 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
13
  gem.name = "motion_model"
14
14
  gem.require_paths = ["lib"]
15
+ gem.add_dependency 'bubble-wrap', '~> 1.1.4'
15
16
  gem.version = MotionModel::VERSION
16
17
  end
data/spec/ext_spec.rb CHANGED
@@ -41,7 +41,7 @@ describe 'Extensions' do
41
41
  end
42
42
 
43
43
  it "handles person to people pluralizing" do
44
- Inflector.inflections.singularize('person').should == 'people'
44
+ Inflector.inflections.pluralize('person').should == 'people'
45
45
  end
46
46
  end
47
47
 
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
- 10.times {|i| Task.create(:name => "task #{i}")}
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 3').first
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 self
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
+
@@ -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.name.should == 'Bob'
78
- @foo.address.should == nil
79
- @foo.should.not.respond_to :desc
80
- @foo.should.respond_to :address
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
- describe "serialization of relations" do
143
- class Parent
144
- include MotionModel::Model
145
- columns :name
146
- has_many :children
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'
@@ -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.6
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-10-24 00:00:00.000000000 Z
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