attributor 5.4 → 5.5

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 (41) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +4 -0
  3. data/lib/attributor.rb +1 -1
  4. data/lib/attributor/attribute.rb +36 -7
  5. data/lib/attributor/extras/field_selector.rb +4 -0
  6. data/lib/attributor/families/numeric.rb +19 -6
  7. data/lib/attributor/families/temporal.rb +16 -9
  8. data/lib/attributor/type.rb +26 -3
  9. data/lib/attributor/types/bigdecimal.rb +6 -1
  10. data/lib/attributor/types/boolean.rb +5 -0
  11. data/lib/attributor/types/collection.rb +16 -0
  12. data/lib/attributor/types/csv.rb +4 -0
  13. data/lib/attributor/types/date.rb +7 -1
  14. data/lib/attributor/types/date_time.rb +7 -1
  15. data/lib/attributor/types/float.rb +4 -3
  16. data/lib/attributor/types/hash.rb +63 -8
  17. data/lib/attributor/types/integer.rb +7 -1
  18. data/lib/attributor/types/object.rb +5 -0
  19. data/lib/attributor/types/polymorphic.rb +0 -1
  20. data/lib/attributor/types/string.rb +19 -0
  21. data/lib/attributor/types/symbol.rb +5 -0
  22. data/lib/attributor/types/tempfile.rb +4 -0
  23. data/lib/attributor/types/time.rb +6 -2
  24. data/lib/attributor/types/uri.rb +8 -0
  25. data/lib/attributor/version.rb +1 -1
  26. data/spec/attribute_spec.rb +37 -6
  27. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  28. data/spec/support/integers.rb +7 -0
  29. data/spec/types/bigdecimal_spec.rb +8 -0
  30. data/spec/types/boolean_spec.rb +10 -0
  31. data/spec/types/collection_spec.rb +16 -0
  32. data/spec/types/date_spec.rb +9 -0
  33. data/spec/types/date_time_spec.rb +9 -0
  34. data/spec/types/float_spec.rb +8 -0
  35. data/spec/types/hash_spec.rb +118 -0
  36. data/spec/types/integer_spec.rb +9 -0
  37. data/spec/types/string_spec.rb +10 -0
  38. data/spec/types/temporal_spec.rb +5 -1
  39. data/spec/types/time_spec.rb +9 -0
  40. data/spec/types/uri_spec.rb +9 -0
  41. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: bb9d8a77a4e71c5f44cc2eaa6559c7d40882c88ec675b0400170d961386d66c1
4
- data.tar.gz: 0b4d4bec370cbca4fa15d61dd08b0bc9b2cdcb8f1f0d39d21287e7471f60075e
2
+ SHA1:
3
+ metadata.gz: 30cd524aa1a60bb34487915e2201e20b075f8eaf
4
+ data.tar.gz: 4a9776a1a772b00636a46caec55f583aa75d54f4
5
5
  SHA512:
6
- metadata.gz: 78a4367ada6bebcb2a164da33b08e05bd0c20207d11df7631b700346bb3e4ec74df47bef3373178cbd619a2f0252609c59586a1fb46c0913830a57b547361b67
7
- data.tar.gz: e2a0fa2ecb088ff61c184624fcadfcc992d1fd60c5694727e8ffd594be1188ee6d60e2b14d7cc1daab4a9e8fbd67b584612d3be6e7574fbda4cfca004a182e00
6
+ metadata.gz: a5a4832825c92b5530352b626ae281c134aa215f167c633a7912b22cee2927fde696d07b92bfd6159b0e88db4dc69b19b38a289181cb523e0c961005608e4f86
7
+ data.tar.gz: a44680f8509acf463ac0c4a22a872db8456300745d01b5c4a31a597bd17760d995e5d6ce26120d998f8df5c4c06fe2d11e7e00c35959f6c987860059425af663
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## next
4
4
 
5
+ ## 5.5 (21/08/2020)
6
+
7
+ - JSON-schema support. Enhanced all of the types to suppport describing themselves as JSON-schema format (`.as_json_schema`). This description includes a few `x-*` extensions to capture some information that Attributor supports, that JSON-schema does not.
8
+
5
9
  ## 5.3 (24/04/2020)
