motion_model 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|