grape-oas 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +27 -10
  4. data/UPGRADING.md +104 -0
  5. data/grape-oas.gemspec +2 -1
  6. data/lib/grape_oas/api_model/api.rb +2 -1
  7. data/lib/grape_oas/api_model/schema.rb +1 -1
  8. data/lib/grape_oas/api_model_builder.rb +3 -2
  9. data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +9 -1
  10. data/lib/grape_oas/api_model_builders/concerns/oas_utilities.rb +13 -0
  11. data/lib/grape_oas/api_model_builders/concerns/route_validations.rb +26 -0
  12. data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +4 -2
  13. data/lib/grape_oas/api_model_builders/operation.rb +3 -4
  14. data/lib/grape_oas/api_model_builders/path.rb +33 -6
  15. data/lib/grape_oas/api_model_builders/request.rb +14 -16
  16. data/lib/grape_oas/api_model_builders/request_params.rb +83 -3
  17. data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +1 -1
  18. data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +1 -1
  19. data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +3 -9
  20. data/lib/grape_oas/api_model_builders/response.rb +28 -4
  21. data/lib/grape_oas/constants.rb +16 -4
  22. data/lib/grape_oas/documentation_extension.rb +7 -9
  23. data/lib/grape_oas/exporter/concerns/schema_indexer.rb +14 -3
  24. data/lib/grape_oas/exporter/oas2/operation.rb +4 -2
  25. data/lib/grape_oas/exporter/oas2/parameter.rb +73 -13
  26. data/lib/grape_oas/exporter/oas2/paths.rb +1 -0
  27. data/lib/grape_oas/exporter/oas2/response.rb +6 -4
  28. data/lib/grape_oas/exporter/oas2/schema.rb +56 -28
  29. data/lib/grape_oas/exporter/oas2_schema.rb +7 -5
  30. data/lib/grape_oas/exporter/oas3/operation.rb +5 -3
  31. data/lib/grape_oas/exporter/oas3/parameter.rb +4 -2
  32. data/lib/grape_oas/exporter/oas3/paths.rb +1 -0
  33. data/lib/grape_oas/exporter/oas3/request_body.rb +5 -8
  34. data/lib/grape_oas/exporter/oas3/response.rb +6 -9
  35. data/lib/grape_oas/exporter/oas3/schema.rb +232 -84
  36. data/lib/grape_oas/exporter/oas31/schema.rb +12 -2
  37. data/lib/grape_oas/exporter/oas31_schema.rb +1 -1
  38. data/lib/grape_oas/exporter/oas3_schema.rb +3 -2
  39. data/lib/grape_oas/introspectors/entity_introspector.rb +2 -1
  40. data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +55 -20
  41. data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +2 -8
  42. data/lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb +4 -20
  43. data/lib/grape_oas/range_utils.rb +25 -2
  44. data/lib/grape_oas/type_resolvers/array_resolver.rb +53 -19
  45. data/lib/grape_oas/type_resolvers/base.rb +2 -2
  46. data/lib/grape_oas/type_resolvers/default_resolver.rb +23 -0
  47. data/lib/grape_oas/type_resolvers/dry_type_resolver.rb +1 -1
  48. data/lib/grape_oas/type_resolvers/primitive_resolver.rb +29 -46
  49. data/lib/grape_oas/type_resolvers/registry.rb +31 -15
  50. data/lib/grape_oas/version.rb +1 -1
  51. data/lib/grape_oas.rb +37 -7
  52. metadata +5 -4
  53. data/CONTRIBUTING.md +0 -87
  54. data/RELEASING.md +0 -109
@@ -4,12 +4,15 @@ module GrapeOAS
4
4
  module Exporter
5
5
  module OAS2
6
6
  class Schema
7
- def initialize(schema, ref_tracker = nil, nullable_strategy: nil)
7
+ def initialize(schema, ref_tracker = nil, nullable_strategy: nil, composition_extensions: false)
8
8
  @schema = schema
9
9
  @ref_tracker = ref_tracker
10
10
  @nullable_strategy = nullable_strategy
11
+ @composition_extensions = composition_extensions
11
12
  end
12
13
 
14
+ # OAS 2.0 (Swagger) natively supports `type: file`, so no
15
+ # file-type normalization is needed here (unlike OAS 3.x).
13
16
  def build
14
17
  return {} unless @schema
15
18
 
@@ -23,6 +26,7 @@ module GrapeOAS
23
26
 
24
27
  schema_hash = build_base_hash
25
28
  apply_constraints(schema_hash)
29
+ apply_compatibility_composition_extension(schema_hash)
26
30
  apply_extensions(schema_hash)
