grape-oas 1.5.0 → 1.6.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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/UPGRADING.md +38 -0
  4. data/lib/grape_oas/api_model_builder.rb +5 -4
  5. data/lib/grape_oas/api_model_builders/request.rb +8 -13
  6. data/lib/grape_oas/api_model_builders/request_params.rb +38 -41
  7. data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +0 -1
  8. data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +33 -27
  9. data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +7 -26
  10. data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +1 -1
  11. data/lib/grape_oas/api_model_builders/response.rb +5 -1
  12. data/lib/grape_oas/api_model_builders/response_parsers/base.rb +17 -0
  13. data/lib/grape_oas/api_model_builders/response_parsers/default_response_parser.rb +4 -7
  14. data/lib/grape_oas/api_model_builders/response_parsers/documentation_responses_parser.rb +5 -2
  15. data/lib/grape_oas/api_model_builders/response_parsers/http_codes_parser.rb +96 -28
  16. data/lib/grape_oas/constants.rb +14 -0
  17. data/lib/grape_oas/exporter/concerns/enum_normalizer.rb +35 -0
  18. data/lib/grape_oas/exporter/concerns/schema_indexer.rb +18 -1
  19. data/lib/grape_oas/exporter/oas2/parameter.rb +55 -30
  20. data/lib/grape_oas/exporter/oas2/schema.rb +9 -26
  21. data/lib/grape_oas/exporter/oas3/schema.rb +26 -39
  22. data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +40 -6
  23. data/lib/grape_oas/type_resolvers/array_resolver.rb +7 -14
  24. data/lib/grape_oas/version.rb +1 -1
  25. data/lib/grape_oas.rb +19 -0
  26. metadata +3 -2
@@ -44,10 +44,18 @@ module GrapeOAS
44
44
  # @return [ApiModel::Schema] the built schema
45
45
  def schema_for_exposure(exposure, doc)
46
46
  opts = exposure_options(exposure)
47
- type = opts[:using] || doc[:type]
47
+ nullable_type = Constants.nullable_type(doc[:types])
48
+ if opts[:using] && doc.key?(:types)
49
+ GrapeOAS.logger.warn("Ignoring types: because using: takes precedence")
50
+ elsif doc.key?(:types) && !nullable_type
51
+ GrapeOAS.logger.warn("Ignoring unsupported entity documentation types: #{doc[:types].inspect}")
52
+ end
48
53
 
54
+ type = opts[:using] || nullable_type || doc[:type]
49
55
  schema = type_resolver.build_exposure_base_schema(type)
50
- schema = apply_exposure_properties(schema, doc)
56
+ schema = apply_exposure_properties(
57
+ schema, doc, inferred_nullable: !nullable_type.nil? && !opts[:using],
58
+ )
51
59
  SchemaConstraints.apply(schema, doc)
52
60
  schema
53
61
  end
@@ -110,7 +118,8 @@ module GrapeOAS
110
118
 
111
119
  # Determines whether a property should be marked required.
112
120
  # Explicit doc[:required] takes precedence; conditional exposures
113
- # default to false; unconditional exposures default to true.
121
+ # default to false; unconditional exposures fall back to
122
+ # GrapeOAS.entity_exposure_required_default (default `true`).
114
123
  #
115
124
  # @param doc [Hash] normalized documentation hash
116
125
  # @param exposure the entity exposure
@@ -119,7 +128,7 @@ module GrapeOAS
119
128
  return doc[:required] unless doc[:required].nil?
120
129
  return false if conditional?(exposure)
121
130
 
122
- true
131
+ GrapeOAS.entity_exposure_required_default
123
132
  end
124
133
 
125
134
  private
@@ -159,10 +168,28 @@ module GrapeOAS
159
168
  return prop_schema unless is_array
160
169
 
161
170
  array_schema = ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: prop_schema)
171
+ if PropertyExtractor.extract_nullable(doc)
172
+ array_schema.nullable = true
173
+ if bare_single_ref_allof?(prop_schema)
174
+ array_schema.items = prop_schema.all_of.first
175
+ else
176
+ prop_schema.nullable = false
177
+ end
178
+ end
162
179
  array_schema.examples = doc[:example] if array_valued_example?(doc)
163
180
  array_schema
164
181
  end
165
182
 
183
+ def bare_single_ref_allof?(schema)
184
+ return false unless schema.all_of&.one? && schema.all_of.first.canonical_name
185
+
186
+ empty_schema = ApiModel::Schema.new
187
+ # Comparing defaults keeps future schema attributes from being discarded.
188
+ (ApiModel::Schema::VALID_ATTRIBUTES - %i[nullable all_of]).all? do |attribute|
189
+ schema.public_send(attribute) == empty_schema.public_send(attribute)
190
+ end
191
+ end
192
+
166
193
  # Detects block-based nesting exposures (NestingExposure) that should become