6
10
 
7
11
  - Fixed deprecation warnings in Ruby 2.7
@@ -2,7 +2,7 @@ require 'json'
2
2
  require 'randexp'
3
3
 
4
4
  require 'hashie'
5
-
5
+ require 'active_support/concern'
6
6
  require 'digest/sha1'
7
7
 
8
8
  module Attributor
@@ -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 = true, example: nil)
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] = options[option_name] if options.key? 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] = 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,37 @@ module Attributor
146
146
  load(generated, context)
147
147
  end
148
148
 
149
- def example(context = nil, parent: nil, values: {})
150
- raise ArgumentError, 'attribute example cannot take a context of type String' if context.is_a? ::String
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
+ description[:default] = self.options[:default] if self.options[:default]
165
+ #TODO description[:title] = "TODO: do we want to use a title??..."
166
+
167
+ # Change the reference option to the actual class name.
168
+ if ( reference = self.options[:reference] )
169
+ description[:'x-reference'] = reference.name
170
+ end
171
+
172
+ # TODO: not sure if that's correct (we used to get it from the described hash...
173
+ description[:example] = self.dump(example) if example
174
+
175
+ description
176
+ end
177
+
178
+ def example(context=nil, parent: nil, values:{})
179
+ raise ArgumentError, "attribute example cannot take a context of type String" if (context.is_a? ::String )
151
180
  if context
152
181
  ctx = Attributor.humanize_context(context)
153
182
  seed, = Digest::SHA1.digest(ctx).unpack('QQ')
@@ -12,6 +12,10 @@ module Attributor
12
12
 
13
13
  include Attributor::Type
14
14
 
15
+ def self.json_schema_type
16
+ :string
17
+ end
18
+
15
19
  def self.native_type
16
20
  ::Hash
17
21
  end
@@ -1,15 +1,28 @@
1
1
  # Abstract type for the 'numeric' family
2
2
 
3
3
  module Attributor
4
- class Numeric
4
+ module Numeric
5
+ extend ActiveSupport::Concern
5
6
  include Type
6
7
 
7
- def self.native_type
8
- raise NotImplementedError
9
- end
8
+ module ClassMethods
9
+
10
+ def native_type
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def family
15
+ 'numeric'
16
+ end
10
17
 
11
- def self.family
12
- 'numeric'
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
- class Temporal
4
+ module Temporal
5
+ extend ActiveSupport::Concern
5
6
  include Type
6
7
 
7
- def self.native_type
8
- raise NotImplementedError
9
- end
8
+ module ClassMethods
9
+ def native_type
10
+ raise NotImplementedError
11
+ end
10
12
 
11
- def self.family
12
- 'temporal'
13
- end
13
+ def family
14
+ 'temporal'
15
+ end
16
+
17
+ def dump(value, **_opts)
18
+ value && value.iso8601
19
+ end
14
20
 
15
- def self.dump(value, **_opts)
16
- value && value.iso8601
21
+ def json_schema_type
22
+ :string
23
+ end
17
24
  end
18
25
  end
19
26
  end
@@ -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
- def self.included(klass)
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?
@@ -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,7 +1,8 @@
1
1
  require 'bigdecimal'
2
2
 
3
3
  module Attributor
4
- class BigDecimal < Numeric
4
+ class BigDecimal
5
+ include Numeric
5
6
  def self.native_type
6
7
  ::BigDecimal
7
8
  end
@@ -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
@@ -26,5 +26,10 @@ module Attributor
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
@@ -117,6 +117,22 @@ 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
+ hash[:default] = opts[:default] if opts[:default]
129
+
130
+ #hash[:examples] = [ example.dump ] if example
131
+ member_example = example && example.first
132
+ hash[:items] = member_attribute.as_json_schema(example: member_example)
133
+ hash
134
+ end
135
+
120
136
  def self.constructable?
121
137
  true
122
138
  end
@@ -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 < Temporal
4
+ class Date
5
+ include Temporal
6
+
5
7
  def self.native_type
6
8
  ::Date
7
9
  end
@@ -27,5 +29,9 @@ module Attributor
27
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
@@ -5,7 +5,9 @@ require_relative '../exceptions'
5
5
  require 'date'
