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 +78 -10
- data/lib/motion_model/ext.rb +149 -6
- data/lib/motion_model/model/column.rb +11 -0
- data/lib/motion_model/model/finder_query.rb +103 -3
- data/lib/motion_model/model/model.rb +109 -14
- data/lib/motion_model/version.rb +1 -1
- data/spec/ext_spec.rb +57 -0
- data/spec/model_spec.rb +14 -0
- data/spec/relation_spec.rb +92 -0
- metadata +6 -2
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
|
-
|
191
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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
|
------------------
|
data/lib/motion_model/ext.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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("@
|
49
|
-
base.instance_variable_set("@
|
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
|
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 = "
|
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
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
data/lib/motion_model/version.rb
CHANGED
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.
|
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-
|
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
|