objective 0.1.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/lib/objective/allow.rb +10 -0
  3. data/lib/objective/deny.rb +10 -0
  4. data/lib/objective/discard.rb +10 -0
  5. data/lib/objective/errors/error_array.rb +21 -0
  6. data/lib/objective/errors/error_atom.rb +27 -0
  7. data/lib/objective/errors/error_hash.rb +65 -0
  8. data/lib/objective/errors/error_message_creator.rb +46 -0
  9. data/lib/objective/errors/validation_error.rb +14 -0
  10. data/lib/objective/filter.rb +166 -0
  11. data/lib/objective/filters/any_filter.rb +7 -0
  12. data/lib/objective/filters/array_filter.rb +49 -0
  13. data/lib/objective/filters/boolean_filter.rb +21 -0
  14. data/lib/objective/filters/date_filter.rb +29 -0
  15. data/lib/objective/filters/decimal_filter.rb +46 -0
  16. data/lib/objective/filters/duck_filter.rb +18 -0
  17. data/lib/objective/filters/file_filter.rb +22 -0
  18. data/lib/objective/filters/float_filter.rb +45 -0
  19. data/lib/objective/filters/hash_filter.rb +42 -0
  20. data/lib/objective/filters/integer_filter.rb +60 -0
  21. data/lib/objective/filters/model_filter.rb +23 -0
  22. data/lib/objective/filters/root_filter.rb +54 -0
  23. data/lib/objective/filters/string_filter.rb +30 -0
  24. data/lib/objective/filters/time_filter.rb +30 -0
  25. data/lib/objective/filters.rb +140 -0
  26. data/lib/objective/none.rb +10 -0
  27. data/lib/objective/outcome.rb +5 -0
  28. data/lib/objective/unit.rb +103 -0
  29. data/lib/objective.rb +53 -0
  30. data/spec/default_spec.rb +33 -0
  31. data/spec/errors_spec.rb +135 -0
  32. data/spec/filters/any_filter_spec.rb +34 -0
  33. data/spec/filters/array_filter_spec.rb +195 -0
  34. data/spec/filters/boolean_filter_spec.rb +66 -0
  35. data/spec/filters/date_filter_spec.rb +145 -0
  36. data/spec/filters/decimal_filter_spec.rb +199 -0
  37. data/spec/filters/duck_filter_spec.rb +49 -0
  38. data/spec/filters/file_filter_spec.rb +93 -0
  39. data/spec/filters/float_filter_spec.rb +140 -0
  40. data/spec/filters/hash_filter_spec.rb +65 -0
  41. data/spec/filters/integer_filter_spec.rb +150 -0
  42. data/spec/filters/model_filter_spec.rb +98 -0
  43. data/spec/filters/root_filter_spec.rb +113 -0
  44. data/spec/filters/string_filter_spec.rb +251 -0
  45. data/spec/filters/time_filter_spec.rb +123 -0
  46. data/spec/inheritance_spec.rb +36 -0
  47. data/spec/simple_unit.rb +18 -0
  48. data/spec/spec_helper.rb +9 -0
  49. data/spec/unit_spec.rb +244 -0
  50. metadata +167 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ class IntegerFilter < Objective::Filter