27
31
  schema_hash.compact
28
32
  end
@@ -32,9 +36,9 @@ module GrapeOAS
32
36
  "type" => @schema.type,
33
37
  "format" => @schema.format,
34
38
  "description" => @schema.description&.to_s,
35
- "properties" => build_properties(@schema.properties),
36
- "enum" => normalize_enum(@schema.enum, @schema.type)
39
+ "properties" => build_properties(@schema.properties)
37
40
  }
41
+ schema_hash["enum"] = normalize_enum(@schema.enum, @schema.type, nullable: nullable?) if @schema.enum
38
42
  if @schema.items
39
43
  schema_hash["items"] = build_schema_or_ref(@schema.items, include_metadata: false)
40
44
  if !schema_hash["description"] && @schema.items.respond_to?(:description) && @schema.items.description
@@ -66,6 +70,7 @@ module GrapeOAS
66
70
  schema_hash["pattern"] = schema.pattern if schema.pattern
67
71
  schema_hash["minItems"] = schema.min_items unless schema.min_items.nil?
68
72
  schema_hash["maxItems"] = schema.max_items unless schema.max_items.nil?
73
+ schema_hash["uniqueItems"] = true if schema.unique_items
69
74
  end
70
75
 
71
76
  def apply_extensions(schema_hash)
@@ -75,24 +80,35 @@ module GrapeOAS
75
80
 
76
81
  private
77
82
 
83
+ def apply_compatibility_composition_extension(result)
84
+ return unless @composition_extensions
85
+
86
+ { "x-oneOf" => @schema.one_of, "x-anyOf" => @schema.any_of }.each do |key, alternatives|
87
+ next unless alternatives && alternatives.size > 1
88
+ next if @schema.extensions&.key?(key)
89
+
90
+ result[key] = alternatives.map { |item| build_schema_or_ref(item) }
91
+ end
92
+ end
93
+
94
+ def schema_nullable?(schema)
95
+ schema.respond_to?(:nullable) && !!schema.nullable
96
+ end
97
+
78
98
  def nullable?
79
- @schema.respond_to?(:nullable) && @schema.nullable
99
+ schema_nullable?(@schema)
80
100
  end
81
101
 
82
- # Build schema from oneOf/anyOf by using first type (OAS2 doesn't support these)
83
- # Extensions are merged to allow x-anyOf/x-oneOf for consumers that support them
84
- #
85
- # Only description and extensions are applied from the composition node.
86
- # Type-specific attributes (default, enum, format, constraints) are omitted
87
- # because they describe the multi-type composition, not the single fallback
88
- # branch selected here.
102
+ # OAS2 keeps a first-alternative fallback for tools that ignore extensions.
89
103
  def build_first_of_schema(composition_type)
90
- schemas = @schema.send(composition_type)
104
+ schemas = composition_type == :any_of ? @schema.any_of : @schema.one_of
91
105
  first_schema = schemas.first
92
106
  return {} unless first_schema
93
107
 
94
108
  result = build_schema_or_ref(first_schema)
95
109
  result["description"] = @schema.description.to_s if @schema.description
110
+
111
+ apply_compatibility_composition_extension(result)
96
112
  apply_extensions(result)
97
113
  if result.key?("$ref") && result.size > 1
98
114
  ref = { "$ref" => result.delete("$ref") }
@@ -114,7 +130,8 @@ module GrapeOAS
114
130
  result["format"] = @schema.format if @schema.format
115
131
  result["description"] = @schema.description.to_s if @schema.description
116
132
  result["default"] = @schema.default unless @schema.default.nil?
117
- result["enum"] = normalize_enum(@schema.enum, @schema.type) if @schema.enum
133
+ result["enum"] = normalize_enum(@schema.enum, @schema.type, nullable: nullable?) if @schema.enum
134
+ result.delete("enum") if result.key?("enum") && result["enum"].nil?
118
135
  apply_constraints(result)
119
136
  apply_extensions(result)
120
137
  end
@@ -131,17 +148,16 @@ module GrapeOAS
131
148
  def build_schema_or_ref(schema, include_metadata: true)
132
149
  if schema.respond_to?(:canonical_name) && schema.canonical_name
133
150
  @ref_tracker << schema.canonical_name if @ref_tracker
134
- ref_name = schema.canonical_name.gsub("::", "_")
151
+ ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
135
152
  ref_hash = { "$ref" => "#/definitions/#{ref_name}" }
136
153
  return ref_hash unless include_metadata
137
154
 
138
155
  result = {}
