motion_model 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .repl_history
2
+ build
3
+ resources/*.nib
4
+ resources/*.momd
5
+ resources/*.storyboardc
6
+ .DS_Store
7
+ doc/**/*.*
8
+ doc
9
+ *.gem
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ MotionModel -- Simple Model, Validation, and Input Mixins for RubyMotion
2
+ ================
3
+
4
+ MotionModel is for cases where Core Data is too heavy to lift but you are
5
+ still intending to work with your data.
6
+
7
+ MotionModel is a bunch of "I don't ever want to have to write that code
8
+ again if I can help it" things extracted into modules. The four modules
9
+ are:
10
+
11
+ - ext: Core Extensions that provide a few Rails-like niceties. Nothing
12
+ new here, moving on...
13
+
14
+ - model.rb: You can read about it in "What Model Can Do" but it's a
15
+ mixin that provides you accessible attributes, row indexing,
16
+ serialization for persistence, and some other niceties like row
17
+ counting.
18
+
19
+ - validatable.rb: Provides a basic validation framework for any
20
+ arbitrary class. Right now, it can only validate for presence,
21
+ but expect that to change soon.
22
+
23
+ - input_helpers: Hooking an array up to a data form, populating
24
+ it, and retrieving the data afterwards can be a bunch of code.
25
+ Not something I'd like to write more often that I have to. These
26
+ helpers are certainly not the focus of this strawman release, but
27
+ I am using these in an app to create Apple-like input forms in
28
+ static tables. I expect some churn in this module.
29
+
30
+ What Model Can Do
31
+ ================
32
+
33
+ You can define your models and their schemas in Ruby. For example:
34
+
35
+ ```ruby
36
+ class Task
37
+ include MotionModel::Model
38
+
39
+ columns :name => :string,
40
+ :description => :string,
41
+ :due_date => :date
42
+ end
43
+
44
+ class MyCoolController
45
+ def some_method
46
+ @task = Task.create :name => 'walk the dog',
47
+ :description => 'get plenty of exercise. pick up the poop',
48
+ :due_date => '2012-09-15'
49
+ end
50
+ end
51
+ ```
52
+
53
+ You can also include the `Validations` module to get field validation. For example:
54
+
55
+ ```ruby
56
+ class Task
57
+ include MotionModel::Model
58
+ include MotionModel::Validations
59
+
60
+ columns :name => :string,
61
+ :description => :string,
62
+ :due_date => :date
63
+ validates :name => :presence => true
64
+ end
65
+
66
+ class MyCoolController
67
+ def some_method
68
+ @task = Task.new :name => 'walk the dog',
69
+ :description => 'get plenty of exercise. pick up the poop',
70
+ :due_date => '2012-09-15'
71
+
72
+ show_scary_warning unless @task.valid?
73
+ end
74
+ end
75
+ ```
76
+
77
+ Model Instances and Unique IDs
78
+ -----------------
79
+
80
+ It is assumed that models can be created from an external source (JSON from a Web
81
+ application or NSCoder from the device) or simply be a stand-alone data store.
82
+ To identify rows properly, the model tracks a special field called `:id`. If it's
83
+ already present, it's left alone. If it's missing, then it is created for you.
84
+ Each row id is guaranteed to be unique, so you can use this when communicating
85
+ with a server or syncing your rowset to a UITableView.
86
+
87
+ Things That Work
88
+ -----------------
89
+
90
+ * Models, in general, work. They aren't ultra full-featured, but more is in the
91
+ works. In particular, finders are just coming online. All column data may be
92
+ accessed by member name, e.g., `@task.name`.
93
+
94
+ * Finders are implemented using chaining. Here is an examples:
95
+
96
+ ```ruby
97
+ @tasks = Task.where(:assigned_to).eq('bob').and(:location).contains('seattle')
98
+ @tasks.all.each { |task| do_something_with(task) }
99
+ ```
100
+
101
+ You can perform ordering using either a field name or block syntax. Here's an example:
102
+
103
+ ```ruby
104
+ @tasks = Task.order(:name).all # Get tasks ordered ascending by :name
105
+ @tasks = Task.order{|one, two| two.details <=> one.details}.all # Get tasks ordered descending by :details
106
+ ```
107
+
108
+ * Serialization using `NSCoder` works. Basically, you might do something like this
109
+ in your `AppDelegate`:
110
+
111
+ ```ruby
112
+ def load_data
113
+ if File.exist? documents_file("my_fine.dat")
114
+ error_ptr = Pointer.new(:object)
115
+
116
+ data = NSData.dataWithContentsOfFile(documents_file('my_fine.dat'), options:NSDataReadingMappedIfSafe, error:error_ptr)
117
+
118
+ if data.nil?
119
+ error = error_ptr[0]
120
+ show_user_scary_warning error
121
+ else
122
+ @my_data_tree = NSKeyedUnarchiver.unarchiveObjectWithData(data)
123
+ end
124
+ else
125
+ show_user_first_time_welcome
126
+ end
127
+ end
128
+ ```
129
+
130
+ and of course on the "save" side:
131
+
132
+ ```ruby
133
+ error_ptr = Pointer.new(:object)
134
+
135
+ data = NSKeyedArchiver.archivedDataWithRootObject App.delegate.events
136
+ unless data.writeToFile(documents_file('my_fine.dat'), options: NSDataWritingAtomic, error: error_ptr)
137
+ error = error_ptr[0]
138
+ show_scary_message error
139
+ end
140
+ ```
141
+
142
+ Note that the archiving of any arbitrarily complex set of relations is
143
+ automatically handled by `NSCoder` provided you conform to the coding
144
+ protocol. When you declare your columns, `MotionModel` understands how
145
+ to serialize your data so you need take no further action.
146
+
147
+ * Relations, in principle work. This is a part I'm still noodling over
148
+ so it's not really safe to use them. In any case, how I expect it will
149
+ shake out is that one-to-one or one-to-many will be supported out of
150
+ the box, but you will have to take some extra steps to implement
151
+ many-to-many, just as you would in Rails' `has_many :through`.
152
+
153
+ * Core extensions work. The following are supplied:
154
+
155
+ - String#humanize
156
+ - String#titleize
157
+ - String#empty?
158
+ - NilClass#empty?
159
+ - Array#empty?
160
+ - Hash#empty?
161
+ - Symbol#titleize
162
+
163
+ Things In The Pipeline
164
+ ----------------------
165
+
166
+ - More tests!
167
+ - More robust id assignment
168
+ - Testing relations
169
+ - Adding validations and custom validations
170
+ - Did I say more tests?
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ $:.unshift("/Library/RubyMotion/lib")
3
+ require 'motion/project'
4
+
5
+ Motion::Project::App.setup do |app|
6
+ # Use `rake config' to see complete project settings.
7
+ app.delegate_class = 'FakeDelegate'
8
+ app.files = Dir.glob('./lib/motion_model/**/*.rb')
9
+ app.files = (Dir.glob('./app/**/*.rb') + app.files).uniq
10
+ end
@@ -0,0 +1,2 @@
1
+ class FakeDelegate
2
+ end
@@ -0,0 +1,37 @@
1
+ class String
2
+ def humanize
3
+ self.split(/_|-| /).join(' ')
4
+ end
5
+
6
+ def titleize
7
+ self.split(/_|-| /).each{|word| word[0...1] = word[0...1].upcase}.join(' ')
8
+ end
9
+
10
+ def empty?
11
+ self.length < 1
12
+ end
13
+ end
14
+
15
+ class NilClass
16
+ def empty?
17
+ true
18
+ end
19
+ end
20
+
21
+ class Array
22
+ def empty?
23
+ self.length < 1
24
+ end
25
+ end
26
+
27
+ class Hash
28
+ def empty?
29
+ self.length < 1
30
+ end
31
+ end
32
+
33
+ class Symbol
34
+ def titleize
35
+ self.to_s.titleize
36
+ end
37
+ end
@@ -0,0 +1,220 @@
1
+ module MotionModel
2
+ module InputHelpers
3
+ class ModelNotSetError < RuntimeError; end
4
+
5
+ # FieldBindingMap contains a simple label to model
6
+ # field binding, and is decorated by a tag to be
7
+ # used on the UI control.
8
+ class FieldBindingMap
9
+ attr_accessor :label, :name, :tag
10
+
11
+ def initialize(options = {})
12
+ @name = options[:name]
13
+ @label = options[:label]
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
19
+ base.instance_variable_set('@data', [])
20
+ end
21
+
22
+ module ClassMethods
23
+ # +field+ is a declarative macro that specifies
24
+ # the field name (i.e., the model field name)
25
+ # and the label. In the absence of a label,
26
+ # +field+ attempts to synthesize one from the
27
+ # model field name. YMMV.
28
+ #
29
+ # Usage:
30
+ #
31
+ # class MyInputSheet < UIViewController
32
+ # include InputHelpers
33
+ #
34
+ # field 'event_name', :label => 'name'
35
+ # field 'event_location', :label => 'location
36
+ #
37
+ # Only one field mapping may be supplied for
38
+ # a given class.
39
+ def field(field, options = {})
40
+ puts "adding field #{field}"
41
+ label = options[:label] || field.humanize
42
+ @data << FieldBindingMap.new(:label => label, :name => field)
43
+ end
44
+ end
45
+
46
+ # +model+ is a mandatory method in which you
47
+ # specify the instance of the model to which
48
+ # your fields are bound.
49
+
50
+ def model(model_instance)
51
+ @model = model_instance
52
+ end
53
+
54
+ # +field_count+ specifies how many fields have
55
+ # been bound.
56
+ #
57
+ # Usage:
58
+ #
59
+ # def tableView(table, numberOfRowsInSection: section)
60
+ # field_count
61
+ # end
62
+
63
+ def field_count
64
+ self.class.instance_variable_get('@data'.to_sym).length
65
+ end
66
+
67
+ # +field_at+ retrieves the field at a given index.
68
+ #
69
+ # Usage:
70
+ #
71
+ # field = field_at(indexPath.row)
72
+ # label_view = subview(UILabel, :label_frame, text: field.label)
73
+
74
+ def field_at(index)
75
+ data = self.class.instance_variable_get('@data'.to_sym)
76
+ data[index].tag = index + 1
77
+ data[index]
78
+ end
79
+
80
+ # +value_at+ retrieves the value from the form that corresponds
81
+ # to the name of the field.
82
+ #
83
+ # Usage:
84
+ #
85
+ # value_edit_view = subview(UITextField, :input_value_frame, text: value_at(field))
86
+
87
+ def value_at(field)
88
+ @model.send(field.name)
89
+ end
90
+
91
+ # +fields+ is the iterator for all fields
92
+ # mapped for this class.
93
+ #
94
+ # Usage:
95
+ #
96
+ # fields do |field|
97
+ # do_something_with field.label, field.value
98
+ # end
99
+
100
+ def fields
101
+ self.class.instance_variable_get('@data'.to_sym).each{|datum| yield datum}
102
+ end
103
+
104
+ # +bind+ fetches all mapped fields from
105
+ # any subview of the current +UIView+
106
+ # and transfers the contents to the
107
+ # corresponding fields of the model
108
+ # specified by the +model+ method.
109
+ def bind
110
+ raise ModelNotSetError.new("You must set the model before binding it.") unless @model
111
+
112
+ fields do |field|
113
+ puts "*** retrieving data for #{field.name} and tag #{field.tag} ***"
114
+ view_obj = view.viewWithTag(field.tag)
115
+ puts "view object with tag is #{view_obj.inspect}"
116
+ @model.send("#{field.name}=".to_sym, view.viewWithTag(field.tag).text)
117
+ end
118
+ end
119
+
120
+ # Handle hiding the keyboard if the user
121
+ # taps "return". If you don't want this behavior,
122
+ # define the function as empty in your class.
123
+ def textFieldShouldReturn(textField)
124
+ textField.resignFirstResponder
125
+ end
126
+
127
+ # Keyboard show/hide handlers do this:
128
+ #
129
+ # * Reset the table insets so that the
130
+ # UITableView knows how large its real
131
+ # visible area.
132
+ # * Scroll the UITableView to reveal the
133
+ # cell that has the +firstResponder+
134
+ # if it is not already showing.
135
+ #
136
+ # Of course, the process is exactly reversed
137
+ # when the keyboard hides.
138
+ #
139
+ # An instance variable +@table+ is assumed to
140
+ # be the table to affect; if this is missing,
141
+ # this code will simply no-op.
142
+ #
143
+ # Rejigger everything under the sun when the
144
+ # keyboard slides up.
145
+ #
146
+ # You *must* handle the +UIKeyboardWillShowNotification+ and
147
+ # when you receive it, call this method to handle the keyboard
148
+ # showing.
149
+ def handle_keyboard_will_show(notification)
150
+ return unless @table
151
+
152
+ animationCurve = notification.userInfo.valueForKey(UIKeyboardAnimationCurveUserInfoKey)
153
+ animationDuration = notification.userInfo.valueForKey(UIKeyboardAnimationDurationUserInfoKey)
154
+ keyboardEndRect = notification.userInfo.valueForKey(UIKeyboardFrameEndUserInfoKey)
155
+
156
+ keyboardEndRect = view.convertRect(keyboardEndRect.CGRectValue, fromView:App.delegate.window)
157
+
158
+ UIView.beginAnimations "changeTableViewContentInset", context:nil
159
+ UIView.setAnimationDuration animationDuration
160
+ UIView.setAnimationCurve animationCurve
161
+
162
+ intersectionOfKeyboardRectAndWindowRect = CGRectIntersection(App.delegate.window.frame, keyboardEndRect)
163
+ bottomInset = intersectionOfKeyboardRectAndWindowRect.size.height;
164
+
165
+ @table.contentInset = UIEdgeInsetsMake(0, 0, bottomInset, 0)
166
+
167
+ # Find active cell
168
+ indexPathOfOwnerCell = nil
169
+ numberOfCells = @table.dataSource.tableView(@table, numberOfRowsInSection:0)
170
+ 0.upto(numberOfCells) do |index|
171
+ indexPath = NSIndexPath.indexPathForRow(index, inSection:0)
172
+ cell = @table.cellForRowAtIndexPath(indexPath)
173
+ if cell_has_first_responder?(cell)
174
+ indexPathOfOwnerCell = indexPath
175
+ break
176
+ end
177
+ end
178
+
179
+ UIView.commitAnimations
180
+
181
+ if indexPathOfOwnerCell
182
+ @table.scrollToRowAtIndexPath(indexPathOfOwnerCell,
183
+ atScrollPosition:UITableViewScrollPositionMiddle,
184
+ animated: true)
185
+ end
186
+ end
187
+
188
+ # Undo all the rejiggering when the keyboard slides
189
+ # down.
190
+ #
191
+ # You *must* handle the +UIKeyboardWillHideNotification+ and
192
+ # when you receive it, call this method to handle the keyboard
193
+ # hiding.
194
+ def handle_keyboard_will_hide(notification)
195
+ return unless @table
196
+
197
+ if UIEdgeInsetsEqualToEdgeInsets(@table.contentInset, UIEdgeInsetsZero)
198
+ return
199
+ end
200
+
201
+ animationCurve = notification.userInfo.valueForKey(UIKeyboardAnimationCurveUserInfoKey)
202
+ animationDuration = notification.userInfo.valueForKey(UIKeyboardAnimationDurationUserInfoKey)
203
+
204
+ UIView.beginAnimations("changeTableViewContentInset", context:nil)
205
+ UIView.setAnimationDuration(animationDuration)
206
+ UIView.setAnimationCurve(animationCurve)
207
+
208
+ @table.contentInset = UIEdgeInsetsZero;
209
+
210
+ UIView.commitAnimations
211
+ end
212
+
213
+ def cell_has_first_responder?(cell)
214
+ cell.subviews.each do |subview|
215
+ return true if subview.isFirstResponder
216
+ end
217
+ false
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,346 @@
1
+ # MotionModel encapsulates a pattern for synthesizing a model
2
+ # out of thin air. The model will have attributes, types,
3
+ # finders, ordering, ... the works.
4
+ #
5
+ # As an example, consider:
6
+ #
7
+ # class Task
8
+ # include MotionModel
9
+ #
10
+ # columns :task_name => :string,
11
+ # :details => :string,
12
+ # :due_date => :date
13
+ #
14
+ # # any business logic you might add...
15
+ # end
16
+ #
17
+ # Now, you can write code like:
18
+ #
19
+ # Task.create :task_name => 'Walk the dog',
20
+ # :details => 'Pick up after yourself',
21
+ # :due_date => '2012-09-17'
22
+ #
23
+ # Recognized types are:
24
+ #
25
+ # * :string
26
+ # * :date (must be in a form that Date.parse can recognize)
27
+ # * :time (must be in a form that Time.parse can recognize)
28
+ # * :integer
29
+ # * :float
30
+ #
31
+ # Assuming you have a bunch of tasks in your data store, you can do this:
32
+ #
33
+ # tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week).order(:due_date)
34
+ #
35
+ # Partial queries are supported so you can do:
36
+ #
37
+ # tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week)
38
+ # ordered_tasks_this_week = tasks_this_week.order(:due_date)
39
+ #
40
+ module MotionModel
41
+ module Model
42
+ def self.included(base)
43
+ base.extend(ClassMethods)
44
+ base.instance_variable_set("@column_attrs", [])
45
+ base.instance_variable_set("@typed_attrs", [])
46
+ base.instance_variable_set("@collection", [])
47
+ base.instance_variable_set("@_next_id", 1)
48
+ end
49
+
50
+ module ClassMethods
51
+ # Macro to define names and types of columns. It can be used in one of
52
+ # two forms:
53
+ #
54
+ # Pass a hash, and you define columns with types. E.g.,
55
+ #
56
+ # columns :name => :string, :age => :integer
57
+ #
58
+ # Pass an array, and you create column names, all of which have type +:string+.
59
+ #
60
+ # columns :name, :age, :hobby
61
+ def columns(*fields)
62
+ return @column_attrs if fields.empty?
63
+
64
+ case fields.first
65
+ when Hash
66
+ fields.first.each_pair do |attr, type|
67
+ add_attribute(attr, type)
68
+ end
69
+ else
70
+ fields.each do |attr|
71
+ add_attribute(attr, :string)
72
+ end
73
+ end
74
+
75
+ unless self.respond_to?(:id)
76
+ add_attribute(:id, :integer)
77
+ end
78
+ end
79
+
80
+ def add_attribute(attr, type) #nodoc
81
+ attr_accessor attr
82
+ @column_attrs << attr
83
+ @typed_attrs << type
84
+ end
85
+
86
+ def next_id #nodoc
87
+ @_next_id
88
+ end
89
+
90
+ def increment_id #nodoc
91
+ @_next_id += 1
92
+ end
93
+
94
+ # Returns true if a column exists on this model, otherwise false.
95
+ def column?(column)
96
+ @column_attrs.each{|key|
97
+ return true if key == column
98
+ }
99
+ false
100
+ end
101
+
102
+ # Returns type of this column.
103
+ def type(column)
104
+ index = @column_attrs.index(column)
105
+ index ? @typed_attrs[index] : nil
106
+ end
107
+
108
+ # Creates an object and saves it. E.g.:
109
+ #
110
+ # @bob = Person.create(:name => 'Bob', :hobby => 'Bird Watching')
111
+ #
112
+ # returns the object created or false.
113
+ def create(options = {})
114
+ row = self.new(options)
115
+ # TODO: Check for Validatable and if it's
116
+ # present, check valid? before saving.
117
+ @collection.push(row)
118
+ row
119
+ end
120
+
121
+ def length
122
+ @collection.length
123
+ end
124
+ alias_method :count, :length
125
+
126
+ # Empties the entire store.
127
+ def delete_all
128
+ @collection = [] # TODO: Handle cascading or let GC take care of it.
129
+ end
130
+
131
+ # Finds row(s) within the data store. E.g.,
132
+ #
133
+ # @post = Post.find(1) # find a specific row by ID
134
+ #
135
+ # or...
136
+ #
137
+ # @posts = Post.find(:author).eq('bob').all
138
+ def find(*args)
139
+ unless args[0].is_a?(Symbol) || args[0].is_a?(String)
140
+ return @collection[args[0].to_i] || nil
141
+ end
142
+
143
+ FinderQuery.new(args[0].to_sym, @collection)
144
+ end
145
+ alias_method :where, :find
146
+
147
+ # Retrieves first row of query
148
+ def first
149
+ @collection.first
150
+ end
151
+
152
+ # Retrieves last row of query
153
+ def last
154
+ @collection.last
155
+ end
156
+
157
+ # Returns query result as an array
158
+ def all
159
+ @collection
160
+ end
161
+
162
+ def order(field_name = nil, &block)
163
+ FinderQuery.new(@collection).order(field_name, &block)
164
+ end
165
+
166
+ def each(&block)
167
+ raise ArgumentError.new("each requires a block") unless block_given?
168
+ @collection.each{|item| yield item}
169
+ end
170
+
171
+ end
172
+
173
+ ####### Instance Methods #######
174
+ def initialize(options = {})
175
+ columns.each{|col| instance_variable_set("@#{col.to_s}", nil) unless options.has_key?(col)}
176
+
177
+ options.each do |key, value|
178
+ instance_variable_set("@#{key.to_s}", value || '') if self.class.column?(key.to_sym)
179
+ end
180
+ unless self.id
181
+ self.id = self.class.next_id
182
+ self.class.increment_id
183
+ end
184
+ end
185
+
186
+ def length
187
+ @collection.length
188
+ end
189
+
190
+ alias_method :count, :length
191
+
192
+ def column?(target_key)
193
+ self.class.column?(target_key)
194
+ end
195
+
196
+ def columns
197
+ self.class.columns
198
+ end
199
+
200
+ def type(field_name)
201
+ self.class.type(field_name)
202
+ end
203
+
204
+ def initWithCoder(coder)
205
+ self.init
206
+ self.class.instance_variable_get("@column_attrs").each do |attr|
207
+ # If a model revision has taken place, don't try to decode
208
+ # something that's not there.
209
+ new_tag_id = 1
210
+ if coder.containsValueForKey(attr.to_s)
211
+ value = coder.decodeObjectForKey(attr.to_s)
212
+ self.instance_variable_set('@' + attr.to_s, value || '')
213
+ else
214
+ self.instance_variable_set('@' + attr.to_s, '') # set to empty string if new attribute
215
+ end
216
+
217
+ # re-issue tags to make sure they are unique
218
+ @tag = new_tag_id
219
+ new_tag_id += 1
220
+ end
221
+ self
222
+ end
223
+
224
+ def encodeWithCoder(coder)
225
+ self.class.instance_variable_get("@column_attrs").each do |attr|
226
+ coder.encodeObject(self.send(attr), forKey: attr.to_s)
227
+ end
228
+ end
229
+
230
+ end
231
+
232
+ class FinderQuery
233
+ attr_accessor :field_name
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
243
+ end
244
+
245
+ def order(field = nil, &block)
246
+ if block_given?
247
+ @collection = @collection.sort{|o1, o2| yield(o1, o2)}
248
+ else
249
+ raise ArgumentError.new('you must supply a field name to sort unless you supply a block.') if field.nil?
250
+ @collection = @collection.sort{|o1, o2| o1.send(field) <=> o2.send(field)}
251
+ end
252
+ self
253
+ end
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
285
+ end
286
+ end
287
+ alias_method :>, :gt
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
344
+ end
345
+ end
346
+
@@ -0,0 +1,64 @@
1
+ module MotionModel
2
+ module Validatable
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ base.instance_variable_set('@validations', [])
6
+ end
7
+
8
+ module ClassMethods
9
+ def validate(field = nil, validation_type = {})
10
+ if field.nil? || field.to_s == ''
11
+ ex = ValidationSpecificationError.new('field not present in validation call')
12
+ raise ex
13
+ end
14
+
15
+ if validation_type == {} # || !(validation_type is_a?(Hash))
16
+ ex = ValidationSpecificationError.new('validation type not present or not a hash')
17
+ raise ex
18
+ end
19
+
20
+ @validations << {field => validation_type}
21
+ end
22
+ end
23
+
24
+ def valid?
25
+ @messages = []
26
+ @valid = true
27
+ self.class.instance_variable_get(@validations).each do |validations|
28
+ validate_each(validations)
29
+ end
30
+ @valid
31
+ end
32
+
33
+ def validate_each(validations)
34
+ validations.each_pair do |field, validation|
35
+ validate_one field, validation
36
+ end
37
+ end
38
+
39
+ def validate_one(field, validation)
40
+ validation.each_pair do |validation_type, setting|
41
+ case validation_type
42
+ when :presence
43
+ @valid &&= validate_presence(field)
44
+ if setting
45
+ additional_message = "non-empty"
46
+ else
47
+ additional_message = "empty"
48
+ end
49
+ @valid = !@valid if setting == false
50
+ @messages << {field => "incorrect value supplied for #{field.to_s} -- should be #{additional_message}"}
51
+ else
52
+ @valid = false
53
+ ex = ValidationSpecificationError.new("unknown validation type :#{validation_type.to_s}")
54
+ end
55
+ end
56
+ end
57
+
58
+ def validate_presence(field)
59
+ value = self.send(field.to_s)
60
+ return false if value.nil?
61
+ return value.strip.length > 0
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module MotionModel
2
+ VERSION = "0.2"
3
+ end
@@ -0,0 +1,5 @@
1
+ Motion::Project::App.setup do |app|
2
+ Dir.glob(File.join(File.dirname(__FILE__), "motion_model/**/*.rb")).each do |file|
3
+ app.files.unshift(file)
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/motion_model/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Steve Ross"]
6
+ gem.email = ["sxross@gmail.com"]
7
+ gem.description = "Simple model and validation mixins for RubyMotion"
8
+ gem.summary = "Simple model and validation mixins for RubyMotion"
9
+ gem.homepage = "https://github.com/sxross/MotionModel"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ puts "gem files are #{`git ls-files`.split($\)}"
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "motion_model"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MotionModel::VERSION
17
+ end
@@ -0,0 +1,200 @@
1
+ class Task
2
+ include MotionModel::Model
3
+ columns :name => :string,
4
+ :details => :string,
5
+ :some_day => :date
6
+ end
7
+
8
+ class ATask
9
+ include MotionModel::Model
10
+ columns :name, :details, :some_day
11
+ end
12
+
13
+ describe "Creating a model" do
14
+ before do
15
+ Task.delete_all
16
+ end
17
+
18
+ describe 'column macro behavior' do
19
+
20
+ it 'succeeds when creating a valid model from attributes' do
21
+ a_task = Task.new(:name => 'name', :details => 'details')
22
+ a_task.name.should.equal('name')
23
+ end
24
+
25
+ it 'creates a model with all attributes even if some omitted' do
26
+ atask = Task.create(:name => 'bob')
27
+ atask.should.respond_to(:details)
28
+ end
29
+
30
+ it 'simply bypasses spurious attributes erroneously set' do
31
+ a_task = Task.new(:name => 'details', :zoo => 'very bad')
32
+ a_task.should.not.respond_to(:zoo)
33
+ a_task.name.should.equal('details')
34
+ end
35
+
36
+ it "can check for a column's existence on a model" do
37
+ Task.column?(:name).should.be.true
38
+ end
39
+
40
+ it "can check for a column's existence on an instance" do
41
+ a_task = Task.new(:name => 'name', :details => 'details')
42
+ a_task.column?(:name).should.be.true
43
+ end
44
+
45
+ it "gets a list of columns on a model" do
46
+ cols = Task.columns
47
+ cols.should.include(:name)
48
+ cols.should.include(:details)
49
+ end
50
+
51
+ it "gets a list of columns on an instance" do
52
+ a_task = Task.new
53
+ cols = a_task.columns
54
+ cols.should.include(:name)
55
+ cols.should.include(:details)
56
+ end
57
+
58
+ it "columns can be specified as a Hash" do
59
+ lambda{Task.new}.should.not.raise
60
+ Task.new.column?(:name).should.be.true
61
+ end
62
+
63
+ it "columns can be specified as an Array" do
64
+ lambda{ATask.new}.should.not.raise
65
+ Task.new.column?(:name).should.be.true
66
+ end
67
+
68
+ it "the type of a column can be retrieved" do
69
+ Task.new.type(:some_day).should.equal(:date)
70
+ end
71
+
72
+ end
73
+
74
+ describe "ID handling" do
75
+
76
+ it 'creates an id if none present' do
77
+ task = Task.create
78
+ task.should.respond_to(:id)
79
+ end
80
+
81
+ it 'does not overwrite an existing ID' do
82
+ task = Task.create(:id => 999)
83
+ task.id.should.equal(999)
84
+ end
85
+
86
+ it 'creates multiple objects with unique ids' do
87
+ Task.create.id.should.not.equal(Task.create.id)
88
+ end
89
+
90
+ end
91
+
92
+ describe 'count and length methods' do
93
+
94
+ it 'has a length method' do
95
+ Task.should.respond_to(:length)
96
+ end
97
+
98
+ it 'has a count method' do
99
+ Task.should.respond_to(:count)
100
+ end
101
+
102
+ it 'when there is one element, length returns 1' do
103
+ task = Task.create
104
+ Task.length.should.equal(1)
105
+ end
106
+
107
+ it 'when there is one element, count returns 1' do
108
+ task = Task.create
109
+ Task.count.should.equal(1)
110
+ end
111
+
112
+ it 'when there is more than one element, length returned is correct' do
113
+ 10.times { Task.create }
114
+ Task.length.should.equal(10)
115
+ end
116
+
117
+ end
118
+
119
+ describe 'finders' do
120
+ before do
121
+ 10.times {|i| Task.create(:name => "task #{i}")}
122
+ end
123
+
124
+ describe 'find' do
125
+ it 'finds elements within the collection' do
126
+ Task.find(3).name.should.equal('task 3')
127
+ end
128
+
129
+ it 'returns nil if find by id is not found' do
130
+ Task.find(999).should.be.nil
131
+ end
132
+
133
+ it 'looks into fields if field name supplied' do
134
+ Task.create(:name => 'find me')
135
+ Task.find(:name).eq('find me').all.length.should.equal(1)
136
+ end
137
+
138
+ it 'allows for multiple (chained) query parameters' do
139
+ Task.create(:name => 'find me', :details => "details 1")
140
+ Task.create(:name => 'find me', :details => "details 2")
141
+ tasks = Task.find(:name).eq('find me').and(:details).like('2')
142
+ tasks.first.details.should.equal('details 2')
143
+ tasks.all.length.should.equal(1)
144
+ end
145
+
146
+ it 'where should respond to finder methods' do
147
+ Task.where(:details).should.respond_to(:contain)
148
+ end
149
+
150
+ it 'returns a FinderQuery object' do
151
+ Task.where(:details).should.is_a(MotionModel::FinderQuery)
152
+ end
153
+
154
+ it 'using where instead of find' do
155
+ atask = Task.create(:name => 'find me', :details => "details 1")
156
+ found_task = Task.where(:details).contain("details 1").first.details.should.equal("details 1")
157
+ end
158
+
159
+ it 'all returns all members of the collection as an array' do
160
+ Task.all.length.should.equal(10)
161
+ end
162
+
163
+ it 'each yields each row in sequence' do
164
+ i = 0
165
+ Task.each do |task|
166
+ task.name.should.equal("task #{i}")
167
+ i += 1
168
+ end
169
+ end
170
+
171
+ end
172
+
173
+ describe 'sorting' do
174
+ before do
175
+ Task.delete_all
176
+ Task.create(:name => 'Task 3', :details => 'detail 3')
177
+ Task.create(:name => 'Task 1', :details => 'detail 1')
178
+ Task.create(:name => 'Task 2', :details => 'detail 6')
179
+ Task.create(:name => 'Random Task', :details => 'another random task')
180
+ end
181
+
182
+ it 'sorts by field' do
183
+ tasks = Task.order(:name).all
184
+ tasks[0].name.should.equal('Random Task')
185
+ tasks[1].name.should.equal('Task 1')
186
+ tasks[2].name.should.equal('Task 2')
187
+ tasks[3].name.should.equal('Task 3')
188
+ end
189
+
190
+ it 'sorts observing block syntax' do
191
+ tasks = Task.order{|one, two| two.details <=> one.details}.all
192
+ tasks[0].details.should.equal('detail 6')
193
+ tasks[1].details.should.equal('detail 3')
194
+ tasks[2].details.should.equal('detail 1')
195
+ tasks[3].details.should.equal('another random task')
196
+ end
197
+ end
198
+
199
+ end
200
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: motion_model
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.2'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Ross
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-20 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Simple model and validation mixins for RubyMotion
15
+ email:
16
+ - sxross@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - README.md
23
+ - Rakefile
24
+ - app/app_delegate.rb
25
+ - lib/motion_model.rb
26
+ - lib/motion_model/ext.rb
27
+ - lib/motion_model/input_helpers.rb
28
+ - lib/motion_model/model.rb
29
+ - lib/motion_model/validatable.rb
30
+ - lib/motion_model/version.rb
31
+ - motion_model.gemspec
32
+ - spec/model_spec.rb
33
+ homepage: https://github.com/sxross/MotionModel
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.24
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Simple model and validation mixins for RubyMotion
57
+ test_files:
58
+ - spec/model_spec.rb