motion_model 0.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ 2012-09-05: Basically rewrote how the data is stored.
2
+
3
+ The API remains consistent, but a certain amount of
4
+ efficiency is added by adding hashes to map column names
5
+ to the column metadata.
6
+
7
+ * Type casting now works, and is a function of initialization
8
+ and of assignment.
9
+
10
+ * Default values have been added to fill in values
11
+ if not specified in new or create.
12
+
13
+ 2012-09-06: Added block-style finders. Added delete method.
data/README.md CHANGED
@@ -23,9 +23,9 @@ are:
23
23
  - input_helpers: Hooking an array up to a data form, populating
24
24
  it, and retrieving the data afterwards can be a bunch of code.
25
25
  Not something I'd like to write more often that I have to. These
26
- helpers are certainly not the focus of this strawman release, but
26
+ helpers are certainly not the focus of this release, but
27
27
  I am using these in an app to create Apple-like input forms in
28
- static tables. I expect some churn in this module.
28
+ static tables.
29
29
 
30
30
  What Model Can Do
31
31
  ================
@@ -50,6 +50,17 @@ class MyCoolController
50
50
  end
51
51
  ```
52
52
 
53
+ Models support default values, so if you specify your model like this, you get defaults:
54
+
55
+ ```ruby
56
+ class Task
57
+ include MotionModel::Model
58
+
59
+ columns :name => :string,
60
+ :due_date => {:type => :date, :default => '2012-09-15'}
61
+ end
62
+ ```
63
+
53
64
  You can also include the `Validations` module to get field validation. For example:
54
65
 
55
66
  ```ruby
@@ -74,6 +85,16 @@ class MyCoolController
74
85
  end
75
86
  ```
76
87
 
88
+ *Important Note*: Type casting occurs at initialization and on assignment. That means
89
+ If you have a field type `int`, it will be changed from a string to an integer when you
90
+ initialize the object of your class type or when you assign to the integer field in your class.
91
+
92
+ ```ruby
93
+ a_task = Task.create(:name => 'joe-bob', :due_date => '2012-09-15') # due_date is cast to NSDate
94
+
95
+ a_task.due_date = '2012-09-19' # due_date is cast to NSDate
96
+ ```
97
+
77
98
  Model Instances and Unique IDs
78
99
  -----------------
79
100
 
@@ -97,6 +118,12 @@ Things That Work
97
118
  @tasks = Task.where(:assigned_to).eq('bob').and(:location).contains('seattle')
98
119
  @tasks.all.each { |task| do_something_with(task) }
99
120
  ```
121
+
122
+ You can use a block with find:
123
+
124
+ ```ruby
125
+ @tasks = Task.find{|task| task.name =~ /dog/i && task.assigned_to == 'Bob'}
126
+ ```
100
127
 
101
128
  You can perform ordering using either a field name or block syntax. Here's an example:
102
129
 
@@ -168,3 +195,9 @@ Things In The Pipeline
168
195
  - Testing relations
169
196
  - Adding validations and custom validations
170
197
  - Did I say more tests?
198
+
199
+ Problems/Comments
200
+ ------------------
201
+
202
+ Please raise an issue if you find something that doesn't work, some
203
+ syntax that smells, etc.
data/Rakefile CHANGED
@@ -6,5 +6,5 @@ Motion::Project::App.setup do |app|
6
6
  # Use `rake config' to see complete project settings.
7
7
  app.delegate_class = 'FakeDelegate'
8
8
  app.files = Dir.glob('./lib/motion_model/**/*.rb')
9
- app.files = (Dir.glob('./app/**/*.rb') + app.files).uniq
9
+ app.files = (app.files + Dir.glob('./app/**/*.rb')).uniq
10
10
  end
data/app/app_delegate.rb CHANGED
@@ -1,2 +1,2 @@
1
1
  class FakeDelegate
