motion_model 0.2.3 → 0.2.4

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/README.md CHANGED
@@ -187,20 +187,59 @@ Things That Work
187
187
  protocol. When you declare your columns, `MotionModel` understands how to
188
188
  serialize your data so you need take no further action.
189
189
 
190
- * Relations, in principle work. They are more embedded documents similar
191
- to CouchDB or MongoDB. So instead of being separate tables, the embedded
192
- documents are model objects contained in a collection.
190
+ **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.
193
192
 
194
- **Relations Are Untested**. This is completely experimental, but to use
195
- them, just define a column as type `:array`. Initializing these properly
196
- and testing them is a high priority for me, so expect it to be addressed
197
- soon.
193
+ * Relations now are usable, although not complete fleshed out:
194
+
195
+ ```ruby
196
+ class Task
197
+ include MotionModel::Model
198
+ columns :name => :string
199
+ has_many :assignees
200
+ end
201
+
202
+ class Assignee
203
+ include MotionModel::Model
204
+ columns :assignee_name => :string
205
+ belongs_to :task
206
+ end
207
+
208
+ # Create a task, then create an assignee as a
209
+ # related object on that task
210
+ a_task = Task.create(:name => "Walk the Dog")
211
+ a_task.assignees.create(:assignee_name => "Howard")
212
+
213
+ # See? It works.
214
+ a_task.assignees.assignee_name # => "Howard"
215
+ Task.first.assignees.assignee_name # => "Howard"
216
+
217
+ # Create another assignee but don't save
218
+ # Add to assignees collection. Both objects
219
+ # are saved.
220
+ another_assignee = Assignee.new(:name => "Douglas")
221
+ a_task.assignees << another_assignee # adds to relation and saves both objects
222
+
223
+ # The count of assignees accurately reflects current state
224
+ a_task.assignees.count # => 2
225
+
226
+ # And backreference access through belongs_to works.
227
+ Assignee.first.task.name # => "Walk the Dog"
228
+ ```
229
+
230
+ At this point, there are a few methods that need to be added
231
+ for relations, and they will.
232
+
233
+ * delete
234
+ * destroy
198
235
 
199
236
  * Core extensions work. The following are supplied:
200
237
 
201
238
  - String#humanize
202
239
  - String#titleize
203
240
  - String#empty?
241
+ - String#singularize
242
+ - String#pluralize
204
243
  - NilClass#empty?
205
244
  - Array#empty?
206
245
  - Hash#empty?
@@ -214,15 +253,44 @@ Things That Work
214
253
  - Debug.info(message)
215
254
  - Debug.warning(message)
216
255
  - Debug.error(message)
256
+ - Debug.silence / Debug.resume to turn on and off logging
257
+ - Debug.colorize (true/false) for pretty console display
258
+
259
+ Finally, there is an inflector singleton class based around the one
260
+ Rails has implemented. You don't need to dig around in this class
261
+ too much, as its core functionality is exposed through two methods:
262
+
263
+ String#singularize
264
+ String#pluralize
265
+
266
+ These work, with the caveats that 1) The inflector is English-language
267
+ based; 2) Irregular nouns are not handled; 3) Singularizing a singular
268
+ or pluralizing a plural makes for good cocktail-party stuff, but in
269
+ code, it mangles things pretty badly.
270
+
271
+ You may want to get into customizing your inflections using:
272
+
273
+ - Inflector.inflections.singular(rule, replacement)
274
+ - Inflector.inflections.plural(rule, replacement)
275
+ - Inflector.inflections.irregular(rule, replacement)
276
+
277
+ These allow you to add to the list of rules the inflector uses when
278
+ processing singularize and pluralize. For each singular rule, you will
279
+ probably want to add a plural one. Note that order matters for rules,
280
+ so if your inflection is getting chewed up in one of the baked-in
281
+ inflections, you may have to use Inflector.inflections.reset to empty
282
+ them all out and build your own.
283
+
284
+ Of particular note is Inflector.inflections.irregular. This is for words
285
+ that defy regular rules such as 'man' => 'men' or 'person' => 'people'.
286
+ Again, a reversing rule is required for both singularize and
287
+ pluralize to work properly.
217
288
 
218
289
  Things In The Pipeline
219
290
  ----------------------
220
291
 
