attributor 5.2.0 → 5.6
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.
- checksums.yaml +5 -5
- data/.travis.yml +4 -3
- data/CHANGELOG.md +146 -140
- data/attributor.gemspec +4 -5
- data/lib/attributor.rb +16 -2
- data/lib/attributor/attribute.rb +42 -9
- data/lib/attributor/extras/field_selector.rb +4 -0
- data/lib/attributor/families/numeric.rb +19 -6
- data/lib/attributor/families/temporal.rb +16 -9
- data/lib/attributor/hash_dsl_compiler.rb +6 -6
- data/lib/attributor/type.rb +27 -4
- data/lib/attributor/types/bigdecimal.rb +7 -2
- data/lib/attributor/types/boolean.rb +7 -2
- data/lib/attributor/types/class.rb +2 -2
- data/lib/attributor/types/collection.rb +24 -5
- data/lib/attributor/types/container.rb +3 -3
- data/lib/attributor/types/csv.rb +5 -1
- data/lib/attributor/types/date.rb +9 -3
- data/lib/attributor/types/date_time.rb +8 -2
- data/lib/attributor/types/float.rb +4 -3
- data/lib/attributor/types/hash.rb +82 -18
- data/lib/attributor/types/integer.rb +7 -1
- data/lib/attributor/types/model.rb +2 -2
- data/lib/attributor/types/object.rb +5 -0
- data/lib/attributor/types/polymorphic.rb +3 -2
- data/lib/attributor/types/string.rb +20 -1
- data/lib/attributor/types/struct.rb +1 -1
- data/lib/attributor/types/symbol.rb +5 -0
- data/lib/attributor/types/tempfile.rb +4 -0
- data/lib/attributor/types/time.rb +7 -3
- data/lib/attributor/types/uri.rb +9 -1
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_spec.rb +42 -7
- data/spec/dsl_compiler_spec.rb +7 -7
- data/spec/extras/field_selector/field_selector_spec.rb +9 -0
- data/spec/hash_dsl_compiler_spec.rb +2 -2
- data/spec/support/integers.rb +7 -0
- data/spec/type_spec.rb +1 -1
- data/spec/types/bigdecimal_spec.rb +8 -0
- data/spec/types/boolean_spec.rb +10 -0
- data/spec/types/class_spec.rb +0 -1
- data/spec/types/collection_spec.rb +16 -0
- data/spec/types/date_spec.rb +9 -0
- data/spec/types/date_time_spec.rb +9 -0
- data/spec/types/float_spec.rb +8 -0
- data/spec/types/hash_spec.rb +127 -9
- data/spec/types/integer_spec.rb +10 -1
- data/spec/types/model_spec.rb +14 -3
- data/spec/types/string_spec.rb +10 -0
- data/spec/types/temporal_spec.rb +5 -1
- data/spec/types/time_spec.rb +9 -0
- data/spec/types/uri_spec.rb +9 -0
- metadata +21 -34
data/lib/attributor/attribute.rb
CHANGED
@@ -93,12 +93,12 @@ module Attributor
|
|
93
93
|
|
94
94
|
TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :required_if, :custom_data].freeze
|
95
95
|
INTERNAL_OPTIONS = [:dsl_compiler, :dsl_compiler_options].freeze # Options we don't want to expose when describing attributes
|
96
|
-
|
97
|
-
def describe(shallow
|
98
|
-
description = {}
|
96
|
+
JSON_SCHEMA_UNSUPPORTED_OPTIONS = [ :required, :required_if ].freeze
|
97
|
+
def describe(shallow=true, example: nil)
|
98
|
+
description = { }
|
99
99
|
# Clone the common options
|
100
100
|
TOP_LEVEL_OPTIONS.each do |option_name|
|
101
|
-
description[option_name] =
|
101
|
+
description[option_name] = self.describe_option(option_name) if self.options.has_key? option_name
|
102
102
|
end
|
103
103
|
|
104
104
|
# Make sure this option definition is not mistaken for the real generated example
|
@@ -109,7 +109,7 @@ module Attributor
|
|
109
109
|
special_options = options.keys - TOP_LEVEL_OPTIONS - INTERNAL_OPTIONS
|
110
110
|
description[:options] = {} unless special_options.empty?
|
111
111
|
special_options.each do |opt_name|
|
112
|
-
description[:options][opt_name] =
|
112
|
+
description[:options][opt_name] = self.describe_option(opt_name)
|
113
113
|
end
|
114
114
|
# Change the reference option to the actual class name.
|
115
115
|
if (reference = options[:reference])
|
@@ -146,8 +146,40 @@ module Attributor
|
|
146
146
|
load(generated, context)
|
147
147
|
end
|
148
148
|
|
149
|
-
def
|
150
|
-
|
149
|
+
def describe_option( option_name )
|
150
|
+
self.type.describe_option( option_name, self.options[option_name] )
|
151
|
+
end
|
152
|
+
|
153
|
+
# FiXME: pass and utilize the "shallow" parameter
|
154
|
+
#required
|
155
|
+
#options
|
156
|
+
#type
|
157
|
+
#example
|
158
|
+
# UTILIZE THIS SITE! http://jsonschema.net/#/
|
159
|
+
def as_json_schema(shallow: true, example: nil)
|
160
|
+
description = self.type.as_json_schema(shallow: shallow, example: example, attribute_options: self.options )
|
161
|
+
|
162
|
+
description[:description] = self.options[:description] if self.options[:description]
|
163
|
+
description[:enum] = self.options[:values] if self.options[:values]
|
164
|
+
if the_default = self.options[:default]
|
165
|
+
the_object = the_default.is_a?(Proc) ? the_default.call : the_default
|
166
|
+
description[:default] = the_object.is_a?(Attributor::Dumpable) ? the_object.dump : the_object
|
167
|
+
end
|
168
|
+
#TODO description[:title] = "TODO: do we want to use a title??..."
|
169
|
+
|
170
|
+
# Change the reference option to the actual class name.
|
171
|
+
if ( reference = self.options[:reference] )
|
172
|
+
description[:'x-reference'] = reference.name
|
173
|
+
end
|
174
|
+
|
175
|
+
# TODO: not sure if that's correct (we used to get it from the described hash...
|
176
|
+
description[:example] = self.dump(example) if example
|
177
|
+
|
178
|
+
description
|
179
|
+
end
|
180
|
+
|
181
|
+
def example(context=nil, parent: nil, values:{})
|
182
|
+
raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
|
151
183
|
if context
|
152
184
|
ctx = Attributor.humanize_context(context)
|
153
185
|
seed, = Digest::SHA1.digest(ctx).unpack('QQ')
|
@@ -158,7 +190,8 @@ module Attributor
|
|
158
190
|
|
159
191
|
if options.key? :example
|
160
192
|
loaded = example_from_options(parent, context)
|
161
|
-
|
193
|
+
# Only validate the type, if the proc-generated example is "complex" (has attributes)
|
194
|
+
errors = loaded.class.respond_to?(:attributes) ? validate_type(loaded, context) : validate(loaded, context)
|
162
195
|
raise AttributorException, "Error generating example for #{Attributor.humanize_context(context)}. Errors: #{errors.inspect}" if errors.any?
|
163
196
|
return loaded
|
164
197
|
end
|
@@ -166,7 +199,7 @@ module Attributor
|
|
166
199
|
return options[:values].pick if options.key? :values
|
167
200
|
|
168
201
|
if type.respond_to?(:attributes)
|
169
|
-
type.example(context, values)
|
202
|
+
type.example(context, **values)
|
170
203
|
else
|
171
204
|
type.example(context, options: options)
|
172
205
|
end
|
@@ -1,15 +1,28 @@
|
|
1
1
|
# Abstract type for the 'numeric' family
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
|
4
|
+
module Numeric
|
5
|
+
extend ActiveSupport::Concern
|
5
6
|
include Type
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def native_type
|
11
|
+
raise NotImplementedError
|
12
|
+
end
|
13
|
+
|
14
|
+
def family
|
15
|
+
'numeric'
|
16
|
+
end
|
10
17
|
|
11
|
-
|
12
|
-
|
18
|
+
def as_json_schema( shallow: false, example: nil, attribute_options: {} )
|
19
|
+
h = super
|
20
|
+
opts = ( self.respond_to?(:options) ) ? self.options.merge( attribute_options ) : attribute_options
|
21
|
+
h[:minimum] = opts[:min] if opts[:min]
|
22
|
+
h[:maximum] = opts[:max] if opts[:max]
|
23
|
+
# We're not explicitly setting false to exclusiveMinimum and exclusiveMaximum (as that's the default)
|
24
|
+
h
|
25
|
+
end
|
13
26
|
end
|
14
27
|
end
|
15
28
|
end
|
@@ -1,19 +1,26 @@
|
|
1
1
|
# Abstract type for the 'temporal' family
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
|
4
|
+
module Temporal
|
5
|
+
extend ActiveSupport::Concern
|
5
6
|
include Type
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
module ClassMethods
|
9
|
+
def native_type
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
13
|
+
def family
|
14
|
+
'temporal'
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump(value, **_opts)
|
18
|
+
value && value.iso8601
|
19
|
+
end
|
14
20
|
|
15
|
-
|
16
|
-
|
21
|
+
def json_schema_type
|
22
|
+
:string
|
23
|
+
end
|
17
24
|
end
|
18
25
|
end
|
19
26
|
end
|
@@ -89,31 +89,31 @@ module Attributor
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def all(*attr_names, **opts)
|
92
|
-
req = Requirement.new(options.merge(opts).merge(all: attr_names))
|
92
|
+
req = Requirement.new(**options.merge(opts).merge(all: attr_names))
|
93
93
|
target.add_requirement req
|
94
94
|
req
|
95
95
|
end
|
96
96
|
|
97
97
|
def at_most(number)
|
98
|
-
req = Requirement.new(options.merge(at_most: number))
|
98
|
+
req = Requirement.new(**options.merge(at_most: number))
|
99
99
|
target.add_requirement req
|
100
100
|
req
|
101
101
|
end
|
102
102
|
|
103
103
|
def at_least(number)
|
104
|
-
req = Requirement.new(options.merge(at_least: number))
|
104
|
+
req = Requirement.new(**options.merge(at_least: number))
|
105
105
|
target.add_requirement req
|
106
106
|
req
|
107
107
|
end
|
108
108
|
|
109
109
|
def exactly(number)
|
110
|
-
req = Requirement.new(options.merge(exactly: number))
|
110
|
+
req = Requirement.new(**options.merge(exactly: number))
|
111
111
|
target.add_requirement req
|
112
112
|
req
|
113
113
|
end
|
114
114
|
|
115
115
|
def exclusive(*attr_names, **opts)
|
116
|
-
req = Requirement.new(options.merge(opts).merge(exclusive: attr_names))
|
116
|
+
req = Requirement.new(**options.merge(opts).merge(exclusive: attr_names))
|
117
117
|
target.add_requirement req
|
118
118
|
req
|
119
119
|
end
|
@@ -132,7 +132,7 @@ module Attributor
|
|
132
132
|
_requirements_dsl
|
133
133
|
end
|
134
134
|
else
|
135
|
-
_requirements_dsl.all(*spec, opts)
|
135
|
+
_requirements_dsl.all(*spec, **opts)
|
136
136
|
end
|
137
137
|
end
|
138
138
|
end
|
data/lib/attributor/type.rb
CHANGED
@@ -2,9 +2,7 @@ module Attributor
|
|
2
2
|
# It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
|
3
3
|
# TODO: should this be a mixin since it is an abstract class?
|
4
4
|
module Type
|
5
|
-
|
6
|
-
klass.extend(ClassMethods)
|
7
|
-
end
|
5
|
+
extend ActiveSupport::Concern
|
8
6
|
|
9
7
|
module ClassMethods
|
10
8
|
# Does this type support the generation of subtypes?
|
@@ -29,7 +27,7 @@ module Attributor
|
|
29
27
|
def load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
30
28
|
return nil if value.nil?
|
31
29
|
unless value.is_a?(native_type)
|
32
|
-
raise Attributor::IncompatibleTypeError
|
30
|
+
raise Attributor::IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
|
33
31
|
end
|
34
32
|
|
35
33
|
value
|
@@ -124,6 +122,31 @@ module Attributor
|
|
124
122
|
def family
|
125
123
|
'any'
|
126
124
|
end
|
125
|
+
|
126
|
+
# Default no format in case it's a string type
|
127
|
+
def json_schema_string_format
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
|
131
|
+
def as_json_schema( shallow: false, example: nil, attribute_options: {} )
|
132
|
+
type_name = self.ancestors.find { |k| k.name && !k.name.empty? }.name
|
133
|
+
hash = { type: json_schema_type, 'x-type_name': type_name.gsub( Attributor::MODULE_PREFIX_REGEX, '' )}
|
134
|
+
# Add a format, if the type has defined
|
135
|
+
if hash[:type] == :string && the_format = json_schema_string_format
|
136
|
+
hash[:format] = the_format
|
137
|
+
end
|
138
|
+
hash
|
139
|
+
end
|
140
|
+
|
141
|
+
def describe_option( option_name, option_value )
|
142
|
+
return case option_name
|
143
|
+
when :description
|
144
|
+
option_value
|
145
|
+
else
|
146
|
+
option_value # By default, describing an option returns the hash with the specification
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
127
150
|
end
|
128
151
|
end
|
129
152
|
end
|
@@ -1,13 +1,14 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
class BigDecimal
|
4
|
+
class BigDecimal
|
5
|
+
include Numeric
|
5
6
|
def self.native_type
|
6
7
|
::BigDecimal
|
7
8
|
end
|
8
9
|
|
9
10
|
def self.example(_context = nil, options: {})
|
10
|
-
|
11
|
+
BigDecimal("#{/\d{3}/.gen}.#{/\d{3}/.gen}")
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
@@ -16,5 +17,9 @@ module Attributor
|
|
16
17
|
return BigDecimal(value, 10) if value.is_a?(::Float)
|
17
18
|
BigDecimal(value)
|
18
19
|
end
|
20
|
+
|
21
|
+
def self.json_schema_type
|
22
|
+
:number
|
23
|
+
end
|
19
24
|
end
|
20
25
|
end
|
@@ -17,14 +17,19 @@ module Attributor
|
|
17
17
|
def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
|
18
18
|
return nil if value.nil?
|
19
19
|
|
20
|
-
raise CoercionError
|
20
|
+
raise CoercionError.new(context: context, from: value.class, to: self, value: value) if value.is_a?(::Float)
|
21
21
|
return false if [false, 'false', 'FALSE', '0', 0, 'f', 'F'].include?(value)
|
22
22
|
return true if [true, 'true', 'TRUE', '1', 1, 't', 'T'].include?(value)
|
23
|
-
raise CoercionError
|
23
|
+
raise CoercionError.new(context: context, from: value.class, to: self)
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.family
|
27
27
|
'boolean'
|
28
28
|
end
|
29
|
+
|
30
|
+
def self.json_schema_type
|
31
|
+
:boolean
|
32
|
+
end
|
33
|
+
|
29
34
|
end
|
30
35
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'active_support'
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
2
|
|
3
3
|
require_relative '../exceptions'
|
4
4
|
|
@@ -16,7 +16,7 @@ module Attributor
|
|
16
16
|
|
17
17
|
# Must be given a String object or nil
|
18
18
|
unless value.is_a?(::String) || value.nil?
|
19
|
-
raise IncompatibleTypeError
|
19
|
+
raise IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
|
20
20
|
end
|
21
21
|
|
22
22
|
value = '::' + value if value[0..1] != '::'
|
@@ -51,7 +51,7 @@ module Attributor
|
|
51
51
|
|
52
52
|
def self.member_attribute
|
53
53
|
@member_attribute ||= begin
|
54
|
-
construct(nil
|
54
|
+
construct(nil)
|
55
55
|
|
56
56
|
@member_attribute
|
57
57
|
end
|
@@ -91,7 +91,7 @@ module Attributor
|
|
91
91
|
elsif value.respond_to?(:to_a)
|
92
92
|
loaded_value = value.to_a
|
93
93
|
else
|
94
|
-
raise Attributor::IncompatibleTypeError
|
94
|
+
raise Attributor::IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
|
95
95
|
end
|
96
96
|
|
97
97
|
new(loaded_value.collect { |member| member_attribute.load(member, context) })
|
@@ -103,7 +103,7 @@ module Attributor
|
|
103
103
|
|
104
104
|
def self.dump(values, **opts)
|
105
105
|
return nil if values.nil?
|
106
|
-
values.collect { |value| member_attribute.dump(value, opts) }
|
106
|
+
values.collect { |value| member_attribute.dump(value, **opts) }
|
107
107
|
end
|
108
108
|
|
109
109
|
def self.describe(shallow = false, example: nil)
|
@@ -117,11 +117,30 @@ module Attributor
|
|
117
117
|
hash
|
118
118
|
end
|
119
119
|
|
120
|
+
def self.json_schema_type
|
121
|
+
:array
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
|
125
|
+
hash = super
|
126
|
+
opts = self.options.merge( attribute_options )
|
127
|
+
hash[:description] = opts[:description] if opts[:description]
|
128
|
+
if the_default = opts[:default]
|
129
|
+
the_object = the_default.is_a?(Proc) ? the_default.call : the_default
|
130
|
+
hash[:description] = the_object.is_a?(Attributor::Dumpable) ? the_object.dump : the_object
|
131
|
+
end
|
132
|
+
|
133
|
+
#hash[:examples] = [ example.dump ] if example
|
134
|
+
member_example = example && example.first
|
135
|
+
hash[:items] = member_attribute.as_json_schema(example: member_example)
|
136
|
+
hash
|
137
|
+
end
|
138
|
+
|
120
139
|
def self.constructable?
|
121
140
|
true
|
122
141
|
end
|
123
142
|
|
124
|
-
def self.construct(constructor_block, options)
|
143
|
+
def self.construct(constructor_block, **options)
|
125
144
|
member_options = (options[:member_options] || {}).clone
|
126
145
|
if options.key?(:reference) && !member_options.key?(:reference)
|
127
146
|
member_options[:reference] = options[:reference]
|
@@ -173,7 +192,7 @@ module Attributor
|
|
173
192
|
end
|
174
193
|
|
175
194
|
def dump(**opts)
|
176
|
-
collect { |value| self.class.member_attribute.dump(value, opts) }
|
195
|
+
collect { |value| self.class.member_attribute.dump(value, **opts) }
|
177
196
|
end
|
178
197
|
end
|
179
198
|
end
|
@@ -19,17 +19,17 @@ module Attributor
|
|
19
19
|
# @return [Array] a normal Ruby Array
|
20
20
|
#
|
21
21
|
def decode_json(value, context = Attributor::DEFAULT_ROOT_CONTEXT)
|
22
|
-
raise Attributor::DeserializationError
|
22
|
+
raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'JSON', value: value) unless value.is_a? ::String
|
23
23
|
|
24
24
|
# attempt to parse as JSON
|
25
25
|
parsed_value = JSON.parse(value)
|
26
26
|
unless valid_type?(parsed_value)
|
27
|
-
raise Attributor::CoercionError
|
27
|
+
raise Attributor::CoercionError.new(context: context, from: parsed_value.class, to: name, value: parsed_value)
|
28
28
|
end
|
29
29
|
|
30
30
|
parsed_value
|
31
31
|
rescue JSON::JSONError
|
32
|
-
raise Attributor::DeserializationError
|
32
|
+
raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'JSON', value: value)
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
data/lib/attributor/types/csv.rb
CHANGED
@@ -9,7 +9,7 @@ module Attributor
|
|
9
9
|
when ::String
|
10
10
|
values
|
11
11
|
when ::Array
|
12
|
-
values.collect { |value| member_attribute.dump(value, opts).to_s }.join(',')
|
12
|
+
values.collect { |value| member_attribute.dump(value, **opts).to_s }.join(',')
|
13
13
|
when nil
|
14
14
|
nil
|
15
15
|
else
|
@@ -37,5 +37,9 @@ module Attributor
|
|
37
37
|
def self.family
|
38
38
|
Collection.family
|
39
39
|
end
|
40
|
+
|
41
|
+
def self.json_schema_type
|
42
|
+
:string
|
43
|
+
end
|
40
44
|
end
|
41
45
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
3
|
module Attributor
|
4
|
-
class Date
|
4
|
+
class Date
|
5
|
+
include Temporal
|
6
|
+
|
5
7
|
def self.native_type
|
6
8
|
::Date
|
7
9
|
end
|
@@ -21,11 +23,15 @@ module Attributor
|
|
21
23
|
begin
|
22
24
|
return ::Date.parse(value)
|
23
25
|
rescue ArgumentError
|
24
|
-
raise Attributor::DeserializationError
|
26
|
+
raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'Date', value: value)
|
25
27
|
end
|
26
28
|
else
|
27
|
-
raise CoercionError
|
29
|
+
raise CoercionError.new(context: context, from: value.class, to: self, value: value)
|
28
30
|
end
|
29
31
|
end
|
32
|
+
|
33
|
+
def self.json_schema_string_format
|
34
|
+
:date
|
35
|
+
end
|
30
36
|
end
|
31
37
|
end
|