2
- end
2
+ end
@@ -0,0 +1,123 @@
1
+ module MotionModel
2
+ class FinderQuery
3
+ attr_accessor :field_name
4
+
5
+ def initialize(*args)
6
+ @field_name = args[0] if args.length > 1
7
+ @collection = args.last
8
+ end
9
+
10
+ def and(field_name)
11
+ @field_name = field_name
12
+ self
13
+ end
14
+
15
+ def order(field = nil, &block)
16
+ if block_given?
17
+ @collection = @collection.sort{|o1, o2| yield(o1, o2)}
18
+ else
19
+ raise ArgumentError.new('you must supply a field name to sort unless you supply a block.') if field.nil?
20
+ @collection = @collection.sort{|o1, o2| o1.send(field) <=> o2.send(field)}
21
+ end
22
+ self
23
+ end
24
+
25
+ ######## relational methods ########
26
+ def do_comparison(query_string, options = {:case_sensitive => false})
27
+ query_string = query_string.downcase if query_string.respond_to?(:downcase) && !options[:case_sensitive]
28
+ @collection = @collection.select do |item|
29
+ comparator = item.send(@field_name.to_sym)
30
+ yield query_string, comparator
31
+ end
32
+ self
33
+ end
34
+
35
+ def contain(query_string, options = {:case_sensitive => false})
36
+ do_comparison(query_string) do |comparator, item|
37
+ if options[:case_sensitive]
38
+ item =~ Regexp.new(comparator, Regexp::MULTILINE)
39
+ else
40
+ item =~ Regexp.new(comparator, Regexp::IGNORECASE | Regexp::MULTILINE)
41
+ end
42
+ end
43
+ end
44
+ alias_method :contains, :contain
45
+ alias_method :like, :contain
46
+
47
+ def eq(query_string, options = {:case_sensitive => false})
48
+ do_comparison(query_string, options) do |comparator, item|
49
+ comparator == item
50
+ end
51
+ end
52
+ alias_method :==, :eq
53
+ alias_method :equal, :eq
54
+
55
+ def gt(query_string, options = {:case_sensitive => false})
56
+ do_comparison(query_string, options) do |comparator, item|
57
+ comparator > item
58
+ end
59
+ end
60
+ alias_method :>, :gt
61
+ alias_method :greater_than, :gt
62
+
63
+ def lt(query_string, options = {:case_sensitive => false})
64
+ do_comparison(query_string, options) do |comparator, item|
65
+ comparator < item
66
+ end
67
+ end
68
+ alias_method :<, :lt
69
+ alias_method :less_than, :lt
70
+
71
+ def gte(query_string, options = {:case_sensitive => false})
72
+ do_comparison(query_string, options) do |comparator, item|
73
+ comparator >= item
74
+ end
75
+ end
76
+ alias_method :>=, :gte
77
+ alias_method :greater_than_or_equal, :gte
78
+
79
+
80
+ def lte(query_string, options = {:case_sensitive => false})
81
+ do_comparison(query_string, options) do |comparator, item|
82
+ comparator <= item
83
+ end
84
+ end
85
+ alias_method :<=, :lte
86
+ alias_method :less_than_or_equal, :lte
87
+
88
+ def ne(query_string, options = {:case_sensitive => false})
89
+ do_comparison(query_string, options) do |comparator, item|
90
+ comparator != item
91
+ end
92
+ end
93
+ alias_method :!=, :ne
94
+ alias_method :not_equal, :ne
95
+
96
+ ########### accessor methods #########
97
+ def first
98
+ @collection.first
99
+ end
100
+
101
+ def last
102
+ @collection.last
103
+ end
104
+
105
+ def all
106
+ @collection
107
+ end
108
+
109
+ # each is a shortcut method to turn a query into an iterator. It allows
110
+ # you to write code like:
111
+ #
112
+ # Task.where(:assignee).eq('bob').each{ |assignee| do_something_with(assignee) }
113
+ def each(&block)
114
+ raise ArgumentError.new("each requires a block") unless block_given?
115
+ @collection.each{|item| yield item}
116
+ end
117
+
118
+ def length
119
+ @collection.length
120
+ end
121
+ alias_method :count, :length
122
+ end
123
+ end
@@ -16,7 +16,7 @@ module MotionModel
16
16
 
17
17
  def self.included(base)
18
18
  base.extend(ClassMethods)
19
- base.instance_variable_set('@data', [])
19
+ base.instance_variable_set('@binding_data', [])
20
20
  end
21
21
 
22
22
  module ClassMethods
@@ -39,7 +39,7 @@ module MotionModel
39
39
  def field(field, options = {})
40
40
  puts "adding field #{field}"
41
41
  label = options[:label] || field.humanize
42
- @data << FieldBindingMap.new(:label => label, :name => field)
42
+ @binding_data << FieldBindingMap.new(:label => label, :name => field)
43
43
  end
