grape-oas 1.1.0 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +6 -0
- data/lib/grape_oas/api_model/api.rb +2 -1
- data/lib/grape_oas/api_model/schema.rb +17 -2
- data/lib/grape_oas/api_model_builder.rb +1 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +10 -5
- data/lib/grape_oas/api_model_builders/request.rb +3 -158
- data/lib/grape_oas/api_model_builders/request_params_support/nested_params_builder.rb +2 -0
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +16 -20
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +26 -53
- data/lib/grape_oas/constants.rb +21 -2
- data/lib/grape_oas/doc_key_normalizer.rb +14 -0
- data/lib/grape_oas/exporter/oas2/operation.rb +4 -2
- data/lib/grape_oas/exporter/oas2/parameter.rb +3 -2
- data/lib/grape_oas/exporter/oas2/paths.rb +1 -0
- data/lib/grape_oas/exporter/oas2/response.rb +3 -2
- data/lib/grape_oas/exporter/oas2/schema.rb +37 -5
- data/lib/grape_oas/exporter/oas2_schema.rb +7 -2
- data/lib/grape_oas/exporter/oas3/operation.rb +4 -4
- data/lib/grape_oas/exporter/oas3/parameter.rb +3 -3
- data/lib/grape_oas/exporter/oas3/paths.rb +2 -3
- data/lib/grape_oas/exporter/oas3/request_body.rb +3 -3
- data/lib/grape_oas/exporter/oas3/response.rb +3 -3
- data/lib/grape_oas/exporter/oas3/schema.rb +84 -26
- data/lib/grape_oas/exporter/oas31_schema.rb +4 -2
- data/lib/grape_oas/exporter/oas3_schema.rb +4 -4
- data/lib/grape_oas/introspectors/dry_introspector_support/argument_extractor.rb +2 -31
- data/lib/grape_oas/introspectors/dry_introspector_support/constraint_extractor.rb +10 -19
- data/lib/grape_oas/introspectors/dry_introspector_support/predicate_handler.rb +2 -10
- data/lib/grape_oas/introspectors/dry_introspector_support/type_unwrapper.rb +15 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/discriminator_handler.rb +2 -25
- data/lib/grape_oas/introspectors/entity_introspector_support/exposure_processor.rb +139 -136
- data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +5 -29
- data/lib/grape_oas/introspectors/entity_introspector_support/nesting_merger.rb +104 -0
- data/lib/grape_oas/introspectors/entity_introspector_support/property_extractor.rb +2 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/type_schema_resolver.rb +139 -0
- data/lib/grape_oas/introspectors/entity_introspector_support.rb +33 -0
- data/lib/grape_oas/range_utils.rb +87 -0
- data/lib/grape_oas/schema_constraints.rb +36 -0
- data/lib/grape_oas/type_resolvers/array_resolver.rb +108 -0
- data/lib/grape_oas/type_resolvers/base.rb +110 -0
- data/lib/grape_oas/type_resolvers/dry_type_resolver.rb +135 -0
- data/lib/grape_oas/type_resolvers/primitive_resolver.rb +105 -0
- data/lib/grape_oas/type_resolvers/registry.rb +137 -0
- data/lib/grape_oas/values_normalizer.rb +47 -0
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +68 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5289691e338b77f7408ed384429c297bfcac143962085e4b02edc1b980b35ff
|
|
4
|
+
data.tar.gz: 1d03adce8e12922b769ce6679013d14cb4e9bb4bc3ceaa2a6d4fdc2a26d34625
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 850eee859637b8e5cab7606b8819931c5b59479aeab6247907fde7578c43b613694a0ab7b2ffb24c1530ac1e878982f555810a7afb83b34cf378d2f6b35cc020
|
|
7
|
+
data.tar.gz: 1298c51a115ec42502e011e50b41ae3b9bf9a553c834c49b7c3b628d285b7e2853ee8251cf55c742766d951f2ecfc525252aee1950a61ffe8fbd064a84550110
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
## [1.3.0] - 2026-03-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- [#48](https://github.com/numbata/grape-oas/pull/48): Add configurable `GrapeOAS.logger` for schema generation warnings - [@numbata](https://github.com/numbata).
|
|
14
|
+
- [#43](https://github.com/numbata/grape-oas/pull/43): Bump actions/upload-artifact from 6 to 7 - [@dependabot[bot]](https://github.com/dependabot[bot]).
|
|
15
|
+
- [#51](https://github.com/numbata/grape-oas/pull/51): Add inline nesting exposure support — block-based `expose :key do ... end` now produces inline object schemas with preserved enum values, min/max constraints, and metadata - [@numbata](https://github.com/numbata).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- [#52](https://github.com/numbata/grape-oas/pull/52): Extract `SchemaConstraints` — centralizes numeric/string constraint application (min/max, exclusive flags, length, pattern) and adds `exclusive_minimum`/`exclusive_maximum` support - [@numbata](https://github.com/numbata).
|
|
20
|
+
- [#50](https://github.com/numbata/grape-oas/pull/50): Extract `ValuesNormalizer` — consolidates Proc/Set/Hash value normalization into a single module used by both request params and entity exposures - [@numbata](https://github.com/numbata).
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- [#55](https://github.com/numbata/grape-oas/pull/55): Preserve enum values on cached entity schemas — dup the shared schema before applying enum instead of silently discarding the constraint; emit a warning naming the entity - [@numbata](https://github.com/numbata).
|
|
25
|
+
- [#56](https://github.com/numbata/grape-oas/pull/56): Make `NestingMerger` depth-cap warning actionable — include property name and depth cap value in the message - [@numbata](https://github.com/numbata).
|
|
26
|
+
- [#50](https://github.com/numbata/grape-oas/pull/50): Fix `[false]` enum silently dropped — `[false].any?` returns `false` in Ruby, causing boolean-only enum constraints to be discarded - [@numbata](https://github.com/numbata).
|
|
27
|
+
|
|
28
|
+
- [#49](https://github.com/numbata/grape-oas/pull/49): Fix OOM on wide string range expansion in `values:` — replace unbounded `range.to_a` with bounded enumeration; non-numeric ranges on numeric schemas and numeric ranges on non-numeric schemas now warn and are ignored instead of silently producing invalid output - [@numbata](https://github.com/numbata).
|
|
29
|
+
- [#46](https://github.com/numbata/grape-oas/pull/46): Fix `is_array: true` in request param documentation being ignored for primitive types — only entity types were wrapped in array schema - [@numbata](https://github.com/numbata).
|
|
30
|
+
- [#42](https://github.com/numbata/grape-oas/pull/42): Fix array items `description` and `nullable` placement — hoist to outer array schema instead of wrapping `items` in `allOf`; fix `:description` field naming collision in `PropertyExtractor` - [@numbata](https://github.com/numbata).
|
|
31
|
+
- [#44](https://github.com/numbata/grape-oas/pull/44): Fix RuboCop 1.85 offenses - [@numbata](https://github.com/numbata).
|
|
32
|
+
- [#47](https://github.com/numbata/grape-oas/pull/47): Fix duplicate entries in `Schema#required` array when the same property is added multiple times with `required: true` - [@numbata](https://github.com/numbata).
|
|
33
|
+
|
|
34
|
+
## [1.2.0] - 2026-03-02
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- [#36](https://github.com/numbata/grape-oas/pull/36): Add extensible TypeResolvers for resolving Grape's stringified parameter types to OpenAPI schemas with rich metadata (format, enum, nullable) - [@numbata](https://github.com/numbata).
|
|
39
|
+
- [#37](https://github.com/numbata/grape-oas/pull/37): Replace boolean `nullable_keyword` with configurable `nullable_strategy` - [@numbata](https://github.com/numbata).
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- [#41](https://github.com/numbata/grape-oas/pull/41): Fix `Set` enum values being silently dropped and `maybe(Coercible::Integer)` resolving to `string` instead of `integer` - [@numbata](https://github.com/numbata).
|
|
44
|
+
- [#40](https://github.com/numbata/grape-oas/pull/40): Remove dead `spec[:allow_nil]` and `spec[:nullable]` checks from `extract_nullable` — these values were never set by Grape or grape-swagger - [@numbata](https://github.com/numbata).
|
|
45
|
+
- [#39](https://github.com/numbata/grape-oas/pull/39): Support `documentation: { x: { nullable: true } }` on nested Hash params — nullable flag was ignored for object container schemas - [@numbata](https://github.com/numbata).
|
|
46
|
+
- [#38](https://github.com/numbata/grape-oas/pull/38): Wrap `$ref` in `allOf` when `description` or `nullable` is present — fixes sibling properties being ignored per OpenAPI spec - [@numbata](https://github.com/numbata).
|
|
47
|
+
- [#37](https://github.com/numbata/grape-oas/pull/37): Fix OAS 3.0 `nullable` keyword being constructed but not emitted in the generated output - [@numbata](https://github.com/numbata).
|
|
48
|
+
|
|
8
49
|
## [1.1.0] - 2026-01-23
|
|
9
50
|
|
|
10
51
|
### Added
|
data/README.md
CHANGED
|
@@ -150,6 +150,12 @@ class Entity::User < Grape::Entity
|
|
|
150
150
|
expose :id, documentation: { type: Integer }
|
|
151
151
|
expose :name, documentation: { type: String }
|
|
152
152
|
expose :posts, using: Entity::Post, documentation: { is_array: true }
|
|
153
|
+
|
|
154
|
+
# Block-based nesting produces an inline object schema
|
|
155
|
+
expose :meta do
|
|
156
|
+
expose :role, documentation: { type: String, values: %w[admin user guest] }
|
|
157
|
+
expose :verified, documentation: { type: 'Boolean' }
|
|
158
|
+
end
|
|
153
159
|
end
|
|
154
160
|
```
|
|
155
161
|
|
|
@@ -11,7 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
class API < Node
|
|
12
12
|
attr_accessor :title, :version, :paths, :servers, :tag_defs, :components,
|
|
13
13
|
:host, :base_path, :schemes, :security_definitions, :security,
|
|
14
|
-
:registered_schemas, :suppress_default_error_response
|
|
14
|
+
:registered_schemas, :suppress_default_error_response, :nullable_strategy
|
|
15
15
|
|
|
16
16
|
def initialize(title:, version:)
|
|
17
17
|
super()
|
|
@@ -28,6 +28,7 @@ module GrapeOAS
|
|
|
28
28
|
@security = []
|
|
29
29
|
@registered_schemas = []
|
|
30
30
|
@suppress_default_error_response = false
|
|
31
|
+
@nullable_strategy = nil
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def add_path(path)
|
|
@@ -51,10 +51,25 @@ module GrapeOAS
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def add_property(name, schema, required: false)
|
|
54
|
-
|
|
55
|
-
@
|
|
54
|
+
key = name.to_s
|
|
55
|
+
@properties[key] = schema
|
|
56
|
+
@required << key if required && !@required.include?(key)
|
|
56
57
|
schema
|
|
57
58
|
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
# Ensure dup produces an independent copy — without this, the shallow
|
|
63
|
+
# copy shares @properties, @required, and @defs with the original.
|
|
64
|
+
# Property values are duped one level deep (via transform_values(&:dup)),
|
|
65
|
+
# which triggers initialize_copy recursively on each nested Schema,
|
|
66
|
+
# producing a full deep copy of the property tree.
|
|
67
|
+
def initialize_copy(source)
|
|
68
|
+
super
|
|
69
|
+
@properties = source.properties.transform_values(&:dup)
|
|
70
|
+
@required = source.required.dup
|
|
71
|
+
@defs = source.defs.dup
|
|
72
|
+
end
|
|
58
73
|
end
|
|
59
74
|
end
|
|
60
75
|
end
|
|
@@ -19,6 +19,7 @@ module GrapeOAS
|
|
|
19
19
|
@api.servers = build_servers(options)
|
|
20
20
|
@api.registered_schemas = build_registered_schemas(options[:models])
|
|
21
21
|
@api.suppress_default_error_response = options[:suppress_default_error_response] || false
|
|
22
|
+
@api.nullable_strategy = options[:nullable_strategy]
|
|
22
23
|
|
|
23
24
|
@namespace_filter = options[:namespace]
|
|
24
25
|
@apis = []
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
begin
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# BigDecimal is an optional default gem dependency.
|
|
7
|
+
end
|
|
4
8
|
|
|
5
9
|
module GrapeOAS
|
|
6
10
|
module ApiModelBuilders
|
|
@@ -8,11 +12,11 @@ module GrapeOAS
|
|
|
8
12
|
# Centralizes Ruby type to OpenAPI schema type resolution.
|
|
9
13
|
# Used by request builders and introspectors to avoid duplicated type switching logic.
|
|
10
14
|
module TypeResolver
|
|
11
|
-
# Regex to match Grape's typed array notation like "[String]", "[Integer]"
|
|
12
|
-
TYPED_ARRAY_PATTERN =
|
|
15
|
+
# Regex to match Grape's typed array notation like "[String]", "[Integer]", "[MyModule::MyType]"
|
|
16
|
+
TYPED_ARRAY_PATTERN = /\A\[(\w+(?:::\w+)*)\]\z/
|
|
13
17
|
|
|
14
18
|
# Regex to match Grape's multi-type notation like "[String, Integer]", "[String, Float]"
|
|
15
|
-
MULTI_TYPE_PATTERN =
|
|
19
|
+
MULTI_TYPE_PATTERN = /\A\[(\w+(?:::\w+)*(?:,\s*\w+(?:::\w+)*)+)\]\z/
|
|
16
20
|
|
|
17
21
|
# Resolves a Ruby class or type name to its OpenAPI schema type string.
|
|
18
22
|
# Handles both Ruby classes (Integer, Float) and string type names ("integer", "float").
|
|
@@ -27,7 +31,8 @@ module GrapeOAS
|
|
|
27
31
|
# Handle Ruby classes directly
|
|
28
32
|
if type.is_a?(Class)
|
|
29
33
|
# Check static mapping first
|
|
30
|
-
|
|
34
|
+
mapped = Constants::RUBY_TYPE_MAPPING[type]
|
|
35
|
+
return mapped if mapped
|
|
31
36
|
|
|
32
37
|
# Handle Grape::API::Boolean dynamically (may not be loaded at constant definition time)
|
|
33
38
|
return Constants::SchemaTypes::BOOLEAN if grape_boolean_type?(type)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "bigdecimal"
|
|
4
|
-
|
|
5
3
|
module GrapeOAS
|
|
6
4
|
module ApiModelBuilders
|
|
7
5
|
class Request
|
|
@@ -202,162 +200,9 @@ module GrapeOAS
|
|
|
202
200
|
|
|
203
201
|
# First try direct lookup (for Ruby class values like String, Integer)
|
|
204
202
|
# Then try class-based lookup (for actual runtime values like "hello", 123)
|
|
205
|
-
Constants::RUBY_TYPE_MAPPING
|
|
206
|
-
Constants::RUBY_TYPE_MAPPING
|
|
207
|
-
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def schema_from_types(types_hash, rule_constraints)
|
|
211
|
-
schema = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::OBJECT)
|
|
212
|
-
types_hash.each do |name, dry_type|
|
|
213
|
-
prop_schema = schema_for_type(dry_type)
|
|
214
|
-
merge_rule_constraints(prop_schema, rule_constraints[name]) if rule_constraints[name]
|
|
215
|
-
required = true
|
|
216
|
-
required = false if dry_type.respond_to?(:optional?) && dry_type.optional?
|
|
217
|
-
required = false if dry_type.respond_to?(:meta) && dry_type.meta[:omittable]
|
|
218
|
-
schema.add_property(name, prop_schema, required: required)
|
|
219
|
-
end
|
|
220
|
-
schema
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def schema_for_type(dry_type)
|
|
224
|
-
if dry_type.respond_to?(:primitive) && dry_type.primitive == Array && dry_type.respond_to?(:member)
|
|
225
|
-
items_schema = schema_for_type(dry_type.member)
|
|
226
|
-
schema = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
227
|
-
apply_array_meta_constraints(schema, dry_type.respond_to?(:meta) ? dry_type.meta : {})
|
|
228
|
-
return schema
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
primitive, member = derive_primitive_and_member(dry_type)
|
|
232
|
-
if dry_type.respond_to?(:primitive) && dry_type.primitive == Array
|
|
233
|
-
member ||= dry_type.respond_to?(:member) ? dry_type.member : nil
|
|
234
|
-
primitive = Array
|
|
235
|
-
end
|
|
236
|
-
meta = dry_type.respond_to?(:meta) ? dry_type.meta : {}
|
|
237
|
-
nullable = dry_type.respond_to?(:optional?) && dry_type.optional?
|
|
238
|
-
enum_vals = dry_type.respond_to?(:values) ? dry_type.values : nil
|
|
239
|
-
|
|
240
|
-
schema = if primitive == Array
|
|
241
|
-
items_schema = member ? schema_for_type(member) : default_string_schema
|
|
242
|
-
s = GrapeOAS::ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
243
|
-
apply_array_meta_constraints(s, meta)
|
|
244
|
-
s
|
|
245
|
-
else
|
|
246
|
-
build_schema_for_primitive(primitive)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
schema.nullable = nullable
|
|
250
|
-
schema.enum = enum_vals if enum_vals
|
|
251
|
-
apply_string_meta_constraints(schema, meta) if primitive == String
|
|
252
|
-
apply_numeric_meta_constraints(schema, meta) if [Integer, Float, BigDecimal].include?(primitive)
|
|
253
|
-
schema
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def derive_primitive_and_member(dry_type)
|
|
257
|
-
if defined?(Dry::Types::Array::Member) && dry_type.respond_to?(:type) && dry_type.type.is_a?(Dry::Types::Array::Member)
|
|
258
|
-
return [Array, dry_type.type.member]
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
return [Array, dry_type.member] if dry_type.respond_to?(:member)
|
|
262
|
-
|
|
263
|
-
primitive = dry_type.respond_to?(:primitive) ? dry_type.primitive : nil
|
|
264
|
-
[primitive, nil]
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def apply_string_meta_constraints(schema, meta)
|
|
268
|
-
min_length = extract_min_constraint(meta)
|
|
269
|
-
max_length = extract_max_constraint(meta)
|
|
270
|
-
schema.min_length = min_length if min_length
|
|
271
|
-
schema.max_length = max_length if max_length
|
|
272
|
-
schema.pattern = meta[:pattern] if meta[:pattern]
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def apply_array_meta_constraints(schema, meta)
|
|
276
|
-
min_items = extract_min_constraint(meta, :min_items)
|
|
277
|
-
max_items = extract_max_constraint(meta, :max_items)
|
|
278
|
-
schema.min_items = min_items if min_items
|
|
279
|
-
schema.max_items = max_items if max_items
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Extract minimum constraint, supporting multiple key names
|
|
283
|
-
def extract_min_constraint(meta, specific_key = :min_length)
|
|
284
|
-
meta[:min_size] || meta[specific_key]
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Extract maximum constraint, supporting multiple key names
|
|
288
|
-
def extract_max_constraint(meta, specific_key = :max_length)
|
|
289
|
-
meta[:max_size] || meta[specific_key]
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def apply_numeric_meta_constraints(schema, meta)
|
|
293
|
-
if meta[:gt]
|
|
294
|
-
schema.minimum = meta[:gt]
|
|
295
|
-
schema.exclusive_minimum = true
|
|
296
|
-
elsif meta[:gteq]
|
|
297
|
-
schema.minimum = meta[:gteq]
|
|
298
|
-
end
|
|
299
|
-
if meta[:lt]
|
|
300
|
-
schema.maximum = meta[:lt]
|
|
301
|
-
schema.exclusive_maximum = true
|
|
302
|
-
elsif meta[:lteq]
|
|
303
|
-
schema.maximum = meta[:lteq]
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def merge_rule_constraints(schema, rule_constraints)
|
|
308
|
-
return unless rule_constraints
|
|
309
|
-
|
|
310
|
-
schema.enum ||= rule_constraints[:enum]
|
|
311
|
-
schema.nullable ||= rule_constraints[:nullable]
|
|
312
|
-
schema.min_length ||= rule_constraints[:min] if rule_constraints[:min]
|
|
313
|
-
schema.max_length ||= rule_constraints[:max] if rule_constraints[:max]
|
|
314
|
-
schema.minimum ||= rule_constraints[:minimum] if rule_constraints[:minimum]
|
|
315
|
-
schema.maximum ||= rule_constraints[:maximum] if rule_constraints[:maximum]
|
|
316
|
-
schema.exclusive_minimum ||= rule_constraints[:exclusive_minimum]
|
|
317
|
-
schema.exclusive_maximum ||= rule_constraints[:exclusive_maximum]
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
# Very small parser for FakeType rule_ast used in tests
|
|
321
|
-
def extract_rule_constraints(schema_obj)
|
|
322
|
-
return {} unless schema_obj.respond_to?(:rules)
|
|
323
|
-
|
|
324
|
-
# Only supports FakeSchema/FakeType used in tests
|
|
325
|
-
constraints = Hash.new { |h, k| h[k] = {} }
|
|
326
|
-
if schema_obj.respond_to?(:types)
|
|
327
|
-
schema_obj.types.each do |name, dry_type|
|
|
328
|
-
next unless dry_type.respond_to?(:rule_ast)
|
|
329
|
-
|
|
330
|
-
rules = dry_type.rule_ast
|
|
331
|
-
Array(rules).each do |rule|
|
|
332
|
-
next unless rule.is_a?(Array)
|
|
333
|
-
|
|
334
|
-
_, pred = rule
|
|
335
|
-
next unless pred.is_a?(Array)
|
|
336
|
-
|
|
337
|
-
pname, pargs = pred
|
|
338
|
-
case pname
|
|
339
|
-
when :size?
|
|
340
|
-
min, max = Array(pargs).first
|
|
341
|
-
constraints[name][:min] = min
|
|
342
|
-
constraints[name][:max] = max
|
|
343
|
-
when :maybe
|
|
344
|
-
constraints[name][:nullable] = true
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
constraints
|
|
350
|
-
rescue NoMethodError, TypeError
|
|
351
|
-
{}
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def extract_enum_from_core_values(core)
|
|
355
|
-
return unless core.respond_to?(:values)
|
|
356
|
-
|
|
357
|
-
vals = core.values
|
|
358
|
-
vals if vals.is_a?(Array)
|
|
359
|
-
rescue NoMethodError
|
|
360
|
-
nil
|
|
203
|
+
Constants::RUBY_TYPE_MAPPING[value] ||
|
|
204
|
+
Constants::RUBY_TYPE_MAPPING[value.class] ||
|
|
205
|
+
Constants::SchemaTypes::STRING
|
|
361
206
|
end
|
|
362
207
|
|
|
363
208
|
def convert_contract_schema_to_params(schema)
|
|
@@ -148,6 +148,8 @@ module GrapeOAS
|
|
|
148
148
|
schema.unevaluated_properties = doc[:unevaluated_properties] if doc.key?(:unevaluated_properties)
|
|
149
149
|
schema.format = doc[:format] if doc[:format]
|
|
150
150
|
schema.examples = doc[:example] if doc[:example]
|
|
151
|
+
nullable = SchemaEnhancer.extract_nullable(doc)
|
|
152
|
+
schema.nullable = (schema.nullable || nullable) if schema.respond_to?(:nullable=)
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
155
|
end
|
|
@@ -33,13 +33,18 @@ module GrapeOAS
|
|
|
33
33
|
|
|
34
34
|
return build_entity_array_schema(spec, raw_type, doc_type) if entity_array_type?(type_source, doc_type, spec)
|
|
35
35
|
return build_doc_entity_array_schema(doc_type) if doc[:is_array] && grape_entity?(doc_type)
|
|
36
|
+
return build_primitive_array_schema(doc_type, raw_type) if doc[:is_array]
|
|
36
37
|
return build_entity_schema(doc_type) if grape_entity?(doc_type)
|
|
37
38
|
return build_entity_schema(raw_type) if grape_entity?(raw_type)
|
|
38
39
|
return build_elements_array_schema(spec) if array_with_elements?(raw_type, spec)
|
|
39
40
|
return build_multi_type_schema(raw_type) if multi_type?(raw_type)
|
|
40
|
-
return build_typed_array_schema(raw_type) if typed_array?(raw_type)
|
|
41
41
|
return build_simple_array_schema if simple_array?(raw_type)
|
|
42
42
|
|
|
43
|
+
# Use TypeResolvers registry for arrays, Dry::Types, and primitives
|
|
44
|
+
# This resolves stringified types back to actual classes and extracts rich metadata
|
|
45
|
+
resolved_schema = GrapeOAS.type_resolvers.build_schema(raw_type)
|
|
46
|
+
return resolved_schema if resolved_schema
|
|
47
|
+
|
|
43
48
|
build_primitive_schema(raw_type, doc)
|
|
44
49
|
end
|
|
45
50
|
|
|
@@ -88,6 +93,15 @@ module GrapeOAS
|
|
|
88
93
|
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
89
94
|
end
|
|
90
95
|
|
|
96
|
+
def build_primitive_array_schema(doc_type, raw_type)
|
|
97
|
+
type_source = doc_type || raw_type
|
|
98
|
+
item_type = resolve_schema_type(type_source)
|
|
99
|
+
ApiModel::Schema.new(
|
|
100
|
+
type: Constants::SchemaTypes::ARRAY,
|
|
101
|
+
items: ApiModel::Schema.new(type: item_type, format: Constants.format_for_type(type_source)),
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
91
105
|
def build_simple_array_schema
|
|
92
106
|
ApiModel::Schema.new(
|
|
93
107
|
type: Constants::SchemaTypes::ARRAY,
|
|
@@ -170,29 +184,11 @@ module GrapeOAS
|
|
|
170
184
|
!!resolve_entity_class(type)
|
|
171
185
|
end
|
|
172
186
|
|
|
173
|
-
# Checks if type is a Grape typed array notation like "[String]"
|
|
174
|
-
def typed_array?(type)
|
|
175
|
-
type.is_a?(String) && type.match?(TYPED_ARRAY_PATTERN)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
187
|
# Checks if type is a simple Array (class or string)
|
|
179
188
|
def simple_array?(type)
|
|
180
189
|
type == Array || type.to_s == "Array"
|
|
181
190
|
end
|
|
182
191
|
|
|
183
|
-
# Builds schema for Grape's typed array notation like "[String]", "[Integer]"
|
|
184
|
-
def build_typed_array_schema(type)
|
|
185
|
-
member_type = extract_typed_array_member(type)
|
|
186
|
-
items_type = resolve_schema_type(member_type)
|
|
187
|
-
ApiModel::Schema.new(
|
|
188
|
-
type: Constants::SchemaTypes::ARRAY,
|
|
189
|
-
items: ApiModel::Schema.new(
|
|
190
|
-
type: items_type,
|
|
191
|
-
format: Constants.format_for_type(member_type),
|
|
192
|
-
),
|
|
193
|
-
)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
192
|
def resolve_entity_class(type)
|
|
197
193
|
return nil unless defined?(Grape::Entity)
|
|
198
194
|
return type if type.is_a?(Class) && type <= Grape::Entity
|
|
@@ -205,7 +201,7 @@ module GrapeOAS
|
|
|
205
201
|
klass = Object.const_get(const_name, false)
|
|
206
202
|
klass if klass.is_a?(Class) && klass <= Grape::Entity
|
|
207
203
|
rescue NameError => e
|
|
208
|
-
warn
|
|
204
|
+
GrapeOAS.logger.warn("Could not resolve entity constant '#{const_name}': #{e.message}")
|
|
209
205
|
nil
|
|
210
206
|
end
|
|
211
207
|
end
|
|
@@ -11,7 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
# @param spec [Hash] the parameter specification
|
|
12
12
|
# @param doc [Hash] the documentation hash
|
|
13
13
|
def self.apply(schema, spec, doc)
|
|
14
|
-
nullable = extract_nullable(
|
|
14
|
+
nullable = extract_nullable(doc)
|
|
15
15
|
|
|
16
16
|
schema.description ||= doc[:desc]
|
|
17
17
|
# Preserve existing nullable: true (e.g., from [Type, Nil] optimization)
|
|
@@ -19,17 +19,16 @@ module GrapeOAS
|
|
|
19
19
|
|
|
20
20
|
apply_additional_properties(schema, doc)
|
|
21
21
|
apply_format_and_example(schema, doc)
|
|
22
|
-
|
|
22
|
+
SchemaConstraints.apply(schema, doc)
|
|
23
23
|
apply_values(schema, spec)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Extracts nullable flag from
|
|
26
|
+
# Extracts nullable flag from a documentation hash.
|
|
27
27
|
#
|
|
28
|
-
# @param spec [Hash] the parameter specification
|
|
29
28
|
# @param doc [Hash] the documentation hash
|
|
30
29
|
# @return [Boolean] true if nullable
|
|
31
|
-
def self.extract_nullable(
|
|
32
|
-
|
|
30
|
+
def self.extract_nullable(doc)
|
|
31
|
+
doc[:nullable] || (doc[:x].is_a?(Hash) && doc[:x][:nullable]) || false
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
class << self
|
|
@@ -51,38 +50,24 @@ module GrapeOAS
|
|
|
51
50
|
schema.examples = doc[:example] if doc[:example] && schema.respond_to?(:examples=)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
|
-
def apply_constraints(schema, doc)
|
|
55
|
-
schema.minimum = doc[:minimum] if doc.key?(:minimum) && schema.respond_to?(:minimum=)
|
|
56
|
-
schema.maximum = doc[:maximum] if doc.key?(:maximum) && schema.respond_to?(:maximum=)
|
|
57
|
-
schema.min_length = doc[:min_length] if doc.key?(:min_length) && schema.respond_to?(:min_length=)
|
|
58
|
-
schema.max_length = doc[:max_length] if doc.key?(:max_length) && schema.respond_to?(:max_length=)
|
|
59
|
-
schema.pattern = doc[:pattern] if doc.key?(:pattern) && schema.respond_to?(:pattern=)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Applies values from spec[:values] - converts Range to min/max,
|
|
63
|
-
# evaluates Proc (arity 0), and sets enum for arrays.
|
|
64
|
-
# Skips Proc/Lambda validators (arity > 0) used for custom validation.
|
|
65
|
-
# For array schemas, applies enum to items (since values constrain array elements).
|
|
66
|
-
# For oneOf schemas, applies enum to each non-null variant.
|
|
67
53
|
def apply_values(schema, spec)
|
|
68
|
-
values = spec[:values]
|
|
54
|
+
values = ValuesNormalizer.normalize(spec[:values], context: "parameter values")
|
|
69
55
|
return unless values
|
|
70
56
|
|
|
71
|
-
# Handle Hash format { value: ..., message: ... } - extract the value
|
|
72
|
-
values = values[:value] if values.is_a?(Hash) && values.key?(:value)
|
|
73
|
-
|
|
74
|
-
# Handle Proc/Lambda
|
|
75
|
-
if values.respond_to?(:call)
|
|
76
|
-
# Skip validators (arity > 0) - they validate individual values
|
|
77
|
-
return if values.arity != 0
|
|
78
|
-
|
|
79
|
-
# Evaluate arity-0 procs - they return enum arrays
|
|
80
|
-
values = values.call
|
|
81
|
-
end
|
|
82
|
-
|
|
83
57
|
if values.is_a?(Range)
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
if one_of_schema?(schema)
|
|
59
|
+
schema.one_of.each do |variant|
|
|
60
|
+
next if null_type_schema?(variant)
|
|
61
|
+
next unless range_compatible_with_schema?(values, variant)
|
|
62
|
+
|
|
63
|
+
RangeUtils.apply_to_schema(variant, values)
|
|
64
|
+
end
|
|
65
|
+
elsif array_schema_with_items?(schema)
|
|
66
|
+
RangeUtils.apply_to_schema(schema.items, values)
|
|
67
|
+
else
|
|
68
|
+
RangeUtils.apply_to_schema(schema, values)
|
|
69
|
+
end
|
|
70
|
+
elsif values.is_a?(Array) && !values.empty?
|
|
86
71
|
apply_enum_values(schema, values)
|
|
87
72
|
end
|
|
88
73
|
end
|
|
@@ -98,7 +83,7 @@ module GrapeOAS
|
|
|
98
83
|
compatible_values = filter_compatible_values(variant, values)
|
|
99
84
|
|
|
100
85
|
# Only apply enum if there are compatible values
|
|
101
|
-
variant.enum = compatible_values if compatible_values.
|
|
86
|
+
variant.enum = compatible_values if !compatible_values.empty? && variant.respond_to?(:enum=)
|
|
102
87
|
end
|
|
103
88
|
elsif array_schema_with_items?(schema)
|
|
104
89
|
# For array schemas, apply enum to items (values constrain array elements)
|
|
@@ -110,7 +95,7 @@ module GrapeOAS
|
|
|
110
95
|
end
|
|
111
96
|
|
|
112
97
|
def one_of_schema?(schema)
|
|
113
|
-
schema.respond_to?(:one_of) && schema.one_of.is_a?(Array) && schema.one_of.
|
|
98
|
+
schema.respond_to?(:one_of) && schema.one_of.is_a?(Array) && !schema.one_of.empty?
|
|
114
99
|
end
|
|
115
100
|
|
|
116
101
|
def null_type_schema?(schema)
|
|
@@ -159,26 +144,14 @@ module GrapeOAS
|
|
|
159
144
|
end
|
|
160
145
|
end
|
|
161
146
|
|
|
162
|
-
# Converts a Range to minimum/maximum constraints.
|
|
163
|
-
# For numeric ranges (Integer, Float), uses min/max.
|
|
164
|
-
# For other ranges (e.g., 'a'..'z'), expands to enum array.
|
|
165
|
-
# Handles endless/beginless ranges (e.g., 1.., ..10).
|
|
166
|
-
def apply_range_values(schema, range)
|
|
167
|
-
first_val = range.begin
|
|
168
|
-
last_val = range.end
|
|
169
|
-
|
|
170
|
-
if first_val.is_a?(Numeric) || last_val.is_a?(Numeric)
|
|
171
|
-
schema.minimum = first_val if first_val && schema.respond_to?(:minimum=)
|
|
172
|
-
schema.maximum = last_val if last_val && schema.respond_to?(:maximum=)
|
|
173
|
-
elsif first_val && last_val && schema.respond_to?(:enum=)
|
|
174
|
-
# Non-numeric bounded range (e.g., 'a'..'z') - expand to enum
|
|
175
|
-
schema.enum = range.to_a
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
147
|
def extract_defs(doc)
|
|
180
148
|
doc[:defs] || doc[:$defs]
|
|
181
149
|
end
|
|
150
|
+
|
|
151
|
+
def range_compatible_with_schema?(range, schema)
|
|
152
|
+
numeric_type = RangeUtils::NUMERIC_TYPES.include?(schema.type)
|
|
153
|
+
RangeUtils.numeric_range?(range) ? numeric_type : !numeric_type
|
|
154
|
+
end
|
|
182
155
|
end
|
|
183
156
|
end
|
|
184
157
|
end
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -35,6 +35,22 @@ module GrapeOAS
|
|
|
35
35
|
ALL = [JSON, XML, FORM_URLENCODED, MULTIPART_FORM].freeze
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Nullable representation strategies for different OpenAPI versions.
|
|
39
|
+
# Passed via `nullable_strategy:` option to control how nullable fields
|
|
40
|
+
# are represented in the generated schema.
|
|
41
|
+
module NullableStrategy
|
|
42
|
+
# OAS 3.0: emits `"nullable": true` alongside the type
|
|
43
|
+
KEYWORD = :keyword
|
|
44
|
+
# OAS 3.1: emits `"type": ["string", "null"]` (JSON Schema style)
|
|
45
|
+
TYPE_ARRAY = :type_array
|
|
46
|
+
# OAS 2.0: emits `"x-nullable": true` extension
|
|
47
|
+
EXTENSION = :extension
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Maximum number of elements to expand from a non-numeric Range into an enum array.
|
|
51
|
+
# Prevents OOM on wide string ranges (e.g. "a".."zzzzzz").
|
|
52
|
+
MAX_ENUM_RANGE_SIZE = 100
|
|
53
|
+
|
|
38
54
|
# Default values for OpenAPI spec when not provided by user
|
|
39
55
|
module Defaults
|
|
40
56
|
LICENSE_NAME = "Proprietary"
|
|
@@ -49,13 +65,16 @@ module GrapeOAS
|
|
|
49
65
|
RUBY_TYPE_MAPPING = {
|
|
50
66
|
Integer => SchemaTypes::INTEGER,
|
|
51
67
|
Float => SchemaTypes::NUMBER,
|
|
52
|
-
BigDecimal => SchemaTypes::NUMBER,
|
|
53
68
|
TrueClass => SchemaTypes::BOOLEAN,
|
|
54
69
|
FalseClass => SchemaTypes::BOOLEAN,
|
|
55
70
|
Array => SchemaTypes::ARRAY,
|
|
56
71
|
Hash => SchemaTypes::OBJECT,
|
|
57
72
|
File => SchemaTypes::FILE
|
|
58
|
-
}.
|
|
73
|
+
}.tap do |mapping|
|
|
74
|
+
mapping.default_proc = lambda do |_hash, key|
|
|
75
|
+
key.is_a?(Class) && key.to_s == "BigDecimal" ? SchemaTypes::NUMBER : nil
|
|
76
|
+
end
|
|
77
|
+
end.freeze
|
|
59
78
|
|
|
60
79
|
# String type name to schema type and format mapping (lowercase).
|
|
61
80
|
# Supports lookup with any case via primitive_type helper.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrapeOAS
|
|
4
|
+
# Normalizes documentation hash keys so callers can use symbol access
|
|
5
|
+
# uniformly. String keys that look like OpenAPI extensions ("x-*") are
|
|
6
|
+
# kept as strings; all other keys are converted to symbols.
|
|
7
|
+
module DocKeyNormalizer
|
|
8
|
+
def self.normalize(doc)
|
|
9
|
+
return doc if doc.empty?
|
|
10
|
+
|
|
11
|
+
doc.transform_keys { |k| k.to_s.start_with?("x-") ? k.to_s : k.to_sym }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -10,11 +10,13 @@ module GrapeOAS
|
|
|
10
10
|
|
|
11
11
|
# OAS2-specific fields: consumes, produces, parameters (including body)
|
|
12
12
|
def build_version_specific_fields
|
|
13
|
+
strategy = @options[:nullable_strategy]
|
|
14
|
+
|
|
13
15
|
{
|
|
14
16
|
"consumes" => consumes,
|
|
15
17
|
"produces" => produces,
|
|
16
|
-
"parameters" => Parameter.new(@op, @ref_tracker).build,
|
|
17
|
-
"responses" => Response.new(@op.responses, @ref_tracker).build
|
|
18
|
+
"parameters" => Parameter.new(@op, @ref_tracker, nullable_strategy: strategy).build,
|
|
19
|
+
"responses" => Response.new(@op.responses, @ref_tracker, nullable_strategy: strategy).build
|
|
18
20
|
}
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -18,9 +18,10 @@ module GrapeOAS
|
|
|
18
18
|
"uuid" => { type: Constants::SchemaTypes::STRING, format: "uuid" }
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
def initialize(operation, ref_tracker = nil)
|
|
21
|
+
def initialize(operation, ref_tracker = nil, nullable_strategy: nil)
|
|
22
22
|
@op = operation
|
|
23
23
|
@ref_tracker = ref_tracker
|
|
24
|
+
@nullable_strategy = nullable_strategy
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def build
|
|
@@ -140,7 +141,7 @@ module GrapeOAS
|
|
|
140
141
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
141
142
|
{ "$ref" => "#/definitions/#{ref_name}" }
|
|
142
143
|
else
|
|
143
|
-
Schema.new(schema, @ref_tracker).build
|
|
144
|
+
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
144
145
|
end
|
|
145
146
|
end
|
|
146
147
|
end
|
|
@@ -11,6 +11,7 @@ module GrapeOAS
|
|
|
11
11
|
# Build OAS2-specific operation
|
|
12
12
|
def build_operation(operation)
|
|
13
13
|
Operation.new(operation, @ref_tracker,
|
|
14
|
+
nullable_strategy: @options[:nullable_strategy],
|
|
14
15
|
suppress_default_error_response: @options[:suppress_default_error_response],).build
|
|
15
16
|
end
|
|
16
17
|
end
|