6
6
 
7
7
  module Attributor
8
- class DateTime < Temporal
8
+ class DateTime
9
+ include Temporal
10
+
9
11
  def self.native_type
10
12
  ::DateTime
11
13
  end
@@ -27,5 +29,9 @@ module Attributor
27
29
  raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'DateTime', value: value)
28
30
  end
29
31
  end
32
+
33
+ def self.json_schema_string_format
34
+ :'date-time'
35
+ end
30
36
  end
31
37
  end
@@ -2,8 +2,9 @@
2
2
  # See: http://ruby-doc.org/core-2.1.0/Float.html
3
3
 
4
4
  module Attributor
5
+
5
6
  class Float
6
- include Type
7
+ include Numeric
7
8
 
8
9
  def self.native_type
9
10
  ::Float
@@ -22,8 +23,8 @@ module Attributor
22
23
  super
23
24
  end
24
25
 
25
- def self.family
26
- 'numeric'
26
+ def self.json_schema_type
27
+ :number
27
28
  end
28
29
  end
29
30
  end
@@ -437,11 +437,11 @@ module Attributor
437
437
 
438
438
  if keys.any?
439
439
  # Spit keys if it's the root or if it's an anonymous structures
440
- if !shallow || name.nil?
441
- required_names = []
440
+ if ( !shallow || self.name == nil)
441
+ required_names_from_attr = []
442
442
  # FIXME: change to :keys when the praxis doc browser supports displaying those
443
- hash[:attributes] = keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
444
- required_names << sub_name if sub_attribute.options[:required] == true
443
+ hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
444
+ required_names_from_attr << sub_name if sub_attribute.options[:required] == true
445
445
  sub_example = example.get(sub_name) if example
446
446
  sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example)
447
447
  end
@@ -449,14 +449,14 @@ module Attributor
449
449
  described_req = req.describe(shallow)
450
450
  if described_req[:type] == :all
451
451
  # Add the names of the attributes that have the required flag too
452
- described_req[:attributes] |= required_names
453
- required_names = []
452
+ described_req[:attributes] |= required_names_from_attr
453
+ required_names_from_attr = []
454
454
  end
455
455
  list << described_req
456
456
  end
457
457
  # Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes
458
- unless required_names.empty?
459
- hash[:requirements] << { type: :all, attributes: required_names }
458
+ unless required_names_from_attr.empty?
459
+ hash[:requirements] << {type: :all, attributes: required_names_from_attr }
460
460
  end
461
461
  end
462
462
  else
@@ -467,6 +467,61 @@ module Attributor
467
467
  hash
468
468
  end
469
469
 
470
+ def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
471
+ hash = super
472
+ opts = self.options.merge( attribute_options )
473
+
474
+ if key_type
475
+ hash[:'x-key_type'] = key_type.as_json_schema
476
+ end
477
+
478
+ if self.keys.any?
479
+ # Spit keys if it's the root or if it's an anonymous structures
480
+ if ( !shallow || self.name == nil)
481
+ required_names_from_attr = []
482
+ # FIXME: change to :keys when the praxis doc browser supports displaying those
483
+ hash[:properties] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
484
+ required_names_from_attr << sub_name if sub_attribute.options[:required] == true
485
+ sub_example = example.get(sub_name) if example
486
+ sub_attributes[sub_name] = sub_attribute.as_json_schema(shallow: true, example: sub_example)
487
+ end
488
+
489
+ # Expose the more complex requirements to in the x-tended attribute
490
+ extended_requirements = self.requirements.each_with_object([]) do |req, list|
491
+ described_req = req.describe(shallow)
492
+ if described_req[:type] == :all
493
+ # Add the names of the attributes that have the required flag too
494
+ described_req[:attributes] |= required_names_from_attr
495
+ required_names_from_attr = []
496
+ end
497
+ list << described_req
498
+ end
499
+ all = extended_requirements.find{|r| r[:type] == :all }
500
+ if ( all && !all[:attributes].empty? )
501
+ hash[:required] = all[:attributes]
502
+ end
503
+ hash[:'x-requirements'] = extended_requirements unless extended_requirements.empty?
504
+ end
505
+ else
506
+ hash[:'x-value_type'] = value_type.as_json_schema(shallow:true)
507
+ end
508
+
509
+ if opts[:allow_extra]
510
+ hash[:additionalProperties] = if value_type == Attributor::Object
511
+ true
512
+ else
513
+ value_type.as_json_schema(shallow: true)
514
+ end
515
+ end
516
+ # TODO: minProperties and maxProperties and patternProperties
517
+ # TODO: map our required_if (and possible our above requirements 'at_least...' to json schema dependencies)
518
+ hash
519
+ end
520
+
521
+ def self.json_schema_type
522
+ :object
523
+ end
524
+
470
525
  # TODO: Think about the format of the subcontexts to use: let's use .at(key.to_s)