44
44
  end
45
45
 
@@ -61,7 +61,7 @@ module MotionModel
61
61
  # end
62
62
 
63
63
  def field_count
64
- self.class.instance_variable_get('@data'.to_sym).length
64
+ self.class.instance_variable_get('@binding_data'.to_sym).length
65
65
  end
66
66
 
67
67
  # +field_at+ retrieves the field at a given index.
@@ -72,7 +72,7 @@ module MotionModel
72
72
  # label_view = subview(UILabel, :label_frame, text: field.label)
73
73
 
74
74
  def field_at(index)
75
- data = self.class.instance_variable_get('@data'.to_sym)
75
+ data = self.class.instance_variable_get('@binding_data'.to_sym)
76
76
  data[index].tag = index + 1
77
77
  data[index]
78
78
  end
@@ -98,7 +98,7 @@ module MotionModel
98
98
  # end
99
99
 
100
100
  def fields
101
- self.class.instance_variable_get('@data'.to_sym).each{|datum| yield datum}
101
+ self.class.instance_variable_get('@binding_data'.to_sym).each{|datum| yield datum}
102
102
  end
103
103
 
104
104
  # +bind+ fetches all mapped fields from
@@ -111,9 +111,9 @@ module MotionModel
111
111
 
112
112
  fields do |field|
113
113
  puts "*** retrieving data for #{field.name} and tag #{field.tag} ***"
114
- view_obj = view.viewWithTag(field.tag)
114
+ view_obj = self.view.viewWithTag(field.tag)
115
115
  puts "view object with tag is #{view_obj.inspect}"
116
- @model.send("#{field.name}=".to_sym, view.viewWithTag(field.tag).text)
116
+ @model.send("#{field.name}=".to_sym, view_obj.text)
117
117
  end
118
118
  end
119
119
 
@@ -23,8 +23,7 @@
23
23
  # Recognized types are:
24
24
  #
25
25
  # * :string
26
- # * :date (must be in a form that Date.parse can recognize)
27
- # * :time (must be in a form that Time.parse can recognize)
26
+ # * :date (must be in YYYY-mm-dd form)
28
27
  # * :integer
29
28
  # * :float
30
29
  #
@@ -37,72 +36,119 @@
37
36
  # tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week)
38
37
  # ordered_tasks_this_week = tasks_this_week.order(:due_date)
39
38
  #
39
+
40
40
  module MotionModel
41
41
  module Model
42
+ class Column
43
+ attr_accessor :name
44
+ attr_accessor :type
45
+ attr_accessor :default
46
+
47
+ def initialize(name = nil, type = nil, default = nil)
48
+ @name = name
49
+ @type = type
50
+ @default = default || nil
51
+ end
52
+
53
+ def add_attr(name, type, default = nil)
54
+ @name = name
55
+ @type = type
56
+ @default = default || nil
57
+ end
58
+ alias_method :add_attribute, :add_attr
59
+ end
60
+
42
61
  def self.included(base)
43
62
  base.extend(ClassMethods)
44
- base.instance_variable_set("@column_attrs", [])
45
- base.instance_variable_set("@typed_attrs", [])
63
+ base.instance_variable_set("@_columns", [])
64
+ base.instance_variable_set("@_column_hashes", {})
46
65
  base.instance_variable_set("@collection", [])
47
66
  base.instance_variable_set("@_next_id", 1)
48
67
  end
49
68
 
50
69
  module ClassMethods
70
+ def add_field(name, options, default = nil) #nodoc
71
+ col = Column.new(name, options, default)
72
+ @_columns.push col
73
+ @_column_hashes[col.name.to_sym] = col
74
+ end
75
+
51
76
  # Macro to define names and types of columns. It can be used in one of
52
77
  # two forms:
53
78
  #
54
79
  # Pass a hash, and you define columns with types. E.g.,
55
80
  #
56
81
  # columns :name => :string, :age => :integer
82
+ #
83
+ # Pass a hash of hashes and you can specify defaults such as:
84
+ #
85
+ # columns :name => {:type => :string, :default => 'Joe Bob'}, :age => :integer
57
86
  #
58
87
  # Pass an array, and you create column names, all of which have type +:string+.
59
88
  #
60
89
  # columns :name, :age, :hobby
90
+
61
91
  def columns(*fields)
