motion_model 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 2012-10-14: Primary New Feature: Notifications
2
+
3
+ MotionModel: Added bulk update, which suppresses notifications and added it to delete_all.
4
+ MotionModel: Added notifications of type MotionModelDataDidChangeNotification on data change.
5
+ MotionModel: Added classification code to save to differentiate between save-new and update
6
+ MotionModel: Added notification calls to save and delete
7
+
8
+
1
9
  2012-09-05: Basically rewrote how the data is stored.
2
10
 
3
11
  The API remains consistent, but a certain amount of
data/README.md CHANGED
@@ -133,12 +133,13 @@ already present, it's left alone. If it's missing, then it is created for you.
133
133
  Each row id is guaranteed to be unique, so you can use this when communicating
134
134
  with a server or syncing your rowset to a UITableView.
135
135
 
136
- Things That Work
136
+ Using MotionModel
137
137
  -----------------
138
138
 
139
- * Models, in general, work. They aren't ultra full-featured, but more is in the
140
- works. In particular, finders are just coming online. All column data may be
141
- accessed by member name, e.g., `@task.name`.
139
+ * Your data in a model is accessed in a very ActiveRecord (or Railsey) way.
140
+ This should make transitioning from Rails or any ORM that follows the
141
+ ActiveRecord pattern pretty easy. Some of the finder syntactic sugar is
142
+ similar to that of Sequel or DataMapper.
142
143
 
143
144
  * Finders are implemented using chaining. Here is an examples:
144
145
 
@@ -169,6 +170,13 @@ Things That Work
169
170
  @tasks = Task.order{|one, two| two.details <=> one.details}.all # Get tasks ordered descending by :details
170
171
  ```
171
172
 
173
+ You can implement some aggregate functions using map/reduce:
174
+
175
+ ```ruby
176
+ @task.all.map{|task| task.number_of_items}.reduce(:+) # implements sum
177
+ @task.all.map{|task| task.number_of_items}.reduce(:+) / @task.count #implements average
178
+ ```
179
+
172
180
  * Serialization is part of MotionModel. So, in your `AppDelegate` you might do something like this:
173
181
 
174
182
  ```ruby
@@ -181,16 +189,18 @@ Things That Work
181
189
  Task.serialize_to_file('tasks.dat')
182
190
  end
183
191
  ```
192
+ After the first serialize or deserialize, your model will remember the file
193
+ name so you can call these methods without the filename argument.
184
194
 
185
- Note that the this serialization of any arbitrarily complex set of relations
195
+ Implementation note: that the this serialization of any arbitrarily complex set of relations
186
196
  is automatically handled by `NSCoder` provided you conform to the coding
187
- protocol. When you declare your columns, `MotionModel` understands how to
188
- serialize your data so you need take no further action.
197
+ protocol (which MotionModel does). When you declare your columns, `MotionModel` understands how to
198
+ serialize your data so you need take no specific action.
189
199
 
190
200
  **Warning**: As of this release, persistence will serialize only one
191
- model at a time and not your entire data store. This will be fixed next.
201
+ model at a time and not your entire data store.
192
202
 
193
- * Relations now are usable, although not complete fleshed out:
203
+ * Relations
194
204
 
195
205
  ```ruby
196
206
  class Task
@@ -230,10 +240,86 @@ Things That Work
230
240
  At this point, there are a few methods that need to be added
231
241
  for relations, and they will.
232
242
 
233
- * delete
234
243
  * destroy
244
+
245
+ Notifications
246
+ -------------
235
247
 