221
- - More tests!
222
292
  - More robust id assignment
223
- - Testing relations
224
293
  - Adding validations and custom validations
225
- - Did I say more tests?
226
294
 
227
295
  Problems/Comments
228
296
  ------------------
@@ -10,6 +10,113 @@ class String
10
10
  def empty?
11
11
  self.length < 1
12
12
  end
13
+
14
+ def pluralize
15
+ Inflector.inflections.pluralize self
16
+ end
17
+
18
+ def singularize
19
+ Inflector.inflections.singularize self
20
+ end
21
+ end
22
+
23
+ # Inflector is a singleton class that helps
24
+ # singularize, pluralize and other-thing-ize
25
+ # words. It is very much based on the Rails
26
+ # ActiveSupport implementation or Inflector
27
+ class Inflector
28
+ def self.instance
29
+ @__instance__ ||= new
30
+ end
31
+
32
+ def initialize
33
+ reset
34
+ end
35
+
36
+ def reset
37
+ @plurals = [
38
+ [/^(.*)ee$/i, '\1ees'], # attendee => attendees
39
+ [/^(.*)us$/i, '\1i'], # alumnus => alumni
40
+ [/^(.*s)$/i, '\1es'], # pass => passes
41
+ [/^(.*)$/, '\1s'] # normal => normals
42
+ ]
43
+
44
+ @singulars = [
45
+ [/^(.*)ees$/i, '\1ee'], # attendees => attendee
46
+ [/^(.*)es$/i, '\1'], # passes => pass
47
+ [/^(.*)i$/i, '\1us'], # alumni => alumnus
48
+ [/^(.*)s$/i, '\1'] # normals => normal
49
+ ]
50
+
51
+ @irregulars = [
52
+ ['person', 'people'],
53
+ ['people', 'person']
54
+ ]
55
+
56
+ @uncountables = [
57
+ 'fish',
58
+ 'sheep'
59
+ ]
60
+ end
61
+
62
+ attr_reader :plurals, :singulars, :uncountables, :irregulars
63
+
64
+ def self.inflections
65
+ if block_given?
66
+ yield Inflector.instance
67
+ else
68
+ Inflector.instance
69
+ end
70
+ end
71
+
72
+ def uncountable(word)
73
+ @uncountables << word
74
+ end
75
+
76
+ def singular(rule, replacement)
77
+ @singulars << [rule, replacement]
78
+ end
79
+
80
+ def plural(rule, replacement)
81
+ @plurals << [rule, replacement]
82
+ end
83
+
84
+ def irregular(rule, replacement)
85
+ @irregulars << [rule, replacement]
86
+ end
87
+
88
+ def uncountable?(word)
89
+ return word if @uncountables.include?(word.downcase)
90
+ false
91
+ end
92
+
93
+ def singularize(word)
94
+ return word if uncountable?(word)
95
+ plural = word.dup
96
+
97
+ @irregulars.each do |rule|
98
+ return plural if plural.gsub!(rule.first, rule.last)
99
+ end
100
+
101
+ @singulars.each do |rule|
102
+ return plural if plural.gsub!(rule.first, rule.last)
103
+ end
104
+ plural
105
+ end
106
+
107
+ def pluralize(word)
108
+ return word if uncountable?(word)
109
+ singular = word.dup
110
+
111
+ @irregulars.each do |rule|
112
+ return singular if singular.gsub!(rule.first, rule.last)
113
+ end
114
+
115
+ @plurals.each do |rule|
116
+ return singular if singular.gsub!(rule.first, rule.last)
117
+ end
118
+ singular
119
+ end
13
120
  end
14
121
 
15
122
  class NilClass
@@ -36,8 +143,33 @@ class Symbol
36
143
  end
37
144
  end
38
145
 
146
+ class Ansi
147
+ ESCAPE = "\033"
148
+
149
+ def self.color(color_constant)
150
+ "#{ESCAPE}[#{color_constant}m"
151
+ end
152
+
153
+ def self.reset_color
154
+ color 0
155
+ end
156
+
157
+ def self.yellow_color
158
+ color 33
159
+ end
160
+
161
+ def self.green_color
162
+ color 32
163
+ end
164
+
165
+ def self.red_color
166
+ color 31
167
+ end
168
+ end
169
+
39
170
  class Debug
