grape-oas 1.3.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 +16 -0
- data/lib/grape_oas/api_model/api.rb +4 -0
- data/lib/grape_oas/api_model/schema.rb +1 -1
- 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 +4 -0
- data/lib/grape_oas/api_model_builders/request_params_support/schema_enhancer.rb +11 -0
- data/lib/grape_oas/constants.rb +9 -0
- data/lib/grape_oas/exporter/oas2/parameter.rb +1 -0
- data/lib/grape_oas/exporter/oas2/schema.rb +36 -17
- data/lib/grape_oas/exporter/oas3/parameter.rb +5 -2
- data/lib/grape_oas/exporter/oas3/schema.rb +48 -40
- data/lib/grape_oas/introspectors/entity_introspector.rb +5 -1
- data/lib/grape_oas/introspectors/entity_introspector_support/inheritance_builder.rb +1 -1
- data/lib/grape_oas/introspectors/entity_introspector_support.rb +24 -0
- data/lib/grape_oas/type_resolvers/array_resolver.rb +3 -5
- data/lib/grape_oas/version.rb +1 -1
- metadata +2 -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,22 @@ 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).
|
|
8
24
|
|
|
9
25
|
## [1.3.0] - 2026-03-27
|
|
10
26
|
|
|
@@ -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
|
|
@@ -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,10 @@ 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)
|
|
36
40
|
return build_primitive_array_schema(doc_type, raw_type) if doc[:is_array]
|
|
37
41
|
return build_entity_schema(doc_type) if grape_entity?(doc_type)
|
|
38
42
|
return build_entity_schema(raw_type) if grape_entity?(raw_type)
|
|
@@ -21,6 +21,7 @@ module GrapeOAS
|
|
|
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,6 +46,16 @@ 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=)
|
data/lib/grape_oas/constants.rb
CHANGED
|
@@ -51,6 +51,15 @@ module GrapeOAS
|
|
|
51
51
|
# Prevents OOM on wide string ranges (e.g. "a".."zzzzzz").
|
|
52
52
|
MAX_ENUM_RANGE_SIZE = 100
|
|
53
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
|
+
|
|
54
63
|
# Default values for OpenAPI spec when not provided by user
|
|
55
64
|
module Defaults
|
|
56
65
|
LICENSE_NAME = "Proprietary"
|
|
@@ -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)
|
|
@@ -52,19 +52,20 @@ module GrapeOAS
|
|
|
52
52
|
schema_hash["example"] = @schema.examples if @schema.examples
|
|
53
53
|
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
54
54
|
schema_hash["discriminator"] = @schema.discriminator if @schema.discriminator
|
|
55
|
+
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
55
56
|
schema_hash
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
def apply_constraints(schema_hash)
|
|
59
|
-
schema_hash["
|
|
60
|
-
schema_hash["
|
|
61
|
-
schema_hash["
|
|
62
|
-
schema_hash["
|
|
63
|
-
schema_hash["
|
|
64
|
-
schema_hash["
|
|
65
|
-
schema_hash["
|
|
66
|
-
schema_hash["minItems"] =
|
|
67
|
-
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?
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def apply_extensions(schema_hash)
|
|
@@ -80,28 +81,42 @@ module GrapeOAS
|
|
|
80
81
|
|
|
81
82
|
# Build schema from oneOf/anyOf by using first type (OAS2 doesn't support these)
|
|
82
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.
|
|
83
89
|
def build_first_of_schema(composition_type)
|
|
84
90
|
schemas = @schema.send(composition_type)
|
|
85
91
|
first_schema = schemas.first
|
|
86
92
|
return {} unless first_schema
|
|
87
93
|
|
|
88
|
-
# Build the first schema as the fallback
|
|
89
94
|
result = build_schema_or_ref(first_schema)
|
|
90
95
|
result["description"] = @schema.description.to_s if @schema.description
|
|
91
96
|
apply_extensions(result)
|
|
97
|
+
if result.key?("$ref") && result.size > 1
|
|
98
|
+
ref = { "$ref" => result.delete("$ref") }
|
|
99
|
+
result["allOf"] = [ref]
|
|
100
|
+
end
|
|
92
101
|
result
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
# Build allOf schema for inheritance
|
|
96
105
|
def build_all_of_schema
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
def apply_composition_attributes(result)
|
|
113
|
+
result["type"] = @schema.type if @schema.type
|
|
114
|
+
result["format"] = @schema.format if @schema.format
|
|
102
115
|
result["description"] = @schema.description.to_s if @schema.description
|
|
103
|
-
result["
|
|
104
|
-
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)
|
|
105
120
|
end
|
|
106
121
|
|
|
107
122
|
def build_properties(properties)
|
|
@@ -125,6 +140,10 @@ module GrapeOAS
|
|
|
125
140
|
result["x-nullable"] = true
|
|
126
141
|
end
|
|
127
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
|
|
128
147
|
if result.empty?
|
|
129
148
|
ref_hash
|
|
130
149
|
else
|
|
@@ -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
|
|
@@ -51,6 +51,7 @@ module GrapeOAS
|
|
|
51
51
|
end
|
|
52
52
|
schema_hash["required"] = @schema.required if @schema.required && !@schema.required.empty?
|
|
53
53
|
schema_hash["enum"] = normalize_enum(@schema.enum, schema_hash["type"]) if @schema.enum
|
|
54
|
+
schema_hash["default"] = @schema.default unless @schema.default.nil?
|
|
54
55
|
schema_hash
|
|
55
56
|
end
|
|
56
57
|
|
|
@@ -72,34 +73,28 @@ module GrapeOAS
|
|
|
72
73
|
schema_hash["discriminator"] = build_discriminator if @schema.discriminator
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
def apply_all_constraints(schema_hash)
|
|
76
|
-
apply_numeric_constraints(schema_hash)
|
|
77
|
-
apply_string_constraints(schema_hash)
|
|
78
|
-
apply_array_constraints(schema_hash)
|
|
76
|
+
def apply_all_constraints(schema_hash, schema = @schema)
|
|
77
|
+
apply_numeric_constraints(schema_hash, schema)
|
|
78
|
+
apply_string_constraints(schema_hash, schema)
|
|
79
|
+
apply_array_constraints(schema_hash, schema)
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
private
|
|
82
83
|
|
|
83
84
|
# Build allOf schema for inheritance
|
|
84
85
|
def build_all_of_schema
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
result = { "allOf" => all_of_items }
|
|
90
|
-
result["description"] = @schema.description.to_s if @schema.description
|
|
86
|
+
items = @schema.all_of.map { |item| build_schema_or_ref(item) }
|
|
87
|
+
result = { "allOf" => items }
|
|
88
|
+
apply_composition_attributes(result)
|
|
91
89
|
apply_nullable(result)
|
|
92
90
|
result
|
|
93
91
|
end
|
|
94
92
|
|
|
95
93
|
# Build oneOf schema for polymorphism
|
|
96
94
|
def build_one_of_schema
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
result = { "oneOf" => one_of_items }
|
|
102
|
-
result["description"] = @schema.description.to_s if @schema.description
|
|
95
|
+
items = @schema.one_of.map { |item| build_schema_or_ref(item) }
|
|
96
|
+
result = { "oneOf" => items }
|
|
97
|
+
apply_composition_attributes(result)
|
|
103
98
|
result["discriminator"] = build_discriminator if @schema.discriminator
|
|
104
99
|
apply_nullable(result)
|
|
105
100
|
result
|
|
@@ -107,17 +102,25 @@ module GrapeOAS
|
|
|
107
102
|
|
|
108
103
|
# Build anyOf schema for polymorphism
|
|
109
104
|
def build_any_of_schema
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
result = { "anyOf" => any_of_items }
|
|
115
|
-
result["description"] = @schema.description.to_s if @schema.description
|
|
105
|
+
items = @schema.any_of.map { |item| build_schema_or_ref(item) }
|
|
106
|
+
result = { "anyOf" => items }
|
|
107
|
+
apply_composition_attributes(result)
|
|
116
108
|
result["discriminator"] = build_discriminator if @schema.discriminator
|
|
117
109
|
apply_nullable(result)
|
|
118
110
|
result
|
|
119
111
|
end
|
|
120
112
|
|
|
113
|
+
def apply_composition_attributes(result)
|
|
114
|
+
result["type"] = nullable_type if @schema.type
|
|
115
|
+
result["format"] = @schema.format if @schema.format
|
|
116
|
+
result["description"] = @schema.description.to_s if @schema.description
|
|
117
|
+
result["default"] = @schema.default unless @schema.default.nil?
|
|
118
|
+
result["enum"] = normalize_enum(@schema.enum, result["type"]) if @schema.enum
|
|
119
|
+
sanitize_enum_against_type(result)
|
|
120
|
+
apply_all_constraints(result)
|
|
121
|
+
result.merge!(@schema.extensions) if @schema.extensions
|
|
122
|
+
end
|
|
123
|
+
|
|
121
124
|
# Build OAS3 discriminator object
|
|
122
125
|
def build_discriminator
|
|
123
126
|
return nil unless @schema.discriminator
|
|
@@ -174,6 +177,11 @@ module GrapeOAS
|
|
|
174
177
|
|
|
175
178
|
result = {}
|
|
176
179
|
result["description"] = schema.description.to_s if schema.description
|
|
180
|
+
result["default"] = schema.default unless schema.default.nil?
|
|
181
|
+
result["enum"] = normalize_enum(schema.enum, schema.type) if schema.enum
|
|
182
|
+
sanitize_enum_against_type(result, type: schema.type)
|
|
183
|
+
apply_all_constraints(result, schema)
|
|
184
|
+
result.merge!(schema.extensions) if schema.extensions
|
|
177
185
|
apply_nullable_to_ref(result, schema)
|
|
178
186
|
if result.empty?
|
|
179
187
|
ref_hash
|
|
@@ -224,40 +232,40 @@ module GrapeOAS
|
|
|
224
232
|
result
|
|
225
233
|
end
|
|
226
234
|
|
|
227
|
-
def apply_numeric_constraints(hash)
|
|
228
|
-
hash["minimum"] =
|
|
229
|
-
hash["maximum"] =
|
|
235
|
+
def apply_numeric_constraints(hash, schema = @schema)
|
|
236
|
+
hash["minimum"] = schema.minimum unless schema.minimum.nil?
|
|
237
|
+
hash["maximum"] = schema.maximum unless schema.maximum.nil?
|
|
230
238
|
|
|
231
239
|
if @nullable_strategy == Constants::NullableStrategy::TYPE_ARRAY
|
|
232
|
-
if
|
|
233
|
-
hash["exclusiveMinimum"] =
|
|
240
|
+
if schema.exclusive_minimum && !schema.minimum.nil?
|
|
241
|
+
hash["exclusiveMinimum"] = schema.minimum
|
|
234
242
|
hash.delete("minimum")
|
|
235
243
|
end
|
|
236
|
-
if
|
|
237
|
-
hash["exclusiveMaximum"] =
|
|
244
|
+
if schema.exclusive_maximum && !schema.maximum.nil?
|
|
245
|
+
hash["exclusiveMaximum"] = schema.maximum
|
|
238
246
|
hash.delete("maximum")
|
|
239
247
|
end
|
|
240
248
|
else
|
|
241
|
-
hash["exclusiveMinimum"] =
|
|
242
|
-
hash["exclusiveMaximum"] =
|
|
249
|
+
hash["exclusiveMinimum"] = schema.exclusive_minimum if schema.exclusive_minimum
|
|
250
|
+
hash["exclusiveMaximum"] = schema.exclusive_maximum if schema.exclusive_maximum
|
|
243
251
|
end
|
|
244
252
|
end
|
|
245
253
|
|
|
246
|
-
def apply_string_constraints(hash)
|
|
247
|
-
hash["minLength"] =
|
|
248
|
-
hash["maxLength"] =
|
|
249
|
-
hash["pattern"] =
|
|
254
|
+
def apply_string_constraints(hash, schema = @schema)
|
|
255
|
+
hash["minLength"] = schema.min_length unless schema.min_length.nil?
|
|
256
|
+
hash["maxLength"] = schema.max_length unless schema.max_length.nil?
|
|
257
|
+
hash["pattern"] = schema.pattern if schema.pattern
|
|
250
258
|
end
|
|
251
259
|
|
|
252
|
-
def apply_array_constraints(hash)
|
|
253
|
-
hash["minItems"] =
|
|
254
|
-
hash["maxItems"] =
|
|
260
|
+
def apply_array_constraints(hash, schema = @schema)
|
|
261
|
+
hash["minItems"] = schema.min_items unless schema.min_items.nil?
|
|
262
|
+
hash["maxItems"] = schema.max_items unless schema.max_items.nil?
|
|
255
263
|
end
|
|
256
264
|
|
|
257
265
|
# Ensure enum values match the declared type; drop enum if incompatible to avoid invalid specs
|
|
258
|
-
def sanitize_enum_against_type(hash)
|
|
266
|
+
def sanitize_enum_against_type(hash, type: nil)
|
|
259
267
|
enum_vals = hash["enum"]
|
|
260
|
-
type_val = hash["type"]
|
|
268
|
+
type_val = type || hash["type"]
|
|
261
269
|
return unless enum_vals && type_val
|
|
262
270
|
|
|
263
271
|
base_type = if type_val.is_a?(Array)
|
|
@@ -88,12 +88,16 @@ module GrapeOAS
|
|
|
88
88
|
def initialize_or_reuse_schema
|
|
89
89
|
@registry[@entity_class] ||= ApiModel::Schema.new(
|
|
90
90
|
type: Constants::SchemaTypes::OBJECT,
|
|
91
|
-
canonical_name:
|
|
91
|
+
canonical_name: resolve_canonical_name,
|
|
92
92
|
description: nil,
|
|
93
93
|
nullable: nil,
|
|
94
94
|
)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
def resolve_canonical_name
|
|
98
|
+
EntityIntrospectorSupport.resolve_canonical_name(@entity_class)
|
|
99
|
+
end
|
|
100
|
+
|
|
97
101
|
def populate_schema(schema)
|
|
98
102
|
doc = entity_doc
|
|
99
103
|
apply_schema_metadata(schema, doc)
|
|
@@ -41,7 +41,7 @@ module GrapeOAS
|
|
|
41
41
|
|
|
42
42
|
# Create allOf schema with ref to parent + child properties
|
|
43
43
|
schema = ApiModel::Schema.new(
|
|
44
|
-
canonical_name: @entity_class
|
|
44
|
+
canonical_name: EntityIntrospectorSupport.resolve_canonical_name(@entity_class),
|
|
45
45
|
all_of: [parent_schema, child_schema],
|
|
46
46
|
)
|
|
47
47
|
|
|
@@ -19,6 +19,30 @@ module GrapeOAS
|
|
|
19
19
|
[]
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Resolves the canonical name for an entity class, preferring entity_name
|
|
23
|
+
# when defined on the class itself (via def self. or extend) and non-blank,
|
|
24
|
+
# falling back to the Ruby class name. Inherited entity_name is ignored to
|
|
25
|
+
# avoid collisions between parent and child schemas.
|
|
26
|
+
def self.resolve_canonical_name(entity_class)
|
|
27
|
+
if defines_own_entity_name?(entity_class)
|
|
28
|
+
name = entity_class.entity_name
|
|
29
|
+
name.is_a?(String) && !name.strip.empty? ? name : entity_class.name
|
|
30
|
+
else
|
|
31
|
+
entity_class.name
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.defines_own_entity_name?(entity_class)
|
|
36
|
+
return false unless entity_class.respond_to?(:entity_name)
|
|
37
|
+
|
|
38
|
+
parent = find_parent_entity(entity_class)
|
|
39
|
+
return true if parent.nil? || !parent.respond_to?(:entity_name)
|
|
40
|
+
|
|
41
|
+
entity_class.method(:entity_name).owner !=
|
|
42
|
+
parent.method(:entity_name).owner
|
|
43
|
+
end
|
|
44
|
+
private_class_method :defines_own_entity_name?
|
|
45
|
+
|
|
22
46
|
# Finds the parent entity class if one exists in the Grape::Entity hierarchy.
|
|
23
47
|
def self.find_parent_entity(entity_class)
|
|
24
48
|
return nil unless defined?(Grape::Entity)
|
|
@@ -20,15 +20,13 @@ module GrapeOAS
|
|
|
20
20
|
class ArrayResolver
|
|
21
21
|
extend Base
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
# Intentionally narrow: avoid treating multi-type strings like "[String, Integer]" as arrays.
|
|
25
|
-
ARRAY_PATTERN = /\A\[(?<inner>(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*)\]\z/
|
|
23
|
+
TYPED_ARRAY_PATTERN = Constants::TypePatterns::TYPED_ARRAY
|
|
26
24
|
|
|
27
25
|
class << self
|
|
28
26
|
def handles?(type)
|
|
29
27
|
return false unless type.is_a?(String)
|
|
30
28
|
|
|
31
|
-
type.match?(
|
|
29
|
+
type.match?(TYPED_ARRAY_PATTERN)
|
|
32
30
|
end
|
|
33
31
|
|
|
34
32
|
def build_schema(type)
|
|
@@ -53,7 +51,7 @@ module GrapeOAS
|
|
|
53
51
|
private
|
|
54
52
|
|
|
55
53
|
def extract_inner_type(type)
|
|
56
|
-
match = type.match(
|
|
54
|
+
match = type.match(TYPED_ARRAY_PATTERN)
|
|
57
55
|
match[:inner] if match
|
|
58
56
|
end
|
|
59
57
|
|
data/lib/grape_oas/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.4.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-
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: grape
|