236
- * Core extensions work. The following are supplied:
248
+ Notifications are issued on object save, update, and delete. They work like this:
249
+
250
+ ```ruby
251
+ def viewDidAppear(animated)
252
+ super
253
+ # other stuff here to set up your view
254
+
255
+ NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:', name:'MotionModelDataDidChangeNotification', object:nil)
256
+ end
257
+
258
+ def viewWillDisappear(animated)
259
+ super
260
+ NSNotificationCenter.defaultCenter.removeObserver self
261
+ end
262
+
263
+ # ... more stuff ...
264
+
265
+ def dataDidChange(notification)
266
+ # code to update or refresh your view based on the object passed back
267
+ # and the userInfo. userInfo keys are:
268
+ # action
269
+ # 'add'
270
+ # 'update'
271
+ # 'delete'
272
+ end
273
+ ```
274
+
275
+ In your dataDidChange notification handler, you can respond to the `'MotionModelDataDidChangeNotification'` notification any way you like,
276
+ but in the instance of a tableView, you might want to use the id of the object passed back to locate
277
+ the correct row in the table and act upon it instead of doing a wholesale `reloadData`.
278
+
279
+ Note that if you do a delete_all, no notifications are issued because there is no single object
280
+ on which to report. You pretty much know what you need to do: Refresh your view.
281
+
282
+ This is implemented as a notification and not a delegate so you can dispatch something
283
+ like a remote synch operation but still be confident you will be updating the UI only on the main thread.
284
+ MotionModel does not currently send notification messages that differentiate by class, so if your
285
+ UI presents `Task`s and you get a notification that an `Assignee` has changed:
286
+
287
+ ```ruby
288
+ class Task
289
+ include MotionModel::Model
290
+ has_many :assignees
291
+ # etc
292
+ end
293
+
294
+ class Assignee
295
+ include MotionModel::Model
296
+ belongs_to :task
297
+ # etc
298
+ end
299
+
300
+ # ...
301
+
302
+ task = Task.create :name => 'Walk the dog' # Triggers notification with a task object
303
+ task.assignees.create :name => 'Adam' # Triggers notification with an assignee object
304
+
305
+ # ...
306
+
307
+ # We set up observers for `MotionModelDataDidChangeNotification` someplace and:
308
+ def dataDidChange(notification)
309
+ if notification.object is_a?(Task)
310
+ # Update our UI
311
+ else
312
+ # This notification is not for us because
313
+ # We don't display anything other than tasks
314
+ end
315
+ ```
316
+
317
+ The above example implies you are only presenting, say, a list of tasks in the current
318
+ view. If, however, you are presenting a list of tasks along with their assignees and
319
+ the assignees could change as a result of a background sync, then your code could and
320
+ should recognize the change to assignee objects.
321
+
322
+ Core Extensions
237
323
 
238
324
  - String#humanize
239
325
  - String#titleize
@@ -245,7 +331,8 @@ Things That Work
245
331
  - Hash#empty?
246
332
  - Symbol#titleize
247
333
 
248
- Also in the extensions is a debug class to log stuff to the console.
334
+ Also in the extensions is a `Debug` class to log stuff to the console.
335
+ It uses NSLog so you will have a separate copy in your application log.
249
336
  This may be preferable to `puts` just because it's easier to spot in
250
337
  your code and it gives you the exact level and file/line number of the
251
338
  info/warning/error in your console output:
@@ -289,7 +376,6 @@ Things That Work
289
376
  Things In The Pipeline
290
377
  ----------------------
291
378
 
292
- - More robust id assignment
293
379
  - Adding validations and custom validations
294
380
 
295
381
  Problems/Comments
@@ -304,8 +390,8 @@ one).
304
390
  Then be sure references to motion_model are commented out or removed from your Gemfile
305
391
  and/or Rakefile and put this in your Rakefile:
306
392
 
