nobrainer 0.20.0 → 0.21.0

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/autoload.rb +0 -5
  3. data/lib/no_brainer/config.rb +14 -9
  4. data/lib/no_brainer/connection.rb +1 -3
  5. data/lib/no_brainer/criteria.rb +1 -1
  6. data/lib/no_brainer/criteria/aggregate.rb +2 -2
  7. data/lib/no_brainer/criteria/cache.rb +8 -3
  8. data/lib/no_brainer/criteria/core.rb +1 -5
  9. data/lib/no_brainer/criteria/delete.rb +1 -1
  10. data/lib/no_brainer/criteria/eager_load.rb +51 -0
  11. data/lib/no_brainer/criteria/order_by.rb +1 -1
  12. data/lib/no_brainer/criteria/scope.rb +3 -10
  13. data/lib/no_brainer/criteria/update.rb +8 -6
  14. data/lib/no_brainer/criteria/where.rb +50 -13
  15. data/lib/no_brainer/document.rb +2 -2
  16. data/lib/no_brainer/document/aliases.rb +0 -8
  17. data/lib/no_brainer/document/association/belongs_to.rb +6 -2
  18. data/lib/no_brainer/document/association/core.rb +5 -4
  19. data/lib/no_brainer/document/association/eager_loader.rb +7 -8
  20. data/lib/no_brainer/document/association/has_many.rb +22 -8
  21. data/lib/no_brainer/document/association/has_many_through.rb +12 -3
  22. data/lib/no_brainer/document/atomic_ops.rb +63 -61
  23. data/lib/no_brainer/document/attributes.rb +11 -3
  24. data/lib/no_brainer/document/core.rb +5 -2
  25. data/lib/no_brainer/document/criteria.rb +14 -5
  26. data/lib/no_brainer/document/dirty.rb +11 -16
  27. data/lib/no_brainer/document/index.rb +0 -6
  28. data/lib/no_brainer/document/index/meta_store.rb +1 -1
  29. data/lib/no_brainer/document/persistance.rb +12 -2
  30. data/lib/no_brainer/document/types.rb +13 -12
  31. data/lib/no_brainer/document/types/binary.rb +0 -4
  32. data/lib/no_brainer/document/types/boolean.rb +0 -1
  33. data/lib/no_brainer/document/types/geo.rb +1 -0
  34. data/lib/no_brainer/document/types/string.rb +3 -0
  35. data/lib/no_brainer/document/types/text.rb +18 -0
  36. data/lib/no_brainer/document/validation.rb +31 -6
  37. data/lib/no_brainer/document/validation/not_null.rb +15 -0
  38. data/lib/no_brainer/document/{uniqueness.rb → validation/uniqueness.rb} +11 -10
  39. data/lib/no_brainer/error.rb +20 -23
  40. data/lib/no_brainer/locale/en.yml +1 -0
  41. data/lib/no_brainer/lock.rb +114 -0
  42. data/lib/no_brainer/query_runner/database_on_demand.rb +0 -1
  43. data/lib/no_brainer/query_runner/missing_index.rb +1 -1
  44. data/lib/no_brainer/query_runner/run_options.rb +0 -3
  45. data/lib/no_brainer/query_runner/table_on_demand.rb +2 -3
  46. data/lib/no_brainer/rql.rb +0 -4
  47. data/lib/nobrainer.rb +1 -1
  48. metadata +8 -4
  49. data/lib/no_brainer/criteria/preload.rb +0 -44
@@ -2,7 +2,7 @@ module NoBrainer::Document::Core
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  singleton_class.class_eval do
5
- attr_accessor :_all
5
+ attr_accessor :_all, :_all_nobrainer
6
6
 
7
7
  def all
8
8
  Rails.application.eager_load! if defined?(Rails.application.eager_load!)
@@ -10,10 +10,12 @@ module NoBrainer::Document::Core
10
10
  end
11
11
  end
12
12
  self._all = []
13
+ self._all_nobrainer = []
13
14
 
14
15
  include ActiveModel::Conversion
15
16
 
16
17
  def to_key
18
+ # ActiveModel::Conversion stuff
17
19
  [pk_value]
18
20
  end
