gitlab-grape-openapi 0.3.0 → 0.5.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.
@@ -6,10 +6,11 @@ module Gitlab
6
6
  module RequestBody
7
7
  class ParameterSchema
8
8
  include Converters::CoercerResolver
9
- include Concerns::Serializable
9
+ include Concerns::ConstraintApplier
10
10
  include Concerns::LimitResolver
11
11
  include Concerns::FailFastAnnotatable
12
12
  include Concerns::RegexConverter
13
+ include Concerns::Nullability
13
14
 
14
15
  def initialize(route:, key:, param_options:)
15
16
  @route = route
@@ -29,8 +30,10 @@ module Gitlab
29
30
  built_schema = build_resolved_schema(object_type, object_format)
30
31
  end
31
32
 
32
- apply_allow_blank(built_schema)
33
+ apply_nullability!(built_schema, param_options)
33
34
  apply_limit!(built_schema, validations)
35
+ apply_array_enum!(built_schema, param_options[:values])
36
+ apply_default!(built_schema, param_options[:default])
34
37
  built_schema
35
38
  end
36
39
 
@@ -109,7 +112,10 @@ module Gitlab
109
112
 
110
113
  def build_union_type_schema
111
114
  types = param_options[:type][1..-2].split(", ")
112
- { oneOf: types.map { |type| Converters::TypeResolver.resolve_union_member(type) } }
115
+ members = types.map { |type| Converters::TypeResolver.resolve_union_member(type) }
116
+ schema = Converters::TypeResolver.union_schema(members)
117
+ schema[:description] = annotated_description if param_options[:desc]
118
+ schema
113
119
  end
114
120
 
115
121
  def build_range_schema(object_type)
@@ -117,10 +123,6 @@ module Gitlab
117
123
  schema = { type: object_type }
118
124
  schema[:minimum] = range.begin if range.begin
119
125
  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
126
  schema[:description] = annotated_description if param_options[:desc]
125
127
  schema
126
128
  end
@@ -128,10 +130,6 @@ module Gitlab
128
130
  def build_enum_schema(object_type)
129
131
  schema = { type: object_type }
130
132
  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
133
  schema[:description] = annotated_description if param_options[:desc]
136
134
  schema
137
135
  end
@@ -202,10 +200,6 @@ module Gitlab
202
200
  def build_basic_schema(object_type, object_format)
203
201
  schema = { type: object_type }
204
202
  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
203
  schema[:description] = annotated_description if param_options[:desc]
210
204
 
211
205
  if param_options.dig(:documentation, :example)
@@ -228,16 +222,6 @@ module Gitlab
228
222
  def validations_for(attribute)
229
223
  GrapeCompat.validations_for(route, attribute)
230
224
  end
231
-
232
- def apply_allow_blank(schema)
233
- if param_options[:allow_blank] == false || (param_options[:required] && param_options[:values])
234
- schema[:minLength] = 1 if schema[:type] == 'string'
235
- elsif schema[:oneOf]
236
- schema[:oneOf].each { |s| s[:nullable] = true }
237
- else
238
- schema[:nullable] = true
239
- end
240
- end
241
225
  end
242
226
  end
243
227
  end
@@ -14,7 +14,7 @@ module Gitlab
14
14
 
15
15
  def extract
16
16
  body_params = params.reject do |key, _|
17
- path_with_params.include?("{#{key}}")
17
+ path_with_params.include?("{#{key}}") || hidden?(key)
18
18
  end
19
19
 
20
20
  restructure_nested_params(body_params)
@@ -22,10 +22,22 @@ module Gitlab
22
22
 
23
23
  private
24
24
 
