attributor 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +30 -0
  3. data/.travis.yml +6 -4
  4. data/CHANGELOG.md +6 -1
  5. data/Gemfile +1 -1
  6. data/Guardfile +14 -8
  7. data/Rakefile +4 -5
  8. data/attributor.gemspec +34 -29
  9. data/lib/attributor.rb +23 -29
  10. data/lib/attributor/attribute.rb +108 -127
  11. data/lib/attributor/attribute_resolver.rb +12 -26
  12. data/lib/attributor/dsl_compiler.rb +17 -21
  13. data/lib/attributor/dumpable.rb +1 -2
  14. data/lib/attributor/example_mixin.rb +5 -8
  15. data/lib/attributor/exceptions.rb +5 -6
  16. data/lib/attributor/extensions/randexp.rb +3 -5
  17. data/lib/attributor/extras/field_selector.rb +4 -4
  18. data/lib/attributor/extras/field_selector/transformer.rb +6 -7
  19. data/lib/attributor/families/numeric.rb +0 -2
  20. data/lib/attributor/families/temporal.rb +1 -4
  21. data/lib/attributor/hash_dsl_compiler.rb +22 -25
  22. data/lib/attributor/type.rb +24 -32
  23. data/lib/attributor/types/bigdecimal.rb +7 -14
  24. data/lib/attributor/types/boolean.rb +5 -8
  25. data/lib/attributor/types/class.rb +9 -10
  26. data/lib/attributor/types/collection.rb +34 -44
  27. data/lib/attributor/types/container.rb +9 -15
  28. data/lib/attributor/types/csv.rb +7 -10
  29. data/lib/attributor/types/date.rb +20 -25
  30. data/lib/attributor/types/date_time.rb +7 -14
  31. data/lib/attributor/types/float.rb +4 -6
  32. data/lib/attributor/types/hash.rb +171 -196
  33. data/lib/attributor/types/ids.rb +2 -6
  34. data/lib/attributor/types/integer.rb +12 -17
  35. data/lib/attributor/types/model.rb +39 -48
  36. data/lib/attributor/types/object.rb +2 -4
  37. data/lib/attributor/types/polymorphic.rb +118 -0
  38. data/lib/attributor/types/regexp.rb +4 -5
  39. data/lib/attributor/types/string.rb +6 -7
  40. data/lib/attributor/types/struct.rb +8 -15
  41. data/lib/attributor/types/symbol.rb +3 -6
  42. data/lib/attributor/types/tempfile.rb +5 -6
  43. data/lib/attributor/types/time.rb +11 -11
  44. data/lib/attributor/types/uri.rb +9 -10
  45. data/lib/attributor/version.rb +1 -1
  46. data/spec/attribute_resolver_spec.rb +57 -78
  47. data/spec/attribute_spec.rb +174 -216
  48. data/spec/attributor_spec.rb +11 -15
  49. data/spec/dsl_compiler_spec.rb +19 -33
  50. data/spec/dumpable_spec.rb +6 -7
  51. data/spec/extras/field_selector/field_selector_spec.rb +1 -1
  52. data/spec/families_spec.rb +1 -3
  53. data/spec/hash_dsl_compiler_spec.rb +65 -74
  54. data/spec/spec_helper.rb +9 -3
  55. data/spec/support/hashes.rb +2 -3
  56. data/spec/support/models.rb +30 -36
  57. data/spec/support/polymorphics.rb +10 -0
  58. data/spec/type_spec.rb +38 -61
  59. data/spec/types/bigdecimal_spec.rb +11 -15
  60. data/spec/types/boolean_spec.rb +12 -39
  61. data/spec/types/class_spec.rb +10 -11
  62. data/spec/types/collection_spec.rb +72 -81
  63. data/spec/types/container_spec.rb +22 -26
  64. data/spec/types/csv_spec.rb +15 -16
  65. data/spec/types/date_spec.rb +16 -33
  66. data/spec/types/date_time_spec.rb +16 -33
  67. data/spec/types/file_upload_spec.rb +1 -2
  68. data/spec/types/float_spec.rb +7 -14
  69. data/spec/types/hash_spec.rb +285 -289
  70. data/spec/types/ids_spec.rb +5 -7
  71. data/spec/types/integer_spec.rb +37 -46
  72. data/spec/types/model_spec.rb +111 -128
  73. data/spec/types/polymorphic_spec.rb +134 -0
  74. data/spec/types/regexp_spec.rb +4 -7
  75. data/spec/types/string_spec.rb +17 -21
  76. data/spec/types/struct_spec.rb +40 -47
  77. data/spec/types/tempfile_spec.rb +1 -2
  78. data/spec/types/temporal_spec.rb +9 -0
  79. data/spec/types/time_spec.rb +16 -32
  80. data/spec/types/type_spec.rb +15 -0
  81. data/spec/types/uri_spec.rb +6 -7
  82. metadata +77 -25