19
21
 
@@ -22,6 +24,7 @@ module NoBrainer::Document::Core
22
24
  extend ActiveModel::Naming
23
25
  extend ActiveModel::Translation
24
26
 
25
- NoBrainer::Document::Core._all << self unless self.name =~ /^NoBrainer::/
27
+ list_name = self.name =~ /^NoBrainer::/ ? :_all_nobrainer : :_all
28
+ NoBrainer::Document::Core.__send__(list_name) << self
26
29
  end
27
30
  end
@@ -6,8 +6,9 @@ module NoBrainer::Document::Criteria
6
6
  end
7
7
 
8
8
  included do
9
- cattr_accessor :default_scope_proc, :instance_accessor => false
10
9
  cattr_accessor :perf_warnings_disabled, :instance_accessor => false
10
+ singleton_class.send(:attr_accessor, :default_scopes)
11
+ self.default_scopes = []
11
12
  end
12
13
 
13
14
  module ClassMethods
@@ -21,7 +22,7 @@ module NoBrainer::Document::Criteria
21
22
  :with_cache, :without_cache, # Cache
22
23
  :count, :empty?, :any?, # Count
23
24
  :delete_all, :destroy_all, # Delete
24
- :includes, :preload, # Preload
25
+ :preload, :eager_load, # EagerLoad
25
26
  :each, :to_a, # Enumerable
26
27
  :first, :last, :first!, :last!, :sample, # First
27
28
  :min, :max, :sum, :avg, # Aggregate
@@ -42,9 +43,17 @@ module NoBrainer::Document::Criteria
42
43
  end
43
44
 
44
45
  def default_scope(criteria=nil, &block)
45
- criteria ||= block
46
- raise "store_in() must be called on the parent class" unless is_root_class?
47
- self.default_scope_proc = criteria.is_a?(Proc) ? criteria : proc { criteria }
46
+ criteria_proc = block || (criteria.is_a?(Proc) ? criteria : proc { criteria })
47
+ raise "default_scope only accepts a criteria or a proc that returns criteria" unless criteria_proc.is_a?(Proc)
48
+
49
+ ([self] + self.descendants).each do |model|
50
+ model.default_scopes << criteria_proc
51
+ end
52
+ end
53
+
54
+ def inherited(subclass)
55
+ subclass.default_scopes = self.default_scopes.dup
56
+ super
48
57
  end
49
58
 
50
59
  def selector_for(pk)
@@ -31,14 +31,10 @@ module NoBrainer::Document::Dirty
31
31
  changes.keys
32
32
  end
33
33
 
34
- def read_attribute_for_change(attr)
35
- read_attribute(attr)
36
- end
37
-
38
34
  def changes
39
35
  result = {}.with_indifferent_access
40
36
  @_old_attributes.each do |attr, old_value|
41
- current_value = read_attribute_for_change(attr)
37
+ current_value = read_attribute(attr)
42
38
  if current_value != old_value || !@_old_attributes_keys.include?(attr)
43
39
  result[attr] = [old_value, current_value]
44
40
  end
@@ -46,16 +42,15 @@ module NoBrainer::Document::Dirty
46
42
  result
47
43
  end
48
44
 
49
- def attribute_may_change(*args)
50
- attr = args.first
51
- current_value = begin
52
- case args.size
53
- when 1 then assert_access_field(attr); read_attribute_for_change(attr)
54
- when 2 then args.last
55
- else raise
45
+ class None; end
46
+ def attribute_may_change(attr, current_value = None)
47
+ if current_value == None
48
+ current_value = begin
49
+ assert_access_field(attr)
50
+ read_attribute(attr)
51
+ rescue NoBrainer::Error::MissingAttribute => e
52
+ e
56
53
  end
57
- rescue NoBrainer::Error::MissingAttribute => e
58
- e
59
54
  end
60
55
 
61
56
  unless @_old_attributes.has_key?(attr)
@@ -84,7 +79,7 @@ module NoBrainer::Document::Dirty
84
79
  inject_in_layer :dirty_tracking do
85
80
  define_method("#{attr}_change") do
86
81
  if @_old_attributes.has_key?(attr)