62
- return @column_attrs if fields.empty?
92
+ return @_columns.map{|c| c.name} if fields.empty?
63
93
 
94
+ col = Column.new
95
+
64
96
  case fields.first
65
97
  when Hash
66
- fields.first.each_pair do |attr, type|
67
- add_attribute(attr, type)
98
+ fields.first.each_pair do |name, options|
99
+ case options
100
+ when Symbol, String
101
+ add_field(name, options)
102
+ when Hash
103
+ add_field(name, options[:type], options[:default])
104
+ else
105
+ raise ArgumentError.new("arguments to fields must be a symbol, a hash, or a hash of hashes.")
106
+ end
68
107
  end
69
108
  else
70
- fields.each do |attr|
71
- add_attribute(attr, :string)
109
+ fields.each do |name|
110
+ add_field(name, :string)
72
111
  end
73
112
  end
74
113
 
75
114
  unless self.respond_to?(:id)
76
- add_attribute(:id, :integer)
115
+ add_field(:id, :integer)
77
116
  end
78
117
  end
79
-
80
- def add_attribute(attr, type) #nodoc
81
- attr_accessor attr
82
- @column_attrs << attr
83
- @typed_attrs << type
118
+
119
+ # Returns a column denoted by +name+
120
+ def column_named(name)
121
+ @_column_hashes[name.to_sym]
84
122
  end
85
123
 
124
+ # Returns next available id
86
125
  def next_id #nodoc
87
126
  @_next_id
88
127
  end
89
128
 
129
+ # Sets next available id
130
+ def next_id=(value)
131
+ @_next_id = value
132
+ end
133
+
134
+ # Increments next available id
90
135
  def increment_id #nodoc
91
136
  @_next_id += 1
92
137
  end
93
138
 
94
139
  # Returns true if a column exists on this model, otherwise false.
95
140
  def column?(column)
96
- @column_attrs.each{|key|
97
- return true if key == column
98
- }
99
- false
141
+ respond_to?(column)
100
142
  end
101
143
 
102
144
  # Returns type of this column.
103
145
  def type(column)
104
- index = @column_attrs.index(column)
105
- index ? @typed_attrs[index] : nil
146
+ column_named(column).type || nil
147
+ end
148
+
149
+ # returns default value for this column or nil.
150
+ def default(column)
151
+ column_named(column).default || nil
106
152
  end
107
153
 
108
154
  # Creates an object and saves it. E.g.:
@@ -112,12 +158,16 @@ module MotionModel
112
158
  # returns the object created or false.
113
159
  def create(options = {})
114
160
  row = self.new(options)
161
+ row.before_create if row.respond_to?(:before_create)
162
+ row.before_save if row.respond_to?(:before_save)
163
+
115
164
  # TODO: Check for Validatable and if it's
116
165
  # present, check valid? before saving.
117
- @collection.push(row)
166
+
167
+ row.save
118
168
  row
119
169
  end
120
-
170
+
121
171
  def length
122
172
  @collection.length
123
173
  end
@@ -135,7 +185,14 @@ module MotionModel
135
185
  # or...
136
186
  #
137
187
  # @posts = Post.find(:author).eq('bob').all
138
- def find(*args)
188
+ def find(*args, &block)
189
+ if block_given?
190
+ matches = @collection.collect do |item|
191
+ item if yield(item)
192
+ end.compact
193
+ return FinderQuery.new(matches)
194
+ end
195
+
139
196
  unless args[0].is_a?(Symbol) || args[0].is_a?(String)
140
197
  return @collection[args[0].to_i] || nil
141
198
  end
@@ -172,15 +229,62 @@ module MotionModel
172
229
 
173
230
  ####### Instance Methods #######
174
231
  def initialize(options = {})
175
- columns.each{|col| instance_variable_set("@#{col.to_s}", nil) unless options.has_key?(col)}
232
+ @data ||= {}
176
233
 
177
- options.each do |key, value|
178
- instance_variable_set("@#{key.to_s}", value || '') if self.class.column?(key.to_sym)
179
- end
180
- unless self.id
181
- self.id = self.class.next_id
234
+ # Time zone, for future use.
235
+ @tz_offset ||= NSDate.date.to_s.gsub(/^.*?( -\d{4})/, '\1')
236
+
237
+ @cached_date_formatter = NSDateFormatter.alloc.init # Create once, as they are expensive to create
238
+ @cached_date_formatter.dateFormat = "yyyy-MM-dd HH:mm"
239
+
240
+ unless options[:id]
241
+ options[:id] = self.class.next_id
182
242
  self.class.increment_id