139
- if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema.respond_to?(:nullable) && schema.nullable
140
- result["x-nullable"] = true
141
- end
156
+ result["x-nullable"] = true if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema_nullable?(schema)
142
157
  result["description"] = schema.description.to_s if schema.description
143
158
  result["default"] = schema.default unless schema.default.nil?
144
- result["enum"] = normalize_enum(schema.enum, schema.type) if schema.enum
159
+ result["enum"] = normalize_enum(schema.enum, schema.type, nullable: schema_nullable?(schema)) if schema.enum
160
+ result.delete("enum") if result.key?("enum") && result["enum"].nil?
145
161
  apply_constraints(result, schema)
146
162
  result.merge!(schema.extensions) if schema.extensions
147
163
  if result.empty?
@@ -151,24 +167,36 @@ module GrapeOAS
151
167
  result
152
168
  end
153
169
  else
154
- built = Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
170
+ # self.class preserves any subclass so nested schemas use
171
+ # the version-correct builder.
172
+ built = self.class.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy,
173
+ composition_extensions: @composition_extensions,).build
155
174
  built.delete("description") unless include_metadata
156
175
  built
157
176
  end
158
177
  end
159
178
 
160
- def normalize_enum(enum_vals, type)
179
+ def normalize_enum(enum_vals, type, nullable: false)
161
180
  return nil unless enum_vals.is_a?(Array)
162
181
 
163
- coerced = enum_vals.map do |v|
164
- case type
165
- when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
166
- when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
167
- else v
168
- end
169
- end.compact
182
+ has_nil = nullable && enum_vals.include?(nil)
170
183
 
171
- coerced.uniq
184
+ result = enum_vals.each_with_object([]) do |v, acc|
185
+ next if v.nil?
186
+
187
+ coerced_v = case type
188
+ when Constants::SchemaTypes::INTEGER then v.to_i if v.respond_to?(:to_i)
189
+ when Constants::SchemaTypes::NUMBER then v.to_f if v.respond_to?(:to_f)
190
+ else v
191
+ end
192
+ acc << coerced_v unless coerced_v.nil?
193
+ end
194
+
195
+ result.uniq!
196
+ result.push(nil) if has_nil
197
+ return nil if result.empty?
198
+
199
+ result
172
200
  end
173
201
  end
174
202
  end
@@ -67,18 +67,19 @@ module GrapeOAS
67
67
  end
68
68
 
69
69
  def nullable_strategy
70
- @api.nullable_strategy
70
+ @api.nullable_strategy || Constants::NullableStrategy::OAS2_DEFAULT
71
71
  end
72
72
 
73
73
  def build_paths
74
74
  OAS2::Paths.new(@api, @ref_tracker,
75
75
  nullable_strategy: nullable_strategy,
76
+ composition_extensions: @api.oas2_composition_extensions,
76
77
  suppress_default_error_response: @api.suppress_default_error_response,).build
77
78
  end
78
79
 
79
80
  def build_schema_or_ref(schema)
80
81
  if schema.respond_to?(:canonical_name) && schema.canonical_name
81
- ref_name = schema.canonical_name.gsub("::", "_")
82
+ ref_name = GrapeOAS.schema_ref_name.call(schema.canonical_name)
82
83
  @ref_tracker << schema.canonical_name
83
84
  { "$ref" => "#/definitions/#{ref_name}" }
84
85
  else
@@ -87,7 +88,8 @@ module GrapeOAS
87
88
  end
88
89
 
89
90
  def build_schema(schema)
90
- OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy).build
91
+ OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy,
92
+ composition_extensions: @api.oas2_composition_extensions,).build
91
93
  end
92
94
 
93
95
  def build_definitions
@@ -106,9 +108,9 @@ module GrapeOAS
106
108
 
107
109
  processed << canonical_name
108
110
 
109
- ref_name = canonical_name.gsub("::", "_")
111
+ ref_name = GrapeOAS.schema_ref_name.call(canonical_name)
110
112
  schema = find_schema_by_canonical_name(canonical_name)
111
- definitions[ref_name] = OAS2::Schema.new(schema, @ref_tracker, nullable_strategy: nullable_strategy).build if schema
113
+ definitions[ref_name] = build_schema(schema) if schema
112
114
  collect_refs(schema, pending) if schema
113
115
 
114
116
  @ref_tracker.to_a.each do |cn|
@@ -12,10 +12,12 @@ module GrapeOAS
12
12
  def build_version_specific_fields
13
13
  strategy = @options[:nullable_strategy] || Constants::NullableStrategy::KEYWORD
14
14
 