40
171
  @@silent = false
172
+ @@colorize = true
41
173
 
42
174
  # Use silence if you want to keep messages from being echoed
43
175
  # to the console.
@@ -45,25 +177,36 @@ class Debug
45
177
  @@silent = true
46
178
  end
47
179
 
180
+ def self.colorize
181
+ @@colorize
182
+ end
183
+
184
+ def self.colorize=(value)
185
+ @@colorize = value == true
186
+ end
187
+
48
188
  # Use resume when you want messages that were silenced to
49
189
  # resume displaying.
50
190
  def self.resume
51
191
  @@silent = false
52
192
  end
53
193
 
54
- def self.put_message(type, message)
55
- puts("#{type} #{caller[1]}: #{message}") unless @@silent
194
+ def self.put_message(type, message, color = Ansi.reset_color)
195
+ open_color = @@colorize ? color : ''
196
+ close_color = @@colorize ? Ansi.reset_color : ''
197
+ NSLog("#{open_color}#{type} #{caller[1]}: #{message}#{close_color}") unless @@silent
56
198
  end
57
199
 
58
200
  def self.info(msg)
59
- put_message 'INFO', msg
201
+ put_message 'INFO', msg, Ansi.green_color
60
202
  end
61
203
 
62
204
  def self.warning(msg)
63
- put_message 'WARNING', msg
205
+ put_message 'WARNING', msg, Ansi.yellow_color
64
206
  end
65
207
 
66
208
  def self.error(msg)
67
- put_message 'ERROR', msg
209
+ put_message 'ERROR', msg, Ansi.red_color
68
210
  end
69
- end
211
+
212
+ end
@@ -17,6 +17,17 @@ module MotionModel
17
17
  @default = default || nil
18
18
  end
19
19
  alias_method :add_attribute, :add_attr
20
+
21
+ def classify
22
+ case @type
23
+ when :belongs_to
24
+ @klass ||= Object.const_get @name.to_s.downcase.capitalize
25
+ when :has_many
26
+ @klass ||= Object.const_get @name.to_s.downcase.singularize.capitalize
27
+ else
28
+ raise "#{@name} is not a relation. This isn't supposed to happen."
29
+ end
30
+ end
20
31
  end
21
32
  end
22
33
  end
@@ -2,16 +2,34 @@ module MotionModel
2
2
  class FinderQuery
3
3
  attr_accessor :field_name
4
4
 
5
- def initialize(*args)
5
+ def initialize(*args)#nodoc
6
6
  @field_name = args[0] if args.length > 1
7
7
  @collection = args.last
8
8
  end
9
9
 
10
+ def belongs_to(obj, klass = nil)
11
+ @related_object = obj
12
+ @klass = klass
13
+ self
14
+ end
15
+
16
+ # Conjunction to add conditions to query.
17
+ #
18
+ # Task.find(:name => 'bob').and(:gender).eq('M')
19
+ # Task.asignees.where(:assignee_name).eq('bob')
10
20
  def and(field_name)
21
+ # TODO: Allow for Task.assignees.where(:assignee_name => 'bob')
22
+
11
23
  @field_name = field_name
12
24
  self
13
25
  end
26
+ alias_method :where, :and
14
27
 
28
+ # Specifies how to sort. only ascending sort is supported in the short
29
+ # form. For descending, implement the block form.
30
+ #
31
+ # Task.where(:name).eq('bob').order(:pay_grade).all => array of bobs ascending by pay grade
32
+ # Task.where(:name).eq('bob').order(:pay_grade){|o1, o2| o2 <=> o1} => array of bobs descending by pay grade
15
33
  def order(field = nil, &block)
16
34
  if block_given?
17
35
  @collection = @collection.sort{|o1, o2| yield(o1, o2)}
@@ -22,7 +40,7 @@ module MotionModel
22
40
  self
23
41
  end
24
42
 
25
- ######## relational methods ########
43
+ ######## relational operators ########
26
44
  def translate_case(item, case_sensitive)#nodoc
27
45
  item = item.downcase if case_sensitive === false && item.respond_to?(:downcase)
28
46
  item
@@ -38,6 +56,9 @@ module MotionModel
38
56
  self
39
57
  end
40
58
 
