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