attributor 5.0.2 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
data/lib/attributor/type.rb
CHANGED
@@ -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
|
25
|
-
|
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, **
|
29
|
+
def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
34
30
|
return nil if value.nil?
|
35
|
-
unless value.is_a?(
|
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
|
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})"
|
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
|
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(
|
73
|
-
raise AttributorException
|
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
|
77
|
+
raise AttributorException, "Value for option :min does not implement '<='. Got: (#{definition.inspect})" unless definition.respond_to?(:<=)
|
84
78
|
when :max
|
85
|
-
raise AttributorException
|
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
|
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
|
-
|
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(
|
107
|
-
raise AttributorException
|
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(
|
110
|
-
|
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(
|
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:
|
119
|
-
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
|
128
|
-
|
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
|
-
|
6
|
+
::BigDecimal
|
9
7
|
end
|
10
8
|
|
11
|
-
def self.example(
|
12
|
-
|
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,
|
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?(
|
18
|
-
if value.
|
19
|
-
|
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(
|
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, **
|
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
|
22
|
-
return false if [
|
23
|
-
return true if [
|
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
|
-
|
10
|
+
::Class
|
12
11
|
end
|
13
12
|
|
14
|
-
def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, **
|
15
|
-
return value if value.is_a?(
|
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.
|
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 =
|
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
|
-
|
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(
|
36
|
-
@klass.nil? ?
|
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
|
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
|
-
|
34
|
-
|
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.
|
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
|
-
|
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 <<
|
76
|
+
result << member_attribute.example(subcontext)
|
80
77
|
end
|
81
78
|
|
82
|
-
|
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, **
|
89
|
-
if value.nil?
|
90
|
-
|
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
|
-
|
96
|
+
new(loaded_value.collect { |member| member_attribute.load(member, context) })
|
102
97
|
end
|
103
98
|
|
104
|
-
|
105
|
-
|
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
|
-
|
119
|
-
|
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 =
|
131
|
-
if options.
|
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
|
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
|
-
|
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,
|
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.
|
163
|
-
raise ArgumentError, "#{
|
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(
|
162
|
+
def self.validate_options(_value, _context, _attribute)
|
170
163
|
errors = []
|
171
164
|
errors
|
172
165
|
end
|
173
166
|
|
174
|
-
|
175
|
-
|
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
|
-
|
175
|
+
collect { |value| self.class.member_attribute.dump(value, opts) }
|
185
176
|
end
|
186
|
-
|
187
177
|
end
|
188
178
|
end
|