243
+ else
244
+ self.class.next_id = [options[:id].to_i, self.class.next_id].max
245
+ end
246
+
247
+ columns.each do |col|
248
+ options[col] ||= self.class.default(col)
249
+ cast_value = cast_to_type(col, options[col])
250
+ @data[col] = cast_value
251
+ end
252
+ end
253
+
254
+ def cast_to_type(column_name, arg)
255
+ return nil if arg.nil?
256
+
257
+ return_value = arg
258
+
259
+ case type(column_name)
260
+ when :string
261
+ return_value = arg.to_s
262
+ when :int, :integer
263
+ return_value = arg.is_a?(Integer) ? arg : arg.to_i
264
+ when :float, :double
265
+ return_value = arg.is_a?(Float) ? arg : arg.to_f
266
+ when :date
267
+ return arg if arg.is_a?(NSDate)
268
+ date_string = arg += ' 00:00'
269
+ return_value = @cached_date_formatter.dateFromString(date_string)
270
+ else
271
+ raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
183
272
  end
273
+ return_value
274
+ end
275
+
276
+ def to_s
277
+ columns.each{|c| "#{c}: #{self.send(c)}"}
278
+ end
279
+
280
+ def save
281
+ self.class.instance_variable_get('@collection') << self
282
+ end
283
+
284
+ def delete
285
+ collection = self.class.instance_variable_get('@collection')
286
+ target_index = collection.index{|item| item.id == self.id}
287
+ collection.delete_at(target_index)
184
288
  end
185
289
 
186
290
  def length
@@ -190,20 +294,24 @@ module MotionModel
190
294
  alias_method :count, :length
191
295
 
192
296
  def column?(target_key)
193
- self.class.column?(target_key)
297
+ self.class.column?(target_key.to_sym)
194
298
  end
195
299
 
196
300
  def columns
197
301
  self.class.columns
198
302
  end
199
303
 
304
+ def column_named(name)
305
+ self.class.column_named(name.to_sym)
306
+ end
307
+
200
308
  def type(field_name)
201
309
  self.class.type(field_name)
202
310
  end
203
311
 
204
312
  def initWithCoder(coder)
205
313
  self.init
206
- self.class.instance_variable_get("@column_attrs").each do |attr|
314
+ self.class.instance_variable_get("@_columns").each do |attr|
207
315
  # If a model revision has taken place, don't try to decode
208
316
  # something that's not there.
209
317
  new_tag_id = 1
@@ -222,125 +330,49 @@ module MotionModel
222
330
  end
223
331
 
224
332
  def encodeWithCoder(coder)
225
- self.class.instance_variable_get("@column_attrs").each do |attr|
333
+ self.class.instance_variable_get("@_columns").each do |attr|
226
334
  coder.encodeObject(self.send(attr), forKey: attr.to_s)
227
335
  end
228
336
  end
229
337
 
230
- end
231
-
232
- class FinderQuery
233
- attr_accessor :field_name
234
-
235
- def initialize(*args)
236
- @field_name = args[0] if args.length > 1
237
- @collection = args.last
238
- end
239
-
240
- def and(field_name)
241
- @field_name = field_name
242
- self
338
+ # Modify respond_to? to add model's attributes.
339
+ alias_method :old_respond_to?, :respond_to?
340
+ def respond_to?(method)
341
+ column_named(method) || old_respond_to?(method)
243
342
  end
244
343
 
245
- def order(field = nil, &block)
246
- if block_given?
247
- @collection = @collection.sort{|o1, o2| yield(o1, o2)}
344
+ # Handle attribute retrieval
345
+ #
346
+ # Gets and sets work as expected, and type casting occurs
347
+ # For example:
348
+ #
349
+ # Task.date = '2012-09-15'
350
+ #
351
+ # This creates a real Date object in the data store.
352
+ #
353
+ # date = Task.date
354
+ #
355
+ # Date is a real date object.
356
+ def method_missing(method, *args, &block)
357
+ base_method = method.to_s.gsub('=', '').to_sym
358
+
359
+ col = column_named(base_method)
360
+
361
+ if col
362
+ if method.to_s.include?('=')
363
+ return @data[base_method] = self.cast_to_type(base_method, args[0])
364
+ else
365
+ return @data[base_method]
366
+ end
248
367
  else
