diff --git a/lib/rspec/openapi.rb b/lib/rspec/openapi.rb index 63215cd..5ea9ab2 100644 --- a/lib/rspec/openapi.rb +++ b/lib/rspec/openapi.rb @@ -13,6 +13,7 @@ require 'rspec/openapi/key_transformer' require 'rspec/openapi/shared_hooks' require 'rspec/openapi/extractors' +require 'rspec/openapi/extractors/shared_extractor' require 'rspec/openapi/extractors/rack' begin @@ -37,7 +38,9 @@ module RSpec::OpenAPI @title = File.basename(Dir.pwd) @comment = nil @enable_example = true + @enable_examples = false @description_builder = ->(example) { example.description } + @examples_description_builder = ->(example) { example.description } @summary_builder = ->(example) { example.metadata[:summary] } @tags_builder = ->(example) { example.metadata[:tags] } @info = {} @@ -60,7 +63,9 @@ class << self :title, :comment, :enable_example, + :enable_examples, :description_builder, + :examples_description_builder, :summary_builder, :tags_builder, :info, diff --git a/lib/rspec/openapi/extractors/hanami.rb b/lib/rspec/openapi/extractors/hanami.rb index ef3a9fd..541feb3 100644 --- a/lib/rspec/openapi/extractors/hanami.rb +++ b/lib/rspec/openapi/extractors/hanami.rb @@ -52,14 +52,9 @@ class << RSpec::OpenAPI::Extractors::Hanami = Object.new # @param [RSpec::Core::Example] example # @return Array def request_attributes(request, example) - metadata = example.metadata[:openapi] || {} - summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) - tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) - operation_id = metadata[:operation_id] - required_request_params = metadata[:required_request_params] || [] - security = metadata[:security] - description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) - deprecated = metadata[:deprecated] + summary, tags, operation_id, required_request_params, security, description, deprecated, enable_examples, + example_description = SharedExtractor.attributes(example) + path = request.path route = Hanami.app.router.recognize(request.path, method: request.method) @@ -74,7 +69,8 @@ def request_attributes(request, example) raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params)) - [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] + [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, + enable_examples, example_description,] end # @param [RSpec::ExampleGroups::*] context diff --git a/lib/rspec/openapi/extractors/rack.rb b/lib/rspec/openapi/extractors/rack.rb index 92d20f5..e0ceec0 100644 --- a/lib/rspec/openapi/extractors/rack.rb +++ b/lib/rspec/openapi/extractors/rack.rb @@ -6,18 +6,15 @@ class << RSpec::OpenAPI::Extractors::Rack = Object.new # @param [RSpec::Core::Example] example # @return Array def request_attributes(request, example) - metadata = example.metadata[:openapi] || {} - summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) - tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) - operation_id = metadata[:operation_id] - required_request_params = metadata[:required_request_params] || [] - security = metadata[:security] - description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) - deprecated = metadata[:deprecated] + summary, tags, operation_id, required_request_params, security, description, deprecated, enable_examples, + example_description = SharedExtractor.attributes(example) + raw_path_params = request.path_parameters path = request.path summary ||= "#{request.method} #{path}" - [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] + + [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, + enable_examples, example_description,] end # @param [RSpec::ExampleGroups::*] context diff --git a/lib/rspec/openapi/extractors/rails.rb b/lib/rspec/openapi/extractors/rails.rb index ef269fb..3ba2406 100644 --- a/lib/rspec/openapi/extractors/rails.rb +++ b/lib/rspec/openapi/extractors/rails.rb @@ -6,15 +6,8 @@ class << RSpec::OpenAPI::Extractors::Rails = Object.new # @param [RSpec::Core::Example] example # @return Array def request_attributes(request, example) - metadata = example.metadata[:openapi] || {} - summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) - tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) - operation_id = metadata[:operation_id] - required_request_params = metadata[:required_request_params] || [] - security = metadata[:security] - description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) - deprecated = metadata[:deprecated] - raw_path_params = request.path_parameters + summary, tags, operation_id, required_request_params, security, description, deprecated, enable_examples, + example_description = SharedExtractor.attributes(example) # Reverse the destructive modification by Rails https://github.com/rails/rails/blob/v6.0.3.4/actionpack/lib/action_dispatch/journey/router.rb#L33-L41 fixed_request = request.dup @@ -32,7 +25,8 @@ def request_attributes(request, example) summary ||= "#{request.method} #{path}" - [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] + [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, + enable_examples, example_description,] end # @param [RSpec::ExampleGroups::*] context diff --git a/lib/rspec/openapi/extractors/shared_extractor.rb b/lib/rspec/openapi/extractors/shared_extractor.rb new file mode 100644 index 0000000..74761fa --- /dev/null +++ b/lib/rspec/openapi/extractors/shared_extractor.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class SharedExtractor + def self.attributes(example) + metadata = example.metadata[:openapi] || {} + summary = metadata[:summary] || RSpec::OpenAPI.summary_builder.call(example) + tags = metadata[:tags] || RSpec::OpenAPI.tags_builder.call(example) + operation_id = metadata[:operation_id] + required_request_params = metadata[:required_request_params] || [] + security = metadata[:security] + description = metadata[:description] || RSpec::OpenAPI.description_builder.call(example) + deprecated = metadata[:deprecated] + enable_examples = metadata[:enable_examples] + example_description = if enable_examples + metadata[:example_description] || RSpec::OpenAPI.examples_description_builder.call(example) + end + + [summary, tags, operation_id, required_request_params, security, description, deprecated, enable_examples, + example_description,] + end +end diff --git a/lib/rspec/openapi/key_transformer.rb b/lib/rspec/openapi/key_transformer.rb index dcf5924..28e0f40 100644 --- a/lib/rspec/openapi/key_transformer.rb +++ b/lib/rspec/openapi/key_transformer.rb @@ -4,7 +4,28 @@ class << RSpec::OpenAPI::KeyTransformer = Object.new def symbolize(value) case value when Hash - value.to_h { |k, v| [k.to_sym, symbolize(v)] } + value.to_h do |k, v| + if k.to_sym == :examples + [k.to_sym, symbolize_examples(v)] + else + [k.to_sym, symbolize(v)] + end + end + when Array + value.map { |v| symbolize(v) } + else + value + end + end + + def symbolize_examples(value) + case value + when Hash + value.to_h do |k, v| + k = k.downcase.tr(' ', '_') unless k.is_a?(Symbol) + + [k.to_sym, symbolize(v)] + end when Array value.map { |v| symbolize(v) } else diff --git a/lib/rspec/openapi/record.rb b/lib/rspec/openapi/record.rb index d336fdc..d82130d 100644 --- a/lib/rspec/openapi/record.rb +++ b/lib/rspec/openapi/record.rb @@ -13,8 +13,10 @@ :tags, # @param [Array] - ["Status"] :operation_id, # @param [String] - "request-1234" :description, # @param [String] - "returns a status" + :example_description, # @param [String] - "returns a status" :security, # @param [Array] - [{securityScheme1: []}] :deprecated, # @param [Boolean] - true + :enable_examples, # @param [Boolean] - true :status, # @param [Integer] - 200 :response_body, # @param [Object] - {"status" => "ok"} :response_headers, # @param [Array] - [["header_key1", "header_value1"], ["header_key2", "header_value2"]] diff --git a/lib/rspec/openapi/record_builder.rb b/lib/rspec/openapi/record_builder.rb index 32adf97..d00fb6a 100644 --- a/lib/rspec/openapi/record_builder.rb +++ b/lib/rspec/openapi/record_builder.rb @@ -11,8 +11,8 @@ def build(context, example:, extractor:) request, response = extractor.request_response(context) return if request.nil? - path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated = - extractor.request_attributes(request, example) + path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated, + enable_examples, example_description = extractor.request_attributes(request, example) return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) } @@ -38,6 +38,8 @@ def build(context, example:, extractor:) response_headers: response_headers, response_content_type: response.media_type, response_content_disposition: response.header['Content-Disposition'], + enable_examples: enable_examples, + example_description: example_description, ).freeze end diff --git a/lib/rspec/openapi/schema_builder.rb b/lib/rspec/openapi/schema_builder.rb index 45ca8f6..6494f13 100644 --- a/lib/rspec/openapi/schema_builder.rb +++ b/lib/rspec/openapi/schema_builder.rb @@ -15,14 +15,8 @@ def build(record) disposition = normalize_content_disposition(record.response_content_disposition) has_content = !normalize_content_type(record.response_content_type).nil? - if has_content - response[:content] = { - normalize_content_type(record.response_content_type) => { - schema: build_property(record.response_body, disposition: disposition), - example: response_example(record, disposition: disposition), - }.compact, - } - end + + response[:content] = build_content(disposition, record) if has_content end http_method = record.http_method.downcase @@ -48,6 +42,24 @@ def build(record) private + def build_content(disposition, record) + if record.enable_examples + { + normalize_content_type(record.response_content_type) => { + schema: build_property(record.response_body, disposition: disposition), + examples: { record.example_description => response_example(record, disposition: disposition) }, + }.compact, + } + else + { + normalize_content_type(record.response_content_type) => { + schema: build_property(record.response_body, disposition: disposition), + example: response_example(record, disposition: disposition), + }.compact, + } + end + end + def enrich_with_required_keys(obj) obj[:required] = obj[:properties]&.keys || [] obj diff --git a/spec/apps/hanami/doc/openapi.yaml b/spec/apps/hanami/doc/openapi.yaml index 8042346..a7d79c5 100644 --- a/spec/apps/hanami/doc/openapi.yaml +++ b/spec/apps/hanami/doc/openapi.yaml @@ -339,17 +339,40 @@ paths: - storage_size - created_at - updated_at - example: - - id: 1 - name: access - description: logs - database: - id: 2 - name: production - null_sample: - storage_size: 12.3 - created_at: '2020-07-17T00:00:00+00:00' - updated_at: '2020-07-17T00:00:00+00:00' + examples: + with_flat_query_parameters: + - id: 1 + name: access + description: logs + database: + id: 2 + name: production + null_sample: + storage_size: 12.3 + created_at: '2020-07-17T00:00:00+00:00' + updated_at: '2020-07-17T00:00:00+00:00' + with_deep_query_parameters: + - id: 1 + name: access + description: logs + database: + id: 2 + name: production + null_sample: + storage_size: 12.3 + created_at: '2020-07-17T00:00:00+00:00' + updated_at: '2020-07-17T00:00:00+00:00' + with_different_deep_query_parameters: + - id: 1 + name: access + description: logs + database: + id: 2 + name: production + null_sample: + storage_size: 12.3 + created_at: '2020-07-17T00:00:00+00:00' + updated_at: '2020-07-17T00:00:00+00:00' '401': description: does not return tables if unauthorized content: diff --git a/spec/requests/hanami_spec.rb b/spec/requests/hanami_spec.rb index 144332c..a54317b 100644 --- a/spec/requests/hanami_spec.rb +++ b/spec/requests/hanami_spec.rb @@ -51,7 +51,7 @@ RSpec.describe 'Tables', type: :request do describe '#index' do - context 'returns a list of tables' do + context 'returns a list of tables', openapi: { enable_examples: true } do it 'with flat query parameters' do get '/tables', { page: '1', per: '10' }, { 'AUTHORIZATION' => 'k0kubun', 'X_AUTHORIZATION_TOKEN' => 'token' } expect(last_response.status).to eq(200)