87
- result = [@_old_attributes[attr], read_attribute_for_change(attr)]
82
+ result = [@_old_attributes[attr], read_attribute(attr)]
88
83
  result if result.first != result.last || !@_old_attributes_keys.include?(attr)
89
84
  end
90
85
  end
@@ -94,7 +89,7 @@ module NoBrainer::Document::Dirty
94
89
  end
95
90
 
96
91
  define_method("#{attr}_was") do
97
- @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute_for_change(attr)
92
+ @_old_attributes.has_key?(attr) ? @_old_attributes[attr] : read_attribute(attr)
98
93
  end
99
94
  end
100
95
  end
@@ -14,12 +14,6 @@ module NoBrainer::Document::Index
14
14
  def index(name, *args)
15
15
  name = name.to_sym
16
16
  options = args.extract_options!
17
-
18
- if options[:as]
19
- STDERR.puts "[NoBrainer] `:as' is deprecated and will be removed. Please use `:store_as' instead (from the #{self} model)"
20
- options[:store_as] = options.delete(:as)
21
- end
22
-
23
17
  options.assert_valid_keys(*VALID_INDEX_OPTIONS)
24
18
 
25
19
  raise "Too many arguments: #{args}" if args.size > 1
@@ -24,7 +24,7 @@ class NoBrainer::Document::Index::MetaStore
24
24
  def self.on(db_name, &block)
25
25
  old_db_name = Thread.current[:nobrainer_meta_store_db]
26
26
  Thread.current[:nobrainer_meta_store_db] = db_name
27
- NoBrainer.with(:auto_create_tables => true) { block.call }
27
+ block.call
28
28
  ensure
29
29
  Thread.current[:nobrainer_meta_store_db] = old_db_name
30
30
  end
@@ -113,13 +113,11 @@ module NoBrainer::Document::Persistance
113
113
  assign_attributes(attrs, options)
114
114
  save?(options)
115
115
  end
116
- alias_method :update_attributes?, :update?
117
116
 
118
117
  def update(*args)
119
118
  update?(*args) or raise NoBrainer::Error::DocumentInvalid, self
120
119
  nil
121
120
  end
122
- alias_method :update_attributes, :update
123
121
 
124
122
  def update!(*args)
125
123
  update(*args)
@@ -127,6 +125,18 @@ module NoBrainer::Document::Persistance
127
125
  end
128
126
  alias_method :update_attributes!, :update!
129
127
 
128
+ def update_attributes?(*args)
129
+ update?(*args).tap { STDERR.puts "[NoBrainer] update_attribute?() is deprecated. Please use update?() instead" }
130
+ end
131
+
132
+ def update_attributes(*args)
133
+ update(*args).tap { STDERR.puts "[NoBrainer] update_attribute() is deprecated. Please use update() instead" }
134
+ end
135
+
136
+ def update_attributes!(*args)
137
+ update!(*args).tap { STDERR.puts "[NoBrainer] update_attribute!() is deprecated. Please use update() instead" }
138
+ end
139
+
130
140
  def delete
131
141
  unless @destroyed
132
142
  NoBrainer.run { selector.delete }
@@ -6,7 +6,7 @@ module NoBrainer::Document::Types
6
6
  def add_type_errors
7
7
  return unless @pending_type_errors
8
8
  @pending_type_errors.each do |name, error|
9
- errors.add(name, :invalid_type, :type => error.human_type_name)
9
+ errors.add(name, :invalid_type, error.error)
10
10
  end
11
11
  end
12
12
 
@@ -22,7 +22,10 @@ module NoBrainer::Document::Types
22
22
  module ClassMethods
23
23
  def cast_user_to_model_for(attr, value)
24
24
  type = fields[attr.to_sym].try(:[], :type)
25
- return value if type.nil? || value.nil? || value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic)
25
+ return value if type.nil? || value.nil? ||
26
+ value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) ||
27
+ value.is_a?(RethinkDB::RQL)
28
+
26
29
  if type.respond_to?(:nobrainer_cast_user_to_model)
27
30
  type.nobrainer_cast_user_to_model(value)
28
31
  else
@@ -30,9 +33,7 @@ module NoBrainer::Document::Types
30
33
  value