59
+ # performs a "like" query.
60
+ #
61
+ # Task.find(:work_group).contain('dev') => ['UI dev', 'Core dev', ...]
41
62
  def contain(query_string, options = {:case_sensitive => false})
42
63
  do_comparison(query_string) do |comparator, item|
43
64
  if options[:case_sensitive]
@@ -50,6 +71,22 @@ module MotionModel
50
71
  alias_method :contains, :contain
51
72
  alias_method :like, :contain
52
73
 
74
+ # performs a set-inclusion test.
75
+ #
76
+ # Task.find(:id).id([3, 5, 9])
77
+ def in(set)
78
+ @collection = @collection.collect do |item|
79
+ item if set.include?(item.send(@field_name.to_sym))
80
+ end.compact
81
+ end
82
+
83
+ # performs strict equality comparison.
84
+ #
85
+ # If arguments are strings, they are, by default,
86
+ # compared case-insensitive, if case-sensitivity
87
+ # is required, use:
88
+ #
89
+ # eq('something', :case_sensitive => true)
53
90
  def eq(query_string, options = {:case_sensitive => false})
54
91
  do_comparison(query_string, options) do |comparator, item|
55
92
  comparator == item
@@ -58,6 +95,9 @@ module MotionModel
58
95
  alias_method :==, :eq
59
96
  alias_method :equal, :eq
60
97
 
98
+ # performs greater-than comparison.
99
+ #
100
+ # see `eq` for notes on case sensitivity.
61
101
  def gt(query_string, options = {:case_sensitive => false})
62
102
  do_comparison(query_string, options) do |comparator, item|
63
103
  comparator > item
@@ -66,6 +106,9 @@ module MotionModel
66
106
  alias_method :>, :gt
67
107
  alias_method :greater_than, :gt
68
108
 
109
+ # performs less-than comparison.
110
+ #
111
+ # see `eq` for notes on case sensitivity.
69
112
  def lt(query_string, options = {:case_sensitive => false})
70
113
  do_comparison(query_string, options) do |comparator, item|
71
114
  comparator < item
@@ -74,6 +117,9 @@ module MotionModel
74
117
  alias_method :<, :lt
75
118
  alias_method :less_than, :lt
76
119
 
120
+ # performs greater-than-or-equal comparison.
121
+ #
122
+ # see `eq` for notes on case sensitivity.
77
123
  def gte(query_string, options = {:case_sensitive => false})
78
124
  do_comparison(query_string, options) do |comparator, item|
79
125
  comparator >= item
@@ -82,7 +128,9 @@ module MotionModel
82
128
  alias_method :>=, :gte
83
129
  alias_method :greater_than_or_equal, :gte
84
130
 
85
-
131
+ # performs less-than-or-equal comparison.
132
+ #
133
+ # see `eq` for notes on case sensitivity.
86
134
  def lte(query_string, options = {:case_sensitive => false})
87
135
  do_comparison(query_string, options) do |comparator, item|
88
136
  comparator <= item
@@ -91,6 +139,9 @@ module MotionModel
91
139
  alias_method :<=, :lte
92
140
  alias_method :less_than_or_equal, :lte
93
141
 
142
+ # performs inequality comparison.
143
+ #
144
+ # see `eq` for notes on case sensitivity.
94
145
  def ne(query_string, options = {:case_sensitive => false})
95
146
  do_comparison(query_string, options) do |comparator, item|
96
147
  comparator != item
@@ -100,14 +151,18 @@ module MotionModel
100
151
  alias_method :not_equal, :ne
101
152
 
102
153
  ########### accessor methods #########
154
+
155
+ # returns first element that matches.
103
156
  def first
104
157
  @collection.first
105
158
  end
106
159
 
160
+ # returns last element that matches.
107
161
  def last
108
162
  @collection.last
109
163
  end
110
164
 
165
+ # returns all elements that match as an array.
111
166
  def all
112
167
  @collection
113
168
  end
@@ -120,10 +175,55 @@ module MotionModel
120
175
  raise ArgumentError.new("each requires a block") unless block_given?
121
176
  @collection.each{|item| yield item}
122
177
  end
