gitlab-grape-openapi 0.2.2 → 0.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/README.md +23 -0
- data/lib/gitlab/grape_openapi/converters/entity_converter.rb +76 -7
- data/lib/gitlab/grape_openapi/converters/operation_converter.rb +21 -18
- data/lib/gitlab/grape_openapi/converters/path_converter.rb +48 -4
- data/lib/gitlab/grape_openapi/converters/response_converter.rb +19 -13
- data/lib/gitlab/grape_openapi/grape_compat.rb +50 -0
- data/lib/gitlab/grape_openapi/models/request_body/parameter_schema.rb +1 -6
- data/lib/gitlab/grape_openapi/models/schema.rb +6 -1
- data/lib/gitlab/grape_openapi/version.rb +1 -1
- data/lib/gitlab-grape-openapi.rb +5 -0
- metadata +13 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79aec4f1d4657df18a7ccbcf6b689c9f00f8f0b5c940d508da34b05112aa8432
|
|
4
|
+
data.tar.gz: 275e4524ae0d61929af41ca0540ff080f49790f0a2cbb1308f4a0e6d6b79f75f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd0dfaa149fb09c6a61bd2b8c9256d506e6bc62da716faed13e9b0c2f48932efed53b69315c330d1f91f8bf550bb5728fc284865748966b39f15b4a7d7598c67
|
|
7
|
+
data.tar.gz: c937af8a3e5f1a4f80ae412e7d444f1b11c7d30268b5c64dd9441c3e2b6125db1b325387effa1ec917821211ee12ca7e140dd1f9e659bcab039c86d99c3e3db4
|
data/README.md
CHANGED
|
@@ -194,6 +194,29 @@ Generator
|
|
|
194
194
|
- **RequestBodyRegistry** - Tracks request body schemas
|
|
195
195
|
- **TagRegistry** - Tracks API tags
|
|
196
196
|
|
|
197
|
+
### Optional path segments
|
|
198
|
+
|
|
199
|
+
Grape lets a route mark a path segment as optional with parentheses, e.g.
|
|
200
|
+
`resource '/settings_sync(/:settings_context_hash)'`. Such a route genuinely
|
|
201
|
+
accepts requests both with and without the segment, but a single OpenAPI path
|
|
202
|
+
key cannot express that optionality — an OpenAPI path parameter is always
|
|
203
|
+
`required: true`.
|
|
204
|
+
|
|
205
|
+
When a route contains **one** optional segment that holds a named parameter,
|
|
206
|
+
`PathConverter` fans it out into two OpenAPI paths:
|
|
207
|
+
|
|
208
|
+
- a **collapsed** path with the segment and its parameter removed entirely
|
|
209
|
+
(the parameter is absent from `parameters`, not marked optional), and
|
|
210
|
+
- an **expanded** path where the parameter is present as a required
|
|
211
|
+
`in: path` parameter.
|
|
212
|
+
|
|
213
|
+
Each variant gets its own `operationId`, so both remain unique. Optional
|
|
214
|
+
segments without a named parameter (e.g. `(-/)`) are left unchanged.
|
|
215
|
+
|
|
216
|
+
Generating the combinatorial set of paths for routes with more than one
|
|
217
|
+
param-bearing optional segment is **not** supported: such a route raises
|
|
218
|
+
`Gitlab::GrapeOpenapi::MultipleOptionalSegmentsError`.
|
|
219
|
+
|
|
197
220
|
## Development
|
|
198
221
|
|
|
199
222
|
```bash
|
|
@@ -53,23 +53,77 @@ module Gitlab
|
|
|
53
53
|
def build_schema
|
|
54
54
|
Models::Schema.new.tap do |schema|
|
|
55
55
|
schema.type = OBJECT_TYPE
|
|
56
|
-
schema.properties =
|
|
56
|
+
schema.properties = build_exposures_properties(root_exposures)
|
|
57
|
+
|
|
58
|
+
required = required_exposure_keys(root_exposures)
|
|
59
|
+
schema.required = required if required.any?
|
|
57
60
|
end
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
def
|
|
61
|
-
|
|
63
|
+
def required_exposure_keys(exposures)
|
|
64
|
+
exposures.flat_map do |exposure|
|
|
65
|
+
if inlineable_merge_exposure?(exposure)
|
|
66
|
+
# Merged entity properties become the parent's, so their
|
|
67
|
+
# `required` entries do too.
|
|
68
|
+
build_or_fetch_nested_schema(nested_entity_class(exposure))&.required || []
|
|
69
|
+
elsif block_exposure?(exposure) && exposure.for_merge
|
|
70
|
+
required_exposure_keys(exposure.nested_exposures)
|
|
71
|
+
elsif exposure_documentation(exposure)[:required]
|
|
72
|
+
[exposure.key.to_s]
|
|
73
|
+
else
|
|
74
|
+
[]
|
|
75
|
+
end
|
|
76
|
+
end.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_exposures_properties(exposures)
|
|
80
|
+
exposures.each_with_object({}) do |exposure, properties|
|
|
62
81
|
if inlineable_merge_exposure?(exposure)
|
|
63
82
|
# `merge: true` flattens the nested entity's exposures into the
|
|
64
83
|
# parent at runtime. Inline its properties instead of emitting a
|
|
65
84
|
# `$ref`, so the generated schema reflects the actual response.
|
|
66
85
|
inline_merged_properties!(properties, exposure)
|
|
86
|
+
elsif block_exposure?(exposure)
|
|
87
|
+
add_block_exposure_properties!(properties, exposure)
|
|
67
88
|
else
|
|
68
89
|
properties[exposure.key] = build_property(exposure)
|
|
69
90
|
end
|
|
70
91
|
end
|
|
71
92
|
end
|
|
72
93
|
|
|
94
|
+
# Grouping blocks (`expose :foo do ... end`) are NestingExposure
|
|
95
|
+
# instances. Blocks that compute a value (`expose(:foo) { |obj, _| }`)
|
|
96
|
+
# are BlockExposure and fall through to the default handling.
|
|
97
|
+
def block_exposure?(exposure)
|
|
98
|
+
exposure.is_a?(Grape::Entity::Exposure::NestingExposure)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def add_block_exposure_properties!(properties, exposure)
|
|
102
|
+
if exposure.for_merge
|
|
103
|
+
# A `merge: true` block merges its nested hash into the parent at
|
|
104
|
+
# runtime, so its key never appears in the response.
|
|
105
|
+
properties.merge!(build_exposures_properties(exposure.nested_exposures))
|
|
106
|
+
else
|
|
107
|
+
properties[exposure.key] = build_block_property(exposure)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_block_property(exposure)
|
|
112
|
+
documentation = exposure_documentation(exposure)
|
|
113
|
+
nested_properties = build_exposures_properties(exposure.nested_exposures)
|
|
114
|
+
object_schema = { type: OBJECT_TYPE }
|
|
115
|
+
object_schema[:properties] = nested_properties if nested_properties.any?
|
|
116
|
+
|
|
117
|
+
nested_required = required_exposure_keys(exposure.nested_exposures)
|
|
118
|
+
object_schema[:required] = nested_required if nested_required.any?
|
|
119
|
+
|
|
120
|
+
if documentation[:is_array]
|
|
121
|
+
{ type: ARRAY_TYPE, description: documentation[:desc], items: object_schema }.compact
|
|
122
|
+
else
|
|
123
|
+
object_schema.merge(description: documentation[:desc]).compact
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
73
127
|
def inlineable_merge_exposure?(exposure)
|
|
74
128
|
return false unless exposure.for_merge
|
|
75
129
|
|
|
@@ -137,11 +191,23 @@ module Gitlab
|
|
|
137
191
|
type: TypeResolver.resolve_type(actual_type) || DEFAULT_TYPE,
|
|
138
192
|
description: documentation[:desc],
|
|
139
193
|
format: TypeResolver.resolve_format(documentation[:format], actual_type),
|
|
194
|
+
enum: enum_values(documentation),
|
|
140
195
|
default: default_value,
|
|
141
196
|
example: documentation[:example]
|
|
142
197
|
}
|
|
143
198
|
end
|
|
144
199
|
|
|
200
|
+
# `values:` may also be a Proc or a Range (runtime validation
|
|
201
|
+
# constructs); only a literal Array translates to a fixed OpenAPI
|
|
202
|
+
# enum. Mirrors `ParameterConverter#build_enum_schema`. Symbols are
|
|
203
|
+
# stringified because the response contains strings, not symbols.
|
|
204
|
+
def enum_values(documentation)
|
|
205
|
+
values = documentation[:values]
|
|
206
|
+
return unless values.is_a?(Array) && values.any?
|
|
207
|
+
|
|
208
|
+
values.map { |value| value.is_a?(Symbol) ? value.to_s : value }
|
|
209
|
+
end
|
|
210
|
+
|
|
145
211
|
def build_type_schema(type, documentation)
|
|
146
212
|
schema = { type: TypeResolver.resolve_type(type) || DEFAULT_TYPE }
|
|
147
213
|
|
|
@@ -197,10 +263,13 @@ module Gitlab
|
|
|
197
263
|
def build_primitive_items(property, item_type)
|
|
198
264
|
items = { type: item_type }
|
|
199
265
|
|
|
200
|
-
# Move format to items if present
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
property[
|
|
266
|
+
# Move format and enum to items if present; they constrain each
|
|
267
|
+
# element, not the array itself
|
|
268
|
+
[:format, :enum].each do |key|
|
|
269
|
+
next unless property[key]
|
|
270
|
+
|
|
271
|
+
items[key] = property[key]
|
|
272
|
+
property[key] = nil
|
|
204
273
|
end
|
|
205
274
|
|
|
206
275
|
items
|
|
@@ -8,20 +8,29 @@ module Gitlab
|
|
|
8
8
|
|
|
9
9
|
DASH_SEGMENT = 'Dash'
|
|
10
10
|
|
|
11
|
-
def self.convert(
|
|
11
|
+
def self.convert(
|
|
12
|
+
route, schema_registry, request_body_registry, inherited_path_params: {},
|
|
13
|
+
path_override: nil, removed_params: [])
|
|
12
14
|
new(
|
|
13
15
|
route,
|
|
14
16
|
schema_registry,
|
|
15
17
|
request_body_registry,
|
|
16
|
-
inherited_path_params: inherited_path_params
|
|
18
|
+
inherited_path_params: inherited_path_params,
|
|
19
|
+
path_override: path_override,
|
|
20
|
+
removed_params: removed_params
|
|
17
21
|
).convert
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
def initialize(
|
|
24
|
+
def initialize(
|
|
25
|
+
route, schema_registry, request_body_registry, inherited_path_params: {},
|
|
26
|
+
path_override: nil, removed_params: [])
|
|
21
27
|
@route = route
|
|
22
28
|
@schema_registry = schema_registry
|
|
23
29
|
@request_body_registry = request_body_registry
|
|
24
30
|
@inherited_path_params = inherited_path_params
|
|
31
|
+
# @path_override and @removed_params are set together when optional-path-segment variants exist
|
|
32
|
+
@path_override = path_override
|
|
33
|
+
@removed_params = removed_params
|
|
25
34
|
@config = Gitlab::GrapeOpenapi.configuration
|
|
26
35
|
@options = route.options
|
|
27
36
|
@pattern = route.pattern
|
|
@@ -46,7 +55,7 @@ module Gitlab
|
|
|
46
55
|
private
|
|
47
56
|
|
|
48
57
|
attr_reader :config, :route, :options, :pattern, :endpoint, :schema_registry, :request_body_registry,
|
|
49
|
-
:inherited_path_params
|
|
58
|
+
:inherited_path_params, :path_override, :removed_params
|
|
50
59
|
|
|
51
60
|
def route_method
|
|
52
61
|
@route.request_method
|
|
@@ -78,7 +87,11 @@ module Gitlab
|
|
|
78
87
|
end
|
|
79
88
|
end
|
|
80
89
|
|
|
81
|
-
inject_missing_path_parameters(params)
|
|
90
|
+
params = inject_missing_path_parameters(params)
|
|
91
|
+
return params if removed_params.empty?
|
|
92
|
+
|
|
93
|
+
removed = removed_params.map(&:to_s)
|
|
94
|
+
params.reject { |param| removed.include?(param.name.to_s) }
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
def inject_missing_path_parameters(params)
|
|
@@ -201,10 +214,7 @@ module Gitlab
|
|
|
201
214
|
end
|
|
202
215
|
|
|
203
216
|
def normalized_path
|
|
204
|
-
@normalized_path ||=
|
|
205
|
-
path = normalize_path_pattern
|
|
206
|
-
path.gsub('{version}', config.api_version)
|
|
207
|
-
end
|
|
217
|
+
@normalized_path ||= path_override || normalize_path_pattern.gsub('{version}', config.api_version)
|
|
208
218
|
end
|
|
209
219
|
|
|
210
220
|
def normalize_path_pattern
|
|
@@ -228,17 +238,10 @@ module Gitlab
|
|
|
228
238
|
# Looks something like:
|
|
229
239
|
# [{:attributes=>[:version_prefix],
|
|
230
240
|
# :options=>/^[\d+.]+/,
|
|
231
|
-
# :
|
|
232
|
-
# :params_scope=>#<Grape::Validations::ParamsScope:0x000000016dc35820
|
|
233
|
-
# :opts=>{:allow_blank=>nil, :fail_fast=>false},
|
|
241
|
+
# :opts=>{:fail_fast=>false},
|
|
234
242
|
# :validator_class=>Grape::Validations::Validators::RegexpValidator}]
|
|
235
243
|
def validations_for(attribute)
|
|
236
|
-
route
|
|
237
|
-
.app
|
|
238
|
-
.inheritable_setting
|
|
239
|
-
.namespace_stackable
|
|
240
|
-
.new_values[:validations]
|
|
241
|
-
&.select { |v| v[:attributes].include?(attribute) }
|
|
244
|
+
GrapeCompat.validations_for(route, attribute)
|
|
242
245
|
end
|
|
243
246
|
|
|
244
247
|
def extract_request_body
|
|
@@ -10,6 +10,12 @@ module Gitlab
|
|
|
10
10
|
# handles all three shapes (Class, Hash with :model, Array of those).
|
|
11
11
|
RESPONSE_DECLARATIONS = %i[entity success].freeze
|
|
12
12
|
|
|
13
|
+
# A Grape optional path segment: a parenthesised group, e.g. `(/:id)`.
|
|
14
|
+
OPTIONAL_SEGMENT = /\(([^()]*)\)/
|
|
15
|
+
|
|
16
|
+
# Optional path segment variants are each represented in the OpenAPI spec as a unique path.
|
|
17
|
+
PathVariant = Struct.new(:key, :path_override, :removed_params)
|
|
18
|
+
|
|
13
19
|
def self.convert(routes, schema_registry, request_body_registry)
|
|
14
20
|
new(routes, schema_registry, request_body_registry).convert
|
|
15
21
|
end
|
|
@@ -25,8 +31,12 @@ module Gitlab
|
|
|
25
31
|
def convert
|
|
26
32
|
register_inherited_path_params
|
|
27
33
|
|
|
28
|
-
paths =
|
|
29
|
-
|
|
34
|
+
paths = {}
|
|
35
|
+
grouped_routes.each do |path_key, routes_for_path|
|
|
36
|
+
path_variants(path_key, routes_for_path.first).each do |variant|
|
|
37
|
+
paths[variant.key] ||= {}
|
|
38
|
+
paths[variant.key].merge!(build_path_item(routes_for_path, variant))
|
|
39
|
+
end
|
|
30
40
|
end
|
|
31
41
|
|
|
32
42
|
paths.reject { |_path, operations| operations.empty? }
|
|
@@ -100,7 +110,7 @@ module Gitlab
|
|
|
100
110
|
normalize_path(route).gsub(/\{[^}]+\}/, '{param}')
|
|
101
111
|
end
|
|
102
112
|
|
|
103
|
-
def build_path_item(routes_for_path)
|
|
113
|
+
def build_path_item(routes_for_path, variant)
|
|
104
114
|
path_item = Models::PathItem.new
|
|
105
115
|
|
|
106
116
|
routes_for_path.each do |route|
|
|
@@ -108,7 +118,9 @@ module Gitlab
|
|
|
108
118
|
route,
|
|
109
119
|
schema_registry,
|
|
110
120
|
request_body_registry,
|
|
111
|
-
inherited_path_params: inherited_path_params
|
|
121
|
+
inherited_path_params: inherited_path_params,
|
|
122
|
+
path_override: variant.path_override,
|
|
123
|
+
removed_params: variant.removed_params
|
|
112
124
|
)
|
|
113
125
|
method = extract_method(route)
|
|
114
126
|
path_item.add_operation(method, operation)
|
|
@@ -117,6 +129,38 @@ module Gitlab
|
|
|
117
129
|
path_item.to_h
|
|
118
130
|
end
|
|
119
131
|
|
|
132
|
+
# Expand a grouped path key into the OpenAPI paths it maps to.
|
|
133
|
+
#
|
|
134
|
+
# A single OpenAPI path key cannot express Grape's optional segments,
|
|
135
|
+
# so a route with one param-bearing optional segment fans out into two
|
|
136
|
+
# keys: a collapsed variant (segment and param removed entirely) and an
|
|
137
|
+
# expanded variant (param present, rendered as a required path param).
|
|
138
|
+
# Routes without such a segment yield a single, unchanged variant.
|
|
139
|
+
def path_variants(path_key, route)
|
|
140
|
+
param_groups = path_key.scan(OPTIONAL_SEGMENT).flatten.select { |inner| inner.match?(/\{\w+\}/) }
|
|
141
|
+
|
|
142
|
+
raise MultipleOptionalSegmentsError, multi_segment_message(route, path_key) if param_groups.length > 1
|
|
143
|
+
|
|
144
|
+
inner = param_groups.first
|
|
145
|
+
return [PathVariant.new(path_key, nil, [])] if inner.nil?
|
|
146
|
+
|
|
147
|
+
removed = inner.scan(/\{(\w+)\}/).flatten
|
|
148
|
+
collapsed = path_key.sub("(#{inner})", '').gsub(%r{//+}, '/').delete_suffix('/')
|
|
149
|
+
collapsed = '/' if collapsed.empty?
|
|
150
|
+
expanded = path_key.sub("(#{inner})", inner).gsub(%r{//+}, '/')
|
|
151
|
+
|
|
152
|
+
[
|
|
153
|
+
PathVariant.new(collapsed, collapsed, removed),
|
|
154
|
+
PathVariant.new(expanded, expanded, [])
|
|
155
|
+
]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def multi_segment_message(route, path_key)
|
|
159
|
+
"Route '#{extract_method(route)} #{path_key}' declares more than one optional path segment " \
|
|
160
|
+
"with a named parameter. gitlab-grape-openapi supports at most one optional segment per route " \
|
|
161
|
+
"and does not generate the combinatorial set of path variants."
|
|
162
|
+
end
|
|
163
|
+
|
|
120
164
|
def extract_method(route)
|
|
121
165
|
route.request_method
|
|
122
166
|
end
|
|
@@ -38,14 +38,15 @@ module Gitlab
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def process_class_entity(entity_class)
|
|
41
|
-
success_code = infer_success_code
|
|
42
41
|
if EntityConverter.grape_entity?(entity_class)
|
|
42
|
+
success_code = infer_success_code(with_body: true)
|
|
43
43
|
add_response_with_entity(
|
|
44
44
|
status_code: success_code,
|
|
45
45
|
description: http_status_text(success_code),
|
|
46
46
|
entity_class: entity_class
|
|
47
47
|
)
|
|
48
48
|
else
|
|
49
|
+
success_code = infer_success_code
|
|
49
50
|
add_simple_response(
|
|
50
51
|
status_code: success_code,
|
|
51
52
|
description: http_status_text(success_code)
|
|
@@ -54,20 +55,20 @@ module Gitlab
|
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
def process_hash_entity(entity_hash)
|
|
57
|
-
success_code = infer_success_code
|
|
58
|
-
|
|
59
58
|
if entity_hash[:model] && EntityConverter.grape_entity?(entity_hash[:model])
|
|
59
|
+
status_code = entity_hash[:code] || infer_success_code(with_body: true)
|
|
60
60
|
add_response_with_entity(
|
|
61
|
-
status_code:
|
|
62
|
-
description: entity_hash[:message] || http_status_text(
|
|
61
|
+
status_code: status_code,
|
|
62
|
+
description: entity_hash[:message] || http_status_text(status_code),
|
|
63
63
|
entity_class: entity_hash[:model],
|
|
64
64
|
example: entity_hash[:example],
|
|
65
65
|
examples: entity_hash[:examples]
|
|
66
66
|
)
|
|
67
67
|
else
|
|
68
|
+
status_code = entity_hash[:code] || infer_success_code
|
|
68
69
|
add_simple_response(
|
|
69
|
-
status_code:
|
|
70
|
-
description: entity_hash[:message] || http_status_text(
|
|
70
|
+
status_code: status_code,
|
|
71
|
+
description: entity_hash[:message] || http_status_text(status_code)
|
|
71
72
|
)
|
|
72
73
|
end
|
|
73
74
|
end
|
|
@@ -85,17 +86,19 @@ module Gitlab
|
|
|
85
86
|
|
|
86
87
|
def process_array_hash_item(definition)
|
|
87
88
|
if definition[:model] && EntityConverter.grape_entity?(definition[:model])
|
|
89
|
+
status_code = definition[:code] || infer_success_code(with_body: true)
|
|
88
90
|
add_response_with_entity(
|
|
89
|
-
status_code:
|
|
90
|
-
description: definition[:message] || http_status_text(
|
|
91
|
+
status_code: status_code,
|
|
92
|
+
description: definition[:message] || http_status_text(status_code),
|
|
91
93
|
entity_class: definition[:model],
|
|
92
94
|
example: definition[:example],
|
|
93
95
|
examples: definition[:examples]
|
|
94
96
|
)
|
|
95
97
|
else
|
|
98
|
+
status_code = definition[:code] || infer_success_code
|
|
96
99
|
add_simple_response(
|
|
97
|
-
status_code:
|
|
98
|
-
description: definition[:message] || http_status_text(
|
|
100
|
+
status_code: status_code,
|
|
101
|
+
description: definition[:message] || http_status_text(status_code)
|
|
99
102
|
)
|
|
100
103
|
end
|
|
101
104
|
end
|
|
@@ -148,10 +151,13 @@ module Gitlab
|
|
|
148
151
|
end
|
|
149
152
|
end
|
|
150
153
|
|
|
151
|
-
|
|
154
|
+
# 204 means "No Content", so it only applies when the response has no
|
|
155
|
+
# body to document. A DELETE route that renders an entity returns 200
|
|
156
|
+
# at runtime (Grape's default for a rendered body).
|
|
157
|
+
def infer_success_code(with_body: false)
|
|
152
158
|
case http_method
|
|
153
159
|
when 'POST' then 201
|
|
154
|
-
when 'DELETE' then 204
|
|
160
|
+
when 'DELETE' then with_body ? 200 : 204
|
|
155
161
|
else 200
|
|
156
162
|
end
|
|
157
163
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
# Isolates the Grape internals this gem reads so version differences live in
|
|
6
|
+
# one place.
|
|
7
|
+
#
|
|
8
|
+
# Grape < 3.2 stores each declared validation as a Hash; Grape >= 3.2 stores
|
|
9
|
+
# a frozen validator instance instead (ruby-grape/grape#2657). Both shapes
|
|
10
|
+
# are normalized to the Hash the converters expect.
|
|
11
|
+
module GrapeCompat
|
|
12
|
+
class << self
|
|
13
|
+
# Declared validations for a single attribute, newest scope only.
|
|
14
|
+
#
|
|
15
|
+
# Reads `new_values` rather than `[]` or `route[:saved_validations]` on
|
|
16
|
+
# purpose: those also include validations inherited from parent scopes,
|
|
17
|
+
# which would change the generated output.
|
|
18
|
+
def validations_for(route, attribute)
|
|
19
|
+
validations = route.app.inheritable_setting.namespace_stackable.new_values[:validations]
|
|
20
|
+
return unless validations
|
|
21
|
+
|
|
22
|
+
validations.filter_map do |validation|
|
|
23
|
+
normalized = normalize(validation)
|
|
24
|
+
normalized if normalized && normalized[:attributes].include?(attribute)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def normalize(validation)
|
|
31
|
+
return validation if validation.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
# Grape 3.2's ContractScopeValidator declares no attributes, so it maps
|
|
34
|
+
# to no parameter.
|
|
35
|
+
return unless validation.respond_to?(:attrs)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
attributes: validation.attrs,
|
|
39
|
+
validator_class: validation.class,
|
|
40
|
+
# Grape 3.2 exposes no public reader for the validator options, which
|
|
41
|
+
# carry regexp patterns and limits. Switch to the reader if one is
|
|
42
|
+
# added upstream.
|
|
43
|
+
options: validation.instance_variable_get(:@options),
|
|
44
|
+
opts: { fail_fast: validation.fail_fast? }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -226,12 +226,7 @@ module Gitlab
|
|
|
226
226
|
end
|
|
227
227
|
|
|
228
228
|
def validations_for(attribute)
|
|
229
|
-
route
|
|
230
|
-
.app
|
|
231
|
-
.inheritable_setting
|
|
232
|
-
.namespace_stackable
|
|
233
|
-
.new_values[:validations]
|
|
234
|
-
&.select { |v| v[:attributes].include?(attribute) }
|
|
229
|
+
GrapeCompat.validations_for(route, attribute)
|
|
235
230
|
end
|
|
236
231
|
|
|
237
232
|
def apply_allow_blank(schema)
|
|
@@ -5,7 +5,7 @@ module Gitlab
|
|
|
5
5
|
module Models
|
|
6
6
|
# https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schema-object
|
|
7
7
|
class Schema
|
|
8
|
-
attr_accessor :properties, :type
|
|
8
|
+
attr_accessor :properties, :required, :type
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
11
11
|
@properties = {}
|
|
@@ -31,6 +31,7 @@ module Gitlab
|
|
|
31
31
|
{}.tap do |hash|
|
|
32
32
|
add_type(hash)
|
|
33
33
|
add_properties(hash)
|
|
34
|
+
add_required(hash)
|
|
34
35
|
end
|
|
35
36
|
end
|
|
36
37
|
|
|
@@ -44,6 +45,10 @@ module Gitlab
|
|
|
44
45
|
hash[:properties] = properties.transform_values(&:to_h)
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def add_required(hash)
|
|
49
|
+
hash[:required] = required if required && !required.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
def description
|
|
48
53
|
@properties[:description]
|
|
49
54
|
end
|
data/lib/gitlab-grape-openapi.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "gitlab/grape_openapi/version"
|
|
4
|
+
require_relative "gitlab/grape_openapi/grape_compat"
|
|
4
5
|
require_relative "gitlab/grape_openapi/configuration"
|
|
5
6
|
require_relative "gitlab/grape_openapi/generator"
|
|
6
7
|
require_relative "gitlab/grape_openapi/schema_registry"
|
|
@@ -45,6 +46,10 @@ module Gitlab
|
|
|
45
46
|
module GrapeOpenapi
|
|
46
47
|
GenerationError = Class.new(StandardError)
|
|
47
48
|
|
|
49
|
+
# Raised when a route declares more than one optional path segment
|
|
50
|
+
# containing a named parameter (e.g. `/foo(/:a)/bar(/:b)`).
|
|
51
|
+
MultipleOptionalSegmentsError = Class.new(GenerationError)
|
|
52
|
+
|
|
48
53
|
class << self
|
|
49
54
|
attr_writer :configuration
|
|
50
55
|
|
metadata
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gitlab-grape-openapi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- group::api
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: grape
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
|
-
- - "
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.4'
|
|
20
|
+
- - "<"
|
|
18
21
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
22
|
+
version: '3.3'
|
|
20
23
|
type: :runtime
|
|
21
24
|
prerelease: false
|
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
26
|
requirements:
|
|
24
|
-
- - "
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '2.4'
|
|
30
|
+
- - "<"
|
|
25
31
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
32
|
+
version: '3.3'
|
|
27
33
|
- !ruby/object:Gem::Dependency
|
|
28
34
|
name: grape-entity
|
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -148,6 +154,7 @@ files:
|
|
|
148
154
|
- lib/gitlab/grape_openapi/converters/tag_converter.rb
|
|
149
155
|
- lib/gitlab/grape_openapi/converters/type_resolver.rb
|
|
150
156
|
- lib/gitlab/grape_openapi/generator.rb
|
|
157
|
+
- lib/gitlab/grape_openapi/grape_compat.rb
|
|
151
158
|
- lib/gitlab/grape_openapi/models/info.rb
|
|
152
159
|
- lib/gitlab/grape_openapi/models/operation.rb
|
|
153
160
|
- lib/gitlab/grape_openapi/models/parameter.rb
|