167
194
  # inline object schemas. Only triggers when no entity class is via `using:`.
168
195
  def nesting_exposure?(exposure)
@@ -206,8 +233,15 @@ module GrapeOAS
206
233
  schema
207
234
  end
208
235
 
209
- def apply_exposure_properties(schema, doc)
210
- nullable = PropertyExtractor.extract_nullable(doc)
236
+ def apply_exposure_properties(schema, doc, inferred_nullable: false)
237
+ schema = schema.dup if inferred_nullable && !schema.canonical_name
238
+ nullable_false = doc[:nullable] == false ||
239
+ (doc[:x].is_a?(Hash) && doc[:x][:nullable] == false)
240
+ nullable = if (schema.nullable || inferred_nullable) && nullable_false
241
+ false
242
+ else
243
+ schema.nullable || inferred_nullable || PropertyExtractor.extract_nullable(doc)
244
+ end
211
245
  if nullable && schema.canonical_name
212
246
  # Don't mutate the shared cached entity schema. Create a wrapper with
213
247
  # all_of so the exporter emits { nullable: true, allOf: [{ $ref }] }.
@@ -58,17 +58,19 @@ module GrapeOAS
58
58
  end
59
59
 
60
60
  def build_items_schema(type_names)
61
- if nullable_type_pair?(type_names)
62
- schema = build_items_for_name(type_names.find { |name| !nil_type_name?(name) })
61
+ if (nullable_type = Constants.nullable_type(type_names))
62
+ schema = build_items_for_name(nullable_type)
63
63
  schema.nullable = true
64
64
  return schema
65
65
  end
66
66
 
67
67
  return build_items_for_name(type_names.first) if type_names.size == 1
68
68
 
69
- has_nil_type = type_names.any? { |name| nil_type_name?(name) }
70
- variants = type_names.reject { |name| nil_type_name?(name) }.map { |name| build_items_for_name(name) }
71
- ApiModel::Schema.new(one_of: variants, nullable: has_nil_type ? true : nil)
69
+ nil_types, other_types = type_names.partition { |type| Constants.nil_type?(type) }
70
+ variants = other_types.map { |type| build_items_for_name(type) }
71
+ schema = ApiModel::Schema.new(one_of: variants)
72
+ schema.nullable = true if nil_types.any?
73
+ schema
72
74
  end
73
75
 
74
76
  def build_items_for_name(type_name)
@@ -80,15 +82,6 @@ module GrapeOAS
80
82
  end
81
83
  end
82
84
 
83
- def nullable_type_pair?(type_names)
84
- type_names.size == 2 && type_names.one? { |name| nil_type_name?(name) }
85
- end
86
-
87
- def nil_type_name?(type_name)
88
- normalized = type_name.to_s
89
- normalized == "NilClass" || normalized == "Nil" || normalized.end_with?("::Nil")
90
- end
91
-
92
85
  def build_schema_from_class(klass)
93
86
  # First, check if Introspectors can handle this class
94
87
  # (e.g., Grape::Entity, Dry::Schema, custom types)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrapeOAS
4
- VERSION = "1.5.0"
4
+ VERSION = "1.6.0"
5
5
  end
data/lib/grape_oas.rb CHANGED
@@ -91,6 +91,25 @@ module GrapeOAS
91
91
 
92
92
  module_function :logger, :logger=
93
93
 
94
+ # @return [Boolean] whether entity exposures without required metadata
95
+ # default to required
96
+ def entity_exposure_required_default
97
+ return true if @entity_exposure_required_default.nil?
98
+
99
+ @entity_exposure_required_default
100
+ end
101
+
102
+ # @param value [true, false, nil] `nil` resets to the default (`true`)
103
+ def entity_exposure_required_default=(value)
104
+ unless value.nil? || value == true || value == false
105
+ raise ArgumentError, "entity_exposure_required_default must be true, false, or nil (got #{value.class})"
106
+ end
107
+
108
+ @entity_exposure_required_default = value
109
+ end
110
+
111
+ module_function :entity_exposure_required_default, :entity_exposure_required_default=
112
+
94
113
  # Returns the global introspector registry.
95
114
  #
96
115
  # The registry manages introspectors that build schemas from various sources
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-oas
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Subbota
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-07 00:00:00.000000000 Z
11
+ date: 2026-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grape
@@ -86,6 +86,7 @@ files:
86
86
  - lib/grape_oas/exporter.rb
87
87
  - lib/grape_oas/exporter/base/operation.rb
88
88
  - lib/grape_oas/exporter/base/paths.rb
89
+ - lib/grape_oas/exporter/concerns/enum_normalizer.rb
89
90
  - lib/grape_oas/exporter/concerns/schema_indexer.rb
90
91
  - lib/grape_oas/exporter/concerns/tag_builder.rb
91
92
  - lib/grape_oas/exporter/oas2/operation.rb