178
+
179
+ # returns length of the result set.
180
+ def length
181
+ @collection.length
182
+ end
183
+ alias_method :count, :length
184
+
185
+ ################ relation support ##############
186
+
187
+ # task.assignees.create(:name => 'bob')
188
+ # creates a new Assignee object on the Task object task
189
+ def create(options)
190
+ raise ArgumentError.new("Creating on a relation requires the parent be saved first.") if @related_object.nil?
191
+ obj = new(options)
192
+ obj.save
193
+ obj
194
+ end
195
+
196
+ # task.assignees.new(:name => 'BoB')
197
+ # creates a new unsaved Assignee object on the Task object task
198
+ def new(options)
199
+ raise ArgumentError.new("Creating on a relation requires the parent be saved first.") if @related_object.nil?
200
+
201
+ id_field = (@related_object.class.to_s.downcase + '_id').to_sym
202
+ new_obj = @klass.new(options.merge(id_field => @related_object.id))
203
+
204
+ new_obj
205
+ end
123
206
 
124
207
  def length
125
208
  @collection.length
126
209
  end
127
210
  alias_method :count, :length
211
+
212
+ # Pushes an object onto an association. For e.g.:
213
+ #
214
+ # Task.find(3).assignees.push(assignee)
215
+ #
216
+ # This both establishes the relation and saves the related
217
+ # object, so make sure the related object is valid.
218
+ def push(object)
219
+ id_field = (@related_object.class.to_s.downcase + '_id=').to_sym
220
+ object.send(id_field, @related_object.id)
221
+ result = object.save
222
+ result ||= @related_object.save
223
+ result
224
+ end
225
+ alias_method :<<, :push
226
+
227
+
128
228
  end
129
229
  end
@@ -43,10 +43,11 @@ module MotionModel
43
43
  module Model
44
44
  def self.included(base)
45
45
  base.extend(ClassMethods)
46
- base.instance_variable_set("@_columns", [])
47
- base.instance_variable_set("@_column_hashes", {})
48
- base.instance_variable_set("@collection", [])
49
- base.instance_variable_set("@_next_id", 1)
46
+ base.instance_variable_set("@_columns", []) # Columns in model
47
+ base.instance_variable_set("@_column_hashes", {}) # Hashes to for quick column lookup
48
+ base.instance_variable_set("@_relations", {}) # relations
49
+ base.instance_variable_set("@collection", []) # Actual data
50
+ base.instance_variable_set("@_next_id", 1) # Next assignable id
50
51
  end
51
52
 
52
53
  module ClassMethods
@@ -101,6 +102,52 @@ module MotionModel
101
102
  end
102
103
  end
103
104
 
105
+ # Use at class level, as follows:
106
+ #
107
+ # class Task
108
+ # include MotionModel::Model
109
+ #
110
+ # columns :name, :details, :assignees
111
+ # has_many :assignees
112
+ #
113
+ # Note that :assignees must be declared as a virtual attribute on the
114
+ # model before you can has_many on it.
115
+ #
116
+ # This enables code like:
117
+ #
118
+ # Task.find(:due_date).gt(Time.now).first.assignees
119
+ #
120
+ # to get the people assigned to first task that is due after right now.
121
+ #
122
+ # This must be used with a belongs_to macro in the related model class
123
+ # if you want to be able to access the inverse relation.
124
+ def has_many(*relations)
125
+ relations.each do |relation|
126
+ raise ArgumentError.new("arguments to has_many must be a symbol, a string or an array of same.") unless relation.is_a?(Symbol) || relation.is_a?(String)
127
+ add_field relation, :has_many # Relation must be plural
128
+ end
129
+ end
130
+
131
+ def belongs_to_id(relation)
132
+ (relation.downcase + '_id').to_sym
133
+ end
134
+
135
+ # Use at class level, as follows
136
+ #
137
+ # class Assignee
138
+ # include MotionModel::Model
139
+ #
140
+ # columns :assignee_name, :department
141
+ # belongs_to :task
142
+ #
143
+ # Allows code like this:
144
+ #
145
+ # Assignee.find(:assignee_name).like('smith').first.task
146
+ def belongs_to(relation)
147
+ add_field relation, :belongs_to
148
+ add_field belongs_to_id(relation), :belongs_to_id # a relation is singular.
149
+ end
150
+
104
151
  # Returns a column denoted by +name+
105
152
  def column_named(name)
106
153
  @_column_hashes[name.to_sym]
@@ -135,7 +182,17 @@ module MotionModel
135
182
  def default(column)
