grape-oas 1.2.0 → 1.4.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 +42 -0
- data/README.md +6 -0
- data/lib/grape_oas/api_model/api.rb +4 -0
- data/lib/grape_oas/api_model/schema.rb +18 -3
- data/lib/grape_oas/api_model_builders/concerns/content_type_resolver.rb +21 -0
- data/lib/grape_oas/api_model_builders/concerns/type_resolver.rb +3 -6
- data/lib/grape_oas/api_model_builders/request.rb +21 -12
- data/lib/grape_oas/api_model_builders/request_params_support/param_location_resolver.rb +7 -2
- data/lib/grape_oas/api_model_builders/request_params_support/param_schema_builder.rb +15 -1
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +34 -50
- data/lib/grape_oas/constants.rb +13 -0
- data/lib/grape_oas/doc_key_normalizer.rb +14 -0
- data/lib/grape_oas/exporter/oas2/parameter.rb +1 -0
- data/lib/grape_oas/exporter/oas2/schema.rb +53 -19
- data/lib/grape_oas/exporter/oas3/parameter.rb +5 -2
- data/lib/grape_oas/exporter/oas3/schema.rb +81 -46
- 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/entity_introspector.rb +5 -1
- 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 +6 -30
- 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 +57 -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 +3 -5
- data/lib/grape_oas/values_normalizer.rb +47 -0
- data/lib/grape_oas/version.rb +1 -1
- data/lib/grape_oas.rb +27 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 585f37dd85b6630ef0d9599e3c1a02172cda35ff81ba6b9782e500d6bdaf8115
|
|
4
|
+
data.tar.gz: b7e2cec6f5c24f7927e7290c87dffd2a488fe77768c039d9bc711cc79e868373
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74106be01a4e9d0a9625d4b328df7b319de5dfe1da3d74d37f605d9328eb7fe200dccc761a865a66963797ad209871f71402f0c601e7f9d27ef9fbfe23408c69
|
|
7
|
+
data.tar.gz: fbc867bb5e8167fe096a6024285986ac410385b3bc07cdca2b1c62c73bb0cbd7a4e9e1f0dff271212637dac1ac43842e8417d36b93bc39984d396cc8cd9329f3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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
|
+
## [1.4.0] - 2026-04-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- [#60](https://github.com/numbata/grape-oas/pull/60): Fix `Dangerfile` to properly look for tests - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
13
|
+
- [#58](https://github.com/numbata/grape-oas/pull/58): Fix contract extraction compatibility with Grape 3.2 - [@numbata](https://github.com/numbata).
|
|
14
|
+
- [#57](https://github.com/numbata/grape-oas/pull/57): Fix `Array<Array<...>>` double-wrap when `is_array: true` is used with typed array notation like `type: [String]` — the redundant `is_array` flag no longer produces a nested array schema - [@numbata](https://github.com/numbata).
|
|
15
|
+
- [#59](https://github.com/numbata/grape-oas/pull/59): Export `default` param values to OAS2 and OAS3 output - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
16
|
+
- [#61](https://github.com/numbata/grape-oas/pull/61): Respect `entity_name` on `grape::entity` subclasses - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
17
|
+
- [#68](https://github.com/numbata/grape-oas/pull/68): De-duplicate parameter `description` between the Parameter Object and its nested `schema` in OAS 3 output - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
18
|
+
- [#70](https://github.com/numbata/grape-oas/pull/70): Propagate schema attributes (`default`, `enum`, constraints, extensions) through `$ref` and composition paths - [@numbata](https://github.com/numbata).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- [#64](https://github.com/numbata/grape-oas/pull/64): Memoize content-type and default-format resolution per generation — eliminates redundant calls that scaled with route × response count - [@JuniorJoanis](https://github.com/JuniorJoanis).
|
|
23
|
+
- [#62](https://github.com/numbata/grape-oas/pull/62): Default to body params for post/put/patch routes - [@olivier-thatch](https://github.com/olivier-thatch).
|
|
24
|
+
|
|
25
|
+
## [1.3.0] - 2026-03-27
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- [#48](https://github.com/numbata/grape-oas/pull/48): Add configurable `GrapeOAS.logger` for schema generation warnings - [@numbata](https://github.com/numbata).
|
|
30
|
+
- [#43](https://github.com/numbata/grape-oas/pull/43): Bump actions/upload-artifact from 6 to 7 - [@dependabot[bot]](https://github.com/dependabot[bot]).
|
|
31
|
+
- [#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).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- [#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).
|
|
36
|
+
- [#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).
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- [#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).
|
|
41
|
+
- [#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).
|
|
42
|
+
- [#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).
|
|
43
|
+
|
|
44
|
+
- [#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).
|
|
45
|
+
- [#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).
|
|
46
|
+
- [#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).
|
|
47
|
+
- [#44](https://github.com/numbata/grape-oas/pull/44): Fix RuboCop 1.85 offenses - [@numbata](https://github.com/numbata).
|
|
48
|
+
- [#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).
|
|
49
|
+
|
|
8
50
|
## [1.2.0] - 2026-03-02
|
|
9
51
|
|
|
10
52
|
### 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
|
VALID_ATTRIBUTES = %i[
|
|
12
12
|
canonical_name type format properties items description
|
|
13
13
|
required nullable enum additional_properties unevaluated_properties defs
|
|
14
|
-
examples extensions
|
|
14
|
+
examples default extensions
|
|
15
15
|
min_length max_length pattern
|
|
16
16
|
minimum maximum exclusive_minimum exclusive_maximum
|
|
17
17
|
min_items max_items
|
|
@@ -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
|
|
@@ -57,6 +57,16 @@ module GrapeOAS
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def default_format_from_app_or_api
|
|
60
|
+
return uncached_default_format_from_app_or_api unless api.respond_to?(:builder_cache)
|
|
61
|
+
|
|
62
|
+
cache = api.builder_cache
|
|
63
|
+
key = [:default_format, app.object_id]
|
|
64
|
+
return cache[key] if cache.key?(key)
|
|
65
|
+
|
|
66
|
+
cache[key] = uncached_default_format_from_app_or_api
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def uncached_default_format_from_app_or_api
|
|
60
70
|
return api.default_format if api.respond_to?(:default_format)
|
|
61
71
|
return app.default_format if app_responds_to?(:default_format)
|
|
62
72
|
|
|
@@ -66,6 +76,17 @@ module GrapeOAS
|
|
|
66
76
|
end
|
|
67
77
|
|
|
68
78
|
def content_types_from_app_or_api(default_format)
|
|
79
|
+
return uncached_content_types_from_app_or_api(default_format) unless api.respond_to?(:builder_cache)
|
|
80
|
+
|
|
81
|
+
cache = api.builder_cache
|
|
82
|
+
key = [:content_types, app.object_id, default_format]
|
|
83
|
+
return cache[key] if cache.key?(key)
|
|
84
|
+
|
|
85
|
+
value = uncached_content_types_from_app_or_api(default_format)
|
|
86
|
+
cache[key] = value.is_a?(Hash) ? value.dup.freeze : value
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def uncached_content_types_from_app_or_api(default_format)
|
|
69
90
|
source = if api.respond_to?(:content_types)
|
|
70
91
|
api.content_types
|
|
71
92
|
elsif app_responds_to?(:content_types)
|
|
@@ -12,11 +12,8 @@ module GrapeOAS
|
|
|
12
12
|
# Centralizes Ruby type to OpenAPI schema type resolution.
|
|
13
13
|
# Used by request builders and introspectors to avoid duplicated type switching logic.
|
|
14
14
|
module TypeResolver
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Regex to match Grape's multi-type notation like "[String, Integer]", "[String, Float]"
|
|
19
|
-
MULTI_TYPE_PATTERN = /\A\[(\w+(?:::\w+)*(?:,\s*\w+(?:::\w+)*)+)\]\z/
|
|
15
|
+
TYPED_ARRAY_PATTERN = Constants::TypePatterns::TYPED_ARRAY
|
|
16
|
+
MULTI_TYPE_PATTERN = Constants::TypePatterns::MULTI_TYPE
|
|
20
17
|
|
|
21
18
|
# Resolves a Ruby class or type name to its OpenAPI schema type string.
|
|
22
19
|
# Handles both Ruby classes (Integer, Float) and string type names ("integer", "float").
|
|
@@ -65,7 +62,7 @@ module GrapeOAS
|
|
|
65
62
|
return nil unless type.is_a?(String)
|
|
66
63
|
|
|
67
64
|
match = type.match(TYPED_ARRAY_PATTERN)
|
|
68
|
-
match ? match[
|
|
65
|
+
match ? match[:inner] : nil
|
|
69
66
|
end
|
|
70
67
|
|
|
71
68
|
# Checks if type is a multi-type notation like "[String, Integer]"
|
|
@@ -134,20 +134,29 @@ module GrapeOAS
|
|
|
134
134
|
validations = setting.route[:saved_validations]
|
|
135
135
|
return unless validations.is_a?(Array)
|
|
136
136
|
|
|
137
|
-
# Find ContractScopeValidator which holds the Dry contract/schema
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
validator_class = v[:validator_class]
|
|
142
|
-
validator_class.is_a?(Class) &&
|
|
143
|
-
defined?(Grape::Validations::Validators::ContractScopeValidator) &&
|
|
144
|
-
validator_class <= Grape::Validations::Validators::ContractScopeValidator
|
|
145
|
-
end
|
|
137
|
+
# Find ContractScopeValidator which holds the Dry contract/schema.
|
|
138
|
+
# Grape < 3.2 stores hashes: {validator_class: ..., opts: {schema: ...}}
|
|
139
|
+
# Grape >= 3.2 stores validator instances directly (instantiated at definition time)
|
|
140
|
+
return unless defined?(Grape::Validations::Validators::ContractScopeValidator)
|
|
146
141
|
|
|
147
|
-
|
|
142
|
+
validations.each do |v|
|
|
143
|
+
case v
|
|
144
|
+
when Hash
|
|
145
|
+
next unless v[:validator_class].is_a?(Class) &&
|
|
146
|
+
v[:validator_class] <= Grape::Validations::Validators::ContractScopeValidator
|
|
147
|
+
|
|
148
|
+
return v.dig(:opts, :schema)
|
|
149
|
+
when Grape::Validations::Validators::ContractScopeValidator
|
|
150
|
+
# Grape 3.2 removed attr_reader :schema and freezes the validator,
|
|
151
|
+
# so instance_variable_get is the only way to access the schema.
|
|
152
|
+
# TODO: use v.schema once ruby-grape/grape#2657 restores the accessor.
|
|
153
|
+
schema = v.instance_variable_get(:@schema)
|
|
154
|
+
GrapeOAS.logger&.warn("ContractScopeValidator found but @schema is nil") if schema.nil?
|
|
155
|
+
return schema
|
|
156
|
+
end
|
|
157
|
+
end
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
contract_validation.dig(:opts, :schema)
|
|
159
|
+
nil
|
|
151
160
|
end
|
|
152
161
|
|
|
153
162
|
def build_contract_schema
|
|
@@ -65,7 +65,7 @@ module GrapeOAS
|
|
|
65
65
|
# Precedence (highest to lowest):
|
|
66
66
|
# 1. `param_type` option (e.g., `documentation: { param_type: 'query' }`)
|
|
67
67
|
# 2. `in` option (e.g., `documentation: { in: 'query' }`)
|
|
68
|
-
# 3.
|
|
68
|
+
# 3. Defaults to "body" for write methods (POST/PUT/PATCH), "query" for read methods
|
|
69
69
|
#
|
|
70
70
|
# Note: If both `param_type` and `in` are specified, `param_type` takes precedence.
|
|
71
71
|
# For example, `{ param_type: 'query', in: 'body' }` will be treated as query.
|
|
@@ -81,7 +81,12 @@ module GrapeOAS
|
|
|
81
81
|
|
|
82
82
|
# Support both param_type and in for grape-swagger compatibility
|
|
83
83
|
# param_type takes precedence over in when both are specified
|
|
84
|
-
(param_type || in_location)&.to_s&.downcase
|
|
84
|
+
explicit_location = (param_type || in_location)&.to_s&.downcase
|
|
85
|
+
return explicit_location if explicit_location
|
|
86
|
+
|
|
87
|
+
# Default: body for write methods (POST/PUT/PATCH), query for read methods (GET/DELETE/HEAD)
|
|
88
|
+
http_method = route.request_method.to_s.downcase
|
|
89
|
+
Constants::HttpMethods::BODYLESS_HTTP_METHODS.include?(http_method) ? "query" : "body"
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
end
|
|
@@ -33,6 +33,11 @@ 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
|
+
|
|
37
|
+
# is_array: true on a typed array like "[String]" is redundant and would
|
|
38
|
+
# double-wrap it as Array<Array<...>> via build_primitive_array_schema.
|
|
39
|
+
return GrapeOAS.type_resolvers.build_schema(raw_type) if doc[:is_array] && extract_typed_array_member(raw_type)
|
|
40
|
+
return build_primitive_array_schema(doc_type, raw_type) if doc[:is_array]
|
|
36
41
|
return build_entity_schema(doc_type) if grape_entity?(doc_type)
|
|
37
42
|
return build_entity_schema(raw_type) if grape_entity?(raw_type)
|
|
38
43
|
return build_elements_array_schema(spec) if array_with_elements?(raw_type, spec)
|
|
@@ -92,6 +97,15 @@ module GrapeOAS
|
|
|
92
97
|
ApiModel::Schema.new(type: Constants::SchemaTypes::ARRAY, items: items_schema)
|
|
93
98
|
end
|
|
94
99
|
|
|
100
|
+
def build_primitive_array_schema(doc_type, raw_type)
|
|
101
|
+
type_source = doc_type || raw_type
|
|
102
|
+
item_type = resolve_schema_type(type_source)
|
|
103
|
+
ApiModel::Schema.new(
|
|
104
|
+
type: Constants::SchemaTypes::ARRAY,
|
|
105
|
+
items: ApiModel::Schema.new(type: item_type, format: Constants.format_for_type(type_source)),
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
95
109
|
def build_simple_array_schema
|
|
96
110
|
ApiModel::Schema.new(
|
|
97
111
|
type: Constants::SchemaTypes::ARRAY,
|
|
@@ -191,7 +205,7 @@ module GrapeOAS
|
|
|
191
205
|
klass = Object.const_get(const_name, false)
|
|
192
206
|
klass if klass.is_a?(Class) && klass <= Grape::Entity
|
|
193
207
|
rescue NameError => e
|
|
194
|
-
warn
|
|
208
|
+
GrapeOAS.logger.warn("Could not resolve entity constant '#{const_name}': #{e.message}")
|
|
195
209
|
nil
|
|
196
210
|
end
|
|
197
211
|
end
|
|
@@ -19,8 +19,9 @@ 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
|
+
apply_default(schema, spec, doc)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# Extracts nullable flag from a documentation hash.
|
|
@@ -45,45 +46,40 @@ module GrapeOAS
|
|
|
45
46
|
schema.defs = defs if defs.is_a?(Hash) && schema.respond_to?(:defs=)
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
def apply_default(schema, spec, doc)
|
|
50
|
+
return unless schema.respond_to?(:default=)
|
|
51
|
+
|
|
52
|
+
if spec.key?(:default)
|
|
53
|
+
schema.default = spec[:default]
|
|
54
|
+
elsif doc.key?(:default)
|
|
55
|
+
schema.default = doc[:default]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
48
59
|
def apply_format_and_example(schema, doc)
|
|
49
60
|
schema.format = doc[:format] if doc[:format] && schema.respond_to?(:format=)
|
|
50
61
|
schema.examples = doc[:example] if doc[:example] && schema.respond_to?(:examples=)
|
|
51
62
|
end
|
|
52
63
|
|
|
53
|
-
def apply_constraints(schema, doc)
|
|
54
|
-
schema.minimum = doc[:minimum] if doc.key?(:minimum) && schema.respond_to?(:minimum=)
|
|
55
|
-
schema.maximum = doc[:maximum] if doc.key?(:maximum) && schema.respond_to?(:maximum=)
|
|
56
|
-
schema.min_length = doc[:min_length] if doc.key?(:min_length) && schema.respond_to?(:min_length=)
|
|
57
|
-
schema.max_length = doc[:max_length] if doc.key?(:max_length) && schema.respond_to?(:max_length=)
|
|
58
|
-
schema.pattern = doc[:pattern] if doc.key?(:pattern) && schema.respond_to?(:pattern=)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Applies values from spec[:values] - converts Range to min/max,
|
|
62
|
-
# evaluates Proc (arity 0), and sets enum for arrays.
|
|
63
|
-
# Skips Proc/Lambda validators (arity > 0) used for custom validation.
|
|
64
|
-
# For array schemas, applies enum to items (since values constrain array elements).
|
|
65
|
-
# For oneOf schemas, applies enum to each non-null variant.
|
|
66
64
|
def apply_values(schema, spec)
|
|
67
|
-
values = spec[:values]
|
|
65
|
+
values = ValuesNormalizer.normalize(spec[:values], context: "parameter values")
|
|
68
66
|
return unless values
|
|
69
67
|
|
|
70
|
-
# Handle Hash format { value: ..., message: ... } - extract the value
|
|
71
|
-
values = values[:value] if values.is_a?(Hash) && values.key?(:value)
|
|
72
|
-
|
|
73
|
-
# Handle Proc/Lambda
|
|
74
|
-
if values.respond_to?(:call)
|
|
75
|
-
# Skip validators (arity > 0) - they validate individual values
|
|
76
|
-
return if values.arity != 0
|
|
77
|
-
|
|
78
|
-
# Evaluate arity-0 procs - they return enum arrays
|
|
79
|
-
values = values.call
|
|
80
|
-
end
|
|
81
|
-
|
|
82
68
|
if values.is_a?(Range)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
if one_of_schema?(schema)
|
|
70
|
+
schema.one_of.each do |variant|
|
|
71
|
+
next if null_type_schema?(variant)
|
|
72
|
+
next unless range_compatible_with_schema?(values, variant)
|
|
73
|
+
|
|
74
|
+
RangeUtils.apply_to_schema(variant, values)
|
|
75
|
+
end
|
|
76
|
+
elsif array_schema_with_items?(schema)
|
|
77
|
+
RangeUtils.apply_to_schema(schema.items, values)
|
|
78
|
+
else
|
|
79
|
+
RangeUtils.apply_to_schema(schema, values)
|
|
80
|
+
end
|
|
81
|
+
elsif values.is_a?(Array) && !values.empty?
|
|
82
|
+
apply_enum_values(schema, values)
|
|
87
83
|
end
|
|
88
84
|
end
|
|
89
85
|
|
|
@@ -98,7 +94,7 @@ module GrapeOAS
|
|
|
98
94
|
compatible_values = filter_compatible_values(variant, values)
|
|
99
95
|
|
|
100
96
|
# Only apply enum if there are compatible values
|
|
101
|
-
variant.enum = compatible_values if compatible_values.
|
|
97
|
+
variant.enum = compatible_values if !compatible_values.empty? && variant.respond_to?(:enum=)
|
|
102
98
|
end
|
|
103
99
|
elsif array_schema_with_items?(schema)
|
|
104
100
|
# For array schemas, apply enum to items (values constrain array elements)
|
|
@@ -110,7 +106,7 @@ module GrapeOAS
|
|
|
110
106
|
end
|
|
111
107
|
|
|
112
108
|
def one_of_schema?(schema)
|
|
113
|
-
schema.respond_to?(:one_of) && schema.one_of.is_a?(Array) && schema.one_of.
|
|
109
|
+
schema.respond_to?(:one_of) && schema.one_of.is_a?(Array) && !schema.one_of.empty?
|
|
114
110
|
end
|
|
115
111
|
|
|
116
112
|
def null_type_schema?(schema)
|
|
@@ -159,26 +155,14 @@ module GrapeOAS
|
|
|
159
155
|
end
|
|
160
156
|
end
|
|
161
157
|
|
|
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
158
|
def extract_defs(doc)
|
|
180
159
|
doc[:defs] || doc[:$defs]
|
|
181
160
|
end
|
|
161
|
+
|
|
162
|
+
def range_compatible_with_schema?(range, schema)
|
|
163
|
+
numeric_type = RangeUtils::NUMERIC_TYPES.include?(schema.type)
|
|
164
|
+
RangeUtils.numeric_range?(range) ? numeric_type : !numeric_type
|
|
165
|
+
end
|
|
182
166
|
end
|
|
183
167
|
end
|
|
184
168
|
end
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -47,6 +47,19 @@ module GrapeOAS
|
|
|
47
47
|
EXTENSION = :extension
|
|
48
48
|
end
|
|
49
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
|
+
|
|
54
|
+
# Regex patterns for Grape's stringified type notations.
|
|
55
|
+
# Grape converts `type: [SomeClass]` to "[SomeClass]" and
|
|
56
|
+
# `type: [String, Integer]` to "[String, Integer]" for documentation.
|
|
57
|
+
module TypePatterns
|
|
58
|
+
CONST_NAME = /(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*/
|
|
59
|
+
TYPED_ARRAY = /\A\[(?<inner>#{CONST_NAME})\]\z/
|
|
60
|
+
MULTI_TYPE = /\A\[(#{CONST_NAME}(?:,\s*#{CONST_NAME})+)\]\z/
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
# Default values for OpenAPI spec when not provided by user
|
|
51
64
|
module Defaults
|
|
52
65
|
LICENSE_NAME = "Proprietary"
|
|
@@ -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
|
|
@@ -78,6 +78,7 @@ module GrapeOAS
|
|
|
78
78
|
result["maxItems"] = schema.max_items if schema.respond_to?(:max_items) && !schema.max_items.nil?
|
|
79
79
|
result["pattern"] = schema.pattern if schema.respond_to?(:pattern) && schema.pattern
|
|
80
80
|
result["enum"] = normalize_enum(schema.enum, result["type"]) if schema.respond_to?(:enum) && schema.enum
|
|
81
|
+
result["default"] = schema.default if schema.respond_to?(:default) && !schema.default.nil?
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def normalize_enum(enum_vals, type)
|
|
@@ -33,28 +33,39 @@ module GrapeOAS
|
|
|
33
33
|
"format" => @schema.format,
|
|
34
34
|
"description" => @schema.description&.to_s,
|
|
35
35
|
"properties" => build_properties(@schema.properties),
|
|
36
|
-
"items" => (@schema.items ? build_schema_or_ref(@schema.items) : nil),
|
|
37
36
|
"enum" => normalize_enum(@schema.enum, @schema.type)
|
|
38
37
|
}
|
|
38
|
+
if @schema.items
|
|
39
|
+
schema_hash["items"] = build_schema_or_ref(@schema.items, include_metadata: false)
|
|
40
|
+
if !schema_hash["description"] && @schema.items.respond_to?(:description) && @schema.items.description
|
|
41
|
+
schema_hash["description"] = @schema.items.description.to_s
|
|
42
|
+
end
|
|
43
|
+
if @schema.items.respond_to?(:canonical_name) && @schema.items.canonical_name &&
|
|
44
|
+
@nullable_strategy == Constants::NullableStrategy::EXTENSION &&
|
|
45
|
+
@schema.items.respond_to?(:nullable) && @schema.items.nullable
|
|
46
|
+
schema_hash["x-nullable"] = true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
39
49
|
if schema_hash["properties"].nil? || schema_hash["properties"].empty? || @schema.type != Constants::SchemaTypes::OBJECT
|
|
40
50
|
schema_hash.delete("properties")
|
|
41
51
|
end
|
|
42
52
|
schema_hash["example"] = @schema.examples if @schema.examples
|
|
43
53
|
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
44
54
|
schema_hash["discriminator"] = @schema.discriminator if @schema.discriminator
|
|
55
|
+
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
45
56
|
schema_hash
|
|
46
57
|
end
|
|
47
58
|
|
|
48
|
-
def apply_constraints(schema_hash)
|
|
49
|
-
schema_hash["
|
|
50
|
-
schema_hash["
|
|
51
|
-
schema_hash["
|
|
52
|
-
schema_hash["
|
|
53
|
-
schema_hash["
|
|
54
|
-
schema_hash["
|
|
55
|
-
schema_hash["
|
|
56
|
-
schema_hash["minItems"] =
|
|
57
|
-
schema_hash["maxItems"] =
|
|
59
|
+
def apply_constraints(schema_hash, schema = @schema)
|
|
60
|
+
schema_hash["minimum"] = schema.minimum unless schema.minimum.nil?
|
|
61
|
+
schema_hash["maximum"] = schema.maximum unless schema.maximum.nil?
|
|
62
|
+
schema_hash["exclusiveMinimum"] = schema.exclusive_minimum if schema.exclusive_minimum
|
|
63
|
+
schema_hash["exclusiveMaximum"] = schema.exclusive_maximum if schema.exclusive_maximum
|
|
64
|
+
schema_hash["minLength"] = schema.min_length unless schema.min_length.nil?
|
|
65
|
+
schema_hash["maxLength"] = schema.max_length unless schema.max_length.nil?
|
|
66
|
+
schema_hash["pattern"] = schema.pattern if schema.pattern
|
|
67
|
+
schema_hash["minItems"] = schema.min_items unless schema.min_items.nil?
|
|
68
|
+
schema_hash["maxItems"] = schema.max_items unless schema.max_items.nil?
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
def apply_extensions(schema_hash)
|
|
@@ -70,27 +81,42 @@ module GrapeOAS
|
|
|
70
81
|
|
|
71
82
|
# Build schema from oneOf/anyOf by using first type (OAS2 doesn't support these)
|
|
72
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.
|
|
73
89
|
def build_first_of_schema(composition_type)
|
|
74
90
|
schemas = @schema.send(composition_type)
|
|
75
91
|
first_schema = schemas.first
|
|
76
92
|
return {} unless first_schema
|
|
77
93
|
|
|
78
|
-
# Build the first schema as the fallback
|
|
79
94
|
result = build_schema_or_ref(first_schema)
|
|
80
95
|
result["description"] = @schema.description.to_s if @schema.description
|
|
81
96
|
apply_extensions(result)
|
|
97
|
+
if result.key?("$ref") && result.size > 1
|
|
98
|
+
ref = { "$ref" => result.delete("$ref") }
|
|
99
|
+
result["allOf"] = [ref]
|
|
100
|
+
end
|
|
82
101
|
result
|
|
83
102
|
end
|
|
84
103
|
|
|
85
104
|
# Build allOf schema for inheritance
|
|
86
105
|
def build_all_of_schema
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
items = @schema.all_of.map { |item| build_schema_or_ref(item) }
|
|
107
|
+
result = { "allOf" => items }
|
|
108
|
+
apply_composition_attributes(result)
|
|
109
|
+
result
|
|
110
|
+
end
|
|
90
111
|
|
|
91
|
-
|
|
112
|
+
def apply_composition_attributes(result)
|
|
113
|
+
result["type"] = @schema.type if @schema.type
|
|
114
|
+
result["format"] = @schema.format if @schema.format
|
|
92
115
|
result["description"] = @schema.description.to_s if @schema.description
|
|
93
|
-
result
|
|
116
|
+
result["default"] = @schema.default unless @schema.default.nil?
|
|
117
|
+
result["enum"] = normalize_enum(@schema.enum, @schema.type) if @schema.enum
|
|
118
|
+
apply_constraints(result)
|
|
119
|
+
apply_extensions(result)
|
|
94
120
|
end
|
|
95
121
|
|
|
96
122
|
def build_properties(properties)
|
|
@@ -102,16 +128,22 @@ module GrapeOAS
|
|
|
102
128
|
end
|
|
103
129
|
end
|
|
104
130
|
|
|
105
|
-
def build_schema_or_ref(schema)
|
|
131
|
+
def build_schema_or_ref(schema, include_metadata: true)
|
|
106
132
|
if schema.respond_to?(:canonical_name) && schema.canonical_name
|
|
107
133
|
@ref_tracker << schema.canonical_name if @ref_tracker
|
|
108
134
|
ref_name = schema.canonical_name.gsub("::", "_")
|
|
109
135
|
ref_hash = { "$ref" => "#/definitions/#{ref_name}" }
|
|
136
|
+
return ref_hash unless include_metadata
|
|
137
|
+
|
|
110
138
|
result = {}
|
|
111
139
|
if @nullable_strategy == Constants::NullableStrategy::EXTENSION && schema.respond_to?(:nullable) && schema.nullable
|
|
112
140
|
result["x-nullable"] = true
|
|
113
141
|
end
|
|
114
142
|
result["description"] = schema.description.to_s if schema.description
|
|
143
|
+
result["default"] = schema.default unless schema.default.nil?
|
|
144
|
+
result["enum"] = normalize_enum(schema.enum, schema.type) if schema.enum
|
|
145
|
+
apply_constraints(result, schema)
|
|
146
|
+
result.merge!(schema.extensions) if schema.extensions
|
|
115
147
|
if result.empty?
|
|
116
148
|
ref_hash
|
|
117
149
|
else
|
|
@@ -119,7 +151,9 @@ module GrapeOAS
|
|
|
119
151
|
result
|
|
120
152
|
end
|
|
121
153
|
else
|
|
122
|
-
Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
154
|
+
built = Schema.new(schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
155
|
+
built.delete("description") unless include_metadata
|
|
156
|
+
built
|
|
123
157
|
end
|
|
124
158
|
end
|
|
125
159
|
|
|
@@ -12,14 +12,17 @@ module GrapeOAS
|
|
|
12
12
|
|
|
13
13
|
def build
|
|
14
14
|
Array(@op.parameters).map do |param|
|
|
15
|
+
schema_hash = Schema.new(param.schema, @ref_tracker, nullable_strategy: @nullable_strategy).build
|
|
16
|
+
schema_description = schema_hash.delete("description")
|
|
17
|
+
description = param.description || schema_description
|
|
15
18
|
{
|
|
16
19
|
"name" => param.name,
|
|
17
20
|
"in" => param.location,
|
|
18
21
|
"required" => param.required,
|
|
19
|
-
"description" =>
|
|
22
|
+
"description" => description,
|
|
20
23
|
"style" => param.style,
|
|
21
24
|
"explode" => param.explode,
|
|
22
|
-
"schema" =>
|
|
25
|
+
"schema" => schema_hash
|
|
23
26
|
}.compact
|
|
24
27
|
end.presence
|
|
25
28
|
end
|