attributor 5.0.2 → 5.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 (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