motion_model 0.2

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/.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