31
34
  end
32
35
  rescue NoBrainer::Error::InvalidType => error
33
- error.type = type
34
- error.value = value
35
- error.attr_name = attr
36
+ error.update(:model => self, :value => value, :attr_name => attr, :type => type)
36
37
  raise error
37
38
  end
38
39
 
@@ -62,14 +63,15 @@ module NoBrainer::Document::Types
62
63
 
63
64
  return unless options[:type]
64
65
 
65
- NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
66
-
66
+ raise "Please use a class for the type option" unless options[:type].is_a?(Class)
67
67
  case options[:type].to_s
68
68
  when "NoBrainer::Geo::Circle" then raise "Cannot store circles :("
69
69
  when "NoBrainer::Geo::Polygon", "NoBrainer::Geo::LineString"
70
70
  raise "Make a request on github if you'd like to store polygons/linestrings"
71
71
  end
72
72
 
73
+ NoBrainer::Document::Types.load_type_extensions(options[:type]) if options[:type]
74
+
73
75
  inject_in_layer :types do
74
76
  define_method("#{attr}=") do |value|
75
77
  begin
@@ -96,11 +98,10 @@ module NoBrainer::Document::Types
96
98
  end
97
99
  end
98
100
 
99
- require File.join(File.dirname(__FILE__), 'types', 'binary')
100
- require File.join(File.dirname(__FILE__), 'types', 'boolean')
101
- Binary = NoBrainer::Binary
102
- Boolean = NoBrainer::Boolean
103
- Geo = NoBrainer::Geo
101
+ %w(binary boolean text geo).each do |type|
102
+ require File.join(File.dirname(__FILE__), 'types', type)
103
+ const_set(type.camelize, NoBrainer.const_get(type.camelize))
104
+ end
104
105
 
105
106
  class << self
106
107
  mattr_accessor :loaded_extensions
@@ -13,10 +13,6 @@ class NoBrainer::Binary
13
13
  else raise InvalidType
14
14
  end
15
15
  end
16
-
17
- def nobrainer_cast_db_to_model(value)
18
- value.is_a?(String) ? RethinkDB::Binary.new(value) : value
19
- end
20
16
  end
21
17
  extend NoBrainerExtentions
22
18
  end
@@ -1,4 +1,3 @@
1
- # We namespace our fake Boolean class to avoid polluting the global namespace
2
1
  class NoBrainer::Boolean
3
2
  def initialize; raise; end
4
3
  def self.inspect; 'Boolean'; end
@@ -0,0 +1 @@
1
+ # Look in lib/no_brainer/geo.rb instead
@@ -7,6 +7,9 @@ class String
7
7
  when String then value
8
8
  when Symbol then value.to_s
9
9
  else raise InvalidType
10
+ end.tap do |str|
11
+ max_length = NoBrainer::Config.max_string_length
12
+ raise InvalidType.new(:error => { :message => :too_long, :count => max_length }) if str.size > max_length
10
13
  end
11
14
  end
12
15
  end
@@ -0,0 +1,18 @@
1
+ class NoBrainer::Text
2
+ def initialize; raise; end
3
+ def self.inspect; 'Text'; end
4
+ def self.to_s; inspect; end
5
+ def self.name; inspect; end
6
+
7
+ module NoBrainerExtentions
8
+ InvalidType = NoBrainer::Error::InvalidType
9
+
10
+ def nobrainer_cast_user_to_model(value)
11
+ case value
12
+ when String then value
13
+ else raise InvalidType
14
+ end
15
+ end
16
+ end
17
+ extend NoBrainerExtentions
18
+ end
@@ -1,8 +1,11 @@
1
1
  module NoBrainer::Document::Validation
2
+ extend NoBrainer::Autoload
2
3
  extend ActiveSupport::Concern
3
4
  include ActiveModel::Validations
4
5
  include ActiveModel::Validations::Callbacks
5
6
 
7
+ autoload_and_include :Uniqueness, :NotNull
8
+
6
9
  included do
7
10
  # We don't want before_validation returning false to halt the chain.
8
11
  define_callbacks :validation, :skip_after_callbacks_if_terminated => true,