471
526
  attr_reader :contents
472
527
 
@@ -1,7 +1,9 @@
1
1
 
2
2
 
3
3
  module Attributor
4
- class Integer < Numeric
4
+ class Integer
5
+ include Attributor::Numeric
6
+
5
7
  EXAMPLE_RANGE = 1000
6
8
 
7
9
  def self.native_type
@@ -53,5 +55,9 @@ module Attributor
53
55
  end
54
56
  true
55
57
  end
58
+
59
+ def self.json_schema_type
60
+ :integer
61
+ end
56
62
  end
57
63
  end
@@ -13,5 +13,10 @@ module Attributor
13
13
  def self.example(_context = nil, options: {})
14
14
  'An Object'
15
15
  end
16
+
17
+ def self.json_schema_type
18
+ :object #FIXME: not sure this is the most appropriate, since an Attributor::Object can be anything
19
+ end
20
+
16
21
  end
17
22
  end
@@ -1,4 +1,3 @@
1
- require 'active_support'
2
1
 
3
2
  require_relative '../exceptions'
4
3
 
@@ -32,5 +32,24 @@ module Attributor
32
32
  def self.family
33
33
  'string'
34
34
  end
35
+
36
+ def self.json_schema_type
37
+ :string
38
+ end
39
+
40
+ # TODO: we're passing the attribute options for now...might need to rethink ...although these are type-specific...
41
+ # TODO: multipleOf, minimum, maximum, exclusiveMinimum and exclusiveMaximum
42
+ def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
43
+ h = super
44
+ opts = ( self.respond_to?(:options) ) ? self.options.merge( attribute_options ) : attribute_options
45
+ h[:pattern] = self.human_readable_regexp(opts[:regexp]) if opts[:regexp]
46
+ # TODO: minLength, maxLength
47
+ h
48
+ end
49
+
50
+ def self.human_readable_regexp( reg )
51
+ return $1 if reg.to_s =~ /\(\?[^:]+:(.+)\)/
52
+ reg
53
+ end
35
54
  end
36
55
  end
@@ -19,5 +19,10 @@ module Attributor
19
19
  def self.family
20
20
  String.family
21
21
  end
22
+
23
+ def self.json_schema_type
24
+ :string
25
+ end
26
+
22
27
  end
23
28
  end
@@ -38,5 +38,9 @@ module Attributor
38
38
  def self.family
39
39
  String.family
40
40
  end
41
+
42
+ def self.json_schema_type
43
+ :string
44
+ end
41
45
  end
42
46
  end
@@ -1,8 +1,8 @@
1
1
  require 'date'
2
2
 
3
3
  module Attributor
4
- class Time < Temporal
5
- include Type
4
+ class Time
5
+ include Temporal
6
6
 
7
7
  def self.native_type
8
8
  ::Time
@@ -35,5 +35,9 @@ module Attributor
35
35
  raise CoercionError, context: context, from: value.class, to: self, value: value
36
36
  end
37
37
  end
38
+
39
+ def self.json_schema_string_format
40
+ :time
41
+ end
38
42
  end
39
43
  end
@@ -22,6 +22,14 @@ module Attributor
22
22
  ::URI::Generic
23
23
  end
24
24
 
25
+ def self.json_schema_type
26
+ :string
27
+ end
28
+
29
+ def self.json_schema_string_format
30
+ :uri
31
+ end
32
+
25
33
  def self.example(_context = nil, options: {})