307
- ```
308
- require "~/github/local//MotionModel/lib/motion_model.rb"
393
+ ```ruby
394
+ require "~/github/local/MotionModel/lib/motion_model.rb"
309
395
  ```
310
396
 
311
397
  The `~/github/local` is where I cloned it, but you can put it anyplace. Next, make
@@ -314,9 +400,9 @@ sure you are following the project on GitHub so you know when there are changes.
314
400
  Submissions/Patches
315
401
  ------------------
316
402
 
317
- Obviously, the ideal one is a pull request from your own fork, complete with passing
403
+ Obviously, the ideal patch request is really a pull request from your own fork, complete with passing
318
404
  specs.
319
405
 
320
- Really, even a failing spec or some proposed code is fine. I really want to make
406
+ Really, for a bug report, even a failing spec or some proposed code is fine. I really want to make
321
407
  this a decent tool for RubyMotion developers who need a straightforward data
322
408
  modeling and persistence framework.
@@ -51,6 +51,20 @@ module MotionModel
51
51
  end
52
52
 
53
53
  module ClassMethods
54
+ # Use to do bulk insertion, updating, or deleting without
55
+ # making repeated calls to a delegate. E.g., when syncing
56
+ # with an external data source.
57
+ def bulk_update(&block)
58
+ @call_delegate_method = false
59
+ yield
60
+ @call_delegate_method = true
61
+ end
62
+
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?
66
+ end
67
+
54
68
  def add_field(name, options, default = nil) #nodoc
55
69
  col = Column.new(name, options, default)
56
70
  @_columns.push col
@@ -217,6 +231,12 @@ module MotionModel
217
231
 
218
232
  # Empties the entire store.
219
233
  def delete_all
234
+ # Do each delete so any on_delete and
235
+ # cascades are called, then empty the
236
+ # collection and compact the array.
237
+ bulk_update do
238
+ self.each{|item| item.delete}
239
+ end
220
240
  @collection = [] # TODO: Handle cascading or let GC take care of it.
221
241
  @_next_id = 1
222
242
  @collection.compact!
@@ -286,10 +306,10 @@ module MotionModel
286
306
 
287
307
  unless options[:id]
288
308
  options[:id] = self.class.next_id
289
- self.class.increment_id
290
309
  else
291
310
  self.class.next_id = [options[:id].to_i, self.class.next_id].max
292
311
  end
312
+ self.class.increment_id
293
313
 
294
314
  columns.each do |col|
295
315
  unless [:belongs_to, :belongs_to_id, :has_many].include? column_named(col).type
@@ -330,24 +350,28 @@ module MotionModel
330
350
  def to_s
331
351
  columns.each{|c| "#{c}: #{self.send(c)}\n"}
332
352
  end
333
-
353
+
334
354
  def save
335
355
  collection = self.class.instance_variable_get('@collection')
336
356
  @dirty = false
337
357
 
338
358
  # Existing object implies update in place
339
359
  # TODO: Optimize location of existing id
360
+ action = 'add'
340
361
  if obj = collection.find{|o| o.id == @data[:id]}
341
- obj = self
362
+ collection = self
363
+ action = 'update'
342
364
  else
343
365
  collection << self
344
366
  end
367
+ self.class.call_delegate(self, :action => action)
345
368
  end
346
369
 
347
370
  def delete
348
371
  collection = self.class.instance_variable_get('@collection')
349
372
  target_index = collection.index{|item| item.id == self.id}
350
373
  collection.delete_at(target_index)
374
+ self.class.call_delegate(self, :action => 'delete')
351
375
  end
352
376
 
353
377
  def length
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.2.5"
2
+ VERSION = "0.2.6"
3
3
  end
@@ -0,0 +1,140 @@
1
+ class Task
2
+ include MotionModel::Model
3
+ columns :name => :string,
4
+ :details => :string,
5
+ :some_day => :date
6
+ end
7
+
8
+ describe 'finders' do
9
+ before do
10
+ Task.delete_all
11
+ 1.upto(10) {|i| Task.create(:name => "task #{i}", :id => i)}
12
+ end
13
+
14
+ describe 'find' do
15
+ it 'finds elements within the collection' do
16
+ Task.count.should == 10
17
+ Task.find(3).name.should.equal("task 3")
18
+ end
19
+
20
+ it 'returns nil if find by id is not found' do
21
+ Task.find(999).should.be.nil
22
+ end
23
+
24
+ it 'looks into fields if field name supplied' do
25
+ Task.create(:name => 'find me')
26
+ tasks = Task.find(:name).eq('find me')
27
+ tasks.count.should.equal(1)
28
+ tasks.first.name.should == 'find me'
29
+ end
30
+
31
+ it "provides an array of valid model instances when doing a find" do
32
+ Task.create(:name => 'find me')
33
+ tasks = Task.find(:name).eq('find me')
34
+ tasks.first.name.should.eql 'find me'
35
+ end
36
+
37
+ it 'allows for multiple (chained) query parameters' do
38
+ Task.create(:name => 'find me', :details => "details 1")
39
+ Task.create(:name => 'find me', :details => "details 2")
40
+ tasks = Task.find(:name).eq('find me').and(:details).like('2')
41
+ tasks.first.details.should.equal('details 2')
42
+ tasks.all.length.should.equal(1)
43
+ end
44
+
45
+ it 'where should respond to finder methods' do
46
+ Task.where(:details).should.respond_to(:contain)
47
+ end
48
+
49
+ it 'returns a FinderQuery object' do
50
+ Task.where(:details).should.is_a(MotionModel::FinderQuery)
51
+ end
52
+
53
+ it 'using where instead of find' do
54
+ atask = Task.create(:name => 'find me', :details => "details 1")
55
+ found_task = Task.where(:details).contain("s 1").first.details.should == 'details 1'
56
+ end
57
+
58
+ it "performs set inclusion(in) queries" do
59
+ class InTest
60
+ include MotionModel::Model
61
+ columns :name
62
+ end
63
+
64
+ 1.upto(10) do |i|
65
+ InTest.create(:id => i, :name => "test #{i}")
66
+ end
67
+
68
+ results = InTest.find(:id).in([3, 5, 7])
69
+ results.length.should == 3
70
+ end
71
+
72
+ it 'handles case-sensitive queries' do
73
+ task = Task.create :name => 'Bob'
74
+ Task.find(:name).eq('bob', :case_sensitive => true).all.length.should == 0
75
+ end
76
+
77
+ it 'all returns all members of the collection as an array' do
78
+ Task.all.length.should.equal(10)
79
+ end
80
+
81
+ it 'each yields each row in sequence' do
82
+ task_id = nil
83
+ Task.each do |task|
84
+ task_id.should.<(task.id) if task_id
85
+ task_id = task.id
86
+ end
87
+ end
88
+
89
+ describe 'block-style finders' do
90
+ before do
91
+ @items_less_than_5 = Task.find{|item| item.name.split(' ').last.to_i < 5}
92
+ end
93
+
94
+ it 'returns a FinderQuery' do
95
+ @items_less_than_5.should.is_a MotionModel::FinderQuery
96
+ end
97
+
98
+ it 'handles block-style finders' do
99
+ @items_less_than_5.length.should == 4
100
+ end
101
+
102
+ it 'deals with any arbitrary block finder' do
103
+ @even_items = Task.find do |item|
104
+ test_item = item.name.split(' ').last.to_i
105
+ test_item % 2 == 0 && test_item <= 6
106
+ end
107
+ @even_items.each{|item| item.name.split(' ').last.to_i.should.even?}
108
+ @even_items.length.should == 3 # [2, 4, 6]
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'sorting' do
114
+ before do
115
+ Task.delete_all
116
+ Task.create(:name => 'Task 3', :details => 'detail 3')
117
+ Task.create(:name => 'Task 1', :details => 'detail 1')
118
+ Task.create(:name => 'Task 2', :details => 'detail 6')
119
+ Task.create(:name => 'Random Task', :details => 'another random task')
120
+ end
121
+
122
+ it 'sorts by field' do
123
+ tasks = Task.order(:name).all
124
+ tasks[0].name.should.equal('Random Task')
125
+ tasks[1].name.should.equal('Task 1')
126
+ tasks[2].name.should.equal('Task 2')
127
+ tasks[3].name.should.equal('Task 3')
128
+ end
129
+
130
+ it 'sorts observing block syntax' do
131
+ tasks = Task.order{|one, two| two.details <=> one.details}.all
132
+ tasks[0].details.should.equal('detail 6')
133
+ tasks[1].details.should.equal('detail 3')
134
+ tasks[2].details.should.equal('detail 1')
135
+ tasks[3].details.should.equal('another random task')
136
+ end
137
+ end
138
+
139
+ end
140
+
data/spec/model_spec.rb CHANGED
@@ -131,138 +131,6 @@ describe "Creating a model" do
131
131
 
132
132
  end
133
133
 
134
- describe 'finders' do
135
- before do
136
- Task.delete_all
137
- @tasks = []
138
- 10.times {|i| @tasks.push Task.create(:name => "task #{i}")}
139
- end
140
-
141
- describe 'find' do
142
- it 'finds elements within the collection' do
143
- task = Task.find(3).name.should.equal(@tasks[2].name) # zero based
144
- end
145
-
146
- it 'returns nil if find by id is not found' do
147
- Task.find(999).should.be.nil
148
- end
149
-
150
- it 'looks into fields if field name supplied' do
151
- Task.create(:name => 'find me')
152
- tasks = Task.find(:name).eq('find me')
153
- tasks.all.length.should.equal(1)
154
- tasks.first.name.should == 'find me'
155
- end
156
-
157
- it "provides an array of valid model instances when doing a find" do
158
- Task.create(:name => 'find me')
159
- tasks = Task.find(:name).eq('find me')
160
- tasks.first.name.should.eql 'find me'
161
- end
162
-
163
- it 'allows for multiple (chained) query parameters' do
164
- Task.create(:name => 'find me', :details => "details 1")
165
- Task.create(:name => 'find me', :details => "details 2")
166
- tasks = Task.find(:name).eq('find me').and(:details).like('2')
167
- tasks.first.details.should.equal('details 2')
168
- tasks.all.length.should.equal(1)
169
- end
170
-
171
- it 'where should respond to finder methods' do
172
- Task.where(:details).should.respond_to(:contain)
173
- end
174
-
175
- it 'returns a FinderQuery object' do
176
- Task.where(:details).should.is_a(MotionModel::FinderQuery)
177
- end
178
-
179
- it 'using where instead of find' do
180
- atask = Task.create(:name => 'find me', :details => "details 1")
181
- found_task = Task.where(:details).contain("s 1").first.details.should == 'details 1'
182
- end
183
-
184
- it "performs set inclusion(in) queries" do
185
- class InTest
186
- include MotionModel::Model
187
- columns :name
188
- end
189
-
190
- 1.upto(10) do |i|
191
- InTest.create(:id => i, :name => "test #{i}")
192
- end
193
-
194
- results = InTest.find(:id).in([3, 5, 7])
195
- results.length.should == 3
196
- end
197
-
198
- it 'handles case-sensitive queries' do
199
- task = Task.create :name => 'Bob'
200
- Task.find(:name).eq('bob', :case_sensitive => true).all.length.should == 0
201
- end
202
-
203
- it 'all returns all members of the collection as an array' do
204
- Task.all.length.should.equal(10)
205
- end
206
-
207
- it 'each yields each row in sequence' do
208
- i = 0
209
- Task.each do |task|
210
- task.name.should.equal("task #{i}")
211
- i += 1
212
- end
213
- end
214
-
215
- describe 'block-style finders' do
216
- before do
217
- @items_less_than_5 = Task.find{|item| item.name.split(' ').last.to_i < 5}
218
- end
219
-
220
- it 'returns a FinderQuery' do
221
- @items_less_than_5.should.is_a MotionModel::FinderQuery
222
- end
223
-
224
- it 'handles block-style finders' do
225
- @items_less_than_5.length.should == 5 # Zero based
226
- end
227
-
228
- it 'deals with any arbitrary block finder' do
229
- @even_items = Task.find do |item|
230
- test_item = item.name.split(' ').last.to_i
231
- test_item % 2 == 0 && test_item < 5
232
- end
233
- @even_items.each{|item| item.name.split(' ').last.to_i.should.even?}
234
- @even_items.length.should == 3 # [0, 2, 4]
235
- end
236
- end
237
- end
238
-
239
- describe 'sorting' do
240
- before do
241
- Task.delete_all
242
- Task.create(:name => 'Task 3', :details => 'detail 3')
243
- Task.create(:name => 'Task 1', :details => 'detail 1')
244
- Task.create(:name => 'Task 2', :details => 'detail 6')
245
- Task.create(:name => 'Random Task', :details => 'another random task')
246
- end
247
-
248
- it 'sorts by field' do
249
- tasks = Task.order(:name).all
250
- tasks[0].name.should.equal('Random Task')
251
- tasks[1].name.should.equal('Task 1')
252
- tasks[2].name.should.equal('Task 2')
253
- tasks[3].name.should.equal('Task 3')
254
- end
255
-
256
- it 'sorts observing block syntax' do
257
- tasks = Task.order{|one, two| two.details <=> one.details}.all
258
- tasks[0].details.should.equal('detail 6')
259
- tasks[1].details.should.equal('detail 3')
260
- tasks[2].details.should.equal('detail 1')
261
- tasks[3].details.should.equal('another random task')
262
- end
263
- end
264
-
265
- end
266
134
 
267
135
  describe 'deleting' do
268
136
  before do
@@ -272,7 +140,7 @@ describe "Creating a model" do
272
140
  it 'deletes a row' do
273
141
  target = Task.find(:name).eq('task 3').first
274
142
  target.delete
275
- Task.find(:description).eq('Task 3').length.should.equal 0
143
+ Task.find(:name).eq('task 3').count.should.equal 0
276
144
  end
277
145
 
278
146
  it 'deleting a row changes length' do
@@ -351,3 +219,65 @@ describe "Creating a model" do
351
219
  end
352
220
  end
353
221
 
222
+ class NotifiableTask
223
+ include MotionModel::Model
224
+ columns :name
225
+ has_many :assignees
226
+
227
+ attr_accessor :notification_called, :notification_details
228
+
229
+ def hookup_events
230
+ NSNotificationCenter.defaultCenter.addObserver(self, selector:'dataDidChange:', name:'MotionModelDataDidChangeNotification', object:nil)
231
+ @notification_details = nil
232
+ end
233
+
234
+ def dataDidChange(notification)
235
+ @notification_called = true
236
+ @notification_details = notification.userInfo
237
+ end
238
+
239
+ def teardown_events
240
+ NSNotificationCenter.defaultCenter.removeObserver self
241
+ end
242
+ end
243
+
244
+ describe 'data change notifications' do
245
+ before do
246
+ @task = NotifiableTask.new
247
+ @task.hookup_events
248
+ end
249
+
250
+ after do
251
+ @task.teardown_events
252
+ end
253
+
254
+ it "fires a change notification when an item is added" do
255
+ @task.notification_called = false
256
+ lambda{@task.save}.should.change{@task.notification_called}
257
+ end
258
+
259
+ it "contains an add notification for new objects" do
260
+ @task.save
261
+ @task.notification_details[:action].should == 'add'
262
+ end
263
+
264
+ it "contans an update notification for an updated object" do
265
+ @task.save
266
+ @task.name = "Bill"
267
+ @task.save
268
+ @task.notification_details[:action].should == 'update'
269
+ end
270
+
271
+ it "contains a delete notification for a deleted object" do
272
+ @task.save
273
+ @task.delete
274
+ @task.notification_details[:action].should == 'delete'
275
+ end
276
+
277
+ it "does not get a delete notification for delete_all" do
278
+ @task = NotifiableTask.create :name => 'Bob'
279
+ @task.notification_called = nil
280
+ lambda{NotifiableTask.delete_all}.should.not.change{@task.notification_called}
281
+ end
282
+ end
283
+
@@ -174,4 +174,5 @@ describe "serialization of relations" do
174
174
  Parent.first.children.count.should == 2
175
175
  Parent.first.children.first.name.should == 'Fergie'
176
176
  end
177
- end
177
+ end
178
+
@@ -99,3 +99,4 @@ describe 'related objects' do
99
99
  end
100
100
  end
101
101
  end
102
+
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.5
4
+ version: 0.2.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-11 00:00:00.000000000 Z
12
+ date: 2012-10-24 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Simple model and validation mixins for RubyMotion
15
15
  email:
@@ -34,6 +34,7 @@ files:
34
34
  - lib/motion_model/version.rb
35
35
  - motion_model.gemspec
36
36
  - spec/ext_spec.rb
37
+ - spec/finder_spec.rb
37
38
  - spec/model_spec.rb
38
39
  - spec/persistence_spec.rb
39
40
  - spec/relation_spec.rb
@@ -63,6 +64,7 @@ specification_version: 3
63
64
  summary: Simple model and validation mixins for RubyMotion
64
65
  test_files:
65
66
  - spec/ext_spec.rb
67
+ - spec/finder_spec.rb
66
68
  - spec/model_spec.rb
67
69
  - spec/persistence_spec.rb
68
70
  - spec/relation_spec.rb