@@ -12,7 +15,7 @@ module NoBrainer::Document::Validation
12
15
  def valid?(context=nil, options={})
13
16
  context ||= new_record? ? :create : :update
14
17
 
15
- # copy/pasted, because we need to have control on errors.clear
18
+ # XXX Monkey Patching, because we need to have control on errors.clear
16
19
  current_context, self.validation_context = validation_context, context
17
20
  errors.clear unless options[:clear_errors] == false
18
21
  run_validations!
@@ -20,15 +23,37 @@ module NoBrainer::Document::Validation
20
23
  self.validation_context = current_context
21
24
  end
22
25
 
26
+ SHORTHANDS = { :format => :format, :length => :length, :required => :presence,
27
+ :uniq => :uniqueness, :unique => :uniqueness, :in => :inclusion }
28
+
23
29
  module ClassMethods
24
30
  def _field(attr, options={})
25
31
  super
26
- validates(attr, :format => { :with => options[:format] }) if options.has_key?(:format)
27
- validates(attr, :presence => options[:required]) if options.has_key?(:required)
28
- validates(attr, :uniqueness => options[:unique]) if options.has_key?(:unique)
29
- validates(attr, :uniqueness => options[:uniq]) if options.has_key?(:uniq)
30
- validates(attr, :inclusion => {:in => options[:in]}) if options.has_key?(:in)
32
+
33
+ shorthands = SHORTHANDS
34
+ shorthands = shorthands.merge(:required => :not_null) if options[:type] == NoBrainer::Boolean
35
+ shorthands.each { |k,v| validates(attr, v => options[k]) if options.has_key?(k) }
36
+
31
37
  validates(attr, options[:validates]) if options[:validates]
38
+ validates(attr, :length => { :minimum => options[:min_length] }) if options[:min_length]
39
+ validates(attr, :length => { :maximum => options[:max_length] }) if options[:max_length]
40
+ end
41
+ end
42
+ end
43
+
44
+ class ActiveModel::EachValidator
45
+ def should_validate_field?(record, attribute)
46
+ record.new_record? || record.__send__("#{attribute}_changed?")
47
+ end
48
+
49
+ # XXX Monkey Patching :(
50
+ def validate(record)
51
+ attributes.each do |attribute|
52
+ next unless should_validate_field?(record, attribute) # <--- Added
53
+ value = record.read_attribute_for_validation(attribute)
54
+ next if value.is_a?(NoBrainer::Document::AtomicOps::PendingAtomic) # <--- Added
55
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
56
+ validate_each(record, attribute, value)
32
57
  end
33
58
  end
34
59
  end
@@ -0,0 +1,15 @@
1
+ module NoBrainer::Document::Validation::NotNull
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def validates_not_null(*attr_names)
6
+ validates_with(NotNullValidator, _merge_attributes(attr_names))
7
+ end
8
+ end
9
+
10
+ class NotNullValidator < ActiveModel::EachValidator
11
+ def validate_each(doc, attr, value)
12
+ doc.errors.add(attr, :undefined, options) if value.nil?
13
+ end
14
+ end
15
+ end
@@ -1,4 +1,4 @@
1
- module NoBrainer::Document::Uniqueness
1
+ module NoBrainer::Document::Validation::Uniqueness
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def _create(options={})
@@ -27,7 +27,7 @@ module NoBrainer::Document::Uniqueness
27
27
  self.class.unique_validators
28
28
  .map { |validator| validator.attributes.map { |attr| [attr, validator] } }
29
29
  .flatten(1)
30
- .select { |f, validator| validator.should_validate_uniquess_of?(self, f) }
30
+ .select { |f, validator| validator.should_validate_field?(self, f) }
31
31
  .map { |f, options| _lock_key_from_field(f) }
32
32
  .sort
33
33
  .uniq
@@ -51,7 +51,7 @@ module NoBrainer::Document::Uniqueness
51
51
 
52
52
  module ClassMethods
53
53
  def validates_uniqueness_of(*attr_names)
54
- validates_with UniquenessValidator, _merge_attributes(attr_names)
54
+ validates_with(UniquenessValidator, _merge_attributes(attr_names))
55
55
  end
