attributor 5.4 → 5.5

Sign up to get free protection for your applications and to get access to all the features.
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