gitlab-grape-openapi 0.0.0 → 0.1.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/LICENSE +1 -1
- data/README.md +222 -4
- data/lib/gitlab/grape_openapi/concerns/fail_fast_annotatable.rb +23 -0
- data/lib/gitlab/grape_openapi/concerns/limit_resolver.rb +31 -0
- data/lib/gitlab/grape_openapi/concerns/regex_converter.rb +58 -0
- data/lib/gitlab/grape_openapi/concerns/serializable.rb +19 -0
- data/lib/gitlab/grape_openapi/configuration.rb +24 -0
- data/lib/gitlab/grape_openapi/converters/coercer_resolver.rb +74 -0
- data/lib/gitlab/grape_openapi/converters/entity_converter.rb +267 -0
- data/lib/gitlab/grape_openapi/converters/operation_converter.rb +250 -0
- data/lib/gitlab/grape_openapi/converters/parameter_converter.rb +252 -0
- data/lib/gitlab/grape_openapi/converters/path_converter.rb +152 -0
- data/lib/gitlab/grape_openapi/converters/request_body_converter.rb +97 -0
- data/lib/gitlab/grape_openapi/converters/response_converter.rb +185 -0
- data/lib/gitlab/grape_openapi/converters/tag_converter.rb +36 -0
- data/lib/gitlab/grape_openapi/converters/type_resolver.rb +75 -0
- data/lib/gitlab/grape_openapi/generator.rb +60 -0
- data/lib/gitlab/grape_openapi/models/info.rb +29 -0
- data/lib/gitlab/grape_openapi/models/operation.rb +47 -0
- data/lib/gitlab/grape_openapi/models/parameter.rb +43 -0
- data/lib/gitlab/grape_openapi/models/path_item.rb +26 -0
- data/lib/gitlab/grape_openapi/models/request_body/parameter_schema.rb +250 -0
- data/lib/gitlab/grape_openapi/models/request_body/parameters.rb +87 -0
- data/lib/gitlab/grape_openapi/models/response.rb +48 -0
- data/lib/gitlab/grape_openapi/models/schema.rb +61 -0
- data/lib/gitlab/grape_openapi/models/security_scheme.rb +130 -0
- data/lib/gitlab/grape_openapi/models/server.rb +31 -0
- data/lib/gitlab/grape_openapi/models/server_variable.rb +25 -0
- data/lib/gitlab/grape_openapi/models/tag.rb +44 -0
- data/lib/gitlab/grape_openapi/request_body_registry.rb +57 -0
- data/lib/gitlab/grape_openapi/schema_registry.rb +26 -0
- data/lib/gitlab/grape_openapi/serializers/time.rb +19 -0
- data/lib/gitlab/grape_openapi/tag_registry.rb +29 -0
- data/lib/gitlab/grape_openapi/version.rb +8 -0
- data/lib/gitlab-grape-openapi.rb +64 -0
- metadata +162 -12
- data/lib/gitlab/grape/openapi/version.rb +0 -9
- data/lib/gitlab/grape/openapi.rb +0 -21
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Converters
|
|
6
|
+
class EntityConverter
|
|
7
|
+
attr_reader :entity_class, :schema_registry
|
|
8
|
+
|
|
9
|
+
OBJECT_TYPE = 'object'
|
|
10
|
+
ARRAY_TYPE = 'array'
|
|
11
|
+
DEFAULT_TYPE = 'string'
|
|
12
|
+
REF_KEY = '$ref'
|
|
13
|
+
SCHEMA_PATH_PREFIX = '#/components/schemas/'
|
|
14
|
+
|
|
15
|
+
def self.register(entity, schema_registry)
|
|
16
|
+
case entity
|
|
17
|
+
when Class
|
|
18
|
+
return unless grape_entity?(entity)
|
|
19
|
+
|
|
20
|
+
new(entity, schema_registry).convert
|
|
21
|
+
when Hash
|
|
22
|
+
return unless entity[:model] && grape_entity?(entity[:model])
|
|
23
|
+
|
|
24
|
+
new(entity[:model], schema_registry).convert
|
|
25
|
+
when Array
|
|
26
|
+
# Array elements may be Class (`success [Entities::Foo]`) or
|
|
27
|
+
# Hash-with-:model (`success [{ code:, model: Foo }]`). Recurse so
|
|
28
|
+
# both are registered. Mirrors `ResponseConverter`'s handling.
|
|
29
|
+
entity.each { |item| register(item, schema_registry) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.grape_entity?(klass)
|
|
34
|
+
klass.is_a?(Class) && klass.ancestors.include?(Grape::Entity)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(entity_class, schema_registry)
|
|
38
|
+
@entity_class = entity_class
|
|
39
|
+
@schema_registry = schema_registry
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def convert(register: true)
|
|
43
|
+
normalized_name = schema_registry.normalize_entity_class(entity_class)
|
|
44
|
+
return schema_registry.schemas[normalized_name] if schema_registry.schemas.key?(normalized_name)
|
|
45
|
+
|
|
46
|
+
schema = build_schema
|
|
47
|
+
schema_registry.register(entity_class, schema) if register
|
|
48
|
+
schema
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def build_schema
|
|
54
|
+
Models::Schema.new.tap do |schema|
|
|
55
|
+
schema.type = OBJECT_TYPE
|
|
56
|
+
schema.properties = build_properties
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_properties
|
|
61
|
+
root_exposures.each_with_object({}) do |exposure, properties|
|
|
62
|
+
if inlineable_merge_exposure?(exposure)
|
|
63
|
+
# `merge: true` flattens the nested entity's exposures into the
|
|
64
|
+
# parent at runtime. Inline its properties instead of emitting a
|
|
65
|
+
# `$ref`, so the generated schema reflects the actual response.
|
|
66
|
+
inline_merged_properties!(properties, exposure)
|
|
67
|
+
else
|
|
68
|
+
properties[exposure.key] = build_property(exposure)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inlineable_merge_exposure?(exposure)
|
|
74
|
+
return false unless exposure.for_merge
|
|
75
|
+
|
|
76
|
+
nested = nested_entity_class(exposure)
|
|
77
|
+
nested.is_a?(Class) && self.class.grape_entity?(nested)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def inline_merged_properties!(properties, exposure)
|
|
81
|
+
nested_schema = build_or_fetch_nested_schema(nested_entity_class(exposure))
|
|
82
|
+
return unless nested_schema&.properties
|
|
83
|
+
|
|
84
|
+
# Grape Entity's `merge: true` lets later exposures override earlier
|
|
85
|
+
# ones with the same key, so merging into `properties` mirrors that.
|
|
86
|
+
properties.merge!(nested_schema.properties)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build the merged entity's schema without registering it as a
|
|
90
|
+
# standalone component. Entities only reached through `merge: true` are
|
|
91
|
+
# inlined into their parents and do not need their own component entry.
|
|
92
|
+
# `convert` still returns an already-registered schema when one exists
|
|
93
|
+
# (for entities also reached via a regular `using:` elsewhere), and
|
|
94
|
+
# `build_schema` still walks nested `using:` exposures, so any schema
|
|
95
|
+
# actually referenced by `$ref` remains registered.
|
|
96
|
+
def build_or_fetch_nested_schema(nested_class)
|
|
97
|
+
self.class.new(nested_class, schema_registry).convert(register: false)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_property(exposure)
|
|
101
|
+
property = extract_basic_attributes(exposure)
|
|
102
|
+
apply_type_specific_attributes!(property, exposure)
|
|
103
|
+
property.compact
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_basic_attributes(exposure)
|
|
107
|
+
documentation = exposure_documentation(exposure)
|
|
108
|
+
default_value = exposure_default(exposure)
|
|
109
|
+
|
|
110
|
+
type = documentation[:type]
|
|
111
|
+
|
|
112
|
+
if multiple_types?(type)
|
|
113
|
+
build_one_of_property(type, documentation, default_value)
|
|
114
|
+
else
|
|
115
|
+
build_single_type_property(type, documentation, default_value)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def multiple_types?(type)
|
|
120
|
+
type.is_a?(Array) && type.length > 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_one_of_property(types, documentation, default_value)
|
|
124
|
+
{
|
|
125
|
+
oneOf: types.map { |type| build_type_schema(type, documentation) },
|
|
126
|
+
description: documentation[:desc],
|
|
127
|
+
default: default_value,
|
|
128
|
+
example: documentation[:example]
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_single_type_property(type, documentation, default_value)
|
|
133
|
+
# Handle single element arrays
|
|
134
|
+
actual_type = type.is_a?(Array) ? type.first : type
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
type: TypeResolver.resolve_type(actual_type) || DEFAULT_TYPE,
|
|
138
|
+
description: documentation[:desc],
|
|
139
|
+
format: TypeResolver.resolve_format(documentation[:format], actual_type),
|
|
140
|
+
default: default_value,
|
|
141
|
+
example: documentation[:example]
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_type_schema(type, documentation)
|
|
146
|
+
schema = { type: TypeResolver.resolve_type(type) || DEFAULT_TYPE }
|
|
147
|
+
|
|
148
|
+
format = TypeResolver.resolve_format(documentation[:format], type)
|
|
149
|
+
schema[:format] = format if format
|
|
150
|
+
|
|
151
|
+
schema
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def apply_type_specific_attributes!(property, exposure)
|
|
155
|
+
# Skip type-specific handling for oneOf properties
|
|
156
|
+
return if property[:oneOf]
|
|
157
|
+
|
|
158
|
+
if array_exposure?(exposure)
|
|
159
|
+
handle_array_property!(property, exposure)
|
|
160
|
+
elsif nested_entity?(exposure)
|
|
161
|
+
handle_entity_reference!(property, exposure)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def handle_array_property!(property, exposure)
|
|
166
|
+
if nested_entity?(exposure)
|
|
167
|
+
register_nested_entity(exposure)
|
|
168
|
+
reference = build_reference(exposure)
|
|
169
|
+
set_array_property!(property, reference)
|
|
170
|
+
else
|
|
171
|
+
set_array_primitive_property!(property)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def handle_entity_reference!(property, exposure)
|
|
176
|
+
register_nested_entity(exposure)
|
|
177
|
+
reference = build_reference(exposure)
|
|
178
|
+
set_reference_property!(property, reference)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Ensure schemas reachable from a route via nested `using:` exposures are
|
|
182
|
+
# registered too, so dropping the over-broad `Grape::Entity.descendants`
|
|
183
|
+
# seed in the generator does not leave dangling `$ref`s.
|
|
184
|
+
def register_nested_entity(exposure)
|
|
185
|
+
nested = nested_entity_class(exposure)
|
|
186
|
+
return unless nested.is_a?(Class)
|
|
187
|
+
|
|
188
|
+
self.class.register(nested, schema_registry)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def set_array_primitive_property!(property)
|
|
192
|
+
item_type = property[:type] || DEFAULT_TYPE
|
|
193
|
+
property[:type] = ARRAY_TYPE
|
|
194
|
+
property[:items] = build_primitive_items(property, item_type)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def build_primitive_items(property, item_type)
|
|
198
|
+
items = { type: item_type }
|
|
199
|
+
|
|
200
|
+
# Move format to items if present
|
|
201
|
+
if property[:format]
|
|
202
|
+
items[:format] = property[:format]
|
|
203
|
+
property[:format] = nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
items
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def set_array_property!(property, reference)
|
|
210
|
+
property[:type] = ARRAY_TYPE
|
|
211
|
+
property[:items] = { REF_KEY => reference }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_reference_property!(property, reference)
|
|
215
|
+
property[:type] = nil
|
|
216
|
+
property[REF_KEY] = reference
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def build_reference(exposure)
|
|
220
|
+
entity_name = nested_entity_class(exposure)
|
|
221
|
+
"#{SCHEMA_PATH_PREFIX}#{normalize_entity_name(entity_name)}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def normalize_entity_name(entity_name)
|
|
225
|
+
if entity_name.is_a?(Class)
|
|
226
|
+
entity_name.name.delete(':')
|
|
227
|
+
else
|
|
228
|
+
entity_name.delete(':')
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def root_exposures
|
|
233
|
+
entity_class.root_exposure.nested_exposures
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def exposure_documentation(exposure)
|
|
237
|
+
exposure.documentation || {}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Grape Entity stores the `:default` option in `@default_value` but
|
|
241
|
+
# does not expose a public reader for it. Reaching for the ivar is the
|
|
242
|
+
# only way to read it without going through `send(:options)`, which the
|
|
243
|
+
# `GitlabSecurity/PublicSend` rule disallows.
|
|
244
|
+
def exposure_default(exposure)
|
|
245
|
+
exposure.instance_variable_get(:@default_value)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def nested_entity?(exposure)
|
|
249
|
+
!nested_entity_class(exposure).nil?
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Only `RepresentExposure` instances (i.e. exposures declared with
|
|
253
|
+
# `using:`) expose a `using_class_name` reader; for any other exposure
|
|
254
|
+
# type there is no nested entity to reference.
|
|
255
|
+
def nested_entity_class(exposure)
|
|
256
|
+
return unless exposure.respond_to?(:using_class_name)
|
|
257
|
+
|
|
258
|
+
exposure.using_class_name
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def array_exposure?(exposure)
|
|
262
|
+
exposure_documentation(exposure)[:is_array]
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Converters
|
|
6
|
+
class OperationConverter
|
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#operation-object
|
|
8
|
+
|
|
9
|
+
DASH_SEGMENT = 'Dash'
|
|
10
|
+
|
|
11
|
+
def self.convert(route, schema_registry, request_body_registry, inherited_path_params: {})
|
|
12
|
+
new(
|
|
13
|
+
route,
|
|
14
|
+
schema_registry,
|
|
15
|
+
request_body_registry,
|
|
16
|
+
inherited_path_params: inherited_path_params
|
|
17
|
+
).convert
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(route, schema_registry, request_body_registry, inherited_path_params: {})
|
|
21
|
+
@route = route
|
|
22
|
+
@schema_registry = schema_registry
|
|
23
|
+
@request_body_registry = request_body_registry
|
|
24
|
+
@inherited_path_params = inherited_path_params
|
|
25
|
+
@config = Gitlab::GrapeOpenapi.configuration
|
|
26
|
+
@options = route.options
|
|
27
|
+
@pattern = route.pattern
|
|
28
|
+
@endpoint = route.app
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def convert
|
|
32
|
+
Models::Operation.new.tap do |operation|
|
|
33
|
+
operation.operation_id = operation_id
|
|
34
|
+
operation.summary = extract_description
|
|
35
|
+
operation.description = extract_detail
|
|
36
|
+
operation.tags = extract_tags
|
|
37
|
+
operation.deprecated = extract_deprecated
|
|
38
|
+
operation.hidden = extract_hidden
|
|
39
|
+
operation.parameters = extract_parameters
|
|
40
|
+
operation.responses = ResponseConverter.new(@route, @schema_registry).convert
|
|
41
|
+
operation.request_body = extract_request_body || {}
|
|
42
|
+
operation.annotations = extract_annotations
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :config, :route, :options, :pattern, :endpoint, :schema_registry, :request_body_registry,
|
|
49
|
+
:inherited_path_params
|
|
50
|
+
|
|
51
|
+
def route_method
|
|
52
|
+
@route.request_method
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_annotations
|
|
56
|
+
return {} unless options[:settings]
|
|
57
|
+
|
|
58
|
+
selected_keys = options[:settings].keys.select do |k|
|
|
59
|
+
config.annotations.key?(k)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
selected_keys.to_h do |key|
|
|
63
|
+
[config.annotations[key], options[:settings][key].to_s]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_parameters
|
|
68
|
+
params = if options[:params].empty?
|
|
69
|
+
[]
|
|
70
|
+
else
|
|
71
|
+
options[:params].filter_map do |key, options|
|
|
72
|
+
Converters::ParameterConverter.convert(
|
|
73
|
+
key,
|
|
74
|
+
options: options,
|
|
75
|
+
validations: validations_for(key.to_sym),
|
|
76
|
+
route: route
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
inject_missing_path_parameters(params)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def inject_missing_path_parameters(params)
|
|
85
|
+
declared_names = params.map(&:name).to_set
|
|
86
|
+
|
|
87
|
+
path_placeholders = normalized_path.scan(/\{(\w+)\}/).flatten
|
|
88
|
+
path_segments = normalized_path.split('/')
|
|
89
|
+
|
|
90
|
+
path_placeholders.each do |placeholder|
|
|
91
|
+
next if declared_names.include?(placeholder)
|
|
92
|
+
|
|
93
|
+
inherited = lookup_inherited_param(path_segments, placeholder)
|
|
94
|
+
|
|
95
|
+
params << if inherited
|
|
96
|
+
ParameterConverter.convert(
|
|
97
|
+
placeholder,
|
|
98
|
+
options: inherited,
|
|
99
|
+
route: route
|
|
100
|
+
)
|
|
101
|
+
else
|
|
102
|
+
# Empty schema because no declaration was found locally or in any
|
|
103
|
+
# sibling route's path prefix. Valid OpenAPI 3.0 ("any type") but
|
|
104
|
+
# incomplete; report the offender to stderr so callers can fix
|
|
105
|
+
# it in source.
|
|
106
|
+
report_synthesized_parameter(placeholder)
|
|
107
|
+
Models::Parameter.new(
|
|
108
|
+
placeholder,
|
|
109
|
+
options: { required: true },
|
|
110
|
+
schema: {},
|
|
111
|
+
in_value: 'path'
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
params
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Emit a one-line stderr diagnostic identifying the route and API
|
|
120
|
+
# class responsible for a `schema: {}` entry in the generated spec.
|
|
121
|
+
# The output is grep-friendly: each line starts with the literal
|
|
122
|
+
# `[gitlab-grape-openapi]` tag and includes the HTTP method, the
|
|
123
|
+
# full normalized path, the placeholder name, and the API class
|
|
124
|
+
# name. From the class name, the source file follows GitLab's
|
|
125
|
+
# autoload conventions (`API::Foo::Bar` -> `lib/api/foo/bar.rb`).
|
|
126
|
+
def report_synthesized_parameter(placeholder)
|
|
127
|
+
api_class = route.app.options[:for] if route.app.respond_to?(:options)
|
|
128
|
+
warn "[gitlab-grape-openapi] synthesized path param: " \
|
|
129
|
+
"#{http_method} #{normalized_path} placeholder=:#{placeholder} " \
|
|
130
|
+
"class=#{api_class || 'unknown'}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Look up a path-prefix-keyed declaration so mounted child routes
|
|
134
|
+
# can inherit `requires :id` (etc.) from the parent that mounted them.
|
|
135
|
+
def lookup_inherited_param(path_segments, placeholder)
|
|
136
|
+
idx = path_segments.index("{#{placeholder}}")
|
|
137
|
+
return unless idx
|
|
138
|
+
|
|
139
|
+
prefix = path_segments[0..idx].join('/')
|
|
140
|
+
inherited_path_params[[prefix, placeholder]]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def operation_id
|
|
144
|
+
method = http_method.downcase
|
|
145
|
+
normalized = normalized_path
|
|
146
|
+
segments = normalized.split('/').reject(&:empty?)
|
|
147
|
+
|
|
148
|
+
parts = segments.filter_map do |seg|
|
|
149
|
+
next DASH_SEGMENT if seg == '-'
|
|
150
|
+
|
|
151
|
+
if seg.include?('{')
|
|
152
|
+
camelize(seg.gsub(/\{[^}]+\}/) { |m| m[1..-2] })
|
|
153
|
+
else
|
|
154
|
+
camelize(seg)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
"#{method}#{parts.join}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def extract_description
|
|
162
|
+
description_from_options = options[:description]
|
|
163
|
+
return description_from_options[:description] if description_from_options.is_a?(Hash)
|
|
164
|
+
return description_from_options if description_from_options.is_a?(String)
|
|
165
|
+
|
|
166
|
+
return unless endpoint
|
|
167
|
+
|
|
168
|
+
description_hash = endpoint.inheritable_setting&.namespace
|
|
169
|
+
description_hash[:description] if description_hash.is_a?(Hash)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def extract_detail
|
|
173
|
+
options.dig(:settings, :description, :detail)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def extract_tags
|
|
177
|
+
Array(@route.settings.dig(:description, :tags)).map do |tag|
|
|
178
|
+
Gitlab::GrapeOpenapi::Models::Tag.normalize_tag_name(tag)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extract_deprecated
|
|
183
|
+
!!options.dig(:settings, :description, :deprecated)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def extract_hidden
|
|
187
|
+
!!options.dig(:settings, :description, :hidden)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def path_segments
|
|
191
|
+
segments = normalized_path.split('/').reject do |segment|
|
|
192
|
+
segment.empty? || segment.start_with?('{')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
segments.reject { |seg| seg == config.api_prefix || seg == config.api_version || seg == '-' }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def normalized_path
|
|
199
|
+
@normalized_path ||= begin
|
|
200
|
+
path = normalize_path_pattern
|
|
201
|
+
path.gsub('{version}', config.api_version)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def normalize_path_pattern
|
|
206
|
+
path = pattern.origin
|
|
207
|
+
path
|
|
208
|
+
.gsub(/\(\.:format\)$/, '')
|
|
209
|
+
.gsub(/[()\\]/, '')
|
|
210
|
+
.gsub(/:\w+/) { |match| "{#{match[1..]}}" }
|
|
211
|
+
.gsub('{version}', config.api_version)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def camelize(string)
|
|
215
|
+
string.gsub(/[^a-zA-Z0-9_]/, '_').split('_').reject(&:empty?).map(&:capitalize).join
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def http_method
|
|
219
|
+
@route.request_method
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get all validations for a single attribute
|
|
223
|
+
# Looks something like:
|
|
224
|
+
# [{:attributes=>[:version_prefix],
|
|
225
|
+
# :options=>/^[\d+.]+/,
|
|
226
|
+
# :required=>false,
|
|
227
|
+
# :params_scope=>#<Grape::Validations::ParamsScope:0x000000016dc35820
|
|
228
|
+
# :opts=>{:allow_blank=>nil, :fail_fast=>false},
|
|
229
|
+
# :validator_class=>Grape::Validations::Validators::RegexpValidator}]
|
|
230
|
+
def validations_for(attribute)
|
|
231
|
+
route
|
|
232
|
+
.app
|
|
233
|
+
.inheritable_setting
|
|
234
|
+
.namespace_stackable
|
|
235
|
+
.new_values[:validations]
|
|
236
|
+
&.select { |v| v[:attributes].include?(attribute) }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def extract_request_body
|
|
240
|
+
RequestBodyConverter.convert(
|
|
241
|
+
route: route,
|
|
242
|
+
options: options,
|
|
243
|
+
params: options[:params],
|
|
244
|
+
request_body_registry: request_body_registry
|
|
245
|
+
)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|