56
56
 
57
57
  def inherited(subclass)
@@ -72,19 +72,20 @@ module NoBrainer::Document::Uniqueness
72
72
  end
73
73
  end
74
74
 
75
- def should_validate_uniquess_of?(doc, field)
76
- (scope + [field]).any? { |f| doc.__send__("#{f}_changed?") }
75
+ def should_validate_field?(doc, field)
76
+ doc.new_record? || (scope + [field]).any? { |f| doc.__send__("#{f}_changed?") }
77
77
  end
78
78
 
79
79
  def validate_each(doc, attr, value)
80
- return true unless should_validate_uniquess_of?(doc, attr)
81
-
82
80
  criteria = doc.root_class.unscoped.where(attr => value)
83
81
  criteria = apply_scopes(criteria, doc)
84
82
  criteria = exclude_doc(criteria, doc) if doc.persisted?
85
- is_unique = criteria.count == 0
86
- doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless is_unique
87
- is_unique
83
+ doc.errors.add(attr, :taken, options.except(:scope).merge(:value => value)) unless criteria.empty?
84
+ rescue NoBrainer::Error::InvalidType
85
+ # We can't run the uniqueness validator: where() won't accept bad types
86
+ # and we have some values that don't have the right type.
87
+ # Note that it's fine to not add errors because the type validations will
88
+ # prevent the document from being saved.
88
89
  end
89
90
 
90
91
  def apply_scopes(criteria, doc)
@@ -5,25 +5,13 @@ module NoBrainer::Error
5
5
  class ChildrenExist < RuntimeError; end
6
6
  class CannotUseIndex < RuntimeError; end
7
7
  class MissingIndex < RuntimeError; end
8
- class InvalidType < RuntimeError; end
9
8
  class AssociationNotPersisted < RuntimeError; end
10
9
  class ReadonlyField < RuntimeError; end
11
10
  class MissingAttribute < RuntimeError; end
12
11
  class UnknownAttribute < RuntimeError; end
13
12
  class AtomicBlock < RuntimeError; end
14
-
15
- class CannotReadAtomic < RuntimeError
16
- attr_accessor :instance, :field, :value
17
- def initialize(instance, field, value)
18
- @instance = instance
19
- @field = field
20
- @value = value
21
- end
22
-
23
- def message
24
- "Cannot read #{field}, atomic operations are pending"
25
- end
26
- end
13
+ class LostLock < RuntimeError; end
14
+ class LockUnavailable < RuntimeError; end
27
15
 
28
16
  class DocumentInvalid < RuntimeError
29
17
  attr_accessor :instance
@@ -37,23 +25,32 @@ module NoBrainer::Error
37
25
  end
38
26
 
39
27
  class InvalidType < RuntimeError
40
- attr_accessor :attr_name, :value, :type
28
+ attr_accessor :model, :attr_name, :value, :type, :error
41
29
  def initialize(options={})
42
- @attr_name = options[:attr_name]
43
- @value = options[:value]
44
- @type = options[:type]
30
+ update(options)
31
+ end
32
+
33
+ def update(options={})
34
+ options.assert_valid_keys(:model, :attr_name, :type, :value, :error)
35
+ options.each { |k,v| instance_variable_set("@#{k}", v) }
45
36
  end
46
37
 
47
38
  def human_type_name
48
39
  type.to_s.underscore.humanize.downcase
49
40
  end
50
41
 
42
+ def error
43
+ # dup because errors.add eventually .delete() on our option.
44
+ @error.nil? ? (type && { :type => human_type_name }) : @error.dup
45
+ end
46
+
51
47
  def message
52
- if attr_name && type && value
53
- "#{attr_name} should be used with a #{human_type_name}. Got `#{value}` (#{value.class})"
54
- else
55
- super
56
- end
48
+ return super unless model && attr_name && error
49
+ value = self.value
50
+ mock = model.allocate
51
+ mock.singleton_class.send(:define_method, :read_attribute_for_validation) { |_| value }
52
+ mock.errors.add(attr_name, :invalid_type, error)
53
+ mock.errors.full_messages.first
57
54
  end
58
55
  end
59
56