motion_model 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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