Add parameter-level annotation support

Overview

As part of the OpenAPI Annotations - REST Parameters epic, the gem needs to emit OpenAPI extensions on individual parameters, the way it already emits them on operations via config.annotations.

This is the foundational issue for the epic. Everything else depends on the release it produces.

Background

The gem already has a route-level annotation pipeline: config.annotations maps route_setting keys to extension names, OperationConverter#extract_annotations reads them off options[:settings], and Operation#to_h injects them at the top level of the operation object.

There is no parameter equivalent. Two output paths need one, because Grape parameters land in two different OpenAPI objects:

  • Query and path parameters → Models::Parameter#to_h, whose output hash is fixed and has no extension slot.
  • POST/PUT body parameters → Models::RequestBody::ParameterSchema#build, which becomes a property inside a RequestBody_* component schema.

OpenAPI 3.0 permits x- extensions on both the Parameter Object and the Schema Object.

The source of the annotation is Grape's documentation: hash. Grape's Validations::AttributesDoc#extract_details deletes it from the validation options and stores it verbatim, so it arrives at route.options[:params][key][:documentation]. The gem already reads options.dig(:documentation, :example) and :default from it.

Proposal

1. Add the configuration

Configuration: add parameter_annotations to attr_accessor, defaulting to {} in initialize so the change is inert for callers that do not set it.

config.parameter_annotations = { tier: 'x-gitlab-tier', role: 'x-gitlab-role' }

Keys are keys within a parameter's documentation: hash; values are the emitted extension names.

2. Add a shared concern

New Concerns::ParameterAnnotatable, mirroring OperationConverter#extract_annotations:

def parameter_annotations_for(options)
  documentation = options[:documentation]
  return {} unless documentation.is_a?(Hash)

  config = Gitlab::GrapeOpenapi.configuration
  documentation.keys.select { |k| config.parameter_annotations.key?(k) }
               .to_h { |k| [config.parameter_annotations[k], documentation[k].to_s] }
end

The is_a?(Hash) guard is required, not defensive: OperationConverter#inject_missing_path_parameters synthesises Models::Parameter.new(placeholder, options: { required: true }, ...) for path placeholders with no local declaration, and those have no :documentation key.

3. Emit on query and path parameters

Models::Parameter: add attr_accessor :annotations, and in to_h apply them after .compact, exactly as Operation#to_h does:

annotations&.each { |k, v| result[k] = v }

Converters::ParameterConverter#convert: include the concern and set param.annotations = parameter_annotations_for(options).

4. Emit on request-body parameters

Models::RequestBody::ParameterSchema#build: merge the annotations into built_schema at the single tail point alongside apply_allow_blank and apply_limit!. Merging there rather than inside each build_* branch covers every shape — basic, enum, range, array, oneOf union, nested hash and nested array — in one place, and nested parameters inherit it for free because build_nested_hash_schema and build_nested_array_schema recurse through the same build.

5. Release

Bump to 0.4.0, update CHANGELOG.md and the README.

Notes

  • No registry change is needed. RequestBodyRegistry#register hashes the whole normalised schema, so adding an extension to a property changes the hash and the dedup stays correct.
  • documentation: is shared with grape-swagger, which generates openapi_v2.yaml from the same declarations, but an unrecognised key is safe: GrapeSwagger::DocMethods::ParseParams.call builds its output from an explicit whitelist and never splats the settings hash, and Extensions.add_extensions_to_root only fires when the hash has an :x key. A key named x must therefore be avoided, since that spelling would leak into the v2 spec.

Acceptance Criteria

  • config.parameter_annotations added, defaulting to {}
  • A query parameter with a mapped documentation: key emits the extension on its Parameter Object
  • A path parameter behaves the same
  • A request-body parameter emits the extension on its property schema
  • A nested body parameter (type: Hash with params) emits it on the nested property
  • A synthesised path parameter with no documentation: does not raise
  • Unmapped documentation: keys (example, default, type) are unaffected
  • Request-body schema dedup still collapses identical schemas and separates schemas that differ only by annotation
  • Specs cover all of the above; CHANGELOG updated; 0.4.0 released
Edited by Max Woolf