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