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