136
183
  column_named(column).default || nil
137
184
  end
138
-
185
+
186
+ def has_relation?(col)
187
+ col = case col
188
+ when MotionModel::Model::Column
189
+ column_named(col.name)
190
+ else
191
+ column_named(col)
192
+ end
193
+ col.type == :has_many || col.type == :belongs_to
194
+ end
195
+
139
196
  # Creates an object and saves it. E.g.:
140
197
  #
141
198
  # @bob = Person.create(:name => 'Bob', :hobby => 'Bird Watching')
@@ -161,6 +218,7 @@ module MotionModel
161
218
  # Empties the entire store.
162
219
  def delete_all
163
220
  @collection = [] # TODO: Handle cascading or let GC take care of it.
221
+ @_next_id = 1
164
222
  @collection.compact!
165
223
  end
166
224
 
@@ -180,7 +238,7 @@ module MotionModel
180
238
  end
181
239
 
182
240
  unless args[0].is_a?(Symbol) || args[0].is_a?(String)
183
- return @collection[args[0].to_i] || nil
241
+ return @collection.select{|c| c.id == args[0].to_i}.first || nil
184
242
  end
185
243
 
186
244
  FinderQuery.new(args[0].to_sym, @collection)
@@ -224,7 +282,7 @@ module MotionModel
224
282
  @tz_offset ||= NSDate.date.to_s.gsub(/^.*?( -\d{4})/, '\1')
225
283
 
226
284
  @cached_date_formatter = NSDateFormatter.alloc.init # Create once, as they are expensive to create
227
- @cached_date_formatter.dateFormat = "yyyy-MM-dd HH:mm"
285
+ @cached_date_formatter.dateFormat = "MM-dd-yyyy HH:mm"
228
286
 
229
287
  unless options[:id]
230
288
  options[:id] = self.class.next_id
@@ -234,9 +292,15 @@ module MotionModel
234
292
  end
235
293
 
236
294
  columns.each do |col|
237
- options[col] ||= self.class.default(col)
238
- cast_value = cast_to_type(col, options[col])
239
- @data[col] = cast_value
295
+ unless [:belongs_to, :belongs_to_id, :has_many].include? column_named(col).type
296
+ options[col] ||= self.class.default(col)
297
+ cast_value = cast_to_type(col, options[col])
298
+ @data[col] = cast_value
299
+ else
300
+ if column_named(col).type == :belongs_to_id
301
+ @data[col] = options[col]
302
+ end
303
+ end
240
304
  end
241
305
 
242
306
  dirty = true
@@ -264,7 +328,7 @@ module MotionModel
264
328
  end
265
329
 
266
330
  def to_s
267
- columns.each{|c| "#{c}: #{self.send(c)}"}
331
+ columns.each{|c| "#{c}: #{self.send(c)}\n"}
268
332
  end
269
333
 
270
334
  def save
@@ -318,6 +382,32 @@ module MotionModel
318
382
  @dirty
319
383
  end
320
384
 
385
+ def relation_for(col)
386
+ # relation is a belongs_to or a has_many
387
+ case col.type
388
+ when :belongs_to
389
+ # column = col.match(/^(.*)_id$/)
390
+ # column = column[0] if column.length > 1
391
+ result = col.classify.find( # for clarity, we get the class
392
+ @data.send( # and look inside it to find the
393
+ :[], :id # parent element that the current
394
+ ) # object belongs to.
395
+ )
396
+ result
397
+ when :has_many
398
+ belongs_to_id = self.class.send(:belongs_to_id, self.class.to_s)
399
+
400
+ # For has_many to work, the finder query needs the
401
+ # actual object, and the class of the relation
402
+ result = col.classify.find(belongs_to_id).belongs_to(self, col.classify).eq( # find all elements of belongs_to
403
+ @data.send(:[], :id) # class that have the ID of this element Task.find(:assignee_id).eq(3)
404
+ )
405
+ result
406
+ else
407
+ nil
408
+ end
409
+ end
410
+
321
411
  # Handle attribute retrieval
322
412
  #
323
413
  # Gets and sets work as expected, and type casting occurs
@@ -330,15 +420,21 @@ module MotionModel
330
420
  # date = Task.date
331
421
  #
332
422
  # Date is a real date object.