249
- raise ArgumentError.new('you must supply a field name to sort unless you supply a block.') if field.nil?
250
- @collection = @collection.sort{|o1, o2| o1.send(field) <=> o2.send(field)}
251
- end
252
- self
253
- end
254
-
255
- ######## relational methods ########
256
- def do_comparison(query_string)
257
- # TODO: Flag case-insensitive searching
258
- query_string = query_string.downcase if query_string.respond_to?(:downcase)
259
- @collection = @collection.select do |item|
260
- comparator = item.send(@field_name.to_sym)
261
- yield query_string, comparator
262
- end
263
- self
264
- end
265
-
266
- def contain(query_string)
267
- do_comparison(query_string) do |comparator, item|
268
- item =~ Regexp.new(comparator)
269
- end
270
- end
271
- alias_method :contains, :contain
272
- alias_method :like, :contain
273
-
274
- def eq(query_string)
275
- do_comparison(query_string) do |comparator, item|
276
- comparator == item
277
- end
278
- end
279
- alias_method :==, :eq
280
- alias_method :equal, :eq
281
-
282
- def gt(query_string)
283
- do_comparison(query_string) do |comparator, item|
284
- comparator > item
368
+ raise NoMethodError, <<ERRORINFO
369
+ method: #{method}
370
+ args: #{args.inspect}
371
+ in: #{self.class.name}
372
+ ERRORINFO
285
373
  end
286
374
  end
287
- alias_method :>, :gt
288
- alias_method :greater_than, :gt
289
-
290
- def lt(query_string)
291
- do_comparison(query_string) do |comparator, item|
292
- comparator < item
293
- end
294
- end
295
- alias_method :<, :lt
296
- alias_method :less_than, :lt
297
-
298
- def gte(query_string)
299
- do_comparison(query_string) do |comparator, item|
300
- comparator >= item
301
- end
302
- end
303
- alias_method :>=, :gte
304
- alias_method :greater_than_or_equal, :gte
305
-
306
-
307
- def lte(query_string)
308
- do_comparison(query_string) do |comparator, item|
309
- comparator <= item
310
- end
311
- end
312
- alias_method :<=, :lte
313
- alias_method :less_than_or_equal, :lte
314
-
315
- def ne(query_string)
316
- do_comparison(query_string) do |comparator, item|
317
- comparator != item
318
- end
319
- end
320
- alias_method :!=, :ne
321
- alias_method :not_equal, :ne
322
-
323
- ########### accessor methods #########
324
- def first
325
- @collection.first
326
- end
327
-
328
- def last
329
- @collection.last
330
- end
331
-
332
- def all
333
- @collection
334
- end
335
-
336
- # each is a shortcut method to turn a query into an iterator. It allows
337
- # you to write code like:
338
- #
339
- # Task.where(:assignee).eq('bob').each{ |assignee| do_something_with(assignee) }
340
- def each(&block)
341
- raise ArgumentError.new("each requires a block") unless block_given?
342
- @collection.each{|item| yield item}
343
- end
375
+
344
376
  end
345
377
  end
346
378
 
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.2"
2
+ VERSION = "0.2.1"
3
3
  end
data/motion_model.gemspec CHANGED
@@ -9,7 +9,6 @@ Gem::Specification.new do |gem|
9
9
  gem.homepage = "https://github.com/sxross/MotionModel"
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
12
- puts "gem files are #{`git ls-files`.split($\)}"
13
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
13
  gem.name = "motion_model"
15
14
  gem.require_paths = ["lib"]
data/spec/model_spec.rb CHANGED
@@ -10,6 +10,16 @@ class ATask
10
10
  columns :name, :details, :some_day
11
11
  end
12
12
 
13
+ class TypeCast
14
+ include MotionModel::Model
15
+ columns :an_int => {:type => :int, :default => 3},
16
+ :an_integer => :integer,
17
+ :a_float => :float,
18
+ :a_double => :double,
19
+ :a_date => :date,
20
+ :a_time => :time
21
+ end
22
+
13
23
  describe "Creating a model" do
14
24
  before do
15
25
  Task.delete_all
@@ -18,8 +28,8 @@ describe "Creating a model" do
18
28
  describe 'column macro behavior' do
19
29
 