5
+ private
6
+
7
+ def coerce(datum)
8
+ return datum if datum.blank?
9
+
10
+ datum_str = raw_to_string(datum)
11
+ return datum unless datum_str
12
+
13
+ clean_str = datum_str.tr(options.delimiter, '').tr(options.decimal_mark, '.')
14
+ return datum unless clean_str =~ /\A[-+]?\d*\.?0*\z/
15
+ clean_str.to_i
16
+ end
17
+
18
+ def raw_to_string(raw)
19
+ if raw.is_a?(Float)
20
+ raw.to_s
21
+ elsif raw.is_a?(BigDecimal)
22
+ raw.to_s('F')
23
+ elsif raw.is_a?(String)
24
+ raw
25
+ end
26
+ end
27
+
28
+ def coerce_error(coerced)
29
+ return :integer unless coerced.is_a?(Integer)
30
+ end
31
+
32
+ def validate(coerced)
33
+ return :in unless included?(coerced)
34
+ return :min unless above_min?(coerced)
35
+ return :max unless below_max?(coerced)
36
+ return :scale unless within_scale?(coerced)
37
+ end
38
+
39
+ def included?(datum)
40
+ return true if options.in.nil?
41
+ options.in.include?(datum)
42
+ end
43
+
44
+ def above_min?(datum)
45
+ return true if options.min.nil?
46
+ datum >= options.min
47
+ end
48
+
49
+ def below_max?(datum)
50
+ return true if options.max.nil?
51
+ datum <= options.max
52
+ end
53
+
54
+ def within_scale?(datum)
55
+ return true if options.scale.nil?
56
+ (datum - datum.round(options.scale)).zero?
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ class ModelFilter < Objective::Filter
5
+ private
6
+
7
+ def coerce_error(coerced)
8
+ return :model unless coerced.is_a?(class_constant)
9
+ end
10
+
11
+ def validate(coerced)
12
+ return :new_records if !options.new_records && (coerced.respond_to?(:new_record?) && coerced.new_record?)
13
+ end
14
+
15
+ def class_constant
16
+ klass = options[:class]
17
+ return key.to_s.camelize.constantize if klass.nil?
18
+ return klass if klass.instance_of? Class
19
+ klass.to_s.constantize
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ class RootFilter < Objective::Filter
5
+ def filter(&block)
6
+ instance_eval(&block)
7
+ end
8
+
9
+ def keys
10
+ sub_filters.map(&:key)
11
+ end
12
+
13
+ def feed(*raw)
14
+ result = OpenStruct.new
15
+ result.raw = raw
16
+ result.coerced = coerce(raw)
17
+
18
+ inputs = OpenStruct.new
19
+ errors = Objective::Errors::ErrorHash.new
20
+
21
+ data = result.coerced
22
+ sub_filters_hash.each_pair do |key, key_filter|
23
+ datum = data.to_h.key?(key) ? data[key] : Objective::NONE
24
+ key_filter_result = key_filter.feed(datum)
25
+ next if key_filter_result == Objective::DISCARD
26
+
27
+ sub_data = key_filter_result.inputs
28
+ sub_error = key_filter_result.errors
29
+
30
+ if sub_error.nil?
31
+ inputs[key] = sub_data
32
+ else
33
+ errors[key] = key_filter.handle_errors(sub_error)
34
+ end
35
+ end
36
+
37
+ result.inputs = inputs
38
+ result.errors = errors.present? ? errors : nil
39
+ result
40
+ end
41
+
42
+ def coerce(raw)
43
+ raw.each_with_object(OpenStruct.new) do |datum, result|
44
+ raise_argument_error unless datum.respond_to?(:each_pair)
45
+ datum.each_pair { |key, value| result[key] = value }
46
+ end
47
+ end
48
+
49
+ def raise_argument_error
50
+ raise(ArgumentError, 'All Objective arguments must be a Hash or OpenStruct')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ class StringFilter < Objective::Filter
5
+ def coerce(raw)
6
+ return raw unless raw.is_a?(String) || coercable?(raw)
7
+ tmp = raw.is_a?(BigDecimal) ? raw.to_s(options.decimal_format) : raw.to_s
8
+ tmp = tmp.gsub(/[^[:print:]\t\r\n]+/, ' ') unless options.allow_control_characters
9
+ tmp = tmp.strip if options.strip
10
+ tmp
11
+ end
12
+
13
+ def coercable?(raw)
14
+ options.coercable_classes.map { |klass| raw.is_a?(klass) }.any?
15
+ end
16
+
17
+ def coerce_error(coerced)
18
+ return :string unless coerced.is_a?(String)
19
+ end
20
+
21
+ def validate(coerced)
22
+ return :empty if coerced.empty?
23
+ return :min if options.min && coerced.length < options.min
24
+ return :max if options.max && coerced.length > options.max
25
+ return :in if options.in && !options.in.include?(coerced)
26
+ return :matches if options.matches && (options.matches !~ coerced)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ class TimeFilter < Objective::Filter
5
+ private
6
+
7
+ def coerce(raw)
8
+ return raw if raw.is_a?(Time)
9
+ return parse(raw) if raw.is_a?(String)
10
+ return raw.to_time if raw.respond_to?(:to_time)
11
+ raw
12
+ end
13
+
14
+ def parse(raw)
15
+ options.format ? Time.strptime(raw, options.format) : Time.parse(raw)
16
+ rescue ArgumentError
17
+ nil
18
+ end
19
+
20
+ def coerce_error(coerced)
21
+ return :time unless coerced.is_a?(Time)
22
+ end
23
+
24
+ def validate(coerced)
25
+ return :after if options.after && coerced <= options.after
26
+ return :before if options.before && coerced >= options.before
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Filters
4
+ Config = OpenStruct.new
5
+
6
+ Config.any = OpenStruct.new(
7
+ none: Objective::DENY,
8
+ nils: Objective::ALLOW,
9
+ invalid: Objective::DENY
10
+ )
11
+
12
+ Config.array = OpenStruct.new(
13
+ none: Objective::DENY,
14
+ nils: Objective::DENY,
15
+ invalid: Objective::DENY,
16
+ wrap: false
17
+ )
18
+
19
+ Config.boolean = OpenStruct.new(
20
+ none: Objective::DENY,
21
+ nils: Objective::DENY,
22
+ invalid: Objective::DENY,
23
+ coercion_map: {
24
+ 'true' => true,
25
+ 'false' => false,
26
+ '1' => true,
27
+ '0' => false
28
+ }.freeze
29
+ )
30
+
31
+ Config.date = OpenStruct.new(
32
+ none: Objective::DENY,
33
+ nils: Objective::DENY,
34
+ invalid: Objective::DENY,
35
+ format: nil, # If nil, Date.parse will be used for coercion. If something like "%Y-%m-%d", Date.strptime is used
36
+ after: nil, # A date object, representing the minimum date allowed, inclusive
37
+ before: nil # A date object, representing the maximum date allowed, inclusive
38
+ )
39
+
40
+ Config.decimal = OpenStruct.new(
41
+ none: Objective::DENY,
42
+ nils: Objective::DENY,
43
+ invalid: Objective::DENY,
44
+ delimiter: ', ',
45
+ decimal_mark: '.',
46
+ min: nil,
47
+ max: nil,
48
+ scale: nil
49
+ )
50
+
51
+ Config.duck = OpenStruct.new(
52
+ none: Objective::DENY,
53
+ nils: Objective::DENY,
54
+ invalid: Objective::DENY,
55
+ methods: nil
56
+ )
57
+
58
+ Config.file = OpenStruct.new(
59
+ none: Objective::DENY,
60
+ nils: Objective::DENY,
61
+ invalid: Objective::DENY,
62
+ upload: false,
63
+ size: nil
64
+ )
65
+
66
+ Config.float = OpenStruct.new(
67
+ none: Objective::DENY,
68
+ nils: Objective::DENY,
69
+ invalid: Objective::DENY,
70
+ delimiter: ', ',
71
+ decimal_mark: '.',
72
+ min: nil,
73
+ max: nil,
74
+ scale: nil
75
+ )
76
+
77
+ Config.hash = OpenStruct.new(
78
+ none: Objective::DENY,
79
+ nils: Objective::DENY,
80
+ invalid: Objective::DENY
81
+ )
82
+
83
+ Config.integer = OpenStruct.new(
84
+ none: Objective::DENY,
85
+ nils: Objective::DENY,
86
+ invalid: Objective::DENY,
87
+ delimiter: ', ',
88
+ decimal_mark: '.',
89
+ min: nil,
90
+ max: nil,
91
+ scale: nil,
92
+ in: nil
93
+ )
94
+
95
+ Config.model = OpenStruct.new(
96
+ none: Objective::DENY,
97
+ nils: Objective::DENY,
98
+ invalid: Objective::DENY,
99
+ class: nil,
100
+ new_records: false
101
+ )
102
+
103
+ Config.root = OpenStruct.new
104
+
105
+ Config.string = OpenStruct.new(
106
+ none: Objective::DENY,
107
+ nils: Objective::DENY,
108
+ invalid: Objective::DENY,
109
+ allow_control_characters: false,
110
+ strip: true,
111
+ empty: Objective::DENY,
112
+ min: nil,
113
+ max: nil,
114
+ in: nil,
115
+ matches: nil,
116
+ decimal_format: 'F',
117
+ coercable_classes: [
118
+ Symbol,
119
+ TrueClass,
120
+ FalseClass,
121
+ Integer,
122
+ Float,
123
+ BigDecimal
124
+ ].freeze
125
+ )
126
+
127
+ Config.time = OpenStruct.new(
128
+ none: Objective::DENY,
129
+ nils: Objective::DENY,
130
+ invalid: Objective::DENY,
131
+ format: nil,
132
+ after: nil,
133
+ before: nil
134
+ )
135
+
136
+ def self.config
137
+ yield Config
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Objective
5
+ class None
6
+ include Singleton
7
+ end
8
+ end
9
+
10
+ Objective::NONE = Objective::None.instance.freeze
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ class Outcome < OpenStruct
4
+ end
5
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+ module Objective
3
+ module Unit
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_reader :inputs, :raw_inputs, :built
8
+ const_set('ALLOW', Objective::ALLOW)
9
+ const_set('DENY', Objective::DENY)
10
+ const_set('DISCARD', Objective::DISCARD)
11
+ end
12
+
13
+ class_methods do
14
+ def filter(&block)
15
+ root_filter.filter(&block)
16
+ root_filter.keys.each do |key|
17
+ define_method(key) { inputs[key] }
18
+ end
19
+ end
20
+
21
+ def root_filter
22
+ @root_filter ||=
23
+ superclass.try(:root_filter).try(:dup) ||
24
+ Objective::Filters::RootFilter.new
25
+ end
26
+
27
+ def build(*args)
28
+ new.build(*args)
29
+ end
30
+
31
+ def build!(*args)
32
+ outcome = build(*args)
33
+ return outcome.result if outcome.success
34
+ raise Objective::ValidationError, outcome.errors
35
+ end
36
+
37
+ def run(*args)
38
+ new.run(*args)
39
+ end
40
+
41
+ def run!(*args)
42
+ outcome = run(*args)
43
+ return outcome.result if outcome.success
44
+ raise Objective::ValidationError, outcome.errors
45
+ end
46
+ end
47
+
48
+ # INSTANCE METHODS
49
+
50
+ def build(*args)
51
+ filter_result = self.class.root_filter.feed(*args)
52
+ @raw_inputs = filter_result.coerced
53
+ @inputs = filter_result.inputs
54
+ @errors = filter_result.errors
55
+ @built = true
56
+ try('validate') if valid?
57
+ outcome
58
+ end
59
+
60
+ def run(*args)
61
+ build(*args) unless built
62
+ result = valid? ? try('execute') : nil
63
+ outcome(result)
64
+ end
65
+
66
+ def valid?
67
+ @errors.nil?
68
+ end
69
+
70
+ def outcome(result = nil)
71
+ Objective::Outcome.new(
72
+ success: valid?,
73
+ result: result,
74
+ errors: @errors,
75
+ inputs: inputs
76
+ )
77
+ end
78
+
79
+ protected
80
+
81
+ def add_error(key, kind, message = nil)
82
+ raise(ArgumentError, 'Invalid kind') unless kind.is_a?(Symbol)
83
+
84
+ @errors ||= Objective::Errors::ErrorHash.new
85
+ @errors.tap do |root_error_hash|
86
+ path = key.to_s.split('.')
87
+ last = path.pop
88
+
89
+ inner = path.inject(root_error_hash) do |current_error_hash, path_key|
90
+ current_error_hash[path_key] ||= Objective::Errors::ErrorHash.new
91
+ end
92
+
93
+ inner[last] = Objective::Errors::ErrorAtom.new(key, kind, message: message)
94
+ end
95
+ end
96
+
97
+ def merge_errors(hash)
98
+ return unless hash.any?
99
+ @errors ||= Objective::Errors::ErrorHash.new
100
+ @errors.merge!(hash)
101
+ end
102
+ end
103
+ end
data/lib/objective.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require 'ostruct'
3
+ require 'date'
4
+ require 'time'
5
+ require 'bigdecimal'
6
+ require 'bigdecimal/util'
7
+
8
+ require 'active_support/concern'
9
+ require 'active_support/core_ext/module/attribute_accessors'
10
+ require 'active_support/core_ext/object/try'
11
+ require 'active_support/core_ext/object/blank'
12
+ require 'active_support/core_ext/hash/indifferent_access'
13
+ require 'active_support/core_ext/hash/deep_merge'
14
+ require 'active_support/core_ext/array/wrap'
15
+ require 'active_support/core_ext/string/inflections'
16
+ require 'active_support/core_ext/integer/inflections'
17
+
18
+ require 'objective/allow'
19
+ require 'objective/deny'
20
+ require 'objective/discard'
21
+ require 'objective/invalid'
22
+ require 'objective/none'
23
+
24
+ require 'objective/errors/error_atom'
25
+ require 'objective/errors/error_hash'
26
+ require 'objective/errors/error_array'
27
+ require 'objective/errors/error_message_creator'
28
+ require 'objective/errors/validation_error'
29
+
30
+ require 'objective/filter'
31
+ require 'objective/filters'
32
+ require 'objective/filters/any_filter'
33
+ require 'objective/filters/array_filter'
34
+ require 'objective/filters/boolean_filter'
35
+ require 'objective/filters/date_filter'
36
+ require 'objective/filters/decimal_filter'
37
+ require 'objective/filters/duck_filter'
38
+ require 'objective/filters/file_filter'
39
+ require 'objective/filters/float_filter'
40
+ require 'objective/filters/hash_filter'
41
+ require 'objective/filters/integer_filter'
42
+ require 'objective/filters/model_filter'
43
+ require 'objective/filters/root_filter'
44
+ require 'objective/filters/string_filter'
45
+ require 'objective/filters/time_filter'
46
+
47
+ require 'objective/unit'
48
+ require 'objective/outcome'
49
+
50
+ module Objective
51
+ mattr_accessor :error_message_creator
52
+ self.error_message_creator = Errors::ErrorMessageCreator.new
53
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+ require 'simple_unit'
4
+
5
+ describe 'Objective - defaults' do
6
+ class DefaultUnit
7
+ include Objective::Unit
8
+ filter do
9
+ string :name, none: 'Bob Jones'
10
+ end
11
+
12
+ def execute
13
+ inputs.to_h
14
+ end
15
+ end
16
+
17
+ it 'should have a default if no value is passed' do
18
+ outcome = DefaultUnit.run
19
+ assert_equal({ name: 'Bob Jones' }, outcome.result)
20
+ assert_equal true, outcome.success
21
+ end
22
+
23
+ it 'should have the passed value if a value is passed' do
24
+ outcome = DefaultUnit.run(name: 'Fred')
25
+ assert_equal true, outcome.success
26
+ assert_equal({ name: 'Fred' }, outcome.result)
27
+ end
28
+
29
+ it 'should be an error if nil is passed on a required field with a default' do
30
+ outcome = DefaultUnit.run(name: nil)
31
+ assert_equal false, outcome.success
32
+ end
33
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ describe 'Objective - errors' do
5
+ class GivesErrors
6
+ include Objective::Unit
7
+ filter do
8
+ string :str1
9
+ string :str2, in: %w(opt1 opt2 opt3)
10
+ integer :int1, none: ALLOW
11
+
12
+ hash :hash1, none: ALLOW do
13
+ boolean :bool1
14
+ boolean :bool2
15
+ end
16
+
17
+ array :arr1, none: ALLOW do
18
+ integer
19
+ end
20
+ end
21
+
22
+ def execute
23
+ inputs
24
+ end
25
+ end
26
+
27
+ it 'returns an ErrorHash as the top level error object, and ErrorAtom\'s inside' do
28
+ o = GivesErrors.run(hash1: 1, arr1: 'bob')
29
+ assert !o.success
30
+ assert o.errors.is_a?(Objective::Errors::ErrorHash)
31
+ assert o.errors[:str1].is_a?(Objective::Errors::ErrorAtom)
32
+ assert o.errors[:str2].is_a?(Objective::Errors::ErrorAtom)
33
+ assert_nil o.errors[:int1]
34
+ assert o.errors[:hash1].is_a?(Objective::Errors::ErrorAtom)
35
+ assert o.errors[:arr1].is_a?(Objective::Errors::ErrorAtom)
36
+ end
37
+
38
+ it 'returns an ErrorHash for nested hashes' do
39
+ o = GivesErrors.run(hash1: { bool1: 'ooooo' })
40
+
41
+ assert !o.success
42
+ assert o.errors.is_a?(Objective::Errors::ErrorHash)
43
+ assert o.errors[:hash1].is_a?(Objective::Errors::ErrorHash)
44
+ assert o.errors[:hash1][:bool1].is_a?(Objective::Errors::ErrorAtom)
45
+ assert o.errors[:hash1][:bool2].is_a?(Objective::Errors::ErrorAtom)
46
+ end
47
+
48
+ it 'returns an ErrorArray for errors in arrays' do
49
+ o = GivesErrors.run(str1: 'a', str2: 'opt1', arr1: ['bob', 1, 'sally'])
50
+
51
+ assert !o.success
52
+ assert o.errors.is_a?(Objective::Errors::ErrorHash)
53
+ assert o.errors[:arr1].is_a?(Objective::Errors::ErrorArray)
54
+ assert o.errors[:arr1][0].is_a?(Objective::Errors::ErrorAtom)
55
+ assert_nil o.errors[:arr1][1]
56
+ assert o.errors[:arr1][2].is_a?(Objective::Errors::ErrorAtom)
57
+ end
58
+
59
+ it 'titleizes keys' do
60
+ atom = Objective::Errors::ErrorAtom.new(:newsletter_subscription, :boolean)
61
+ assert_equal 'Newsletter Subscription must be a boolean', atom.message
62
+ end
63
+
64
+ describe 'Bunch o errors' do
65
+ before do
66
+ @outcome = GivesErrors.run(
67
+ str1: '', str2: 'opt9', int1: 'zero', hash1: { bool1: 'bob' }, arr1: ['bob', 1, 'sally']
68
+ )
69
+ end
70
+
71
+ it 'gives coded errors' do
72
+ expected = {
73
+ 'str1' => :empty,
74
+ 'str2' => :in,
75
+ 'int1' => :integer,
76
+ 'hash1' => { 'bool1' => :boolean, 'bool2' => :required },
77
+ 'arr1' => [:integer, nil, :integer]
78
+ }
79
+
80
+ assert_equal expected, @outcome.errors.codes
81
+ end
82
+
83
+ it 'gives messages' do
84
+ expected = {
85
+ 'str1' => 'Str1 cannot be empty',
86
+ 'str2' => 'Str2 is not an available option',
87
+ 'int1' => 'Int1 must be an integer',
88
+ 'hash1' => {
89
+ 'bool1' => 'Bool1 must be a boolean',
90
+ 'bool2' => 'Bool2 is required'
91
+ },
92
+ 'arr1' => ['1st Arr1 must be an integer', nil, '3rd Arr1 must be an integer']
93
+ }
94
+
95
+ assert_equal expected, @outcome.errors.message
96
+ end
97
+
98
+ it 'can flatten those messages' do
99
+ expected = [
100
+ 'Str1 cannot be empty',
101
+ 'Str2 is not an available option',
102
+ 'Int1 must be an integer',
103
+ 'Bool1 must be a boolean',
104
+ 'Bool2 is required',
105
+ '1st Arr1 must be an integer',
106
+ '3rd Arr1 must be an integer'
107
+ ]
108
+
109
+ assert_equal expected, @outcome.errors.message_list
110
+ expected.each { |e| assert @outcome.errors.message_list.include?(e) }
111
+
112
+ assert_equal expected.size, @outcome.errors.message_list.size
113
+ expected.each { |e| assert @outcome.errors.message_list.include?(e) }
114
+ end
115
+ end
116
+
117
+ class WithShrinkingArray
118
+ include Objective::Unit
119
+ filter do
120
+ array :arr_one do
121
+ integer nils: DISCARD
122
+ end
123
+ end
124
+ end
125
+
126
+ it 'returns an ErrorArray for errors in arrays' do
127
+ o = WithShrinkingArray.run(arr_one: [nil, 1, 'sally'])
128
+
129
+ assert !o.success
130
+ assert o.errors.is_a?(Objective::Errors::ErrorHash)
131
+ assert o.errors[:arr_one].is_a?(Objective::Errors::ErrorArray)
132
+ assert_nil o.errors[:arr_one][0]
133
+ assert o.errors[:arr_one][1].is_a?(Objective::Errors::ErrorAtom)
134
+ end
135
+ end