333
- def method_missing(method, *args, &block)
423
+ def method_missing(method, *args, &block)
334
424
  base_method = method.to_s.gsub('=', '').to_sym
335
425
 
336
426
  col = column_named(base_method)
427
+ raise RuntimeError.new("nil column #{method} accessed.") if col.nil?
428
+
429
+ unless col.type == :belongs_to_id
430
+ has_relation = relation_for(col) if self.class.has_relation?(col)
431
+ return has_relation if has_relation
432
+ end
337
433
 
338
434
  if col
339
435
  if method.to_s.include?('=')
340
436
  @dirty = true
341
- return @data[base_method] = self.cast_to_type(base_method, args[0])
437
+ return @data[base_method] = col.type == :belongs_to_id ? args[0] : self.cast_to_type(base_method, args[0])
342
438
  else
343
439
  return @data[base_method]
344
440
  end
@@ -353,4 +449,3 @@ ERRORINFO
353
449
 
354
450
  end
355
451
  end
356
-
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.2.3"
2
+ VERSION = "0.2.4"
3
3
  end
data/spec/ext_spec.rb ADDED
@@ -0,0 +1,57 @@
1
+ describe 'Extensions' do
2
+ describe 'Pluralization' do
3
+ it 'pluralizes a normal word: dog' do
4
+ Inflector.inflections.pluralize('dog').should == 'dogs'
5
+ end
6
+
7
+ it 'pluralizes words that end in "s": pass' do
8
+ Inflector.inflections.pluralize('pass').should == 'passes'
9
+ end
10
+
11
+ it "pluralizes words that end in 'us'" do
12
+ Inflector.inflections.pluralize('alumnus').should == 'alumni'
13
+ end
14
+
15
+ it "pluralizes words that end in 'ee'" do
16
+ Inflector.inflections.pluralize('attendee').should == 'attendees'
17
+ end
18
+ end
19
+
20
+ describe 'Singularization' do
21
+ it 'singularizes a normal word: "dogs"' do
22
+ Inflector.inflections.singularize('dogs').should == 'dog'
23
+ end
24
+
25
+ it "singualarizes a word that ends in 's': passes" do
26
+ Inflector.inflections.singularize('passes').should == 'pass'
27
+ end
28
+
29
+ it "singualarizes a word that ends in 'ee': assignees" do
30
+ Inflector.inflections.singularize('assignees').should == 'assignee'
31
+ end
32
+
33
+ it "singualarizes words that end in 'us'" do
34
+ Inflector.inflections.singularize('alumni').should == 'alumnus'
35
+ end
36
+ end
37
+
38
+ describe 'Irregular Patterns' do
39
+ it "handles person to people singularizing" do
40
+ Inflector.inflections.singularize('people').should == 'person'
41
+ end
42
+
43
+ it "handles person to people pluralizing" do
44
+ Inflector.inflections.singularize('person').should == 'people'
45
+ end
46
+ end
47
+
48
+ describe 'Adding Rules to Inflector' do
49
+ it 'accepts new rules' do
50
+ Inflector.inflections.irregular /^foot$/, 'feet'
51
+ Inflector.inflections.irregular /^feet$/, 'foot'
52
+ Inflector.inflections.pluralize('foot').should == 'feet'
53
+ Inflector.inflections.singularize('feet').should == 'foot'
54
+ end
55
+ end
56
+ end
57
+
data/spec/model_spec.rb CHANGED
@@ -180,6 +180,20 @@ describe "Creating a model" do
180
180
  found_task = Task.where(:details).contain("s 1").first.details.should == 'details 1'
181
181
  end
182
182
 
183
+ it "performs set inclusion(in) queries" do
184
+ class InTest
185
+ include MotionModel::Model
186
+ columns :name
187
+ end
188
+
189
+ 1.upto(10) do |i|
190
+ InTest.create(:id => i, :name => "test #{i}")
191
+ end
192
+
193
+ results = InTest.find(:id).in([3, 5, 7])
194
+ results.length.should == 3
195
+ end
196
+
183
197
  it 'handles case-sensitive queries' do
184
198
  task = Task.create :name => 'Bob'
185
199
  Task.find(:name).eq('bob', :case_sensitive => true).all.length.should == 0
