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 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
- @dirty = false
413
-
414
- # Existing object implies update in place
415
- # TODO: Optimize location of existing id
416
- action = 'add'
417
- if obj = collection.find{|o| o.id == @data[:id]}
418
- obj = self
419
- action = 'update'
420
- else
421
- collection << self
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
- target_index = collection.index{|item| item.id == self.id}
430
- collection.delete_at(target_index)
431
- self.class.issue_notification(self, :action => 'delete')
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
- # Handle attribute retrieval
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 == {} # || !(validation_type is_a?(Hash))
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.instance_variable_get(@validations).each do |validations|
39
+ self.class.validations.each do |validations|
28
40
  validate_each(validations)
29
41
  end
30
42
  @valid
31
43
  end
32
44
 
33
- def validate_each(validations)
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
- 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}"}
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
- def validate_presence(field)
59
- value = self.send(field.to_s)
60
- return false if value.nil?
61
- return value.strip.length > 0
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
@@ -1,3 +1,3 @@
1
1
  module MotionModel
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -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 :an_int => {:type => :int, :default => 3},
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.0
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-07 00:00:00.000000000 Z
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