@@ -1,38 +1,34 @@
1
1
  module Attributor
2
-
3
2
  # It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
4
3
  # TODO: should this be a mixin since it is an abstract class?
5
4
  module Type
6
-
7
5
  def self.included(klass)
8
6
  klass.extend(ClassMethods)
9
7
  end
10
8
 
11
9
  module ClassMethods
12
-
13
10
  # Does this type support the generation of subtypes?
14
11
  def constructable?
15
12
  false
16
13
  end
17
14
 
18
15
  # Allow a type to be marked as if it was anonymous (i.e. not referenceable by name)
19
- def anonymous_type(val=true)
16
+ def anonymous_type(val = true)
20
17
  @_anonymous = val
21
18
  end
22
19
 
23
20
  def anonymous?
24
- if @_anonymous == nil
25
- self.name == nil # if nothing is set, consider it anonymous if the class does not have a name
21
+ if @_anonymous.nil?
22
+ name.nil? # if nothing is set, consider it anonymous if the class does not have a name
26
23
  else
27
24
  @_anonymous
28
25
  end
29
26
  end
30
27
 
31
-
32
28
  # Generic decoding and coercion of the attribute.
33
- def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
29
+ def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
34
30
  return nil if value.nil?
35
- unless value.is_a?(self.native_type)
31
+ unless value.is_a?(native_type)
36
32
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
37
33
  end
38
34
 
@@ -40,13 +36,13 @@ module Attributor
40
36
  end
41
37
 
42
38
  # Generic encoding of the attribute
43
- def dump(value,**opts)
39
+ def dump(value, **_opts)
44
40
  value
45
41
  end
46
42
 
47
43
  # TODO: refactor this to take just the options instead of the full attribute?
48
44
  # TODO: delegate to subclass
49
- def validate(value,context=Attributor::DEFAULT_ROOT_CONTEXT,attribute)
45
+ def validate(value, context = Attributor::DEFAULT_ROOT_CONTEXT, attribute) # rubocop:disable Style/OptionalArguments
50
46
  errors = []
51
47
  attribute.options.each do |option, opt_definition|
52
48
  case option
@@ -55,7 +51,7 @@ module Attributor
55
51
  when :min
56
52
  errors << "#{Attributor.humanize_context(context)} value (#{value}) is smaller than the allowed min (#{opt_definition.inspect})" unless value >= opt_definition
57
53
  when :regexp
58
- errors << "#{Attributor.humanize_context(context)} value (#{value}) does not match regexp (#{opt_definition.inspect})" unless value =~ opt_definition
54
+ errors << "#{Attributor.humanize_context(context)} value (#{value}) does not match regexp (#{opt_definition.inspect})" unless value =~ opt_definition
59
55
  end
60
56
  end
61
57
  errors
@@ -65,35 +61,32 @@ module Attributor
65
61
  def valid_type?(value)
66
62
  return value.is_a?(native_type) if respond_to?(:native_type)
67
63
 
68
- raise AttributorException.new("#{self} must implement #valid_type? or #native_type")
64
+ raise AttributorException, "#{self} must implement #valid_type? or #native_type"
69
65
  end
70
66
 
71
67
  # Default, overridable example function
72
- def example(context=nil, options:{})
73
- raise AttributorException.new("#{self} must implement #example")
68
+ def example(_context = nil, options: {})
69
+ raise AttributorException, "#{self} must implement #example"
74
70
  end
75
71
 
76
-
77
72
  # HELPER FUNCTIONS
78
73
 
79
-
80
74
  def check_option!(name, definition)
81
75
  case name
82
76
  when :min
83
- raise AttributorException.new("Value for option :min does not implement '<='. Got: (#{definition.inspect})") unless definition.respond_to?(:<=)
77
+ raise AttributorException, "Value for option :min does not implement '<='. Got: (#{definition.inspect})" unless definition.respond_to?(:<=)
84
78
  when :max
85
- raise AttributorException.new("Value for option :max does not implement '>='. Got(#{definition.inspect})") unless definition.respond_to?(:>=)
79
+ raise AttributorException, "Value for option :max does not implement '>='. Got(#{definition.inspect})" unless definition.respond_to?(:>=)
86
80
  when :regexp
