Class: SharedExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/rspec/openapi/extractors/shared_extractor.rb

Overview

Shared extractor for extracting OpenAPI metadata from RSpec examples

Constant Summary collapse

VALID_EXAMPLE_MODES =
[:none, :single, :multiple].freeze
EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING =
<<~MSG.tr("\n", ' ').strip.freeze
  [rspec-openapi] DEPRECATION: example_mode: :multiple currently means
  { request: :single, response: :multiple }. A future major version will
  change it to { request: :multiple, response: :multiple } (both sides
  multi). Specify the hash form explicitly to lock in current behavior or
  opt in early.
MSG

Class Method Summary collapse

Class Method Details

.attributes(example) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 21

def self.attributes(example)
   = (example.)
  request_example_mode, response_example_mode = normalize_example_mode([:example_mode], example)
  response_additional_properties, request_additional_properties = resolve_additional_properties()
  response_hybrid_additional_properties, request_hybrid_additional_properties =
    resolve_hybrid_additional_properties()
  # Enum support: response_enum and request_enum can override the general enum
  base_enum = normalize_enum([:enum])

  {
    summary: [:summary] || RSpec::OpenAPI.summary_builder.call(example),
    tags: [:tags] || RSpec::OpenAPI.tags_builder.call(example),
    formats: [:formats] || RSpec::OpenAPI.formats_builder.curry.call(example),
    operation_id: [:operation_id],
    required_request_params: [:required_request_params] || [],
    security: [:security],
    description: [:description] || RSpec::OpenAPI.description_builder.call(example),
    deprecated: [:deprecated],
    request_example_mode: request_example_mode,
    response_example_mode: response_example_mode,
    example_key: resolve_example_key(, example),
    example_name: [:example_name] || RSpec::OpenAPI.example_name_builder.call(example),
    response_enum: normalize_enum([:response_enum]) || base_enum,
    request_enum: normalize_enum([:request_enum]) || base_enum,
    response_additional_properties: response_additional_properties,
    request_additional_properties: request_additional_properties,
    response_hybrid_additional_properties: response_hybrid_additional_properties,
    request_hybrid_additional_properties: request_hybrid_additional_properties,
  }
end

.build_request_response(env, response_array) ⇒ Object



15
16
17
18
19
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 15

def self.build_request_response(env, response_array)
  request = ActionDispatch::Request.new(env)
  request.body.rewind if request.body.respond_to?(:rewind)
  [request, ActionDispatch::TestResponse.new(*response_array)]
end

.coerce_example_mode_value(value, example) ⇒ Object

Raises:

  • (ArgumentError)


131
132
133
134
135
136
137
138
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 131

def self.coerce_example_mode_value(value, example)
  raise ArgumentError, example_mode_error(value, example) unless value.is_a?(String) || value.is_a?(Symbol)

  mode = value.to_s.strip.downcase.to_sym
  return mode if VALID_EXAMPLE_MODES.include?(mode)

  raise ArgumentError, example_mode_error(value, example)
end

.collect_openapi_metadata(metadata) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 93

def self.()
  [].tap do |result|
    result.unshift([:openapi]) if [:openapi]

    group = .fetch(:example_group) { [:parent_example_group] }
    while group
      result.unshift(group[:openapi]) if group[:openapi]
      group = group[:parent_example_group]
    end
  end
end

.example_mode_error(value, example) ⇒ Object



155
156
157
158
159
160
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 155

def self.example_mode_error(value, example)
  context = example&.full_description
  context = " (example: #{context})" if context
  "example_mode must be a Symbol/String in #{VALID_EXAMPLE_MODES.inspect} " \
    "or a Hash with :request/:response keys, got #{value.inspect}#{context}"
end

.merge_openapi_metadata(metadata) ⇒ Object



89
90
91
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 89

def self.()
  ().reduce({}, &:merge)
end

.normalize_additional_properties(hash) ⇒ Object



81
82
83
84
85
86
87
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 81

def self.normalize_additional_properties(hash)
  return nil if hash.nil? || hash.empty?

  hash.each_with_object({}) do |(path, schema), result|
    result[path.to_s] = RSpec::OpenAPI::KeyTransformer.symbolize(schema)
  end
end

.normalize_enum(enum_hash) ⇒ Object



74
75
76
77
78
79
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 74

def self.normalize_enum(enum_hash)
  return nil if enum_hash.nil? || enum_hash.empty?

  # Convert all keys to strings for consistent lookup
  enum_hash.transform_keys(&:to_s)
end

.normalize_example_mode(value, example = nil) ⇒ Object

Returns [request_mode, response_mode]. Accepts either a bare Symbol/String (applied to both sides, except :multiple which is treated as a backward-compat shorthand for { request: :single, response: :multiple } and emits a one-time deprecation warning) or a Hash with :request / :response keys.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 109

def self.normalize_example_mode(value, example = nil)
  return [:single, :single] if value.nil?

  case value
  when Hash
    [
      normalize_example_mode_hash_value(value, :request, example),
      normalize_example_mode_hash_value(value, :response, example),
    ]
  when Symbol, String
    mode = coerce_example_mode_value(value, example)
    if mode == :multiple
      warn_example_mode_multiple_shorthand
      [:single, :multiple]
    else
      [mode, mode]
    end
  else
    raise ArgumentError, example_mode_error(value, example)
  end
end

.normalize_example_mode_hash_value(hash, key, example) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 140

def self.normalize_example_mode_hash_value(hash, key, example)
  raw = hash[key]
  raw = hash[key.to_s] if raw.nil?
  return :single if raw.nil?

  coerce_example_mode_value(raw, example)
end

.resolve_additional_properties(metadata) ⇒ Object



60
61
62
63
64
65
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 60

def self.resolve_additional_properties()
  base = normalize_additional_properties([:additional_properties])
  response = normalize_additional_properties([:response_additional_properties]) || base
  request = normalize_additional_properties([:request_additional_properties]) || base
  [response, request]
end

.resolve_example_key(metadata, example) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 52

def self.resolve_example_key(, example)
  example_name = [:example_name] || RSpec::OpenAPI.example_name_builder.call(example)
  raw_example_key = [:example_key] || example_name
  example_key = RSpec::OpenAPI::ExampleKey.normalize(raw_example_key)
  example_key = 'default' if example_key.nil? || example_key.empty?
  example_key
end

.resolve_hybrid_additional_properties(metadata) ⇒ Object



67
68
69
70
71
72
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 67

def self.resolve_hybrid_additional_properties()
  base = normalize_additional_properties([:hybrid_additional_properties])
  response = normalize_additional_properties([:response_hybrid_additional_properties]) || base
  request = normalize_additional_properties([:request_hybrid_additional_properties]) || base
  [response, request]
end

.warn_example_mode_multiple_shorthandObject



148
149
150
151
152
153
# File 'lib/rspec/openapi/extractors/shared_extractor.rb', line 148

def self.warn_example_mode_multiple_shorthand
  return if @warned_example_mode_multiple_shorthand

  @warned_example_mode_multiple_shorthand = true
  Kernel.warn(EXAMPLE_MODE_MULTIPLE_SHORTHAND_WARNING)
end