20
30
  it 'succeeds when creating a valid model from attributes' do
21
- a_task = Task.new(:name => 'name', :details => 'details')
22
- a_task.name.should.equal('name')
31
+ a_task = Task.new(:name => 'name', :details => 'details')
32
+ a_task.name.should.equal('name')
23
33
  end
24
34
 
25
35
  it 'creates a model with all attributes even if some omitted' do
@@ -28,45 +38,50 @@ describe "Creating a model" do
28
38
  end
29
39
 
30
40
  it 'simply bypasses spurious attributes erroneously set' do
31
- a_task = Task.new(:name => 'details', :zoo => 'very bad')
32
- a_task.should.not.respond_to(:zoo)
33
- a_task.name.should.equal('details')
41
+ a_task = Task.new(:name => 'details', :zoo => 'very bad')
42
+ a_task.should.not.respond_to(:zoo)
43
+ a_task.name.should.equal('details')
44
+ end
45
+
46
+ it "adds a default value if none supplied" do
47
+ a_type_test = TypeCast.new
48
+ a_type_test.an_int.should.equal(3)
34
49
  end
35
50
 
36
51
  it "can check for a column's existence on a model" do
37
- Task.column?(:name).should.be.true
52
+ Task.column?(:name).should.be.true
38
53
  end
39
54
 
40
55
  it "can check for a column's existence on an instance" do
41
- a_task = Task.new(:name => 'name', :details => 'details')
42
- a_task.column?(:name).should.be.true
56
+ a_task = Task.new(:name => 'name', :details => 'details')
57
+ a_task.column?(:name).should.be.true
43
58
  end
44
59
 
45
60
  it "gets a list of columns on a model" do
46
- cols = Task.columns
47
- cols.should.include(:name)
48
- cols.should.include(:details)
61
+ cols = Task.columns
62
+ cols.should.include(:name)
63
+ cols.should.include(:details)
49
64
  end
50
65
 
51
66
  it "gets a list of columns on an instance" do
52
- a_task = Task.new
53
- cols = a_task.columns
54
- cols.should.include(:name)
55
- cols.should.include(:details)
67
+ a_task = Task.new
68
+ cols = a_task.columns
69
+ cols.should.include(:name)
70
+ cols.should.include(:details)
56
71
  end
57
72
 
58
73
  it "columns can be specified as a Hash" do
59
- lambda{Task.new}.should.not.raise
60
- Task.new.column?(:name).should.be.true
74
+ lambda{Task.new}.should.not.raise
75
+ Task.new.column?(:name).should.be.true
61
76
  end
62
77
 
63
78
  it "columns can be specified as an Array" do
64
- lambda{ATask.new}.should.not.raise
65
- Task.new.column?(:name).should.be.true
79
+ lambda{ATask.new}.should.not.raise
80
+ Task.new.column?(:name).should.be.true
66
81
  end
67
82
 
68
83
  it "the type of a column can be retrieved" do
69
- Task.new.type(:some_day).should.equal(:date)
84
+ Task.new.type(:some_day).should.equal(:date)
70
85
  end
71
86
 
72
87
  end
@@ -155,6 +170,11 @@ describe "Creating a model" do
155
170
  atask = Task.create(:name => 'find me', :details => "details 1")
156
171
  found_task = Task.where(:details).contain("details 1").first.details.should.equal("details 1")
157
172
  end
173
+
174
+ it 'handles case-sensitive queries' do
175
+ task = Task.create :name => 'Bob'
176
+ Task.find(:name).eq('bob', :case_sensitive => true).all.should.be.empty
177
+ end
158
178
 
159
179
  it 'all returns all members of the collection as an array' do
160
180
  Task.all.length.should.equal(10)
@@ -167,7 +187,29 @@ describe "Creating a model" do
167
187
  i += 1
168
188
  end
169
189
  end
170
-
190
+
191
+ describe 'block-style finders' do
192
+ before do
193
+ @items_less_than_5 = Task.find{|item| item.name.split(' ').last.to_i < 5}
194
+ end
195
+
196
+ it 'returns a FinderQuery' do
197
+ @items_less_than_5.should.is_a MotionModel::FinderQuery
198
+ end
199
+
200
+ it 'handles block-style finders' do
201
+ @items_less_than_5.length.should == 5 # Zero based
202
+ end
203
+
204
+ it 'deals with any arbitrary block finder' do
205
+ @even_items = Task.find do |item|
206
+ test_item = item.name.split(' ').last.to_i
207
+ test_item % 2 == 0 && test_item < 5
208
+ end
209
+ @even_items.each{|item| item.name.split(' ').last.to_i.should.even?}
210
+ @even_items.length.should == 3 # [0, 2, 4]
211
+ end
212
+ end
171
213
  end