25
+ # `documentation: { hidden: true }` marks a param the author does not want
26
+ # documented. Params nested below it, in bracket notation, go with it.
27
+ def hidden?(key)
28
+ key_str = key.to_s
29
+
30
+ hidden_keys.any? { |hidden| key_str == hidden || key_str.start_with?("#{hidden}[") }
31
+ end
32
+
33
+ def hidden_keys
34
+ @hidden_keys ||= params.filter_map do |key, param_options|
35
+ key.to_s if param_options.is_a?(Hash) && param_options.dig(:documentation, :hidden)
36
+ end
37
+ end
38
+
25
39
  def path_with_params
26
- @path_with_params ||= route.origin
27
- .gsub(/\(\.:format\)$/, '')
28
- .gsub(/:\w+/) { |match| "{#{match[1..]}}" }
40
+ @path_with_params ||= NormalizedPath.new(route.origin).to_s
29
41
  end
30
42
 
31
43
  def restructure_nested_params(body_params)
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module GrapeOpenapi
5
+ # A Grape route pattern rendered as an OpenAPI path template.
6
+ #
7
+ # Grape spells path placeholders two ways: `:name` matches a single segment,
8
+ # and `*name` (a splat) matches one or more segments, so its value may contain
9
+ # slashes. OpenAPI 3.0 has no splat syntax, so both collapse to `{name}`.
10
+ #
11
+ # The splat mapping is lossy - `{name}` implies a single segment - but the
12
+ # endpoint is documented, which is strictly better than omitting it. Do not
13
+ # "fix" this by dropping splat routes: that is the bug in issue #22, which
14
+ # silently removed the entire package registry surface from the spec.
15
+ class NormalizedPath
16
+ # Grape appends the format suffix to `route.path`, not to `route.pattern.origin`,
17
+ # so this only strips a suffix an author wrote into the pattern by hand. Without
18
+ # it, a declared `format` param would be mistaken for a path parameter.
19
+ FORMAT_SUFFIX = /\(\.:format\)$/
20
+
21
+ PLACEHOLDER = /[:*](\w+)/
22
+ NORMALIZED_PLACEHOLDER = /\{(\w+)\}/
23
+
24
+ # Grape's optional-segment markup: the parentheses grouping an optional
25
+ # segment, and the backslashes escaping a literal parenthesis. Both are
26
+ # noise once a path is rendered for a human.
27
+ OPTIONAL_SEGMENT_MARKUP = /[()\\]/
28
+
29
+ # The API version is substituted away with the configured value before a path
30
+ # is emitted, so it never surfaces as a path parameter.
31
+ API_VERSION_PLACEHOLDER = 'version'
32
+
33
+ attr_reader :origin
34
+
35
+ # Takes a `route.pattern.origin` - the pattern as the author declared it.
36
+ # Do not pass `route.path`: Grape rewrites that one, appending the format
37
+ # suffix and turning a trailing `*path` into `?*path`.
38
+ def initialize(origin)
39
+ @origin = origin
40
+ end
41
+
42
+ def to_s
43
+ @to_s ||= origin
44
+ .sub(FORMAT_SUFFIX, '')
45
+ .gsub(PLACEHOLDER) { "{#{Regexp.last_match(1)}}" }
46
+ end
47
+
48
+ # The path as shown to humans in emitted paths and warnings: placeholders
49
+ # collapsed (via `to_s`), optional-segment markup removed, and the API
50
+ # version substituted in for `{version}`.
51
+ def to_display_path(api_version)
52
+ to_s.gsub(OPTIONAL_SEGMENT_MARKUP, '').gsub("{#{API_VERSION_PLACEHOLDER}}", api_version)
53
+ end
54
+
55
+ def placeholder_names
56
+ # Scanning whole `{name}` placeholders sidesteps the boundary problem a regex
57
+ # over the raw pattern has: real routes introduce a placeholder after `/`,
58
+ # `(`, `)` and `'` - `(*path`, `):file_name`, `Id='*package_name'` - so there
59
+ # is no single delimiter to anchor on.
60
+ @placeholder_names ||= to_s.scan(NORMALIZED_PLACEHOLDER).flatten
61
+ end
62
+
63
+ def path_parameter_names
64
+ placeholder_names - [API_VERSION_PLACEHOLDER]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
3
+ require 'openssl'
4
4
 