@@ -0,0 +1,92 @@
1
+ class Assignee
2
+ include MotionModel::Model
3
+ columns :assignee_name => :string
4
+ belongs_to :task
5
+ end
6
+
7
+ class Task
8
+ include MotionModel::Model
9
+ columns :name => :string,
10
+ :details => :string,
11
+ :some_day => :date
12
+ has_many :assignees
13
+ end
14
+
15
+
16
+ Inflector.inflections.irregular 'assignees', 'assignee'
17
+ Inflector.inflections.irregular 'assignee', 'assignees'
18
+
19
+ describe 'related objects' do
20
+ describe 'has_many' do
21
+ it "is wired up right" do
22
+ lambda {Task.new}.should.not.raise
23
+ lambda {Task.new.assignees}.should.not.raise
24
+ end
25
+
26
+ it 'relation objects are empty on initialization' do
27
+ a_task = Task.create
28
+ a_task.assignees.all.should.be.empty
29
+ end
30
+
31
+ it "supports creating related objects directly on parents" do
32
+ a_task = Task.create(:name => 'Walk the Dog')
33
+ a_task.assignees.create(:assignee_name => 'bob')
34
+ a_task.assignees.length.should == 1
35
+ a_task.assignees.first.assignee_name.should == 'bob'
36
+ Assignee.count.should == 1
37
+ end
38
+
39
+ describe "supporting has_many" do
40
+ before do
41
+ Task.delete_all
42
+ Assignee.delete_all
43
+
44
+ @tasks = []
45
+ @assignees = []
46
+ 1.upto(3) do |task|
47
+ t = Task.create(:name => "task #{task}")
48
+ assignee_index = 1
49
+ @tasks << t
50
+ 1.upto(task * 2) do |assignee|
51
+ @assignees << t.assignees.create(:assignee_name => "employee #{assignee_index}_assignee_for_task_#{t.id}")
52
+ assignee_index += 1
53
+ end
54
+ end
55
+ end
56
+
57
+ it "is wired up right" do
58
+ Task.count.should == 3
59
+ Assignee.count.should == 12
60
+ end
61
+
62
+ it "has 2 assignees for the first task" do
63
+ Task.first.assignees.count.should == 2
64
+ end
65
+
66
+ it "the first assignee for the second task is employee 7" do
67
+ Task.find(2).name.should == @tasks[1].name
68
+ Task.find(2).assignees.first.assignee_name.should == @assignees[2].assignee_name
69
+ end
70
+ end
71
+
72
+ it 'supports adding related objects to parents' do
73
+ assignee = Assignee.new(:assignee_name => 'Zoe')
74
+ assignee_count = Task.find(3).assignees.count
75
+ Task.find(3).assignees.push(assignee)
76
+ Task.find(3).assignees.count.should == assignee_count + 1
77
+ end
78
+ end
79
+
80
+ describe "supporting belongs_to" do
81
+ before do
82
+ Task.delete_all
83
+ Assignee.delete_all
84
+ end
85
+
86
+ it "allows a child to back-reference its parent" do
87
+ t = Task.create(:name => "Walk the Dog")
88
+ t.assignees.create(:assignee_name => "Rihanna")
89
+ Assignee.first.task.name.should == "Walk the Dog"
90
+ end
91
+ end
92
+ 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.3
4
+ version: 0.2.4
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-09-13 00:00:00.000000000 Z
12
+ date: 2012-09-22 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Simple model and validation mixins for RubyMotion
15
15
  email:
@@ -33,8 +33,10 @@ files:
33
33
  - lib/motion_model/validatable.rb
34
34
  - lib/motion_model/version.rb
35
35
  - motion_model.gemspec
36
+ - spec/ext_spec.rb
36
37
  - spec/model_spec.rb
37
38
  - spec/persistence_spec.rb
39
+ - spec/relation_spec.rb
38
40
  homepage: https://github.com/sxross/MotionModel
39
41
  licenses: []
40
42
  post_install_message:
@@ -60,5 +62,7 @@ signing_key:
60
62
  specification_version: 3
61
63
  summary: Simple model and validation mixins for RubyMotion
62
64
  test_files:
65
+ - spec/ext_spec.rb
63
66
  - spec/model_spec.rb
64
67
  - spec/persistence_spec.rb
68
+ - spec/relation_spec.rb