motion_model 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +40 -0
- data/lib/motion_model/model/column.rb +0 -10
- data/lib/motion_model/model/finder_query.rb +3 -7
- data/lib/motion_model/model/model.rb +33 -57
- data/lib/motion_model/model/model_casts.rb +44 -0
- data/lib/motion_model/validatable.rb +111 -22
- data/lib/motion_model/version.rb +1 -1
- data/spec/model_casting_spec.rb +122 -0
- data/spec/model_hook_spec.rb +60 -0
- data/spec/model_spec.rb +4 -63
- data/spec/validation_spec.rb +110 -0
- metadata +9 -2
data/README.md
CHANGED
@@ -128,6 +128,46 @@ a_task = Task.create(:name => 'joe-bob', :due_date => '2012-09-15') # due_da
|
|
128
128
|
a_task.due_date = '2012-09-19' # due_date is cast to NSDate
|
129
129
|
```
|
130
130
|
|
131
|
+
Currently supported types are:
|
132
|
+
|
133
|
+
* `:string`
|
134
|
+
* `:boolean`, `:bool`
|
135
|
+
* `:int`, `:integer`
|
136
|
+
* `:float`, `:double`
|
137
|
+
* `:date`
|
138
|
+
* `:array`
|
139
|
+
|
140
|
+
You are really not encouraged to stuff big things in your models, which is why a blob type
|
141
|
+
is not implemented. The smaller your data, the less overhead involved in saving/loading.
|
142
|
+
|
143
|
+
What Validation Methods Exist
|
144
|
+
-----------------
|
145
|
+
|
146
|
+
validate :field_name, :presence => true
|
147
|
+
validate :field_name, :length => 5..8 # specify a range
|
148
|
+
validate :field_name, :email
|
149
|
+
validate :field_name, :format
|
150
|
+
|
151
|
+
The framework is sufficiently flexible that you can add in custom validators like so:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
module MotionModel
|
155
|
+
module Validatable
|
156
|
+
def validate_foo(field, value, setting)
|
157
|
+
# do whatever you need to make sure that the value
|
158
|
+
# denoted by *value* for the field corresponds to
|
159
|
+
# whatever is passed in setting.
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
validate :my_field, :foo => 42
|
165
|
+
```
|
166
|
+
|
167
|
+
In the above example, your new `validate_foo` method will get the arguments
|
168
|
+
pretty much as you expect. The value of the
|
169
|
+
last hash is passed intact via the `settings` argument.
|
170
|
+
|
131
171
|
Model Instances and Unique IDs
|
132
172
|
-----------------
|
133
173
|
|
@@ -13,16 +13,6 @@ module MotionModel
|
|
13
13
|
@destroy = options[:dependent]
|
14
14
|
end
|
15
15
|
|
16
|
-
# REVIEW: Dead code?
|
17
|
-
def add_attr(name, type, options)
|
18
|
-
@name = name
|
19
|
-
@type = type
|
20
|
-
@default = options[:default]
|
21
|
-
@destroy = options[:dependent]
|
22
|
-
end
|
23
|
-
|
24
|
-
alias_method :add_attribute, :add_attr
|
25
|
-
|
26
16
|
def classify
|
27
17
|
case @type
|
28
18
|
when :belongs_to
|
@@ -7,7 +7,7 @@ module MotionModel
|
|
7
7
|
@collection = args.last
|
8
8
|
end
|
9
9
|
|
10
|
-
def belongs_to(obj, klass = nil)
|
10
|
+
def belongs_to(obj, klass = nil) #nodoc
|
11
11
|
@related_object = obj
|
12
12
|
@klass = klass
|
13
13
|
self
|
@@ -18,8 +18,6 @@ module MotionModel
|
|
18
18
|
# Task.find(:name => 'bob').and(:gender).eq('M')
|
19
19
|
# Task.asignees.where(:assignee_name).eq('bob')
|
20
20
|
def and(field_name)
|
21
|
-
# TODO: Allow for Task.assignees.where(:assignee_name => 'bob')
|
22
|
-
|
23
21
|
@field_name = field_name
|
24
22
|
self
|
25
23
|
end
|
@@ -40,7 +38,6 @@ module MotionModel
|
|
40
38
|
self
|
41
39
|
end
|
42
40
|
|
43
|
-
######## relational operators ########
|
44
41
|
def translate_case(item, case_sensitive)#nodoc
|
45
42
|
item = item.underscore if case_sensitive === false && item.respond_to?(:underscore)
|
46
43
|
item
|
@@ -204,6 +201,7 @@ module MotionModel
|
|
204
201
|
new_obj
|
205
202
|
end
|
206
203
|
|
204
|
+
# Returns number of objects (rows) in collection
|
207
205
|
def length
|
208
206
|
@collection.length
|
209
207
|
end
|
@@ -223,7 +221,5 @@ module MotionModel
|
|
223
221
|
result
|
224
222
|
end
|
225
223
|
alias_method :<<, :push
|
226
|
-
|
227
|
-
|
228
224
|
end
|
229
|
-
end
|
225
|
+
end
|
@@ -16,9 +16,6 @@
|
|
16
16
|
#
|
17
17
|
# Now, you can write code like:
|
18
18
|
#
|
19
|
-
# Task.create :task_name => 'Walk the dog',
|
20
|
-
# :details => 'Pick up after yourself',
|
21
|
-
# :due_date => '2012-09-17'
|
22
19
|
#
|
23
20
|
# Recognized types are:
|
24
21
|
#
|
@@ -36,7 +33,6 @@
|
|
36
33
|
# tasks_this_week = Task.where(:due_date).ge(beginning_of_week).and(:due_date).le(end_of_week)
|
37
34
|
# ordered_tasks_this_week = tasks_this_week.order(:due_date)
|
38
35
|
#
|
39
|
-
|
40
36
|
module MotionModel
|
41
37
|
class PersistFileError < Exception; end
|
42
38
|
class RelationIsNilError < Exception; end
|
@@ -81,8 +77,6 @@ module MotionModel
|
|
81
77
|
def columns(*fields)
|
82
78
|
return @_columns.map{|c| c.name} if fields.empty?
|
83
79
|
|
84
|
-
# col = Column.new # REVIEW: Dead code?
|
85
|
-
|
86
80
|
case fields.first
|
87
81
|
when Hash
|
88
82
|
column_from_hash fields
|
@@ -164,12 +158,6 @@ module MotionModel
|
|
164
158
|
# returns the object created or false.
|
165
159
|
def create(options = {})
|
166
160
|
row = self.new(options)
|
167
|
-
row.before_create if row.respond_to?(:before_create)
|
168
|
-
row.before_save if row.respond_to?(:before_save)
|
169
|
-
|
170
|
-
# TODO: Check for Validatable and if it's
|
171
|
-
# present, check valid? before saving.
|
172
|
-
|
173
161
|
row.save
|
174
162
|
row
|
175
163
|
end
|
@@ -409,26 +397,40 @@ module MotionModel
|
|
409
397
|
# databases, this inserts a row if it's a new one, or updates
|
410
398
|
# in place if not.
|
411
399
|
def save
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
400
|
+
call_hooks 'save' do
|
401
|
+
@dirty = false
|
402
|
+
|
403
|
+
# Existing object implies update in place
|
404
|
+
action = 'add'
|
405
|
+
if obj = collection.find{|o| o.id == @data[:id]}
|
406
|
+
obj = self
|
407
|
+
action = 'update'
|
408
|
+
else
|
409
|
+
collection << self
|
410
|
+
end
|
411
|
+
self.class.issue_notification(self, :action => action)
|
422
412
|
end
|
423
|
-
self.class.issue_notification(self, :action => action)
|
424
413
|
end
|
425
414
|
|
426
415
|
# Deletes the current object. The object can still be used.
|
427
416
|
|
417
|
+
def call_hook(hook_name, postfix)
|
418
|
+
hook = "#{hook_name}_#{postfix}"
|
419
|
+
self.send(hook, self) if respond_to? hook.to_sym
|
420
|
+
end
|
421
|
+
|
422
|
+
def call_hooks(hook_name, &block)
|
423
|
+
call_hook('before', hook_name)
|
424
|
+
block.call
|
425
|
+
call_hook('after', hook_name)
|
426
|
+
end
|
427
|
+
|
428
428
|
def delete
|
429
|
-
|
430
|
-
|
431
|
-
|
429
|
+
call_hooks('delete') do
|
430
|
+
target_index = collection.index{|item| item.id == self.id}
|
431
|
+
collection.delete_at(target_index)
|
432
|
+
self.class.issue_notification(self, :action => 'delete')
|
433
|
+
end
|
432
434
|
end
|
433
435
|
|
434
436
|
# Destroys the current object. The difference between delete
|
@@ -437,8 +439,10 @@ module MotionModel
|
|
437
439
|
# into related objects, deleting them if they are related
|
438
440
|
# using <tt>:delete => :destroy</tt> in the <tt>has_many</tt>
|
439
441
|
# declaration
|
442
|
+
#
|
443
|
+
# Note: lifecycle hooks are only called when individual objects
|
444
|
+
# are deleted.
|
440
445
|
def destroy
|
441
|
-
before_delete if respond_to? :before_delete
|
442
446
|
has_many_columns.each do |col|
|
443
447
|
delete_candidates = self.send(col.name)
|
444
448
|
|
@@ -448,7 +452,6 @@ module MotionModel
|
|
448
452
|
end
|
449
453
|
end
|
450
454
|
delete
|
451
|
-
after_delete if respond_to? :after_delete
|
452
455
|
end
|
453
456
|
|
454
457
|
# Undelete does pretty much as its name implies. However,
|
@@ -515,22 +518,6 @@ module MotionModel
|
|
515
518
|
@data[column] = cast_value
|
516
519
|
end
|
517
520
|
|
518
|
-
def cast_to_type(column_name, arg) #nodoc
|
519
|
-
return nil if arg.nil?
|
520
|
-
|
521
|
-
return case type(column_name)
|
522
|
-
when :string then arg.to_s
|
523
|
-
when :int, :integer, :belongs_to_id
|
524
|
-
arg.is_a?(Integer) ? arg : arg.to_i
|
525
|
-
when :float, :double
|
526
|
-
arg.is_a?(Float) ? arg : arg.to_f
|
527
|
-
when :date
|
528
|
-
arg.is_a?(NSDate) ? arg : NSDate.dateWithNaturalLanguageString(arg, locale:NSUserDefaults.standardUserDefaults.dictionaryRepresentation)
|
529
|
-
else
|
530
|
-
raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
531
|
-
end
|
532
|
-
end
|
533
|
-
|
534
521
|
def collection #nodoc
|
535
522
|
self.class.instance_variable_get('@collection')
|
536
523
|
end
|
@@ -561,19 +548,8 @@ module MotionModel
|
|
561
548
|
end
|
562
549
|
end
|
563
550
|
|
564
|
-
|
565
|
-
#
|
566
|
-
#
|
567
|
-
# Gets and sets work as expected, and type casting occurs
|
568
|
-
# For example:
|
569
|
-
#
|
570
|
-
# Task.date = '2012-09-15'
|
571
|
-
#
|
572
|
-
# This creates a real Date object in the data store.
|
573
|
-
#
|
574
|
-
# date = Task.date
|
575
|
-
#
|
576
|
-
# Date is a real date object.
|
551
|
+
# Any way you reach this means you've tried to access a method
|
552
|
+
# not defined on this model.
|
577
553
|
def method_missing(method, *args, &block) #nodoc
|
578
554
|
if self.respond_to? method
|
579
555
|
return method(args, &block)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module MotionModel
|
2
|
+
class Model
|
3
|
+
def cast_to_bool(arg)
|
4
|
+
case arg
|
5
|
+
when NilClass then false
|
6
|
+
when TrueClass, FalseClass then arg
|
7
|
+
when Integer then arg != 0
|
8
|
+
when String then (arg =~ /^true/i) != nil
|
9
|
+
else raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def cast_to_integer(arg)
|
14
|
+
arg.is_a?(Integer) ? arg : arg.to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
def cast_to_float(arg)
|
18
|
+
arg.is_a?(Float) ? arg : arg.to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def cast_to_date(arg)
|
22
|
+
arg.is_a?(NSDate) ? arg : NSDate.dateWithNaturalLanguageString(arg, locale:NSUserDefaults.standardUserDefaults.dictionaryRepresentation)
|
23
|
+
end
|
24
|
+
|
25
|
+
def cast_to_array(arg)
|
26
|
+
arg.is_a?(Array) ? arg : arg.to_a
|
27
|
+
end
|
28
|
+
|
29
|
+
def cast_to_type(column_name, arg) #nodoc
|
30
|
+
return nil if arg.nil? && ![ :boolean, :bool ].include?(type(column_name))
|
31
|
+
|
32
|
+
return case type(column_name)
|
33
|
+
when :string then arg.to_s
|
34
|
+
when :boolean, :bool then cast_to_bool(arg)
|
35
|
+
when :int, :integer, :belongs_to_id then cast_to_integer(arg)
|
36
|
+
when :float, :double then cast_to_float(arg)
|
37
|
+
when :date then cast_to_date(arg)
|
38
|
+
when :array then cast_to_array(arg)
|
39
|
+
else
|
40
|
+
raise ArgumentError.new("type #{column_name} : #{type(column_name)} is not possible to cast.")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module MotionModel
|
2
2
|
module Validatable
|
3
|
+
class ValidationSpecificationError < RuntimeError; end
|
4
|
+
|
3
5
|
def self.included(base)
|
4
6
|
base.extend(ClassMethods)
|
5
7
|
base.instance_variable_set('@validations', [])
|
@@ -12,53 +14,140 @@ module MotionModel
|
|
12
14
|
raise ex
|
13
15
|
end
|
14
16
|
|
15
|
-
if validation_type == {}
|
17
|
+
if validation_type == {}
|
16
18
|
ex = ValidationSpecificationError.new('validation type not present or not a hash')
|
17
19
|
raise ex
|
18
20
|
end
|
19
21
|
|
20
22
|
@validations << {field => validation_type}
|
21
|
-
end
|
23
|
+
end
|
24
|
+
alias_method :validates, :validate
|
25
|
+
|
26
|
+
def validations
|
27
|
+
@validations
|
28
|
+
end
|
22
29
|
end
|
23
30
|
|
31
|
+
# This has two functions:
|
32
|
+
#
|
33
|
+
# * First, it triggers validations.
|
34
|
+
#
|
35
|
+
# * Second, it returns the result of performing the validations.
|
24
36
|
def valid?
|
25
37
|
@messages = []
|
26
38
|
@valid = true
|
27
|
-
self.class.
|
39
|
+
self.class.validations.each do |validations|
|
28
40
|
validate_each(validations)
|
29
41
|
end
|
30
42
|
@valid
|
31
43
|
end
|
32
44
|
|
33
|
-
|
45
|
+
# Raw array of hashes of error messages.
|
46
|
+
def error_messages
|
47
|
+
@messages
|
48
|
+
end
|
49
|
+
|
50
|
+
# Array of messages for a given field. Results are always an array
|
51
|
+
# because a field can fail multiple validations.
|
52
|
+
def error_messages_for(field)
|
53
|
+
key = field.to_sym
|
54
|
+
error_messages.select{|message| message.has_key?(key)}.map{|message| message[key]}
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_each(validations) #nodoc
|
34
58
|
validations.each_pair do |field, validation|
|
35
|
-
validate_one field, validation
|
59
|
+
@valid &&= validate_one field, validation
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def validation_method(validation_type) #nodoc
|
64
|
+
validation_method = "validate_#{validation_type}".to_sym
|
65
|
+
end
|
66
|
+
|
67
|
+
def each_validation_for(field) #nodoc
|
68
|
+
self.class.validations.select{|validation| validation.has_key?(field)}.each do |validation|
|
69
|
+
validation.each_pair do |field, validation_hash|
|
70
|
+
yield validation_hash
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Validates an arbitrary string against a specific field's validators.
|
76
|
+
# Useful before setting the value of a model's field. I.e., you get data
|
77
|
+
# from a form, do a <tt>validate_for(:my_field, that_data)</tt> and
|
78
|
+
# if it succeeds, you do <tt>obj.my_field = that_data</tt>.
|
79
|
+
def validate_for(field, value)
|
80
|
+
@messages = []
|
81
|
+
key = field.to_sym
|
82
|
+
result = true
|
83
|
+
each_validation_for(key) do |validation|
|
84
|
+
validation.each_pair do |validation_type, setting|
|
85
|
+
method = validation_method(validation_type)
|
86
|
+
if self.respond_to? method
|
87
|
+
value.strip! if value.is_a?(String)
|
88
|
+
result &&= self.send(method, field, value, setting)
|
89
|
+
end
|
90
|
+
end
|
36
91
|
end
|
92
|
+
result
|
37
93
|
end
|
38
94
|
|
39
|
-
def validate_one(field, validation)
|
95
|
+
def validate_one(field, validation) #nodoc
|
96
|
+
result = true
|
40
97
|
validation.each_pair do |validation_type, setting|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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}"}
|
98
|
+
if self.respond_to? validation_method(validation_type)
|
99
|
+
value = self.send(field)
|
100
|
+
value.strip! if value.is_a?(String)
|
101
|
+
result &&= self.send(validation_method(validation_type), field, value, setting)
|
51
102
|
else
|
52
|
-
@valid = false
|
53
103
|
ex = ValidationSpecificationError.new("unknown validation type :#{validation_type.to_s}")
|
54
104
|
end
|
55
105
|
end
|
106
|
+
result
|
56
107
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
108
|
+
|
109
|
+
# Validates that something has been entered in a field
|
110
|
+
def validate_presence(field, value, setting)
|
111
|
+
if value.is_a?(String) || value.nil?
|
112
|
+
result = value.nil? || ((value.length == 0) == setting)
|
113
|
+
additional_message = setting ? "non-empty" : "non-empty"
|
114
|
+
add_message(field, "incorrect value supplied for #{field.to_s} -- should be #{additional_message}.") if result
|
115
|
+
return !result
|
116
|
+
end
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
|
120
|
+
# Validates that the length is in a given range of characters. E.g.,
|
121
|
+
#
|
122
|
+
# validate :name, :length => 5..8
|
123
|
+
def validate_length(field, value, setting)
|
124
|
+
if value.is_a?(String) || value.nil?
|
125
|
+
result = value.nil? || (value.length < setting.first || value.length > setting.last)
|
126
|
+
add_message(field, "incorrect value supplied for #{field.to_s} -- should be between #{setting.first} and #{setting.last} characters long.") if result
|
127
|
+
return !result
|
128
|
+
end
|
129
|
+
return false
|
130
|
+
end
|
131
|
+
|
132
|
+
def validate_email(field, value, setting)
|
133
|
+
if value.is_a?(String) || value.nil?
|
134
|
+
result = value.nil? || value.match(/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i).nil?
|
135
|
+
add_message(field, "#{field.to_s} does not appear to be an email address.") if result
|
136
|
+
end
|
137
|
+
return !result
|
138
|
+
end
|
139
|
+
|
140
|
+
# Validates contents of field against a given Regexp. This can be tricky because you need
|
141
|
+
# to anchor both sides in most cases using \A and \Z to get a reliable match.
|
142
|
+
def validate_format(field, value, setting)
|
143
|
+
result = value.nil? || setting.match(value).nil?
|
144
|
+
add_message(field, "#{field.to_s} does not appear to be in the proper format.") if result
|
145
|
+
return !result
|
146
|
+
end
|
147
|
+
|
148
|
+
# Add a message for <tt>field</tt> to the messages collection.
|
149
|
+
def add_message(field, message)
|
150
|
+
@messages.push({field.to_sym => message})
|
62
151
|
end
|
63
152
|
end
|
64
153
|
end
|
data/lib/motion_model/version.rb
CHANGED
@@ -0,0 +1,122 @@
|
|
1
|
+
class TypeCast
|
2
|
+
include MotionModel::Model
|
3
|
+
columns :a_boolean => :boolean,
|
4
|
+
:an_int => {:type => :int, :default => 3},
|
5
|
+
:an_integer => :integer,
|
6
|
+
:a_float => :float,
|
7
|
+
:a_double => :double,
|
8
|
+
:a_date => :date,
|
9
|
+
:a_time => :time,
|
10
|
+
:an_array => :array
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'Type casting' do
|
14
|
+
before do
|
15
|
+
@convertible = TypeCast.new
|
16
|
+
@convertible.a_boolean = 'false'
|
17
|
+
@convertible.an_int = '1'
|
18
|
+
@convertible.an_integer = '2'
|
19
|
+
@convertible.a_float = '3.7'
|
20
|
+
@convertible.a_double = '3.41459'
|
21
|
+
@convertible.a_date = '2012-09-15'
|
22
|
+
@convertible.an_array = 1..10
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'does the type casting on instantiation' do
|
26
|
+
@convertible.a_boolean.should.is_a FalseClass
|
27
|
+
@convertible.an_int.should.is_a Integer
|
28
|
+
@convertible.an_integer.should.is_a Integer
|
29
|
+
@convertible.a_float.should.is_a Float
|
30
|
+
@convertible.a_double.should.is_a Float
|
31
|
+
@convertible.a_date.should.is_a NSDate
|
32
|
+
@convertible.an_array.should.is_a Array
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns a boolean for a boolean field' do
|
36
|
+
@convertible.a_boolean.should.is_a(FalseClass)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'the boolean field should be the same as it was in string form' do
|
40
|
+
@convertible.a_boolean.to_s.should.equal('false')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'the boolean field accepts a non-zero integer as true' do
|
44
|
+
@convertible.a_boolean = 1
|
45
|
+
@convertible.a_boolean.should.is_a(TrueClass)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'the boolean field accepts a zero valued integer as false' do
|
49
|
+
@convertible.a_boolean = 0
|
50
|
+
@convertible.a_boolean.should.is_a(FalseClass)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'the boolean field accepts a string that starts with "true" as true' do
|
54
|
+
@convertible.a_boolean = 'true'
|
55
|
+
@convertible.a_boolean.should.is_a(TrueClass)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'the boolean field treats a string with "true" not at the start as false' do
|
59
|
+
@convertible.a_boolean = 'something true'
|
60
|
+
@convertible.a_boolean.should.is_a(FalseClass)
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'the boolean field accepts a string that does not contain "true" as false' do
|
64
|
+
@convertible.a_boolean = 'something'
|
65
|
+
@convertible.a_boolean.should.is_a(FalseClass)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'the boolean field accepts nil as false' do
|
69
|
+
@convertible.a_boolean = nil
|
70
|
+
@convertible.a_boolean.should.is_a(FalseClass)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns an integer for an int field' do
|
74
|
+
@convertible.an_int.should.is_a(Integer)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'the int field should be the same as it was in string form' do
|
78
|
+
@convertible.an_int.to_s.should.equal('1')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns an integer for an integer field' do
|
82
|
+
@convertible.an_integer.should.is_a(Integer)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'the integer field should be the same as it was in string form' do
|
86
|
+
@convertible.an_integer.to_s.should.equal('2')
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns a float for a float field' do
|
90
|
+
@convertible.a_float.should.is_a(Float)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'the float field should be the same as it was in string form' do
|
94
|
+
@convertible.a_float.should.>(3.6)
|
95
|
+
@convertible.a_float.should.<(3.8)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns a double for a double field' do
|
99
|
+
@convertible.a_double.should.is_a(Float)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'the double field should be the same as it was in string form' do
|
103
|
+
@convertible.a_double.should.>(3.41458)
|
104
|
+
@convertible.a_double.should.<(3.41460)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'returns a NSDate for a date field' do
|
108
|
+
@convertible.a_date.should.is_a(NSDate)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'the date field should be the same as it was in string form' do
|
112
|
+
@convertible.a_date.to_s.should.match(/^2012-09-15/)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns an Array for an array field' do
|
116
|
+
@convertible.an_array.should.is_a(Array)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'the array field should be the same as the range form' do
|
120
|
+
(@convertible.an_array.first..@convertible.an_array.last).should.equal(1..10)
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Task
|
2
|
+
attr_reader :before_delete_called, :after_delete_called
|
3
|
+
attr_reader :before_save_called, :after_save_called
|
4
|
+
|
5
|
+
include MotionModel::Model
|
6
|
+
columns :name => :string,
|
7
|
+
:details => :string,
|
8
|
+
:some_day => :date
|
9
|
+
|
10
|
+
def before_delete(object)
|
11
|
+
@before_delete_called = true
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_delete(object)
|
15
|
+
@after_delete_called = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def before_save(object)
|
19
|
+
@before_save_called = true
|
20
|
+
end
|
21
|
+
|
22
|
+
def after_save(object)
|
23
|
+
@after_save_called = true
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "lifecycle hooks" do
|
29
|
+
describe "delete and destroy" do
|
30
|
+
before{@task = Task.create(:name => 'joe')}
|
31
|
+
|
32
|
+
it "calls the before delete hook when delete is called" do
|
33
|
+
lambda{@task.delete}.should.change{@task.before_delete_called}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "calls the after delete hook when delete is called" do
|
37
|
+
lambda{@task.delete}.should.change{@task.after_delete_called}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "calls the before delete hook when destroy is called" do
|
41
|
+
lambda{@task.destroy}.should.change{@task.before_delete_called}
|
42
|
+
end
|
43
|
+
|
44
|
+
it "calls the after delete hook when destroy is called" do
|
45
|
+
lambda{@task.destroy}.should.change{@task.after_delete_called}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "create and save" do
|
50
|
+
before{@task = Task.new(:name => 'joe')}
|
51
|
+
|
52
|
+
it "calls before_save hook on save" do
|
53
|
+
lambda{@task.save}.should.change{@task.before_save_called}
|
54
|
+
end
|
55
|
+
|
56
|
+
it "calls after_save hook on save" do
|
57
|
+
lambda{@task.save}.should.change{@task.after_save_called}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/model_spec.rb
CHANGED
@@ -16,12 +16,14 @@ end
|
|
16
16
|
|
17
17
|
class TypeCast
|
18
18
|
include MotionModel::Model
|
19
|
-
columns :
|
19
|
+
columns :a_boolean => :boolean,
|
20
|
+
:an_int => {:type => :int, :default => 3},
|
20
21
|
:an_integer => :integer,
|
21
22
|
:a_float => :float,
|
22
23
|
:a_double => :double,
|
23
24
|
:a_date => :date,
|
24
|
-
:a_time => :time
|
25
|
+
:a_time => :time,
|
26
|
+
:an_array => :array
|
25
27
|
end
|
26
28
|
|
27
29
|
describe "Creating a model" do
|
@@ -231,67 +233,6 @@ describe "Creating a model" do
|
|
231
233
|
end
|
232
234
|
end
|
233
235
|
|
234
|
-
describe 'Type casting' do
|
235
|
-
before do
|
236
|
-
@convertible = TypeCast.new
|
237
|
-
@convertible.an_int = '1'
|
238
|
-
@convertible.an_integer = '2'
|
239
|
-
@convertible.a_float = '3.7'
|
240
|
-
@convertible.a_double = '3.41459'
|
241
|
-
@convertible.a_date = '2012-09-15'
|
242
|
-
end
|
243
|
-
|
244
|
-
it 'does the type casting on instantiation' do
|
245
|
-
@convertible.an_int.should.is_a Integer
|
246
|
-
@convertible.an_integer.should.is_a Integer
|
247
|
-
@convertible.a_float.should.is_a Float
|
248
|
-
@convertible.a_double.should.is_a Float
|
249
|
-
@convertible.a_date.should.is_a NSDate
|
250
|
-
end
|
251
|
-
|
252
|
-
it 'returns an integer for an int field' do
|
253
|
-
@convertible.an_int.should.is_a(Integer)
|
254
|
-
end
|
255
|
-
|
256
|
-
it 'the int field should be the same as it was in string form' do
|
257
|
-
@convertible.an_int.to_s.should.equal('1')
|
258
|
-
end
|
259
|
-
|
260
|
-
it 'returns an integer for an integer field' do
|
261
|
-
@convertible.an_integer.should.is_a(Integer)
|
262
|
-
end
|
263
|
-
|
264
|
-
it 'the integer field should be the same as it was in string form' do
|
265
|
-
@convertible.an_integer.to_s.should.equal('2')
|
266
|
-
end
|
267
|
-
|
268
|
-
it 'returns a float for a float field' do
|
269
|
-
@convertible.a_float.should.is_a(Float)
|
270
|
-
end
|
271
|
-
|
272
|
-
it 'the float field should be the same as it was in string form' do
|
273
|
-
@convertible.a_float.should.>(3.6)
|
274
|
-
@convertible.a_float.should.<(3.8)
|
275
|
-
end
|
276
|
-
|
277
|
-
it 'returns a double for a double field' do
|
278
|
-
@convertible.a_double.should.is_a(Float)
|
279
|
-
end
|
280
|
-
|
281
|
-
it 'the double field should be the same as it was in string form' do
|
282
|
-
@convertible.a_double.should.>(3.41458)
|
283
|
-
@convertible.a_double.should.<(3.41460)
|
284
|
-
end
|
285
|
-
|
286
|
-
it 'returns a NSDate for a date field' do
|
287
|
-
@convertible.a_date.should.is_a(NSDate)
|
288
|
-
end
|
289
|
-
|
290
|
-
it 'the date field should be the same as it was in string form' do
|
291
|
-
@convertible.a_date.to_s.should.match(/^2012-09-15/)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
236
|
describe 'defining custom attributes' do
|
296
237
|
before do
|
297
238
|
Task.delete_all
|
@@ -0,0 +1,110 @@
|
|
1
|
+
class Hash
|
2
|
+
def except(keys)
|
3
|
+
self.dup.reject{|k, v| keys.include?(k)}
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
class ValidatableTask
|
8
|
+
include MotionModel::Model
|
9
|
+
include MotionModel::Validatable
|
10
|
+
columns :name => :string,
|
11
|
+
:email => :string,
|
12
|
+
:some_day => :string
|
13
|
+
|
14
|
+
validate :name, :presence => true
|
15
|
+
validate :name, :length => 2..10
|
16
|
+
validate :email, :email => true
|
17
|
+
validate :some_day, :format => /\A\d?\d-\d?\d-\d\d\Z/
|
18
|
+
validate :some_day, :length => 8..10
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "validations" do
|
22
|
+
before do
|
23
|
+
@valid_tasks = {
|
24
|
+
:name => 'bob',
|
25
|
+
:email => 'bob@domain.com',
|
26
|
+
:some_day => '12-12-12'
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "presence" do
|
31
|
+
it "is initially false if name is blank" do
|
32
|
+
task = ValidatableTask.new(@valid_tasks.except(:name))
|
33
|
+
task.valid?.should === false
|
34
|
+
end
|
35
|
+
|
36
|
+
it "contains correct error message if name is blank" do
|
37
|
+
task = ValidatableTask.new(@valid_tasks.except(:name))
|
38
|
+
task.valid?
|
39
|
+
task.error_messages_for(:name).first.should ==
|
40
|
+
"incorrect value supplied for name -- should be non-empty."
|
41
|
+
end
|
42
|
+
|
43
|
+
it "is true if name is filled in" do
|
44
|
+
task = ValidatableTask.create(@valid_tasks.except(:name))
|
45
|
+
task.name = 'bob'
|
46
|
+
task.valid?.should === true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "length" do
|
51
|
+
it "succeeds when in range of 2-10 characters" do
|
52
|
+
task = ValidatableTask.create(@valid_tasks.except(:name))
|
53
|
+
task.name = '123456'
|
54
|
+
task.valid?.should === true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "fails when length less than two characters" do
|
58
|
+
task = ValidatableTask.create(@valid_tasks.except(:name))
|
59
|
+
task.name = '1'
|
60
|
+
task.valid?.should === false
|
61
|
+
task.error_messages_for(:name).first.should ==
|
62
|
+
"incorrect value supplied for name -- should be between 2 and 10 characters long."
|
63
|
+
end
|
64
|
+
|
65
|
+
it "fails when length greater than 10 characters" do
|
66
|
+
task = ValidatableTask.create(@valid_tasks.except(:name))
|
67
|
+
task.name = '123456709AB'
|
68
|
+
task.valid?.should === false
|
69
|
+
task.error_messages_for(:name).first.should ==
|
70
|
+
"incorrect value supplied for name -- should be between 2 and 10 characters long."
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "email" do
|
75
|
+
it "succeeds when a valid email address is supplied" do
|
76
|
+
ValidatableTask.new(@valid_tasks).should.be.valid?
|
77
|
+
end
|
78
|
+
|
79
|
+
it "fails when an empty email address is supplied" do
|
80
|
+
ValidatableTask.new(@valid_tasks.except(:email)).should.not.be.valid?
|
81
|
+
end
|
82
|
+
|
83
|
+
it "fails when a bogus email address is supplied" do
|
84
|
+
ValidatableTask.new(@valid_tasks.except(:email).merge({:email => 'bogus'})).should.not.be.valid?
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "format" do
|
89
|
+
it "succeeds when date is in the correct format" do
|
90
|
+
ValidatableTask.new(@valid_tasks).should.be.valid?
|
91
|
+
end
|
92
|
+
|
93
|
+
it "fails when date is in incorrect format" do
|
94
|
+
ValidatableTask.new(@valid_tasks.except(:some_day).merge({:some_day => 'a-12-12'})).should.not.be.valid?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "validating one element" do
|
99
|
+
it "validates any properly formatted arbitrary string and succeeds" do
|
100
|
+
task = ValidatableTask.new
|
101
|
+
task.validate_for(:some_day, '12-12-12').should == true
|
102
|
+
end
|
103
|
+
|
104
|
+
it "validates any improperly formatted arbitrary string and fails" do
|
105
|
+
task = ValidatableTask.new
|
106
|
+
task.validate_for(:some_day, 'a-12-12').should == false
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: motion_model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.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-
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bubble-wrap
|
@@ -46,6 +46,7 @@ files:
|
|
46
46
|
- lib/motion_model/model/column.rb
|
47
47
|
- lib/motion_model/model/finder_query.rb
|
48
48
|
- lib/motion_model/model/model.rb
|
49
|
+
- lib/motion_model/model/model_casts.rb
|
49
50
|
- lib/motion_model/model/persistence.rb
|
50
51
|
- lib/motion_model/validatable.rb
|
51
52
|
- lib/motion_model/version.rb
|
@@ -53,10 +54,13 @@ files:
|
|
53
54
|
- spec/cascading_delete_spec.rb
|
54
55
|
- spec/ext_spec.rb
|
55
56
|
- spec/finder_spec.rb
|
57
|
+
- spec/model_casting_spec.rb
|
58
|
+
- spec/model_hook_spec.rb
|
56
59
|
- spec/model_spec.rb
|
57
60
|
- spec/notification_spec.rb
|
58
61
|
- spec/persistence_spec.rb
|
59
62
|
- spec/relation_spec.rb
|
63
|
+
- spec/validation_spec.rb
|
60
64
|
homepage: https://github.com/sxross/MotionModel
|
61
65
|
licenses: []
|
62
66
|
post_install_message:
|
@@ -85,7 +89,10 @@ test_files:
|
|
85
89
|
- spec/cascading_delete_spec.rb
|
86
90
|
- spec/ext_spec.rb
|
87
91
|
- spec/finder_spec.rb
|
92
|
+
- spec/model_casting_spec.rb
|
93
|
+
- spec/model_hook_spec.rb
|
88
94
|
- spec/model_spec.rb
|
89
95
|
- spec/notification_spec.rb
|
90
96
|
- spec/persistence_spec.rb
|
91
97
|
- spec/relation_spec.rb
|
98
|
+
- spec/validation_spec.rb
|