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,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Converters
|
|
6
|
+
class TagConverter
|
|
7
|
+
attr_reader :api_class, :tag_registry
|
|
8
|
+
|
|
9
|
+
def initialize(api_class, tag_registry)
|
|
10
|
+
@api_class = api_class
|
|
11
|
+
@tag_registry = tag_registry
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def convert
|
|
15
|
+
tags = build_tags(api_class)
|
|
16
|
+
|
|
17
|
+
tags.each do |tag|
|
|
18
|
+
tag_registry.register(tag)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_tags(api_class)
|
|
25
|
+
tags = api_class.routes.flat_map do |route|
|
|
26
|
+
route.settings.dig(:description, :tags)
|
|
27
|
+
end.compact
|
|
28
|
+
|
|
29
|
+
tags.map do |tag_name|
|
|
30
|
+
Gitlab::GrapeOpenapi::Models::Tag.new(tag_name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Converters
|
|
6
|
+
class TypeResolver
|
|
7
|
+
TYPE_MAPPINGS = {
|
|
8
|
+
'API::Validations::Types::WorkhorseFile' => 'string',
|
|
9
|
+
'Array' => 'array',
|
|
10
|
+
'BigDecimal' => 'number',
|
|
11
|
+
'Boolean' => 'boolean',
|
|
12
|
+
'date' => 'string',
|
|
13
|
+
'Date' => 'string',
|
|
14
|
+
'date-time' => 'string',
|
|
15
|
+
'dateTime' => 'string',
|
|
16
|
+
'DateTime' => 'string',
|
|
17
|
+
'FalseClass' => 'boolean',
|
|
18
|
+
'Grape::API::Boolean' => 'boolean',
|
|
19
|
+
'Gitlab::Color' => 'string',
|
|
20
|
+
:int => 'integer',
|
|
21
|
+
'int' => 'integer',
|
|
22
|
+
Integer => 'integer',
|
|
23
|
+
'Integer' => 'integer',
|
|
24
|
+
'File' => 'string',
|
|
25
|
+
Float => 'number',
|
|
26
|
+
'Float' => 'number',
|
|
27
|
+
:hash => 'object',
|
|
28
|
+
'hash' => 'object',
|
|
29
|
+
'Hash' => 'object',
|
|
30
|
+
'JSON' => 'object',
|
|
31
|
+
'Numeric' => 'number',
|
|
32
|
+
String => 'string',
|
|
33
|
+
'String' => 'string',
|
|
34
|
+
'symbol' => 'string',
|
|
35
|
+
'Symbol' => 'string',
|
|
36
|
+
'text' => 'string',
|
|
37
|
+
'Time' => 'string',
|
|
38
|
+
'TrueClass' => 'boolean'
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
FORMAT_MAPPINGS = {
|
|
42
|
+
'API::Validations::Types::WorkhorseFile' => 'binary',
|
|
43
|
+
'date' => 'date',
|
|
44
|
+
'Date' => 'date',
|
|
45
|
+
'date-time' => 'date-time',
|
|
46
|
+
'dateTime' => 'date-time',
|
|
47
|
+
'DateTime' => 'date-time',
|
|
48
|
+
'File' => 'binary',
|
|
49
|
+
'Time' => 'date-time'
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
def self.resolve_type(type)
|
|
53
|
+
return TYPE_MAPPINGS[type] if TYPE_MAPPINGS[type]
|
|
54
|
+
return type unless type.is_a?(String)
|
|
55
|
+
return 'object' if type.delete_prefix('::').start_with?('API::')
|
|
56
|
+
|
|
57
|
+
type
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.resolve_format(format, type)
|
|
61
|
+
format || FORMAT_MAPPINGS[type]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.resolve_union_member(type)
|
|
65
|
+
if type.start_with?('[') && type.end_with?(']')
|
|
66
|
+
item_type = type[1..-2]
|
|
67
|
+
{ type: 'array', items: { type: resolve_type(item_type) || 'string' } }
|
|
68
|
+
else
|
|
69
|
+
{ type: resolve_type(type) || 'string' }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
class Generator
|
|
6
|
+
attr_reader :tag_registry
|
|
7
|
+
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
@api_classes = Array(options[:api_classes]).reject do |api_class|
|
|
10
|
+
Gitlab::GrapeOpenapi.configuration.excluded_api_classes.include?(api_class.name)
|
|
11
|
+
end
|
|
12
|
+
@schema_registry = SchemaRegistry.new
|
|
13
|
+
@request_body_registry = RequestBodyRegistry.new
|
|
14
|
+
@tag_registry = TagRegistry.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def generate
|
|
18
|
+
initialize_tags
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
openapi: '3.0.0',
|
|
22
|
+
info: Gitlab::GrapeOpenapi.configuration.info.to_h,
|
|
23
|
+
tags: tag_registry.tags.sort_by { |t| t.fetch(:name, '') },
|
|
24
|
+
servers: Gitlab::GrapeOpenapi.configuration.servers.map(&:to_h),
|
|
25
|
+
paths: paths,
|
|
26
|
+
components: {
|
|
27
|
+
securitySchemes: security_schemes,
|
|
28
|
+
schemas: schemas
|
|
29
|
+
},
|
|
30
|
+
security: security_schemes.keys.map { |s| { s => [] } }
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def security_schemes
|
|
35
|
+
Gitlab::GrapeOpenapi.configuration.security_schemes.to_h do |scheme|
|
|
36
|
+
[scheme.type, scheme.to_h]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize_tags
|
|
41
|
+
@api_classes.each do |api_class|
|
|
42
|
+
Converters::TagConverter.new(api_class, tag_registry).convert
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def paths
|
|
47
|
+
all_routes = @api_classes.flat_map(&:routes)
|
|
48
|
+
Converters::PathConverter.convert(all_routes, @schema_registry, @request_body_registry)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def schemas
|
|
54
|
+
entity_schemas = @schema_registry.schemas.transform_values(&:to_h)
|
|
55
|
+
request_body_schemas = @request_body_registry.schemas
|
|
56
|
+
entity_schemas.merge(request_body_schemas)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
class Info
|
|
7
|
+
attr_accessor :title, :description, :terms_of_service, :version, :license
|
|
8
|
+
|
|
9
|
+
def initialize(**options)
|
|
10
|
+
@title = options[:title]
|
|
11
|
+
@description = options[:description]
|
|
12
|
+
@terms_of_service = options[:terms_of_service]
|
|
13
|
+
@version = options[:version]
|
|
14
|
+
@license = options[:license]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
title: title,
|
|
20
|
+
description: description,
|
|
21
|
+
termsOfService: terms_of_service,
|
|
22
|
+
version: version,
|
|
23
|
+
license: license
|
|
24
|
+
}.compact
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
class Operation
|
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#operation-object
|
|
8
|
+
attr_accessor :operation_id, :description, :tags, :responses, :parameters, :request_body, :summary,
|
|
9
|
+
:deprecated, :hidden, :annotations
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@tags = []
|
|
13
|
+
@request_body = {}
|
|
14
|
+
@parameters = []
|
|
15
|
+
@deprecated = false
|
|
16
|
+
@annotations = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def hidden?
|
|
20
|
+
@hidden
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
@parameters ||= []
|
|
25
|
+
|
|
26
|
+
o = {
|
|
27
|
+
operationId: operation_id,
|
|
28
|
+
summary: summary,
|
|
29
|
+
description: description,
|
|
30
|
+
tags: tags && tags.empty? ? nil : tags,
|
|
31
|
+
responses: responses
|
|
32
|
+
}.compact
|
|
33
|
+
|
|
34
|
+
o[:deprecated] = deprecated if deprecated
|
|
35
|
+
o[:parameters] = parameters.map(&:to_h) if parameters.any?
|
|
36
|
+
o[:requestBody] = request_body if request_body.keys.any?
|
|
37
|
+
|
|
38
|
+
annotations&.each do |k, v|
|
|
39
|
+
o[k] = v
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
o
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
class Parameter
|
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#parameter-object
|
|
8
|
+
attr_reader :name, :required, :in_value, :description, :options, :schema, :example
|
|
9
|
+
attr_accessor :style, :explode
|
|
10
|
+
|
|
11
|
+
def initialize(name, options:, schema:, in_value:)
|
|
12
|
+
@options = options
|
|
13
|
+
@name = name
|
|
14
|
+
# From https://spec.openapis.org/oas/v3.2.0.html#common-fixed-fields: "If the parameter
|
|
15
|
+
# location is 'path', this property is REQUIRED and its value MUST be true.
|
|
16
|
+
@required = in_value == 'path' ? true : options[:required]
|
|
17
|
+
@description = options[:desc]
|
|
18
|
+
@schema = schema
|
|
19
|
+
@in_value = in_value
|
|
20
|
+
@example = options.dig(:documentation, :example)
|
|
21
|
+
@default = options.dig(:documentation, :default)
|
|
22
|
+
@style = nil
|
|
23
|
+
@explode = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
result = {
|
|
28
|
+
name: name,
|
|
29
|
+
required: required,
|
|
30
|
+
description: description,
|
|
31
|
+
schema: schema,
|
|
32
|
+
in: in_value,
|
|
33
|
+
example: example
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
result[:style] = style if style
|
|
37
|
+
result[:explode] = explode unless explode.nil?
|
|
38
|
+
result.compact
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
class PathItem
|
|
7
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#path-item-object
|
|
8
|
+
attr_reader :operations
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@operations = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_operation(method, operation)
|
|
15
|
+
return if operation.hidden?
|
|
16
|
+
|
|
17
|
+
@operations[method.to_s.downcase] = operation
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
operations.transform_values(&:to_h)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
module RequestBody
|
|
7
|
+
class ParameterSchema
|
|
8
|
+
include Converters::CoercerResolver
|
|
9
|
+
include Concerns::Serializable
|
|
10
|
+
include Concerns::LimitResolver
|
|
11
|
+
include Concerns::FailFastAnnotatable
|
|
12
|
+
include Concerns::RegexConverter
|
|
13
|
+
|
|
14
|
+
def initialize(route:, key:, param_options:)
|
|
15
|
+
@route = route
|
|
16
|
+
@key = key
|
|
17
|
+
@param_options = param_options
|
|
18
|
+
@validations = validations_for(key.to_sym)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build
|
|
22
|
+
fail_fast = fail_fast_in_validations?(validations)
|
|
23
|
+
@param_options = param_options.merge(fail_fast: fail_fast) if fail_fast
|
|
24
|
+
built_schema = build_raw_type_schema
|
|
25
|
+
|
|
26
|
+
unless built_schema
|
|
27
|
+
object_type = Converters::TypeResolver.resolve_type(param_options[:type]) || 'string'
|
|
28
|
+
object_format = Converters::TypeResolver.resolve_format(nil, param_options[:type])
|
|
29
|
+
built_schema = build_resolved_schema(object_type, object_format)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
apply_allow_blank(built_schema)
|
|
33
|
+
apply_limit!(built_schema, validations)
|
|
34
|
+
built_schema
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :route, :key, :param_options, :validations
|
|
40
|
+
|
|
41
|
+
def annotated_description
|
|
42
|
+
return param_options[:desc] unless param_options[:fail_fast]
|
|
43
|
+
|
|
44
|
+
annotate_fail_fast(param_options[:desc])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Handles schema building for types that cannot safely be passed through TypeResolver
|
|
48
|
+
def build_raw_type_schema
|
|
49
|
+
type_str = param_options[:type].to_s
|
|
50
|
+
mapping = coercer_mapping_for(validations)
|
|
51
|
+
|
|
52
|
+
if mapping
|
|
53
|
+
# Handle coerced types(e.g., coerce_with: option used)
|
|
54
|
+
build_coerced_schema_with_description(mapping)
|
|
55
|
+
elsif type_str.start_with?('[') && type_str.exclude?(',')
|
|
56
|
+
# Handle array types like [String] (single type in brackets)
|
|
57
|
+
build_simple_array_from_bracket_notation
|
|
58
|
+
elsif type_str.include?('API::Validations::Types::WorkhorseFile')
|
|
59
|
+
# Handle file types (e.g., API::Validations::Types::WorkhorseFile)
|
|
60
|
+
build_file_schema
|
|
61
|
+
elsif type_str.start_with?('[')
|
|
62
|
+
# Handle union types (e.g., [String, Integer])
|
|
63
|
+
build_union_type_schema
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Handles schema building for types that have been resolved through TypeResolver
|
|
68
|
+
def build_resolved_schema(object_type, object_format)
|
|
69
|
+
if param_options[:values].is_a?(Range)
|
|
70
|
+
# Handle range values
|
|
71
|
+
build_range_schema(object_type)
|
|
72
|
+
elsif param_options[:values]
|
|
73
|
+
# Handle enum/values
|
|
74
|
+
build_enum_schema(object_type)
|
|
75
|
+
elsif param_options[:type] == 'Array' && param_options[:params]
|
|
76
|
+
# Handle array types with nested params
|
|
77
|
+
build_nested_array_schema
|
|
78
|
+
elsif object_type.include?('[')
|
|
79
|
+
# Handle array types (simple, like Array[String])
|
|
80
|
+
build_array_schema
|
|
81
|
+
elsif param_options[:type] == 'Hash' && param_options[:params]
|
|
82
|
+
# Handle Hash types with nested params
|
|
83
|
+
build_nested_hash_schema
|
|
84
|
+
else
|
|
85
|
+
# Build basic schema
|
|
86
|
+
build_basic_schema(object_type, object_format)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_coerced_schema_with_description(mapping)
|
|
91
|
+
schema = build_coerced_schema(mapping)
|
|
92
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
93
|
+
schema
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_simple_array_from_bracket_notation
|
|
97
|
+
# Handle types like [String] or [Integer]
|
|
98
|
+
item_type = param_options[:type].to_s.delete('[').delete(']')
|
|
99
|
+
schema = { type: 'array', items: { type: Converters::TypeResolver.resolve_type(item_type) } }
|
|
100
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
101
|
+
schema
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_file_schema
|
|
105
|
+
schema = { type: 'string', format: 'binary' }
|
|
106
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
107
|
+
schema
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_union_type_schema
|
|
111
|
+
types = param_options[:type][1..-2].split(", ")
|
|
112
|
+
{ oneOf: types.map { |type| Converters::TypeResolver.resolve_union_member(type) } }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def build_range_schema(object_type)
|
|
116
|
+
range = param_options[:values]
|
|
117
|
+
schema = { type: object_type }
|
|
118
|
+
schema[:minimum] = range.begin if range.begin
|
|
119
|
+
schema[:maximum] = range.end if range.end
|
|
120
|
+
if param_options[:default] && serializable?(param_options[:default])
|
|
121
|
+
schema[:default] = param_options[:default]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
125
|
+
schema
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_enum_schema(object_type)
|
|
129
|
+
schema = { type: object_type }
|
|
130
|
+
schema[:enum] = param_options[:values] unless param_options[:values].is_a?(Proc)
|
|
131
|
+
if param_options[:default] && serializable?(param_options[:default])
|
|
132
|
+
schema[:default] = param_options[:default]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
136
|
+
schema
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_array_schema
|
|
140
|
+
item_type = param_options[:type].delete('[').delete(']').downcase
|
|
141
|
+
schema = { type: 'array', items: { type: Converters::TypeResolver.resolve_type(item_type) } }
|
|
142
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
143
|
+
schema
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_nested_array_schema
|
|
147
|
+
schema = { type: 'array' }
|
|
148
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
149
|
+
|
|
150
|
+
# Build the items schema from nested params
|
|
151
|
+
nested_params = param_options[:params]
|
|
152
|
+
if nested_params && !nested_params.empty?
|
|
153
|
+
properties = {}
|
|
154
|
+
required_params = []
|
|
155
|
+
|
|
156
|
+
nested_params.each do |nested_key, nested_options|
|
|
157
|
+
properties[nested_key.to_s] = self.class.new(
|
|
158
|
+
route: route, key: nested_key, param_options: nested_options
|
|
159
|
+
).build
|
|
160
|
+
required_params << nested_key.to_s if nested_options[:required]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
items_schema = {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: properties
|
|
166
|
+
}
|
|
167
|
+
items_schema[:required] = required_params unless required_params.empty?
|
|
168
|
+
|
|
169
|
+
schema[:items] = items_schema
|
|
170
|
+
else
|
|
171
|
+
# If no nested params, default to object type
|
|
172
|
+
schema[:items] = { type: 'object' }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
schema
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def build_nested_hash_schema
|
|
179
|
+
schema = { type: 'object' }
|
|
180
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
181
|
+
|
|
182
|
+
# Build the properties schema from nested params
|
|
183
|
+
nested_params = param_options[:params]
|
|
184
|
+
if nested_params && !nested_params.empty?
|
|
185
|
+
properties = {}
|
|
186
|
+
required_params = []
|
|
187
|
+
|
|
188
|
+
nested_params.each do |nested_key, nested_options|
|
|
189
|
+
properties[nested_key.to_s] = self.class.new(
|
|
190
|
+
route: route, key: nested_key, param_options: nested_options
|
|
191
|
+
).build
|
|
192
|
+
required_params << nested_key.to_s if nested_options[:required]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
schema[:properties] = properties
|
|
196
|
+
schema[:required] = required_params unless required_params.empty?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
schema
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def build_basic_schema(object_type, object_format)
|
|
203
|
+
schema = { type: object_type }
|
|
204
|
+
schema[:format] = object_format if object_format
|
|
205
|
+
if param_options[:default] && serializable?(param_options[:default])
|
|
206
|
+
schema[:default] = param_options[:default]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
schema[:description] = annotated_description if param_options[:desc]
|
|
210
|
+
|
|
211
|
+
if param_options.dig(:documentation, :example)
|
|
212
|
+
schema[:example] = param_options.dig(:documentation, :example)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Add regex validations
|
|
216
|
+
validation = validations&.find do |v|
|
|
217
|
+
v[:validator_class] == Grape::Validations::Validators::RegexpValidator
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if validation
|
|
221
|
+
pattern = regexp_to_pattern(validation[:options])
|
|
222
|
+
schema[:pattern] = pattern if pattern
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
schema
|
|
226
|
+
end
|
|
227
|
+
|
|
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) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def apply_allow_blank(schema)
|
|
238
|
+
if param_options[:allow_blank] == false || (param_options[:required] && param_options[:values])
|
|
239
|
+
schema[:minLength] = 1 if schema[:type] == 'string'
|
|
240
|
+
elsif schema[:oneOf]
|
|
241
|
+
schema[:oneOf].each { |s| s[:nullable] = true }
|
|
242
|
+
else
|
|
243
|
+
schema[:nullable] = true
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitlab
|
|
4
|
+
module GrapeOpenapi
|
|
5
|
+
module Models
|
|
6
|
+
module RequestBody
|
|
7
|
+
class Parameters
|
|
8
|
+
attr_reader :route, :params
|
|
9
|
+
|
|
10
|
+
def initialize(route:, params:)
|
|
11
|
+
@route = route
|
|
12
|
+
@params = params
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def extract
|
|
16
|
+
body_params = params.reject do |key, _|
|
|
17
|
+
path_with_params.include?("{#{key}}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
restructure_nested_params(body_params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def path_with_params
|
|
26
|
+
@path_with_params ||= route.origin
|
|
27
|
+
.gsub(/\(\.:format\)$/, '')
|
|
28
|
+
.gsub(/:\w+/) { |match| "{#{match[1..]}}" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def restructure_nested_params(body_params)
|
|
32
|
+
# Separate root params from nested params (with bracket notation)
|
|
33
|
+
root_params = {}
|
|
34
|
+
nested_map = {}
|
|
35
|
+
|
|
36
|
+
body_params.each do |key, param_options|
|
|
37
|
+
key_str = key.to_s
|
|
38
|
+
if key_str.include?('[')
|
|
39
|
+
# Parse bracket notation like "assets[links][name]"
|
|
40
|
+
parts = parse_bracket_notation(key_str)
|
|
41
|
+
nested_map[parts] = param_options
|
|
42
|
+
else
|
|
43
|
+
root_params[key] = param_options
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build nested structure
|
|
48
|
+
nested_map.each do |parts, param_options|
|
|
49
|
+
insert_nested_param(root_params, parts, param_options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
root_params
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_bracket_notation(key)
|
|
56
|
+
key.scan(/\w+/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def insert_nested_param(root_params, parts, param_options)
|
|
60
|
+
return if parts.empty?
|
|
61
|
+
|
|
62
|
+
first = parts[0]
|
|
63
|
+
rest = parts[1..]
|
|
64
|
+
|
|
65
|
+
# Ensure the root param exists as a Hash/object
|
|
66
|
+
root_params[first] ||= { type: 'Hash', required: false }
|
|
67
|
+
|
|
68
|
+
if rest.empty?
|
|
69
|
+
# This is a direct property (e.g., just "config")
|
|
70
|
+
# Merge param_options to preserve description and other metadata
|
|
71
|
+
root_params[first] = root_params[first].merge(param_options)
|
|
72
|
+
elsif rest.length == 1
|
|
73
|
+
# This is the parent of the final property (e.g., "assets[links]")
|
|
74
|
+
root_params[first][:params] ||= {}
|
|
75
|
+
root_params[first][:params][rest[0]] = param_options
|
|
76
|
+
else
|
|
77
|
+
# Multiple levels remaining - recurse to handle arbitrary depth
|
|
78
|
+
# (e.g., "config[database][pool][max_connections]")
|
|
79
|
+
root_params[first][:params] ||= {}
|
|
80
|
+
insert_nested_param(root_params[first][:params], rest, param_options)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|