diff --git a/lib/rspec/openapi.rb b/lib/rspec/openapi.rb index 826eb32..63215cd 100644 --- a/lib/rspec/openapi.rb +++ b/lib/rspec/openapi.rb @@ -11,6 +11,23 @@ require 'rspec/openapi/schema_cleaner' require 'rspec/openapi/schema_sorter' require 'rspec/openapi/key_transformer' +require 'rspec/openapi/shared_hooks' +require 'rspec/openapi/extractors' +require 'rspec/openapi/extractors/rack' + +begin + require 'hanami' + require 'rspec/openapi/extractors/hanami' +rescue LoadError + puts 'Hanami not detected' +end + +begin + require 'rails' + require 'rspec/openapi/extractors/rails' +rescue LoadError + puts 'Rails not detected' +end require 'rspec/openapi/minitest_hooks' if Object.const_defined?('Minitest') require 'rspec/openapi/rspec_hooks' if ENV['OPENAPI'] && Object.const_defined?('RSpec') diff --git a/lib/rspec/openapi/extractors.rb b/lib/rspec/openapi/extractors.rb new file mode 100644 index 0000000..c8322ae --- /dev/null +++ b/lib/rspec/openapi/extractors.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Create namespace +module RSpec::OpenAPI::Extractors +end diff --git a/lib/rspec/openapi/extractors/hanami.rb b/lib/rspec/openapi/extractors/hanami.rb new file mode 100644 index 0000000..ef3a9fd --- /dev/null +++ b/lib/rspec/openapi/extractors/hanami.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'dry/inflector' +require 'hanami' + +# https://github.com/hanami/router/blob/97f75b8529574bd4ff23165460e82a6587bc323c/lib/hanami/router/inspector.rb#L13 +class Inspector + attr_accessor :routes, :inflector + + def initialize(routes: []) + @routes = routes + @inflector = Dry::Inflector.new + end + + def add_route(route) + routes.push(route) + end + + def call(verb, path) + route = routes.find { |r| r.http_method == verb && r.path == path } + + if route.to.is_a?(Proc) + { + tags: [], + summary: "#{verb} #{path}", + } + else + data = route.to.split('.') + + { + tags: [inflector.classify(data[0])], + summary: data[1], + } + end + end +end + +InspectorAnalyzer = Inspector.new + +# Add default parameter to load inspector before test cases run +module InspectorAnalyzerPrepender + def router(inspector: InspectorAnalyzer) + super + end +end + +Hanami::Slice::ClassMethods.prepend(InspectorAnalyzerPrepender) + +# Extractor for hanami +class << RSpec::OpenAPI::Extractors::Hanami = Object.new + # @param [RSpec::ExampleGroups::*] context + # @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] + path = request.path + + route = Hanami.app.router.recognize(request.path, method: request.method) + + raw_path_params = route.params.filter { |_key, value| number_or_nil(value) } + + result = InspectorAnalyzer.call(request.method, add_id(path, route)) + + summary ||= result[:summary] + tags ||= result[:tags] + path = add_openapi_id(path, route) + + 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] + end + + # @param [RSpec::ExampleGroups::*] context + def request_response(context) + request = ActionDispatch::Request.new(context.last_request.env) + request.body.rewind if request.body.respond_to?(:rewind) + response = ActionDispatch::TestResponse.new(*context.last_response.to_a) + + [request, response] + end + + def add_id(path, route) + return path if route.params.empty? + + route.params.each_pair do |key, value| + next unless number_or_nil(value) + + path = path.sub("/#{value}", "/:#{key}") + end + + path + end + + def add_openapi_id(path, route) + return path if route.params.empty? + + route.params.each_pair do |key, value| + next unless number_or_nil(value) + + path = path.sub("/#{value}", "/{#{key}}") + end + + path + end + + def number_or_nil(string) + Integer(string || '') + rescue ArgumentError + nil + end +end diff --git a/lib/rspec/openapi/extractors/rack.rb b/lib/rspec/openapi/extractors/rack.rb new file mode 100644 index 0000000..92d20f5 --- /dev/null +++ b/lib/rspec/openapi/extractors/rack.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Extractor for rack +class << RSpec::OpenAPI::Extractors::Rack = Object.new + # @param [RSpec::ExampleGroups::*] context + # @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 + path = request.path + summary ||= "#{request.method} #{path}" + [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] + end + + # @param [RSpec::ExampleGroups::*] context + def request_response(context) + request = ActionDispatch::Request.new(context.last_request.env) + request.body.rewind if request.body.respond_to?(:rewind) + response = ActionDispatch::TestResponse.new(*context.last_response.to_a) + + [request, response] + end +end diff --git a/lib/rspec/openapi/extractors/rails.rb b/lib/rspec/openapi/extractors/rails.rb new file mode 100644 index 0000000..ef269fb --- /dev/null +++ b/lib/rspec/openapi/extractors/rails.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Extractor for rails +class << RSpec::OpenAPI::Extractors::Rails = Object.new + # @param [RSpec::ExampleGroups::*] context + # @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 + + # 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 + fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present? + + route, path = find_rails_route(fixed_request) + raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil? + + path = path.delete_suffix('(.:format)') + summary ||= route.requirements[:action] + tags ||= [route.requirements[:controller]&.classify].compact + # :controller and :action always exist. :format is added when routes is configured as such. + # TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x + raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params)) + + summary ||= "#{request.method} #{path}" + + [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] + end + + # @param [RSpec::ExampleGroups::*] context + def request_response(context) + [context.request, context.response] + end + + # @param [ActionDispatch::Request] request + def find_rails_route(request, app: Rails.application, path_prefix: '') + app.routes.router.recognize(request) do |route| + path = route.path.spec.to_s + if route.app.matches?(request) + if route.app.engine? + route, path = find_rails_route(request, app: route.app.app, path_prefix: path) + next if route.nil? + end + return [route, path_prefix + path] + end + end + + nil + end +end diff --git a/lib/rspec/openapi/minitest_hooks.rb b/lib/rspec/openapi/minitest_hooks.rb index 7f00f61..15682d5 100644 --- a/lib/rspec/openapi/minitest_hooks.rb +++ b/lib/rspec/openapi/minitest_hooks.rb @@ -13,7 +13,7 @@ def run(*args) human_name = name.sub(/^test_/, '').gsub('_', ' ') example = Example.new(self, human_name, {}, file_path) path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p } - record = RSpec::OpenAPI::RecordBuilder.build(self, example: example) + record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor) RSpec::OpenAPI.path_records[path] << record if record end result diff --git a/lib/rspec/openapi/record_builder.rb b/lib/rspec/openapi/record_builder.rb index 31e7adf..32adf97 100644 --- a/lib/rspec/openapi/record_builder.rb +++ b/lib/rspec/openapi/record_builder.rb @@ -7,12 +7,12 @@ class << RSpec::OpenAPI::RecordBuilder = Object.new # @param [RSpec::ExampleGroups::*] context # @param [RSpec::Core::Example] example # @return [RSpec::OpenAPI::Record,nil] - def build(context, example:) - request, response = extract_request_response(context) + 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 = - extract_request_attributes(request, example) + extractor.request_attributes(request, example) return if RSpec::OpenAPI.ignored_paths.any? { |ignored_path| path.match?(ignored_path) } @@ -69,71 +69,6 @@ def extract_headers(request, response) [request_headers, response_headers] end - def extract_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 - path = request.path - if rails? - # 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 - fixed_request.path_info = File.join(request.script_name, request.path_info) if request.script_name.present? - - route, path = find_rails_route(fixed_request) - raise "No route matched for #{fixed_request.request_method} #{fixed_request.path_info}" if route.nil? - - path = path.delete_suffix('(.:format)') - summary ||= route.requirements[:action] - tags ||= [route.requirements[:controller]&.classify].compact - # :controller and :action always exist. :format is added when routes is configured as such. - # TODO: Use .except(:controller, :action, :format) when we drop support for Ruby 2.x - raw_path_params = raw_path_params.slice(*(raw_path_params.keys - RSpec::OpenAPI.ignored_path_params)) - end - summary ||= "#{request.method} #{path}" - [path, summary, tags, operation_id, required_request_params, raw_path_params, description, security, deprecated] - end - - def extract_request_response(context) - if rack_test?(context) - request = ActionDispatch::Request.new(context.last_request.env) - request.body.rewind if request.body.respond_to?(:rewind) - response = ActionDispatch::TestResponse.new(*context.last_response.to_a) - else - request = context.request - response = context.response - end - [request, response] - end - - def rails? - defined?(Rails) && Rails.respond_to?(:application) && Rails.application - end - - def rack_test?(context) - defined?(Rack::Test::Methods) && context.class < Rack::Test::Methods - end - - # @param [ActionDispatch::Request] request - def find_rails_route(request, app: Rails.application, path_prefix: '') - app.routes.router.recognize(request) do |route| - path = route.path.spec.to_s - if route.app.matches?(request) - if route.app.engine? - route, path = find_rails_route(request, app: route.app.app, path_prefix: path) - next if route.nil? - end - return [route, path_prefix + path] - end - end - nil - end - # workaround to get real request parameters # because ActionController::ParamsWrapper overwrites request_parameters def raw_request_params(request) diff --git a/lib/rspec/openapi/rspec_hooks.rb b/lib/rspec/openapi/rspec_hooks.rb index 499c15e..753f345 100644 --- a/lib/rspec/openapi/rspec_hooks.rb +++ b/lib/rspec/openapi/rspec_hooks.rb @@ -5,7 +5,7 @@ RSpec.configuration.after(:each) do |example| if RSpec::OpenAPI.example_types.include?(example.metadata[:type]) && example.metadata[:openapi] != false path = RSpec::OpenAPI.path.then { |p| p.is_a?(Proc) ? p.call(example) : p } - record = RSpec::OpenAPI::RecordBuilder.build(self, example: example) + record = RSpec::OpenAPI::RecordBuilder.build(self, example: example, extractor: SharedHooks.find_extractor) RSpec::OpenAPI.path_records[path] << record if record end end diff --git a/lib/rspec/openapi/shared_hooks.rb b/lib/rspec/openapi/shared_hooks.rb new file mode 100644 index 0000000..2069cea --- /dev/null +++ b/lib/rspec/openapi/shared_hooks.rb @@ -0,0 +1,13 @@ +module SharedHooks + def self.find_extractor + if defined?(Rails) && Rails.respond_to?(:application) && Rails.application + RSpec::OpenAPI::Extractors::Rails + elsif defined?(Hanami) && Hanami.respond_to?(:app) && Hanami.app? + RSpec::OpenAPI::Extractors::Hanami + # elsif defined?(Roda) + # some Roda extractor + else + RSpec::OpenAPI::Extractors::Rack + end + end +end diff --git a/spec/apps/hanami/doc/openapi.json b/spec/apps/hanami/doc/openapi.json index 9d62894..cf4e326 100644 --- a/spec/apps/hanami/doc/openapi.json +++ b/spec/apps/hanami/doc/openapi.json @@ -17,7 +17,10 @@ "paths": { "/images": { "get": { - "summary": "GET /images", + "summary": "index", + "tags": [ + "Image" + ], "responses": { "200": { "description": "can return an object with an attribute of empty array", @@ -57,27 +60,12 @@ } } }, - "/images/1": { - "get": { - "summary": "GET /images/1", - "responses": { - "200": { - "description": "returns a image payload", - "content": { - "image/png": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - } - } - } - }, "/images/upload": { "post": { - "summary": "POST /images/upload", + "summary": "upload", + "tags": [ + "Image" + ], "requestBody": { "content": { "multipart/form-data": { @@ -116,7 +104,10 @@ }, "/images/upload_multiple": { "post": { - "summary": "POST /images/upload_multiple", + "summary": "upload_multiple", + "tags": [ + "Image" + ], "requestBody": { "content": { "multipart/form-data": { @@ -161,7 +152,10 @@ }, "/images/upload_multiple_nested": { "post": { - "summary": "POST /images/upload_multiple_nested", + "summary": "upload_multiple_nested", + "tags": [ + "Image" + ], "requestBody": { "content": { "multipart/form-data": { @@ -218,7 +212,10 @@ }, "/images/upload_nested": { "post": { - "summary": "POST /images/upload_nested", + "summary": "upload_nested", + "tags": [ + "Image" + ], "requestBody": { "content": { "multipart/form-data": { @@ -270,9 +267,44 @@ } } }, + "/images/{id}": { + "get": { + "summary": "show", + "tags": [ + "Image" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 1 + } + ], + "responses": { + "200": { + "description": "returns a image payload", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, "/my_engine/eng/example": { "get": { - "summary": "GET /my_engine/eng/example", + "summary": "example", + "tags": [ + "Eng" + ], "responses": { "200": { "description": "returns the block content", @@ -291,6 +323,9 @@ "/my_engine/test": { "get": { "summary": "GET /my_engine/test", + "tags": [ + + ], "responses": { "200": { "description": "returns some content from the engine", @@ -308,7 +343,10 @@ }, "/secret_items": { "get": { - "summary": "GET /secret_items", + "summary": "index", + "tags": [ + "SecretItem" + ], "security": [ { "SecretApiKeyAuth": [ @@ -342,13 +380,94 @@ } } } + }, + "401": { + "description": "authorizes with secret key", + "content": { + "text/html": { + "schema": { + "type": "string" + }, + "example": "" + } + } } } } }, "/tables": { "get": { - "summary": "GET /tables", + "summary": "index", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "X-Authorization-Token", + "in": "header", + "required": true, + "schema": { + "type": "string" + }, + "example": "token" + }, + { + "name": "filter[name]", + "in": "query", + "required": false, + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "example": { + "name": "Example Table" + } + }, + { + "name": "filter[price]", + "in": "query", + "required": false, + "schema": { + "type": "object", + "properties": { + "price": { + "type": "string" + } + }, + "required": [ + "price" + ] + }, + "example": { + "price": "0" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "example": 1 + }, + { + "name": "per", + "in": "query", + "required": false, + "schema": { + "type": "integer" + }, + "example": 10 + } + ], "responses": { "200": { "description": "with different deep query parameters", @@ -455,107 +574,13 @@ } } } - }, - "parameters": [ - { - "name": "X-Authorization-Token", - "in": "header", - "required": true, - "schema": { - "type": "string" - }, - "example": "token" - }, - { - "name": "filter[name]", - "in": "query", - "required": false, - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ] - }, - "example": { - "name": "Example Table" - } - }, - { - "name": "filter[price]", - "in": "query", - "required": false, - "schema": { - "type": "object", - "properties": { - "price": { - "type": "string" - } - }, - "required": [ - "price" - ] - }, - "example": { - "price": "0" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer" - }, - "example": 1 - }, - { - "name": "per", - "in": "query", - "required": false, - "schema": { - "type": "integer" - }, - "example": 10 - } - ] + } }, "post": { - "summary": "POST /tables", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "database_id": { - "type": "integer" - } - }, - "required": [ - "name", - "description", - "database_id" - ] - }, - "example": { - "name": "k0kubun", - "description": "description", - "database_id": 2 - } - } - } - }, + "summary": "create", + "tags": [ + "Table" + ], "responses": { "201": { "description": "returns a table", @@ -650,12 +675,56 @@ } } } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database_id": { + "type": "integer" + } + }, + "required": [ + "name", + "description", + "database_id" + ] + }, + "example": { + "name": "k0kubun", + "description": "description", + "database_id": 2 + } + } + } } } }, - "/tables/1": { + "/tables/{id}": { "delete": { - "summary": "DELETE /tables/1", + "summary": "destroy", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 1 + } + ], "responses": { "200": { "description": "returns a table", @@ -752,7 +821,21 @@ } }, "get": { - "summary": "GET /tables/1", + "summary": "show", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 2 + } + ], "responses": { "200": { "description": "returns a table", @@ -846,11 +929,46 @@ } } } + }, + "404": { + "description": "does not return a table if not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "example": { + "message": "not found" + } + } + } } } }, "patch": { - "summary": "PATCH /tables/1", + "summary": "update", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 1 + } + ], "requestBody": { "content": { "application/x-www-form-urlencoded": { @@ -947,37 +1065,12 @@ } } }, - "/tables/2": { - "get": { - "summary": "GET /tables/2", - "responses": { - "404": { - "description": "does not return a table if not found", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - }, - "example": { - "message": "not found" - } - } - } - } - } - } - }, "/test_block": { "get": { "summary": "GET /test_block", + "tags": [ + + ], "responses": { "200": { "description": "returns the block content", diff --git a/spec/apps/hanami/doc/openapi.yaml b/spec/apps/hanami/doc/openapi.yaml index f26243a..8042346 100644 --- a/spec/apps/hanami/doc/openapi.yaml +++ b/spec/apps/hanami/doc/openapi.yaml @@ -16,7 +16,9 @@ servers: paths: "/images": get: - summary: GET /images + summary: index + tags: + - Image responses: '200': description: can return an object with an attribute of empty array @@ -38,20 +40,11 @@ paths: example: - name: file.png tags: [] - "/images/1": - get: - summary: GET /images/1 - responses: - '200': - description: returns a image payload - content: - image/png: - schema: - type: string - format: binary "/images/upload": post: - summary: POST /images/upload + summary: upload + tags: + - Image requestBody: content: multipart/form-data: @@ -75,7 +68,9 @@ paths: format: binary "/images/upload_multiple": post: - summary: POST /images/upload_multiple + summary: upload_multiple + tags: + - Image requestBody: content: multipart/form-data: @@ -103,7 +98,9 @@ paths: format: binary "/images/upload_multiple_nested": post: - summary: POST /images/upload_multiple_nested + summary: upload_multiple_nested + tags: + - Image requestBody: content: multipart/form-data: @@ -136,7 +133,9 @@ paths: format: binary "/images/upload_nested": post: - summary: POST /images/upload_nested + summary: upload_nested + tags: + - Image requestBody: content: multipart/form-data: @@ -168,9 +167,31 @@ paths: schema: type: string format: binary + "/images/{id}": + get: + summary: show + tags: + - Image + parameters: + - name: id + in: path + required: true + schema: + type: integer + example: 1 + responses: + '200': + description: returns a image payload + content: + image/png: + schema: + type: string + format: binary "/my_engine/eng/example": get: - summary: GET /my_engine/eng/example + summary: example + tags: + - Eng responses: '200': description: returns the block content @@ -182,6 +203,7 @@ paths: "/my_engine/test": get: summary: GET /my_engine/test + tags: [] responses: '200': description: returns some content from the engine @@ -192,7 +214,9 @@ paths: example: ANOTHER TEST "/secret_items": get: - summary: GET /secret_items + summary: index + tags: + - SecretItem security: - SecretApiKeyAuth: [] responses: @@ -221,7 +245,9 @@ paths: example: '' "/tables": get: - summary: GET /tables + summary: index + tags: + - Table parameters: - name: X-Authorization-Token in: header @@ -320,7 +346,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -338,7 +364,9 @@ paths: example: message: Unauthorized post: - summary: POST /tables + summary: create + tags: + - Table responses: '201': description: returns a table @@ -388,7 +416,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -425,9 +453,18 @@ paths: name: k0kubun description: description database_id: 2 - "/tables/1": + "/tables/{id}": delete: - summary: DELETE /tables/1 + summary: destroy + tags: + - Table + parameters: + - name: id + in: path + required: true + schema: + type: integer + example: 1 responses: '200': description: returns a table @@ -477,7 +514,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -494,7 +531,16 @@ paths: example: no_content: 'true' get: - summary: GET /tables/1 + summary: show + tags: + - Table + parameters: + - name: id + in: path + required: true + schema: + type: integer + example: 2 responses: '200': description: returns a table @@ -544,7 +590,7 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' @@ -561,8 +607,30 @@ paths: - message example: message: Unauthorized + '404': + description: does not return a table if not found + content: + application/json: + schema: + type: object + properties: + message: + type: string + required: + - message + example: + message: not found patch: - summary: PATCH /tables/1 + summary: update + tags: + - Table + parameters: + - name: id + in: path + required: true + schema: + type: integer + example: 1 requestBody: content: application/x-www-form-urlencoded: @@ -624,30 +692,14 @@ paths: database: id: 2 name: production - null_sample: + null_sample: storage_size: 12.3 created_at: '2020-07-17T00:00:00+00:00' updated_at: '2020-07-17T00:00:00+00:00' - "/tables/2": - get: - summary: GET /tables/2 - responses: - '404': - description: does not return a table if not found - content: - application/json: - schema: - type: object - properties: - message: - type: string - required: - - message - example: - message: not found "/test_block": get: summary: GET /test_block + tags: [] responses: '200': description: returns the block content diff --git a/spec/requests/hanami_spec.rb b/spec/requests/hanami_spec.rb index 1dadb4b..144332c 100644 --- a/spec/requests/hanami_spec.rb +++ b/spec/requests/hanami_spec.rb @@ -54,7 +54,6 @@ context 'returns a list of tables' do it 'with flat query parameters' do get '/tables', { page: '1', per: '10' }, { 'AUTHORIZATION' => 'k0kubun', 'X_AUTHORIZATION_TOKEN' => 'token' } - # binding.irb expect(last_response.status).to eq(200) end