15
+ schema_options = { nullable_strategy: strategy, schema_builder: @options[:schema_builder] || Schema }
16
+
15
17
  {
16
- "parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy).build,
17
- "requestBody" => RequestBody.new(@op.request_body, @ref_tracker, nullable_strategy: strategy).build,
18
- "responses" => Response.new(@op.responses, @ref_tracker, nullable_strategy: strategy).build
18
+ "parameters" => Parameter.new(@op, @ref_tracker, **schema_options).build,
19
+ "requestBody" => RequestBody.new(@op.request_body, @ref_tracker, **schema_options).build,
20
+ "responses" => Response.new(@op.responses, @ref_tracker, **schema_options).build
19
21
  }
20
22
  end
21
23
  end
@@ -4,15 +4,17 @@ module GrapeOAS
4
4
  module Exporter
5
5
  module OAS3
6
6
  class Parameter
7
- def initialize(operation, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
7
+ def initialize(operation, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
8
+ schema_builder: Schema)
8
9
  @op = operation
9
10
  @ref_tracker = ref_tracker
10
11
  @nullable_strategy = nullable_strategy
12
+ @schema_builder = schema_builder
11
13
  end
12
14
 
13
15
  def build
14
16
  Array(@op.parameters).map do |param|
15
- schema_hash = Schema.new(param.schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
17
+ schema_hash = @schema_builder.new(param.schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
16
18
  schema_description = schema_hash.delete("description")
17
19
  description = param.description || schema_description
18
20
  {
@@ -12,6 +12,7 @@ module GrapeOAS
12
12
  def build_operation(operation)
13
13
  Operation.new(operation, @ref_tracker,
14
14
  nullable_strategy: @options[:nullable_strategy] || Constants::NullableStrategy::KEYWORD,
15
+ schema_builder: @options[:schema_builder] || Schema,
15
16
  suppress_default_error_response: @options[:suppress_default_error_response],).build
16
17
  end
17
18
  end
@@ -4,10 +4,12 @@ module GrapeOAS
4
4
  module Exporter
5
5
  module OAS3
6
6
  class RequestBody
7
- def initialize(request_body, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
7
+ def initialize(request_body, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
8
+ schema_builder: Schema)
8
9
  @request_body = request_body
9
10
  @ref_tracker = ref_tracker
10
11
  @nullable_strategy = nullable_strategy
12
+ @schema_builder = schema_builder
11
13
  end
12
14
 
13
15
  def build
@@ -40,13 +42,8 @@ module GrapeOAS
40
42
  end
41
43
 
42
44
  def build_schema_or_ref(schema)
43
- if schema.respond_to?(:canonical_name) && schema.canonical_name
44
- @ref_tracker << schema.canonical_name if @ref_tracker
45
- ref_name = schema.canonical_name.gsub("::", "_")
46
- { "$ref" => "#/components/schemas/#{ref_name}" }
47
- else
48
- Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
49
- end
45
+ @schema_builder.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy)
46
+ .build_schema_or_ref(schema)
50
47
  end
51
48
  end
52
49
  end
@@ -4,10 +4,12 @@ module GrapeOAS
4
4
  module Exporter
5
5
  module OAS3
6
6
  class Response
7
- def initialize(responses, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD)
7
+ def initialize(responses, ref_tracker = nil, nullable_strategy: Constants::NullableStrategy::KEYWORD,
8
+ schema_builder: Schema)
8
9
  @responses = responses
9
10
  @ref_tracker = ref_tracker
10
11
  @nullable_strategy = nullable_strategy
12
+ @schema_builder = schema_builder
11
13
  end
12
14
 
13
15
  def build
@@ -42,7 +44,7 @@ module GrapeOAS
42
44
  end
43
45
 
44
46
  def build_content(media_types, response_examples = nil)
45
- return nil unless media_types
47
+ return nil if media_types.nil? || media_types.empty?
46
48
 
47
49
  media_types.each_with_object({}) do |mt, h|
48
50
  entry = { "schema" => build_schema_or_ref(mt.schema) }
@@ -71,13 +73,8 @@ module GrapeOAS
71
73
  end
72
74
 
73
75
  def build_schema_or_ref(schema)
74
- if schema.respond_to?(:canonical_name) && schema.canonical_name
75
- @ref_tracker << schema.canonical_name if @ref_tracker
76
- ref_name = schema.canonical_name.gsub("::", "_")
77
- { "$ref" => "#/components/schemas/#{ref_name}" }
78
- else
79
- Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
80
- end
76
+ @schema_builder.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy)
77
+ .build_schema_or_ref(schema)
81
78
  end
82
79
  end
83
80
  end