5
5
  module Gitlab
6
6
  module GrapeOpenapi
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  module GrapeOpenapi
5
- VERSION = "0.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -7,19 +7,24 @@ require_relative "gitlab/grape_openapi/generator"
7
7
  require_relative "gitlab/grape_openapi/schema_registry"
8
8
  require_relative "gitlab/grape_openapi/request_body_registry"
9
9
  require_relative "gitlab/grape_openapi/tag_registry"
10
+ require_relative "gitlab/grape_openapi/normalized_path"
10
11
 
11
12
  # Concerns
12
13
  require_relative "gitlab/grape_openapi/concerns/serializable"
14
+ require_relative "gitlab/grape_openapi/concerns/constraint_applier"
13
15
  require_relative "gitlab/grape_openapi/concerns/limit_resolver"
14
16
  require_relative "gitlab/grape_openapi/concerns/fail_fast_annotatable"
15
17
  require_relative "gitlab/grape_openapi/concerns/regex_converter"
18
+ require_relative "gitlab/grape_openapi/concerns/nullability"
16
19
 
17
20
  # Serializers
18
21
  require_relative "gitlab/grape_openapi/serializers/time"
19
22
 
20
23
  # Converters
21
24
  require_relative "gitlab/grape_openapi/converters/coercer_resolver"
25
+ require_relative "gitlab/grape_openapi/converters/cross_field_validation_resolver"
22
26
  require_relative "gitlab/grape_openapi/converters/entity_converter"
27
+ require_relative "gitlab/grape_openapi/converters/media_type_resolver"
23
28
  require_relative "gitlab/grape_openapi/converters/type_resolver"
24
29
  require_relative "gitlab/grape_openapi/converters/tag_converter"
25
30
  require_relative "gitlab/grape_openapi/converters/operation_converter"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-grape-openapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.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-08-10 00:00:00.000000000 Z
11
+ date: 2026-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grape
@@ -139,13 +139,17 @@ files:
139
139
  - LICENSE
140
140
  - README.md
141
141
  - lib/gitlab-grape-openapi.rb
142
+ - lib/gitlab/grape_openapi/concerns/constraint_applier.rb
142
143
  - lib/gitlab/grape_openapi/concerns/fail_fast_annotatable.rb
143
144
  - lib/gitlab/grape_openapi/concerns/limit_resolver.rb
145
+ - lib/gitlab/grape_openapi/concerns/nullability.rb
144
146
  - lib/gitlab/grape_openapi/concerns/regex_converter.rb
145
147
  - lib/gitlab/grape_openapi/concerns/serializable.rb
146
148
  - lib/gitlab/grape_openapi/configuration.rb
147
149
  - lib/gitlab/grape_openapi/converters/coercer_resolver.rb
150
+ - lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb
148
151
  - lib/gitlab/grape_openapi/converters/entity_converter.rb
152
+ - lib/gitlab/grape_openapi/converters/media_type_resolver.rb
149
153
  - lib/gitlab/grape_openapi/converters/operation_converter.rb
150
154
  - lib/gitlab/grape_openapi/converters/parameter_converter.rb
151
155
  - lib/gitlab/grape_openapi/converters/path_converter.rb
@@ -167,6 +171,7 @@ files:
167
171
  - lib/gitlab/grape_openapi/models/server.rb
168
172
  - lib/gitlab/grape_openapi/models/server_variable.rb
169
173
  - lib/gitlab/grape_openapi/models/tag.rb
174
+ - lib/gitlab/grape_openapi/normalized_path.rb
170
175
  - lib/gitlab/grape_openapi/request_body_registry.rb
171
176
  - lib/gitlab/grape_openapi/schema_registry.rb
172
177
  - lib/gitlab/grape_openapi/serializers/time.rb