26
34
  URI(Randgen.uri)
27
35
  end
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '5.4'.freeze
2
+ VERSION = '5.5'.freeze
3
3
  end
@@ -36,17 +36,48 @@ describe Attributor::Attribute do
36
36
  it { should eq other_attribute }
37
37
  end
38
38
 
39
+ context 'describe_json_schema' do
40
+ let(:type) { PositiveIntegerType }
41
+
42
+ let(:attribute_options) do
43
+ {
44
+ values: [1,20],
45
+ description: "something",
46
+ example: 20,
47
+ max: 1000,
48
+ default: 1
49
+ }
50
+ end
51
+
52
+ context 'reports all of the possible attributes' do
53
+ let(:js){ subject.as_json_schema(example: 20) }
54
+
55
+ it 'including the attribute-specific ones' do
56
+ expect(js[:enum]).to eq( [1,20])
57
+ expect(js[:description]).to eq( "something")
58
+ expect(js[:default]).to eq(1)
59
+ expect(js[:example]).to eq(20)
60
+ end
61
+
62
+ it 'as well as the type-specific ones' do
63
+ expect(js[:type]).to eq(:integer)
64
+ end
65
+ end
66
+
67
+ end
68
+
39
69
  context 'describe' do
40
- let(:attribute_options) { { required: true, values: ['one'], description: 'something', min: 0 } }
70
+ let(:attribute_options) { {required: true, values: ['one'], description: "something", min: 0} }
41
71
  let(:expected) do