87
81
  # could go for a respoind_to? :=~ here, but that seems overly... cute... and not useful.
88
- raise AttributorException.new("Value for option :regexp is not a Regexp object. Got (#{definition.inspect})") unless definition.is_a? ::Regexp
82
+ raise AttributorException, "Value for option :regexp is not a Regexp object. Got (#{definition.inspect})" unless definition.is_a? ::Regexp
89
83
  else
90
84
  return :unknown
91
85
  end
92
86
 
93
- return :ok
87
+ :ok
94
88
  end
95
89
 
96
-
97
90
  def generate_subcontext(context, subname)
98
91
  context + [subname]
99
92
  end
@@ -103,20 +96,20 @@ module Attributor
103
96
  end
104
97
 
105
98
  # By default, non complex types will not have a DSL subdefinition this handles such case
106
- def compile_dsl( options, block )
107
- raise AttributorException.new("Basic structures cannot take extra block definitions") if block
99
+ def compile_dsl(options, block)
100
+ raise AttributorException, 'Basic structures cannot take extra block definitions' if block
108
101
  # Simply create a DSL compiler to store the options, and not to parse any DSL
109
- sub_definition=dsl_compiler.new( options )
110
- return sub_definition
102
+ sub_definition = dsl_compiler.new(options)
103
+ sub_definition
111
104
  end
112
105
 
113
106
  # Default describe for simple types...only their name (stripping the base attributor module)
114
- def describe(root=false, example: nil)
107
+ def describe(_root = false, example: nil)
115
108
  type_name = Attributor.type_name(self)
116
109
  hash = {
117
110
  name: type_name.gsub(Attributor::MODULE_PREFIX_REGEX, ''),
118
- family: self.family,
119
- id: self.id
111
+ family: family,
112
+ id: id
120
113
  }
121
114
  hash[:anonymous] = @_anonymous unless @_anonymous.nil?
122
115
  hash[:example] = example if example
@@ -124,14 +117,13 @@ module Attributor
124
117
  end
125
118
 
126
119
  def id
127
- return nil if self.name.nil?
128
- self.name.gsub('::'.freeze,'-'.freeze)
120
+ return nil if name.nil?
121
+ name.gsub('::'.freeze, '-'.freeze)
129
122
  end
130
123
 
131
124
  def family
132
125
  'any'
133
126
  end
134
-
135
127
  end
136
128
  end
137
129
  end
@@ -1,27 +1,20 @@
1
1
  require 'bigdecimal'
2
2
 
3
3
  module Attributor
4
-
5
4
  class BigDecimal < Numeric
6
-
7
5
  def self.native_type
8
- return ::BigDecimal
6
+ ::BigDecimal
9
7
  end
10
8
 
