motion_model 0.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +13 -0
- data/README.md +35 -2
- data/Rakefile +1 -1
- data/app/app_delegate.rb +1 -1
- data/lib/motion_model/finder_query.rb +123 -0
- data/lib/motion_model/input_helpers.rb +7 -7
- data/lib/motion_model/model.rb +174 -142
- data/lib/motion_model/version.rb +1 -1
- data/motion_model.gemspec +0 -1
- data/spec/model_spec.rb +150 -21
- metadata +4 -2
data/CHANGELOG
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
2012-09-05: Basically rewrote how the data is stored.
|
2
|
+
|
3
|
+
The API remains consistent, but a certain amount of
|
4
|
+
efficiency is added by adding hashes to map column names
|
5
|
+
to the column metadata.
|
6
|
+
|
7
|
+
* Type casting now works, and is a function of initialization
|
8
|
+
and of assignment.
|
9
|
+
|
10
|
+
* Default values have been added to fill in values
|
11
|
+
if not specified in new or create.
|
12
|
+
|
13
|
+
2012-09-06: Added block-style finders. Added delete method.
|
data/README.md
CHANGED
@@ -23,9 +23,9 @@ are:
|
|
23
23
|
- input_helpers: Hooking an array up to a data form, populating
|
24
24
|
it, and retrieving the data afterwards can be a bunch of code.
|
25
25
|
Not something I'd like to write more often that I have to. These
|
26
|
-
helpers are certainly not the focus of this
|
26
|
+
helpers are certainly not the focus of this release, but
|
27
27
|
I am using these in an app to create Apple-like input forms in
|
28
|
-
static tables.
|
28
|
+
static tables.
|
29
29
|
|
30
30
|
What Model Can Do
|
31
31
|
================
|
@@ -50,6 +50,17 @@ class MyCoolController
|
|
50
50
|
end
|
51
51
|
```
|
52
52
|
|
53
|
+
Models support default values, so if you specify your model like this, you get defaults:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Task
|
57
|
+
include MotionModel::Model
|
58
|
+
|
59
|
+
columns :name => :string,
|
60
|
+
:due_date => {:type => :date, :default => '2012-09-15'}
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
53
64
|
You can also include the `Validations` module to get field validation. For example:
|
54
65
|
|
55
66
|
```ruby
|
@@ -74,6 +85,16 @@ class MyCoolController
|
|
74
85
|
end
|
75
86
|
```
|
76
87
|
|
88
|
+
*Important Note*: Type casting occurs at initialization and on assignment. That means
|
89
|
+
If you have a field type `int`, it will be changed from a string to an integer when you
|
90
|
+
initialize the object of your class type or when you assign to the integer field in your class.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
a_task = Task.create(:name => 'joe-bob', :due_date => '2012-09-15') # due_date is cast to NSDate
|
94
|
+
|
95
|
+
a_task.due_date = '2012-09-19' # due_date is cast to NSDate
|
96
|
+
```
|
97
|
+
|
77
98
|
Model Instances and Unique IDs
|
78
99
|
-----------------
|
79
100
|
|
@@ -97,6 +118,12 @@ Things That Work
|
|
97
118
|
@tasks = Task.where(:assigned_to).eq('bob').and(:location).contains('seattle')
|
98
119
|
@tasks.all.each { |task| do_something_with(task) }
|
99
120
|
```
|
121
|
+
|
122
|
+
You can use a block with find:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
@tasks = Task.find{|task| task.name =~ /dog/i && task.assigned_to == 'Bob'}
|
126
|
+
```
|
100
127
|
|
101
128
|
You can perform ordering using either a field name or block syntax. Here's an example:
|
102
129
|
|
@@ -168,3 +195,9 @@ Things In The Pipeline
|
|
168
195
|
- Testing relations
|
169
196
|
- Adding validations and custom validations
|
170
197
|
- Did I say more tests?
|
198
|
+
|
199
|
+
Problems/Comments
|
200
|
+
------------------
|
201
|
+
|
202
|
+
Please raise an issue if you find something that doesn't work, some
|
203
|
+
syntax that smells, etc.
|
data/Rakefile
CHANGED
@@ -6,5 +6,5 @@ Motion::Project::App.setup do |app|
|
|
6
6
|
# Use `rake config' to see complete project settings.
|
7
7
|
app.delegate_class = 'FakeDelegate'
|
8
8
|
app.files = Dir.glob('./lib/motion_model/**/*.rb')
|
9
|
-
app.files = (Dir.glob('./app/**/*.rb')
|
9
|
+
app.files = (app.files + Dir.glob('./app/**/*.rb')).uniq
|
10
10
|
end
|
data/app/app_delegate.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
class FakeDelegate
|
2
|
-
end
|
2
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module MotionModel
|
2
|
+
class FinderQuery
|
3
|
+
attr_accessor :field_name
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
@field_name = args[0] if args.length > 1
|
7
|
+
@collection = args.last
|
8
|
+
end
|
9
|
+
|
10
|
+
def and(field_name)
|
11
|
+
@field_name = field_name
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def order(field = nil, &block)
|
16
|
+
if block_given?
|
17
|
+
@collection = @collection.sort{|o1, o2| yield(o1, o2)}
|
18
|
+
else
|
19
|
+
raise ArgumentError.new('you must supply a field name to sort unless you supply a block.') if field.nil?
|
20
|
+
@collection = @collection.sort{|o1, o2| o1.send(field) <=> o2.send(field)}
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
######## relational methods ########
|
26
|
+
def do_comparison(query_string, options = {:case_sensitive => false})
|
27
|
+
query_string = query_string.downcase if query_string.respond_to?(:downcase) && !options[:case_sensitive]
|
28
|
+
@collection = @collection.select do |item|
|
29
|
+
comparator = item.send(@field_name.to_sym)
|
30
|
+
yield query_string, comparator
|
31
|
+
end
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def contain(query_string, options = {:case_sensitive => false})
|
36
|
+
do_comparison(query_string) do |comparator, item|
|
37
|
+
if options[:case_sensitive]
|
38
|
+
item =~ Regexp.new(comparator, Regexp::MULTILINE)
|
39
|
+
else
|
40
|
+
item =~ Regexp.new(comparator, Regexp::IGNORECASE | Regexp::MULTILINE)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
alias_method :contains, :contain
|
45
|
+
alias_method :like, :contain
|
46
|
+
|
47
|
+
def eq(query_string, options = {:case_sensitive => false})
|
48
|
+
do_comparison(query_string, options) do |comparator, item|
|
49
|
+
comparator == item
|
50
|
+
end
|
51
|
+
end
|
52
|
+
alias_method :==, :eq
|
53
|
+
alias_method :equal, :eq
|
54
|
+
|
55
|
+
def gt(query_string, options = {:case_sensitive => false})
|
56
|
+
do_comparison(query_string, options) do |comparator, item|
|
57
|
+
comparator > item
|
58
|
+
end
|
59
|
+
end
|
60
|
+
alias_method :>, :gt
|
61
|
+
alias_method :greater_than, :gt
|
62
|
+
|
63
|
+
def lt(query_string, options = {:case_sensitive => false})
|
64
|
+
do_comparison(query_string, options) do |comparator, item|
|
65
|
+
comparator < item
|
66
|
+
end
|
67
|
+
end
|
68
|
+
alias_method :<, :lt
|
69
|
+
alias_method :less_than, :lt
|
70
|
+
|
71
|
+
def gte(query_string, options = {:case_sensitive => false})
|
72
|
+
do_comparison(query_string, options) do |comparator, item|
|
73
|
+
comparator >= item
|
74
|
+
end
|
75
|
+
end
|
76
|
+
alias_method :>=, :gte
|
77
|
+
alias_method :greater_than_or_equal, :gte
|
78
|
+
|
79
|
+
|
80
|
+
def lte(query_string, options = {:case_sensitive => false})
|
81
|
+
do_comparison(query_string, options) do |comparator, item|
|
82
|
+
comparator <= item
|
83
|
+
end
|
84
|
+
end
|
85
|
+
alias_method :<=, :lte
|
86
|
+
alias_method :less_than_or_equal, :lte
|
87
|
+
|
88
|
+
def ne(query_string, options = {:case_sensitive => false})
|
89
|
+
do_comparison(query_string, options) do |comparator, item|
|
90
|
+
comparator != item
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method :!=, :ne
|
94
|
+
alias_method :not_equal, :ne
|
95
|
+
|
96
|
+
########### accessor methods #########
|
97
|
+
def first
|
98
|
+
@collection.first
|
99
|
+
end
|
100
|
+
|
101
|
+
def last
|
102
|
+
@collection.last
|
103
|
+
end
|
104
|
+
|
105
|
+
def all
|
106
|
+
@collection
|
107
|
+
end
|
108
|
+
|
109
|
+
# each is a shortcut method to turn a query into an iterator. It allows
|
110
|
+
# you to write code like:
|
111
|
+
#
|
112
|
+
# Task.where(:assignee).eq('bob').each{ |assignee| do_something_with(assignee) }
|
113
|
+
def each(&block)
|
114
|
+
raise ArgumentError.new("each requires a block") unless block_given?
|
115
|
+
@collection.each{|item| yield item}
|
116
|
+
end
|
117
|
+
|
118
|
+
def length
|
119
|
+
@collection.length
|
120
|
+
end
|
121
|
+
alias_method :count, :length
|
122
|
+
end
|
123
|
+
end
|
@@ -16,7 +16,7 @@ module MotionModel
|
|
16
16
|
|
17
17
|
def self.included(base)
|
18
18
|
base.extend(ClassMethods)
|
19
|
-
base.instance_variable_set('@
|
19
|
+
base.instance_variable_set('@binding_data', [])
|
20
20
|
end
|
21
21
|
|
22
22
|
module ClassMethods
|
@@ -39,7 +39,7 @@ module MotionModel
|
|
39
39
|
def field(field, options = {})
|
40
40
|
puts "adding field #{field}"
|
41
41
|
label = options[:label] || field.humanize
|
42
|
-
@
|
42
|
+
@binding_data << FieldBindingMap.new(:label => label, :name => field)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -61,7 +61,7 @@ module MotionModel
|
|
61
61
|
# end
|
62
62
|
|
63
63
|
def field_count
|
64
|
-
self.class.instance_variable_get('@
|
64
|
+
self.class.instance_variable_get('@binding_data'.to_sym).length
|
65
65
|
end
|
66
66
|
|
67
67
|
# +field_at+ retrieves the field at a given index.
|
@@ -72,7 +72,7 @@ module MotionModel
|
|
72
72
|
# label_view = subview(UILabel, :label_frame, text: field.label)
|
73
73
|
|
74
74
|
def field_at(index)
|
75
|
-
data = self.class.instance_variable_get('@
|
75
|
+
data = self.class.instance_variable_get('@binding_data'.to_sym)
|
76
76
|
data[index].tag = index + 1
|
77
77
|
data[index]
|
78
78
|
end
|
@@ -98,7 +98,7 @@ module MotionModel
|
|
98
98
|
# end
|
99
99
|
|
100
100
|
def fields
|
101
|
-
self.class.instance_variable_get('@
|
101
|
+
self.class.instance_variable_get('@binding_data'.to_sym).each{|datum| yield datum}
|
102
102
|
end
|
103
103
|
|
104
104
|
# +bind+ fetches all mapped fields from
|
@@ -111,9 +111,9 @@ module MotionModel
|
|
111
111
|
|
112
112
|
fields do |field|
|
113
113
|
puts "*** retrieving data for #{field.name} and tag #{field.tag} ***"
|
114
|
-
view_obj = view.viewWithTag(field.tag)
|
114
|
+
view_obj = self.view.viewWithTag(field.tag)
|
115
115
|
puts "view object with tag is #{view_obj.inspect}"
|
116
|
-
@model.send("#{field.name}=".to_sym,
|
116
|
+
@model.send("#{field.name}=".to_sym, view_obj.text)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
data/lib/motion_model/model.rb
CHANGED
@@ -23,8 +23,7 @@
|
|
23
23
|
# Recognized types are:
|
24
24
|
#
|
25
25
|
# * :string
|
26
|
-
# * :date (must be in
|
27
|
-
# * :time (must be in a form that Time.parse can recognize)
|
26
|
+
# * :date (must be in YYYY-mm-dd form)
|
28
27
|
# * :integer
|
29
28
|
# * :float
|
30
29
|
#
|
@@ -37,72 +36,119 @@
|
|
37
36
|
# tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week)
|
38
37
|
# ordered_tasks_this_week = tasks_this_week.order(:due_date)
|
39
38
|
#
|
39
|
+
|
40
40
|
module MotionModel
|
41
41
|
module Model
|
42
|
+
class Column
|
43
|
+
attr_accessor :name
|
44
|
+
attr_accessor :type
|
45
|
+
attr_accessor :default
|
46
|
+
|
47
|
+
def initialize(name = nil, type = nil, default = nil)
|
48
|
+
@name = name
|
49
|
+
@type = type
|
50
|
+
@default = default || nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_attr(name, type, default = nil)
|
54
|
+
@name = name
|
55
|
+
@type = type
|
56
|
+
@default = default || nil
|
57
|
+
end
|
58
|
+
alias_method :add_attribute, :add_attr
|
59
|
+
end
|
60
|
+
|
42
61
|
def self.included(base)
|
43
62
|
base.extend(ClassMethods)
|
44
|
-
base.instance_variable_set("@
|
45
|
-
base.instance_variable_set("@
|
63
|
+
base.instance_variable_set("@_columns", [])
|
64
|
+
base.instance_variable_set("@_column_hashes", {})
|
46
65
|
base.instance_variable_set("@collection", [])
|
47
66
|
base.instance_variable_set("@_next_id", 1)
|
48
67
|
end
|
49
68
|
|
50
69
|
module ClassMethods
|
70
|
+
def add_field(name, options, default = nil) #nodoc
|
71
|
+
col = Column.new(name, options, default)
|
72
|
+
@_columns.push col
|
73
|
+
@_column_hashes[col.name.to_sym] = col
|
74
|
+
end
|
75
|
+
|
51
76
|
# Macro to define names and types of columns. It can be used in one of
|
52
77
|
# two forms:
|
53
78
|
#
|
54
79
|
# Pass a hash, and you define columns with types. E.g.,
|
55
80
|
#
|
56
81
|
# columns :name => :string, :age => :integer
|
82
|
+
#
|
83
|
+
# Pass a hash of hashes and you can specify defaults such as:
|
84
|
+
#
|
85
|
+
# columns :name => {:type => :string, :default => 'Joe Bob'}, :age => :integer
|
57
86
|
#
|
58
87
|
# Pass an array, and you create column names, all of which have type +:string+.
|
59
88
|
#
|
60
89
|
# columns :name, :age, :hobby
|
90
|
+
|
61
91
|
def columns(*fields)
|
62
|
-
return @
|
92
|
+
return @_columns.map{|c| c.name} if fields.empty?
|
63
93
|
|
94
|
+
col = Column.new
|
95
|
+
|
64
96
|
case fields.first
|
65
97
|
when Hash
|
66
|
-
fields.first.each_pair do |
|
67
|
-
|
98
|
+
fields.first.each_pair do |name, options|
|
99
|
+
case options
|
100
|
+
when Symbol, String
|
101
|
+
add_field(name, options)
|
102
|
+
when Hash
|
103
|
+
add_field(name, options[:type], options[:default])
|
104
|
+
else
|
105
|
+
raise ArgumentError.new("arguments to fields must be a symbol, a hash, or a hash of hashes.")
|
106
|
+
end
|
68
107
|
end
|
69
108
|
else
|
70
|
-
fields.each do |
|
71
|
-
|
109
|
+
fields.each do |name|
|
110
|
+
add_field(name, :string)
|
72
111
|
end
|
73
112
|
end
|
74
113
|
|
75
114
|
unless self.respond_to?(:id)
|
76
|
-
|
115
|
+
add_field(:id, :integer)
|
77
116
|
end
|
78
117
|
end
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@
|
83
|
-
@typed_attrs << type
|
118
|
+
|
119
|
+
# Returns a column denoted by +name+
|
120
|
+
def column_named(name)
|
121
|
+
@_column_hashes[name.to_sym]
|
84
122
|
end
|
85
123
|
|
124
|
+
# Returns next available id
|
86
125
|
def next_id #nodoc
|
87
126
|
@_next_id
|
88
127
|
end
|
89
128
|
|
129
|
+
# Sets next available id
|
130
|
+
def next_id=(value)
|
131
|
+
@_next_id = value
|
132
|
+
end
|
133
|
+
|
134
|
+
# Increments next available id
|
90
135
|
def increment_id #nodoc
|
91
136
|
@_next_id += 1
|
92
137
|
end
|
93
138
|
|
94
139
|
# Returns true if a column exists on this model, otherwise false.
|
95
140
|
def column?(column)
|
96
|
-
|
97
|
-
return true if key == column
|
98
|
-
}
|
99
|
-
false
|
141
|
+
respond_to?(column)
|
100
142
|
end
|
101
143
|
|
102
144
|
# Returns type of this column.
|
103
145
|
def type(column)
|
104
|
-
|
105
|
-
|
146
|
+
column_named(column).type || nil
|
147
|
+
end
|
148
|
+
|
149
|
+
# returns default value for this column or nil.
|
150
|
+
def default(column)
|
151
|
+
column_named(column).default || nil
|
106
152
|
end
|
107
153
|
|
108
154
|
# Creates an object and saves it. E.g.:
|
@@ -112,12 +158,16 @@ module MotionModel
|
|
112
158
|
# returns the object created or false.
|
113
159
|
def create(options = {})
|
114
160
|
row = self.new(options)
|
161
|
+
row.before_create if row.respond_to?(:before_create)
|
162
|
+
row.before_save if row.respond_to?(:before_save)
|
163
|
+
|
115
164
|
# TODO: Check for Validatable and if it's
|
116
165
|
# present, check valid? before saving.
|
117
|
-
|
166
|
+
|
167
|
+
row.save
|
118
168
|
row
|
119
169
|
end
|
120
|
-
|
170
|
+
|
121
171
|
def length
|
122
172
|
@collection.length
|
123
173
|
end
|
@@ -135,7 +185,14 @@ module MotionModel
|
|
135
185
|
# or...
|
136
186
|
#
|
137
187
|
# @posts = Post.find(:author).eq('bob').all
|
138
|
-
def find(*args)
|
188
|
+
def find(*args, &block)
|
189
|
+
if block_given?
|
190
|
+
matches = @collection.collect do |item|
|
191
|
+
item if yield(item)
|
192
|
+
end.compact
|
193
|
+
return FinderQuery.new(matches)
|
194
|
+
end
|
195
|
+
|
139
196
|
unless args[0].is_a?(Symbol) || args[0].is_a?(String)
|
140
197
|
return @collection[args[0].to_i] || nil
|
141
198
|
end
|
@@ -172,15 +229,62 @@ module MotionModel
|
|
172
229
|
|
173
230
|
####### Instance Methods #######
|
174
231
|
def initialize(options = {})
|
175
|
-
|
232
|
+
@data ||= {}
|
176
233
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
234
|
+
# Time zone, for future use.
|
235
|
+
@tz_offset ||= NSDate.date.to_s.gsub(/^.*?( -\d{4})/, '\1')
|
236
|
+
|
237
|
+
@cached_date_formatter = NSDateFormatter.alloc.init # Create once, as they are expensive to create
|
238
|
+
@cached_date_formatter.dateFormat = "yyyy-MM-dd HH:mm"
|
239
|
+
|
240
|
+
unless options[:id]
|
241
|
+
options[:id] = self.class.next_id
|
182
242
|
self.class.increment_id
|
243
|
+
else
|
244
|
+
self.class.next_id = [options[:id].to_i, self.class.next_id].max
|
245
|
+
end
|
246
|
+
|
247
|
+
columns.each do |col|
|
248
|
+
options[col] ||= self.class.default(col)
|
249
|
+
cast_value = cast_to_type(col, options[col])
|
250
|
+
@data[col] = cast_value
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def cast_to_type(column_name, arg)
|
255
|
+
return nil if arg.nil?
|
256
|
+
|
257
|
+
return_value = arg
|
258
|
+
|
259
|
+
case type(column_name)
|
260
|
+
when :string
|
261
|
+
return_value = arg.to_s
|
262
|
+
when :int, :integer
|
263
|
+
return_value = arg.is_a?(Integer) ? arg : arg.to_i
|
264
|
+
when :float, :double
|
265
|
+
return_value = arg.is_a?(Float) ? arg : arg.to_f
|
266
|
+
when :date
|
267
|
+
return arg if arg.is_a?(NSDate)
|
268
|
+
date_string = arg += ' 00:00'
|
269
|
+
return_value = @cached_date_formatter.dateFromString(date_string)
|
270
|
+
else
|
271
|
+
raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
183
272
|
end
|
273
|
+
return_value
|
274
|
+
end
|
275
|
+
|
276
|
+
def to_s
|
277
|
+
columns.each{|c| "#{c}: #{self.send(c)}"}
|
278
|
+
end
|
279
|
+
|
280
|
+
def save
|
281
|
+
self.class.instance_variable_get('@collection') << self
|
282
|
+
end
|
283
|
+
|
284
|
+
def delete
|
285
|
+
collection = self.class.instance_variable_get('@collection')
|
286
|
+
target_index = collection.index{|item| item.id == self.id}
|
287
|
+
collection.delete_at(target_index)
|
184
288
|
end
|
185
289
|
|
186
290
|
def length
|
@@ -190,20 +294,24 @@ module MotionModel
|
|
190
294
|
alias_method :count, :length
|
191
295
|
|
192
296
|
def column?(target_key)
|
193
|
-
self.class.column?(target_key)
|
297
|
+
self.class.column?(target_key.to_sym)
|
194
298
|
end
|
195
299
|
|
196
300
|
def columns
|
197
301
|
self.class.columns
|
198
302
|
end
|
199
303
|
|
304
|
+
def column_named(name)
|
305
|
+
self.class.column_named(name.to_sym)
|
306
|
+
end
|
307
|
+
|
200
308
|
def type(field_name)
|
201
309
|
self.class.type(field_name)
|
202
310
|
end
|
203
311
|
|
204
312
|
def initWithCoder(coder)
|
205
313
|
self.init
|
206
|
-
self.class.instance_variable_get("@
|
314
|
+
self.class.instance_variable_get("@_columns").each do |attr|
|
207
315
|
# If a model revision has taken place, don't try to decode
|
208
316
|
# something that's not there.
|
209
317
|
new_tag_id = 1
|
@@ -222,125 +330,49 @@ module MotionModel
|
|
222
330
|
end
|
223
331
|
|
224
332
|
def encodeWithCoder(coder)
|
225
|
-
self.class.instance_variable_get("@
|
333
|
+
self.class.instance_variable_get("@_columns").each do |attr|
|
226
334
|
coder.encodeObject(self.send(attr), forKey: attr.to_s)
|
227
335
|
end
|
228
336
|
end
|
229
337
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
def initialize(*args)
|
236
|
-
@field_name = args[0] if args.length > 1
|
237
|
-
@collection = args.last
|
238
|
-
end
|
239
|
-
|
240
|
-
def and(field_name)
|
241
|
-
@field_name = field_name
|
242
|
-
self
|
338
|
+
# Modify respond_to? to add model's attributes.
|
339
|
+
alias_method :old_respond_to?, :respond_to?
|
340
|
+
def respond_to?(method)
|
341
|
+
column_named(method) || old_respond_to?(method)
|
243
342
|
end
|
244
343
|
|
245
|
-
|
246
|
-
|
247
|
-
|
344
|
+
# Handle attribute retrieval
|
345
|
+
#
|
346
|
+
# Gets and sets work as expected, and type casting occurs
|
347
|
+
# For example:
|
348
|
+
#
|
349
|
+
# Task.date = '2012-09-15'
|
350
|
+
#
|
351
|
+
# This creates a real Date object in the data store.
|
352
|
+
#
|
353
|
+
# date = Task.date
|
354
|
+
#
|
355
|
+
# Date is a real date object.
|
356
|
+
def method_missing(method, *args, &block)
|
357
|
+
base_method = method.to_s.gsub('=', '').to_sym
|
358
|
+
|
359
|
+
col = column_named(base_method)
|
360
|
+
|
361
|
+
if col
|
362
|
+
if method.to_s.include?('=')
|
363
|
+
return @data[base_method] = self.cast_to_type(base_method, args[0])
|
364
|
+
else
|
365
|
+
return @data[base_method]
|
366
|
+
end
|
248
367
|
else
|
249
|
-
raise
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
######## relational methods ########
|
256
|
-
def do_comparison(query_string)
|
257
|
-
# TODO: Flag case-insensitive searching
|
258
|
-
query_string = query_string.downcase if query_string.respond_to?(:downcase)
|
259
|
-
@collection = @collection.select do |item|
|
260
|
-
comparator = item.send(@field_name.to_sym)
|
261
|
-
yield query_string, comparator
|
262
|
-
end
|
263
|
-
self
|
264
|
-
end
|
265
|
-
|
266
|
-
def contain(query_string)
|
267
|
-
do_comparison(query_string) do |comparator, item|
|
268
|
-
item =~ Regexp.new(comparator)
|
269
|
-
end
|
270
|
-
end
|
271
|
-
alias_method :contains, :contain
|
272
|
-
alias_method :like, :contain
|
273
|
-
|
274
|
-
def eq(query_string)
|
275
|
-
do_comparison(query_string) do |comparator, item|
|
276
|
-
comparator == item
|
277
|
-
end
|
278
|
-
end
|
279
|
-
alias_method :==, :eq
|
280
|
-
alias_method :equal, :eq
|
281
|
-
|
282
|
-
def gt(query_string)
|
283
|
-
do_comparison(query_string) do |comparator, item|
|
284
|
-
comparator > item
|
368
|
+
raise NoMethodError, <<ERRORINFO
|
369
|
+
method: #{method}
|
370
|
+
args: #{args.inspect}
|
371
|
+
in: #{self.class.name}
|
372
|
+
ERRORINFO
|
285
373
|
end
|
286
374
|
end
|
287
|
-
|
288
|
-
alias_method :greater_than, :gt
|
289
|
-
|
290
|
-
def lt(query_string)
|
291
|
-
do_comparison(query_string) do |comparator, item|
|
292
|
-
comparator < item
|
293
|
-
end
|
294
|
-
end
|
295
|
-
alias_method :<, :lt
|
296
|
-
alias_method :less_than, :lt
|
297
|
-
|
298
|
-
def gte(query_string)
|
299
|
-
do_comparison(query_string) do |comparator, item|
|
300
|
-
comparator >= item
|
301
|
-
end
|
302
|
-
end
|
303
|
-
alias_method :>=, :gte
|
304
|
-
alias_method :greater_than_or_equal, :gte
|
305
|
-
|
306
|
-
|
307
|
-
def lte(query_string)
|
308
|
-
do_comparison(query_string) do |comparator, item|
|
309
|
-
comparator <= item
|
310
|
-
end
|
311
|
-
end
|
312
|
-
alias_method :<=, :lte
|
313
|
-
alias_method :less_than_or_equal, :lte
|
314
|
-
|
315
|
-
def ne(query_string)
|
316
|
-
do_comparison(query_string) do |comparator, item|
|
317
|
-
comparator != item
|
318
|
-
end
|
319
|
-
end
|
320
|
-
alias_method :!=, :ne
|
321
|
-
alias_method :not_equal, :ne
|
322
|
-
|
323
|
-
########### accessor methods #########
|
324
|
-
def first
|
325
|
-
@collection.first
|
326
|
-
end
|
327
|
-
|
328
|
-
def last
|
329
|
-
@collection.last
|
330
|
-
end
|
331
|
-
|
332
|
-
def all
|
333
|
-
@collection
|
334
|
-
end
|
335
|
-
|
336
|
-
# each is a shortcut method to turn a query into an iterator. It allows
|
337
|
-
# you to write code like:
|
338
|
-
#
|
339
|
-
# Task.where(:assignee).eq('bob').each{ |assignee| do_something_with(assignee) }
|
340
|
-
def each(&block)
|
341
|
-
raise ArgumentError.new("each requires a block") unless block_given?
|
342
|
-
@collection.each{|item| yield item}
|
343
|
-
end
|
375
|
+
|
344
376
|
end
|
345
377
|
end
|
346
378
|
|
data/lib/motion_model/version.rb
CHANGED
data/motion_model.gemspec
CHANGED
@@ -9,7 +9,6 @@ Gem::Specification.new do |gem|
|
|
9
9
|
gem.homepage = "https://github.com/sxross/MotionModel"
|
10
10
|
|
11
11
|
gem.files = `git ls-files`.split($\)
|
12
|
-
puts "gem files are #{`git ls-files`.split($\)}"
|
13
12
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
13
|
gem.name = "motion_model"
|
15
14
|
gem.require_paths = ["lib"]
|
data/spec/model_spec.rb
CHANGED
@@ -10,6 +10,16 @@ class ATask
|
|
10
10
|
columns :name, :details, :some_day
|
11
11
|
end
|
12
12
|
|
13
|
+
class TypeCast
|
14
|
+
include MotionModel::Model
|
15
|
+
columns :an_int => {:type => :int, :default => 3},
|
16
|
+
:an_integer => :integer,
|
17
|
+
:a_float => :float,
|
18
|
+
:a_double => :double,
|
19
|
+
:a_date => :date,
|
20
|
+
:a_time => :time
|
21
|
+
end
|
22
|
+
|
13
23
|
describe "Creating a model" do
|
14
24
|
before do
|
15
25
|
Task.delete_all
|
@@ -18,8 +28,8 @@ describe "Creating a model" do
|
|
18
28
|
describe 'column macro behavior' do
|
19
29
|
|
20
30
|
it 'succeeds when creating a valid model from attributes' do
|
21
|
-
|
22
|
-
|
31
|
+
a_task = Task.new(:name => 'name', :details => 'details')
|
32
|
+
a_task.name.should.equal('name')
|
23
33
|
end
|
24
34
|
|
25
35
|
it 'creates a model with all attributes even if some omitted' do
|
@@ -28,45 +38,50 @@ describe "Creating a model" do
|
|
28
38
|
end
|
29
39
|
|
30
40
|
it 'simply bypasses spurious attributes erroneously set' do
|
31
|
-
|
32
|
-
|
33
|
-
|
41
|
+
a_task = Task.new(:name => 'details', :zoo => 'very bad')
|
42
|
+
a_task.should.not.respond_to(:zoo)
|
43
|
+
a_task.name.should.equal('details')
|
44
|
+
end
|
45
|
+
|
46
|
+
it "adds a default value if none supplied" do
|
47
|
+
a_type_test = TypeCast.new
|
48
|
+
a_type_test.an_int.should.equal(3)
|
34
49
|
end
|
35
50
|
|
36
51
|
it "can check for a column's existence on a model" do
|
37
|
-
|
52
|
+
Task.column?(:name).should.be.true
|
38
53
|
end
|
39
54
|
|
40
55
|
it "can check for a column's existence on an instance" do
|
41
|
-
|
42
|
-
|
56
|
+
a_task = Task.new(:name => 'name', :details => 'details')
|
57
|
+
a_task.column?(:name).should.be.true
|
43
58
|
end
|
44
59
|
|
45
60
|
it "gets a list of columns on a model" do
|
46
|
-
|
47
|
-
|
48
|
-
|
61
|
+
cols = Task.columns
|
62
|
+
cols.should.include(:name)
|
63
|
+
cols.should.include(:details)
|
49
64
|
end
|
50
65
|
|
51
66
|
it "gets a list of columns on an instance" do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
67
|
+
a_task = Task.new
|
68
|
+
cols = a_task.columns
|
69
|
+
cols.should.include(:name)
|
70
|
+
cols.should.include(:details)
|
56
71
|
end
|
57
72
|
|
58
73
|
it "columns can be specified as a Hash" do
|
59
|
-
|
60
|
-
|
74
|
+
lambda{Task.new}.should.not.raise
|
75
|
+
Task.new.column?(:name).should.be.true
|
61
76
|
end
|
62
77
|
|
63
78
|
it "columns can be specified as an Array" do
|
64
|
-
|
65
|
-
|
79
|
+
lambda{ATask.new}.should.not.raise
|
80
|
+
Task.new.column?(:name).should.be.true
|
66
81
|
end
|
67
82
|
|
68
83
|
it "the type of a column can be retrieved" do
|
69
|
-
|
84
|
+
Task.new.type(:some_day).should.equal(:date)
|
70
85
|
end
|
71
86
|
|
72
87
|
end
|
@@ -155,6 +170,11 @@ describe "Creating a model" do
|
|
155
170
|
atask = Task.create(:name => 'find me', :details => "details 1")
|
156
171
|
found_task = Task.where(:details).contain("details 1").first.details.should.equal("details 1")
|
157
172
|
end
|
173
|
+
|
174
|
+
it 'handles case-sensitive queries' do
|
175
|
+
task = Task.create :name => 'Bob'
|
176
|
+
Task.find(:name).eq('bob', :case_sensitive => true).all.should.be.empty
|
177
|
+
end
|
158
178
|
|
159
179
|
it 'all returns all members of the collection as an array' do
|
160
180
|
Task.all.length.should.equal(10)
|
@@ -167,7 +187,29 @@ describe "Creating a model" do
|
|
167
187
|
i += 1
|
168
188
|
end
|
169
189
|
end
|
170
|
-
|
190
|
+
|
191
|
+
describe 'block-style finders' do
|
192
|
+
before do
|
193
|
+
@items_less_than_5 = Task.find{|item| item.name.split(' ').last.to_i < 5}
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'returns a FinderQuery' do
|
197
|
+
@items_less_than_5.should.is_a MotionModel::FinderQuery
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'handles block-style finders' do
|
201
|
+
@items_less_than_5.length.should == 5 # Zero based
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'deals with any arbitrary block finder' do
|
205
|
+
@even_items = Task.find do |item|
|
206
|
+
test_item = item.name.split(' ').last.to_i
|
207
|
+
test_item % 2 == 0 && test_item < 5
|
208
|
+
end
|
209
|
+
@even_items.each{|item| item.name.split(' ').last.to_i.should.even?}
|
210
|
+
@even_items.length.should == 3 # [0, 2, 4]
|
211
|
+
end
|
212
|
+
end
|
171
213
|
end
|
172
214
|
|
173
215
|
describe 'sorting' do
|
@@ -197,4 +239,91 @@ describe "Creating a model" do
|
|
197
239
|
end
|
198
240
|
|
199
241
|
end
|
242
|
+
|
243
|
+
describe 'deleting' do
|
244
|
+
before do
|
245
|
+
10.times {|i| Task.create(:name => "task #{i}")}
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'deletes a row' do
|
249
|
+
target = Task.find(:name).eq('task 3').first
|
250
|
+
target.delete
|
251
|
+
Task.find(:description).eq('Task 3').should == nil
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'deleting a row changes length' do
|
255
|
+
target = Task.find(:name).eq('task 3').first
|
256
|
+
lambda{target.delete}.should.change{Task.length}
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
describe 'Handling Attribute method_missing Implementation' do
|
261
|
+
it 'raises a NoMethodError exception when an unknown attribute it referenced' do
|
262
|
+
task = Task.new
|
263
|
+
lambda{task.bar}.should.raise(NoMethodError)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
describe 'Type casting' do
|
268
|
+
before do
|
269
|
+
@convertible = TypeCast.new
|
270
|
+
@convertible.an_int = '1'
|
271
|
+
@convertible.an_integer = '2'
|
272
|
+
@convertible.a_float = '3.7'
|
273
|
+
@convertible.a_double = '3.41459'
|
274
|
+
@convertible.a_date = '2012-09-15'
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'does the type casting on instantiation' do
|
278
|
+
@convertible.an_int.should.is_a Integer
|
279
|
+
@convertible.an_integer.should.is_a Integer
|
280
|
+
@convertible.a_float.should.is_a Float
|
281
|
+
@convertible.a_double.should.is_a Float
|
282
|
+
@convertible.a_date.should.is_a NSDate
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'returns an integer for an int field' do
|
286
|
+
@convertible.an_int.should.is_a(Integer)
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'the int field should be the same as it was in string form' do
|
290
|
+
@convertible.an_int.to_s.should.equal('1')
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'returns an integer for an integer field' do
|
294
|
+
@convertible.an_integer.should.is_a(Integer)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'the integer field should be the same as it was in string form' do
|
298
|
+
@convertible.an_integer.to_s.should.equal('2')
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'returns a float for a float field' do
|
302
|
+
@convertible.a_float.should.is_a(Float)
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'the float field should be the same as it was in string form' do
|
306
|
+
@convertible.a_float.should.>(3.6)
|
307
|
+
@convertible.a_float.should.<(3.8)
|
308
|
+
end
|
309
|
+
|
310
|
+
it 'returns a double for a double field' do
|
311
|
+
@convertible.a_double.should.is_a(Float)
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'the double field should be the same as it was in string form' do
|
315
|
+
@convertible.a_double.should.>(3.41458)
|
316
|
+
@convertible.a_double.should.<(3.41460)
|
317
|
+
end
|
318
|
+
|
319
|
+
it 'returns a NSDate for a date field' do
|
320
|
+
@convertible.a_date.should.is_a(NSDate)
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'the date field should be the same as it was in string form' do
|
324
|
+
@convertible.a_date.to_s.should.match(/^2012-09-15/)
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
|
200
329
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: motion_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-06 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Simple model and validation mixins for RubyMotion
|
15
15
|
email:
|
@@ -19,11 +19,13 @@ extensions: []
|
|
19
19
|
extra_rdoc_files: []
|
20
20
|
files:
|
21
21
|
- .gitignore
|
22
|
+
- CHANGELOG
|
22
23
|
- README.md
|
23
24
|
- Rakefile
|
24
25
|
- app/app_delegate.rb
|
25
26
|
- lib/motion_model.rb
|
26
27
|
- lib/motion_model/ext.rb
|
28
|
+
- lib/motion_model/finder_query.rb
|
27
29
|
- lib/motion_model/input_helpers.rb
|
28
30
|
- lib/motion_model/model.rb
|
29
31
|
- lib/motion_model/validatable.rb
|