motion_model 0.2 → 0.2.1

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/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