42
- h = { type: { name: 'String', id: type.id, family: type.family } }
43
- common = attribute_options.select { |k, _v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
44
- h.merge!(common)
45
- h[:options] = { min: 0 }
72
+ h = {type: {name: 'String', id: type.id, family: type.family}}
73
+ common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
74
+ h.merge!( common )
75
+ h[:options] = {:min => 0 }
46
76
  h
47
77
  end
48
78
 
49
- its(:describe) { should eq expected }
79
+ # It has both the type-included options (min) as well as the attribute options (max)
80
+ its(:describe) { should == expected }
50
81
 
51
82
  context 'with example options' do
52
83
  let(:attribute_options) { { description: 'something', example: 'ex_def' } }
@@ -33,4 +33,13 @@ describe Attributor::FieldSelector do
33
33
  end
34
34
  end
35
35
  end
36
+
37
+ context '.as_json_schema' do
38
+ subject(:js){ type.as_json_schema }
39
+ it 'adds the right attributes' do
40
+ expect(js.keys).to include(:type, :'x-type_name')
41
+ expect(js[:type]).to eq(:string)
42
+ expect(js[:'x-type_name']).to eq('FieldSelector')
43
+ end
44
+ end
36
45
  end
@@ -0,0 +1,7 @@
1
+ class PositiveIntegerType < Attributor::Integer
2
+
3
+ def self.options
4
+ { min: 0 }
5
+ end
6
+
7
+ end
@@ -45,4 +45,12 @@ describe Attributor::BigDecimal do
45
45
  end
46
46
  end
47
47
  end
48
+ context '.as_json_schema' do
49
+ subject(:js){ type.as_json_schema }
50
+ it 'adds the right attributes' do
51
+ expect(js.keys).to include(:type, :'x-type_name')
52
+ expect(js[:type]).to eq(:number)
53
+ expect(js[:'x-type_name']).to eq('BigDecimal')
54
+ end
55
+ end
48
56
  end
@@ -7,6 +7,8 @@ describe Attributor::Boolean do
7
7
  expect(type.new.is_a?(Attributor::Dumpable)).not_to be(true)
8
8
  end
9
9
 
10
+ its(:json_schema_type){ should eq(:boolean)}
11
+
10
12
  context '.valid_type?' do
11
13
  context 'for incoming Boolean values' do
12
14
  [false, true].each do |value|
@@ -63,4 +65,12 @@ describe Attributor::Boolean do
63
65
  end
64
66
  end
65
67
  end
68
+ context '.as_json_schema' do
69
+ subject(:js){ type.as_json_schema }
70
+ it 'adds the right attributes' do
71
+ expect(js.keys).to include(:type, :'x-type_name')
72
+ expect(js[:type]).to eq(:boolean)
73
+ expect(js[:'x-type_name']).to eq('Boolean')
74
+ end
75
+ end
66
76
  end
@@ -344,4 +344,20 @@ describe Attributor::Collection do
344
344
  end.to_not raise_error
345
345
  end
346
346
  end
347
+
348
+ context '.as_json_schema' do
349
+ let(:member_type) { Attributor::String }
350
+ let(:type) { Attributor::Collection.of(member_type) }
351
+ let(:attribute_options) do
352
+ {}
353
+ end
354
+ subject(:js){ type.as_json_schema(attribute_options: attribute_options) }
355
+
356
+ it 'adds the right attributes' do
357
+ expect(js.keys).to include(:type, :'x-type_name', :items)
358
+ expect(js[:type]).to eq(:array)
359
+ expect(js[:'x-type_name']).to eq('Collection')
360
+ expect(js[:items]).to eq(member_type.as_json_schema)
361
+ end
362
+ end
347
363
  end
@@ -92,4 +92,13 @@ describe Attributor::Date do
92
92
  end
93
93
  end
94
94
  end
95
+ context '.as_json_schema' do
96
+ subject(:js){ type.as_json_schema }
97
+ it 'adds the right attributes' do
98
+ expect(js.keys).to include(:type, :'x-type_name')
99
+ expect(js[:type]).to eq(:string)
100
+ expect(js[:format]).to eq(:'date')
101
+ expect(js[:'x-type_name']).to eq('Date')
102
+ end
103
+ end
95
104
  end
@@ -92,4 +92,13 @@ describe Attributor::DateTime do
92
92
  end
93
93
  end
94
94
  end
95
+ context '.as_json_schema' do
96
+ subject(:js){ type.as_json_schema }
97
+ it 'adds the right attributes' do
98
+ expect(js.keys).to include(:type, :'x-type_name', :format)
99
+ expect(js[:type]).to eq(:string)
100
+ expect(js[:format]).to eq(:'date-time')
101
+ expect(js[:'x-type_name']).to eq('DateTime')
102
+ end
103
+ end
95
104
  end
@@ -76,4 +76,12 @@ describe Attributor::Float do
76
76
  end
77
77
  end
78
78
  end
79
+ context '.as_json_schema' do
80
+ subject(:js){ type.as_json_schema }
81
+ it 'adds the right attributes' do
82
+ expect(js.keys).to include(:type, :'x-type_name')
83
+ expect(js[:type]).to eq(:number)
84
+ expect(js[:'x-type_name']).to eq('Float')
85
+ end
86
+ end
79
87
  end
@@ -7,6 +7,7 @@ describe Attributor::Hash do
7
7
  its(:key_type) { should be(Attributor::Object) }
8
8
  its(:value_type) { should be(Attributor::Object) }
9
9
  its(:dsl_class) { should be(Attributor::HashDSLCompiler) }
10
+ its(:json_schema_type) { should be(:object) }
10
11
 
11
12
  context 'attributes' do
12
13
  context 'with an exception from the definition block' do
@@ -1172,4 +1173,121 @@ describe Attributor::Hash do
1172
1173
 
1173
1174
  context Attributor::InvalidDefinition do
1174
1175
  end
1176
+
1177
+
1178
+ context '.as_json_hash' do
1179
+ let(:example){ nil }
1180
+ subject(:description) { type.as_json_schema(example: example) }
1181
+ its([:type]){ should eq(:object)}
1182
+ its([:'x-type_name']){ should eq('Hash')}
1183
+
1184
+ context 'for hashes with explicit key and value types' do
1185
+ let(:key_type){ String }
1186
+ let(:value_type){ Integer }
1187
+
1188
+ subject(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
1189
+
1190
+ it 'describes the key type correctly' do
1191
+ expect(description.keys).to include( :'x-key_type' )
1192
+ expect(description[:'x-key_type']).to be_kind_of(::Hash)
1193
+ expect(description[:'x-key_type'][:type]).to eq( :string )
1194
+ end
1195
+
1196
+ it 'describes the value type correctly' do
1197
+ expect(description.keys).to include( :'x-value_type' )
1198
+ expect(description[:'x-value_type']).to be_kind_of(::Hash)
1199
+ expect(description[:'x-value_type'][:type]).to eq( :integer )
1200
+ end
1201
+
1202
+ end
1203
+
1204
+
1205
+ context 'for hashes with specific keys defined' do
1206
+ let(:block) do
1207
+ proc do
1208
+ key 'a string', String
1209
+ key '1', Integer, min: 1, max: 20
1210
+ key 'some_date', DateTime
1211
+ key 'defaulted', String, default: 'default value'
1212
+ requires do
1213
+ all.of '1','some_date'
1214
+ exclusive 'some_date', 'defaulted'
1215
+ at_least(1).of 'a string', 'some_date'
1216
+ at_most(2).of 'a string', 'some_date'
1217
+ exactly(1).of 'a string', 'some_date'
1218
+ end
1219
+ end
1220
+ end
1221
+
1222
+ let(:type) { Attributor::Hash.of(key: String).construct(block) }
1223
+
1224
+ it 'describes the basic type options correctly' do
1225
+ expect(description[:type]).to eq(:object)
1226
+ expect(description[:'x-key_type']).to eq( type: :string , 'x-type_name': 'String')
1227
+ expect(description).to_not have_key(:'x-value_type')
1228
+ end
1229
+
1230
+ it 'describes the type attributes correctly' do
1231
+ props = description[:properties]
1232
+
1233
+ expect(props['a string']).to eq(type: :string, 'x-type_name': 'String')
1234
+ expect(props['1']).to eq(type: :integer, 'x-type_name': 'Integer', minimum: 1, maximum: 20)
1235
+ expect(props['some_date']).to eq(type: :string, 'x-type_name': 'DateTime', format: :'date-time')
1236
+ expect(props['defaulted']).to eq(type: :string, 'x-type_name': 'String', default: 'default value')
1237
+ end
1238
+
1239
+ it 'describes the attribute requirements correctly' do
1240
+ reqs = description[:required]
1241
+ expect(reqs).to be_kind_of(Array)
1242
+ expect(reqs).to eq( ['1','some_date'] )
1243
+ end
1244
+
1245
+ it 'describes the extended requirements correctly' do
1246
+ reqs = description[:'x-requirements']
1247
+ expect(reqs).to be_kind_of(Array)
1248
+ expect(reqs.size).to be(5)
1249
+ expect(reqs).to include( type: :all, attributes: ['1','some_date'] )
1250
+ expect(reqs).to include( type: :exclusive, attributes: ['some_date','defaulted'] )
1251
+ expect(reqs).to include( type: :at_least, attributes: ['a string','some_date'], count: 1 )
1252
+ expect(reqs).to include( type: :at_most, attributes: ['a string','some_date'], count: 2 )
1253
+ expect(reqs).to include( type: :exactly, attributes: ['a string','some_date'], count: 1 )
1254
+ end
1255
+
1256
+ context 'merging requires.all with attribute required: true' do
1257
+ let(:block) do
1258
+ proc do
1259
+ key 'required string', String, required: true
1260
+ key '1', Integer
1261
+ key 'some_date', DateTime
1262
+ requires do
1263
+ all.of 'some_date'
1264
+ end
1265
+ end
1266
+ end
1267
+ it 'includes attributes with required: true into :required' do
1268
+ expect(description[:required].size).to eq(2)
1269
+ expect(description[:required]).to include( 'required string','some_date' )
1270
+ end
1271
+
1272
+ it 'includes attributes with required: true into the :all requirements' do
1273
+ req_all = description[:'x-requirements'].select{|r| r[:type] == :all}.first
1274
+ expect(req_all[:attributes]).to include( 'required string','some_date' )
1275
+ end
1276
+ end
1277
+
1278
+
1279
+ context 'with an example' do
1280
+ let(:example){ type.example }
1281
+
1282
+ it 'should have the matching example for each leaf key' do
1283
+ expect(description[:properties].keys).to include(*type.keys.keys)
1284
+ description[:properties].each do |name,sub_description|
1285
+ expect(sub_description).to have_key(:example)
1286
+ val = type.attributes[name].dump(example[name])
1287
+ expect(sub_description[:example]).to eq val
1288
+ end
1289
+ end
1290
+ end
1291
+ end
1292
+ end
1175
1293
  end
@@ -146,4 +146,13 @@ describe Attributor::Integer do
146
146
  end
147
147
  end
148
148
  end
149
+
150
+ context '.as_json_schema' do
151
+ subject(:js){ type.as_json_schema }
152
+ it 'adds the right stuff' do
153
+ expect(js.keys).to include(:type, :'x-type_name')
154
+ expect(js[:type]).to eq(:integer)
155
+ expect(js[:'x-type_name']).to eq('Integer')
156
+ end
157
+ end
149
158
  end
@@ -64,4 +64,14 @@ describe Attributor::String do
64
64
  end.to raise_error(Attributor::IncompatibleTypeError)
65
65
  end
66
66
  end
67
+
68
+ context '.as_json_schema' do
69
+ subject(:js){ type.as_json_schema(attribute_options: { regexp: /^Foobar$/ }) }
70
+ it 'adds the right attributes' do
71
+ expect(js.keys).to include(:type, :'x-type_name')
72
+ expect(js[:type]).to eq(:string)
73
+ expect(js[:'x-type_name']).to eq('String')
74
+ expect(js[:pattern]).to eq('^Foobar$')
75
+ end
76
+ end
67
77
  end
@@ -1,7 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Attributor::Temporal do
4
- subject(:type) { Attributor::Temporal }
4
+ subject(:type) do
5
+ Class.new do
6
+ include Attributor::Temporal
7
+ end
8
+ end
5
9
 
6
10
  it 'raises an exception for native_type' do
7
11
  expect { type.native_type }.to raise_error(NotImplementedError)
@@ -90,4 +90,13 @@ describe Attributor::Time do
90
90
  end
91
91
  end
92
92
  end
93
+ context '.as_json_schema' do
94
+ subject(:js){ type.as_json_schema }
95
+ it 'adds the right attributes' do
96
+ expect(js.keys).to include(:type, :'x-type_name')
97
+ expect(js[:type]).to eq(:string)
98
+ expect(js[:format]).to eq(:'time')
99
+ expect(js[:'x-type_name']).to eq('Time')
100
+ end
101
+ end
93
102
  end
@@ -109,4 +109,13 @@ describe Attributor::URI do
109
109
  end
110
110
  end
111
111
  end
112
+ context '.as_json_schema' do
113
+ subject(:js){ type.as_json_schema }
114
+ it 'adds the right attributes' do
115
+ expect(js.keys).to include(:type, :'x-type_name')
116
+ expect(js[:type]).to eq(:string)
117
+ expect(js[:format]).to eq(:uri)
118
+ expect(js[:'x-type_name']).to eq('URI')
119
+ end
120
+ end
112
121
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attributor
3
3
  version: !ruby/object:Gem::Version
4
- version: '5.4'
4
+ version: '5.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-05-01 00:00:00.000000000 Z
12
+ date: 2020-08-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie
@@ -361,6 +361,7 @@ files:
361
361
  - spec/smart_attribute_selector_spec.rb
362
362
  - spec/spec_helper.rb
363
363
  - spec/support/hashes.rb
364
+ - spec/support/integers.rb
364
365
  - spec/support/models.rb
365
366
  - spec/support/polymorphics.rb
366
367
  - spec/type_spec.rb
@@ -406,7 +407,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
406
407
  - !ruby/object:Gem::Version
407
408
  version: '0'
408
409
  requirements: []
409
- rubygems_version: 3.0.3
410
+ rubyforge_project:
411
+ rubygems_version: 2.6.14
410
412
  signing_key:
411
413
  specification_version: 4
412
414
  summary: A powerful attribute and type management library for Ruby
@@ -422,6 +424,7 @@ test_files:
422
424
  - spec/smart_attribute_selector_spec.rb
423
425
  - spec/spec_helper.rb
424
426
  - spec/support/hashes.rb
427
+ - spec/support/integers.rb
425
428
  - spec/support/models.rb
426
429
  - spec/support/polymorphics.rb
427
430
  - spec/type_spec.rb