172
214
 
173
215
  describe 'sorting' do
@@ -197,4 +239,91 @@ describe "Creating a model" do
197
239
  end
198
240
 
199
241
  end
242
+
243
+ describe 'deleting' do
244
+ before do
245
+ 10.times {|i| Task.create(:name => "task #{i}")}
246
+ end
247
+
248
+ it 'deletes a row' do
249
+ target = Task.find(:name).eq('task 3').first
250
+ target.delete
251
+ Task.find(:description).eq('Task 3').should == nil
252
+ end
253
+
254
+ it 'deleting a row changes length' do
255
+ target = Task.find(:name).eq('task 3').first
256
+ lambda{target.delete}.should.change{Task.length}
257
+ end
258
+ end
259
+
260
+ describe 'Handling Attribute method_missing Implementation' do
261
+ it 'raises a NoMethodError exception when an unknown attribute it referenced' do
262
+ task = Task.new
263
+ lambda{task.bar}.should.raise(NoMethodError)
264
+ end
265
+ end
266
+
267
+ describe 'Type casting' do
268
+ before do
269
+ @convertible = TypeCast.new
270
+ @convertible.an_int = '1'
271
+ @convertible.an_integer = '2'
272
+ @convertible.a_float = '3.7'
273
+ @convertible.a_double = '3.41459'
274
+ @convertible.a_date = '2012-09-15'
275
+ end
276
+
277
+ it 'does the type casting on instantiation' do
278
+ @convertible.an_int.should.is_a Integer
279
+ @convertible.an_integer.should.is_a Integer
280
+ @convertible.a_float.should.is_a Float
281
+ @convertible.a_double.should.is_a Float
282
+ @convertible.a_date.should.is_a NSDate
283
+ end
284
+
285
+ it 'returns an integer for an int field' do
286
+ @convertible.an_int.should.is_a(Integer)
287
+ end
288
+
289
+ it 'the int field should be the same as it was in string form' do
290
+ @convertible.an_int.to_s.should.equal('1')
291
+ end
292
+
293
+ it 'returns an integer for an integer field' do
294
+ @convertible.an_integer.should.is_a(Integer)
295
+ end
296
+
297
+ it 'the integer field should be the same as it was in string form' do
298
+ @convertible.an_integer.to_s.should.equal('2')
299
+ end
300
+
301
+ it 'returns a float for a float field' do
302
+ @convertible.a_float.should.is_a(Float)
303
+ end
304
+
305
+ it 'the float field should be the same as it was in string form' do
306
+ @convertible.a_float.should.>(3.6)
307
+ @convertible.a_float.should.<(3.8)
308
+ end
309
+
310
+ it 'returns a double for a double field' do
311
+ @convertible.a_double.should.is_a(Float)
312
+ end
313
+
314
+ it 'the double field should be the same as it was in string form' do
315
+ @convertible.a_double.should.>(3.41458)
316
+ @convertible.a_double.should.<(3.41460)
317
+ end
318
+
319
+ it 'returns a NSDate for a date field' do
320
+ @convertible.a_date.should.is_a(NSDate)
321
+ end
322
+
323
+ it 'the date field should be the same as it was in string form' do
324
+ @convertible.a_date.to_s.should.match(/^2012-09-15/)
325
+ end
326
+
327
+ end
328
+
200
329
  end
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.1
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-08-20 00:00:00.000000000 Z
12
+ date: 2012-09-06 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Simple model and validation mixins for RubyMotion
15
15
  email:
@@ -19,11 +19,13 @@ extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
21
  - .gitignore
22
+ - CHANGELOG
22
23
  - README.md
23
24
  - Rakefile
24
25
  - app/app_delegate.rb
25
26
  - lib/motion_model.rb
26
27
  - lib/motion_model/ext.rb
28
+ - lib/motion_model/finder_query.rb
27
29
  - lib/motion_model/input_helpers.rb
28
30
  - lib/motion_model/model.rb
29
31
  - lib/motion_model/validatable.rb