11
- def self.example(context=nil, **options)
12
- return ::BigDecimal.new("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
9
+ def self.example(_context = nil, options: {})
10
+ ::BigDecimal.new("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
13
11
  end
14
12
 
15
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
13
+ def self.load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
16
14
  return nil if value.nil?
17
- return value if value.is_a?(self.native_type)
18
- if value.kind_of?(::Float)
19
- return BigDecimal(value, 10)
20
- end
21
- return BigDecimal(value)
15
+ return value if value.is_a?(native_type)
16
+ return BigDecimal(value, 10) if value.is_a?(::Float)
17
+ BigDecimal(value)
22
18
  end
23
-
24
19
  end
25
-
26
20
  end
27
-
@@ -3,7 +3,6 @@
3
3
  require_relative '../exceptions'
4
4
 
5
5
  module Attributor
6
-
7
6
  class Boolean
8
7
  include Type
9
8
 
@@ -11,23 +10,21 @@ module Attributor
11
10
  value == true || value == false
12
11
  end
13
12
 
14
- def self.example(context=nil, options: {})
13
+ def self.example(_context = nil, options: {})
15
14
  [true, false].sample
16
15
  end
17
16
 
18
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
17
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
19
18
  return nil if value.nil?
20
19
 
21
- raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
22
- return false if [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].include?(value)
23
- return true if [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].include?(value)
20
+ raise CoercionError, context: context, from: value.class, to: self, value: value if value.is_a?(::Float)
21
+ return false if [false, 'false', 'FALSE', '0', 0, 'f', 'F'].include?(value)
22
+ return true if [true, 'true', 'TRUE', '1', 1, 't', 'T'].include?(value)
24
23
  raise CoercionError, context: context, from: value.class, to: self
25
24
  end
26
25
 
27
26
  def self.family
28
27
  'boolean'
29
28
  end
30
-
31
29
  end
32
30
  end
33
-
@@ -2,38 +2,37 @@ require 'active_support'
2
2
 
3
3
  require_relative '../exceptions'
4
4
 
5
-
6
5
  module Attributor
7
6
  class Class
8
7
  include Type
9
8
 
10
9
  def self.native_type
11
- return ::Class
10
+ ::Class
12
11
  end
13
12
 
14
- def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
15
- return value if value.is_a?(self.native_type)
13
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
14
+ return value if value.is_a?(native_type)
16
15
  return @klass || nil if value.nil?
17
16
 
18
17
  # Must be given a String object or nil
19
- unless value.kind_of?(::String) || value.nil?
18
+ unless value.is_a?(::String) || value.nil?
20
19
  raise IncompatibleTypeError, context: context, value_type: value.class, type: self
21
20
  end
22
21
 
23
- value = "::" + value if value[0..1] != '::'
22
+ value = '::' + value if value[0..1] != '::'
24
23
  result = value.constantize
25
24
 
26
25
  # Class given must match class specified when type created using .of() method
27
26
  unless @klass.nil? || result == @klass
28
- raise LoadError, "Error loading class #{value} for attribute with " +
29
- "defined class #{@klass} while loading #{Attributor.humanize_context(context)}."
27
+ raise LoadError, "Error loading class #{value} for attribute with " \
28
+ "defined class #{@klass} while loading #{Attributor.humanize_context(context)}."
30
29
  end
31
30
 
32
31
  result
33
32
  end
34
33
 
35
- def self.example(context=nil, options:{})
36
- @klass.nil? ? "MyClass" : @klass.name
34
+ def self.example(_context = nil, options: {})
35
+ @klass.nil? ? 'MyClass' : @klass.name
37
36
  end
38
37
 
39
38
  # Create a Class attribute type of a specific Class.
@@ -2,7 +2,6 @@
2
2
  #
3
3
 
4
4
  module Attributor
5
-
6
5
  class Collection < Array
7
6
  include Container
8
7
  include Dumpable
@@ -15,7 +14,7 @@ module Attributor
15
14
  def self.of(type)
16
15
  resolved_type = Attributor.resolve_type(type)
17
16
  unless resolved_type.ancestors.include?(Attributor::Type)
18
- raise Attributor::AttributorException.new("Collections can only have members that are Attributor::Types")
17
+ raise Attributor::AttributorException, 'Collections can only have members that are Attributor::Types'
19
18
  end
20
19
  ::Class.new(self) do
21
20
  @member_type = resolved_type
@@ -30,17 +29,16 @@ module Attributor
30
29
  end
31
30
  end
32
31
 
33
- def self.options
34
- @options
32
+ class << self
33
+ attr_reader :options
35
34
  end
36
35
 
37
-
38
36
  def self.native_type
39
37
  self
40
38
  end
41
39
 
42
40
  def self.valid_type?(type)
43
- type.kind_of?(self) || type.kind_of?(::Enumerable)
41
+ type.is_a?(self) || type.is_a?(::Enumerable)
44
42
  end
45
43
 
46
44
  def self.family
@@ -53,16 +51,15 @@ module Attributor
53
51
 
54
52
  def self.member_attribute
55
53
  @member_attribute ||= begin
56
- self.construct(nil,{})
54
+ construct(nil, {})
57
55
 
58
56
  @member_attribute
59
57
  end
60
58
  end
61
59
 
62
-
63
60
  # generates an example Collection
64
61
  # @return An Array of native type objects conforming to the specified member_type
65
- def self.example(context=nil, options: {})
62
+ def self.example(context = nil, options: {})
66
63
  result = []
67
64
  size = options[:size] || (rand(3) + 1)
68
65
  size = [*size].sample if size.is_a?(Range)
@@ -76,74 +73,70 @@ module Attributor
76
73
 
77
74
  size.times do |i|
78
75
  subcontext = context + ["at(#{i})"]
79
- result << self.member_attribute.example(subcontext)
76
+ result << member_attribute.example(subcontext)
80
77
  end
81
78
 
82
- self.new(result)
79
+ new(result)
83
80
  end
84
81
 
85
-
86
82
  # The incoming value should be array-like here, so the only decoding that we need to do
87
83
  # is from the members (if there's an :member_type defined option).
88
- def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
89
- if value.nil?
90
- return nil
91
- elsif value.is_a?(Enumerable)
84
+ def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
85
+ return nil if value.nil?
86
+ if value.is_a?(Enumerable)
92
87
  loaded_value = value
93
88
  elsif value.is_a?(::String)
94
- loaded_value = decode_string(value,context)
89
+ loaded_value = decode_string(value, context)
95
90
  elsif value.respond_to?(:to_a)
96
91
  loaded_value = value.to_a
97
92
  else
98
93
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
99
94
  end
100
95
 
101
- self.new(loaded_value.collect { |member| self.member_attribute.load(member,context) })
96
+ new(loaded_value.collect { |member| member_attribute.load(member, context) })
102
97
  end
103
98
 
104
-
105
- def self.decode_string(value,context)
106
- decode_json(value,context)
99
+ def self.decode_string(value, context)
100
+ decode_json(value, context)
107
101
  end
108
102
 
109
-
110
103
  def self.dump(values, **opts)
111
104
  return nil if values.nil?
112
- values.collect { |value| member_attribute.dump(value,opts) }
105
+ values.collect { |value| member_attribute.dump(value, opts) }
113
106
  end
114
107
 
115
- def self.describe(shallow=false, example: nil)
108
+ def self.describe(shallow = false, example: nil)
116
109
  hash = super(shallow)
117
110
  hash[:options] = {} unless hash[:options]
118
- member_example = example.first if example
119
- hash[:member_attribute] = self.member_attribute.describe(true, example: member_example )
111
+ if example
112
+ hash[:example] = example
113
+ member_example = example.first
114
+ end
115
+ hash[:member_attribute] = member_attribute.describe(true, example: member_example)
120
116
  hash
121
117
  end
122
118
 
123
-
124
119
  def self.constructable?
125
120
  true
126
121
  end
127
122
 
128
-
129
123
  def self.construct(constructor_block, options)
130
- member_options = (options[:member_options] || {} ).clone
131
- if options.has_key?(:reference) && !member_options.has_key?(:reference)
124
+ member_options = (options[:member_options] || {}).clone
125
+ if options.key?(:reference) && !member_options.key?(:reference)
132
126
  member_options[:reference] = options[:reference]
133
127
  end
134
128
 
135
129
  # create the member_attribute, passing in our member_type and whatever constructor_block is.
136
130
  # that in turn will call construct on the type if applicable.
137
- @member_attribute = Attributor::Attribute.new self.member_type, member_options, &constructor_block
131
+ @member_attribute = Attributor::Attribute.new member_type, member_options, &constructor_block
138
132
 
139
133
  # overwrite our type with whatever type comes out of the attribute
140
134
  @member_type = @member_attribute.type
141
135
 
142
- return self
136
+ self
143
137
  end
144
138
 
145
-
146
- def self.check_option!(name, definition)
139
+ def self.check_option!(name, _definition)
147
140
  # TODO: support more options like :max_size
148
141
  case name
149
142
  when :reference
@@ -156,33 +149,30 @@ module Attributor
156
149
  end
157
150
 
158
151
  # @param object [Collection] Collection instance to validate.
159
- def self.validate(object, context=Attributor::DEFAULT_ROOT_CONTEXT, attribute=nil)
152
+ def self.validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT, _attribute = nil)
160
153
  context = [context] if context.is_a? ::String
161
154
 
162
- unless object.kind_of?(self)
163
- raise ArgumentError, "#{self.name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
155
+ unless object.is_a?(self)
156
+ raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
164
157
  end
165
158
 
166
159
  object.validate(context)
167
160
  end
168
161
 
169
- def self.validate_options( value, context, attribute )
162
+ def self.validate_options(_value, _context, _attribute)
170
163
  errors = []
171
164
  errors
172
165
  end
173
166
 
174
-
175
- def validate(context=Attributor::DEFAULT_ROOT_CONTEXT)
176
- self.each_with_index.collect do |value, i|
167
+ def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
168
+ each_with_index.collect do |value, i|
177
169
  subcontext = context + ["at(#{i})"]
178
170
  self.class.member_attribute.validate(value, subcontext)
179
171
  end.flatten.compact
180
172
  end
181
173
 
182
-
183
174
  def dump(**opts)
184
- self.collect { |value| self.class.member_attribute.dump(value,opts) }
175
+ collect { |value| self.class.member_attribute.dump(value, opts) }
185
176
  end
186
-
187
177
  end
188
178
  end