diff --git a/lib/rspec/openapi.rb b/lib/rspec/openapi.rb index aba3348..bbd42de 100644 --- a/lib/rspec/openapi.rb +++ b/lib/rspec/openapi.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'bundler/setup' require 'rspec/openapi/version' require 'rspec/openapi/components_updater' require 'rspec/openapi/default_schema' @@ -13,8 +14,9 @@ require 'rspec/openapi/key_transformer' require 'rspec/openapi/extractors' require 'rspec/openapi/extractors/rack' -require 'rspec/openapi/extractors/rails' -require 'rspec/openapi/extractors/hanami' + +require 'rspec/openapi/extractors/hanami' if Bundler.load.specs.map(&:name).include?('hanami') +require 'rspec/openapi/extractors/rails' if Bundler.load.specs.map(&:name).include?('rails') 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 index f117f13..c8322ae 100644 --- a/lib/rspec/openapi/extractors.rb +++ b/lib/rspec/openapi/extractors.rb @@ -1,4 +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 index 7ff5d9e..d45238c 100644 --- a/lib/rspec/openapi/extractors/hanami.rb +++ b/lib/rspec/openapi/extractors/hanami.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true + require 'dry/inflector' require 'hanami' -require 'hanami/router/inspector' # Hanami::Router::Inspector original code class Inspector @@ -38,22 +38,18 @@ def call(verb, path) InspectorAnalyzer = Inspector.new # Monkey-patch hanami-router -module Hanami - class Slice - module ClassMethods - def router(inspector: InspectorAnalyzer) - raise SliceLoadError, "#{self} must be prepared before loading the router" unless prepared? - - @_mutex.synchronize do - @_router ||= load_router(inspector: inspector) - end - end +module Hanami::Slice::ClassMethods + def router(inspector: InspectorAnalyzer) + raise SliceLoadError, "#{self} must be prepared before loading the router" unless prepared? + + @_mutex.synchronize do + @_router ||= load_router(inspector: inspector) end end end +# Extractor for hanami class << RSpec::OpenAPI::Extractors::Hanami = Object.new - # @param [RSpec::ExampleGroups::*] context # @param [RSpec::Core::Example] example # @return Array @@ -69,8 +65,8 @@ def request_attributes(request, example) path = request.path route = Hanami.app.router.recognize(request.path, method: request.method) - # binding.irb unless route.params.empty? - raw_path_params = route.params.filter { |key, value| number_or_nil(value) } + + raw_path_params = route.params.filter { |_key, value| number_or_nil(value) } result = InspectorAnalyzer.call(request.method, add_id(path, route)) diff --git a/lib/rspec/openapi/extractors/rack.rb b/lib/rspec/openapi/extractors/rack.rb index 4209c6a..92d20f5 100644 --- a/lib/rspec/openapi/extractors/rack.rb +++ b/lib/rspec/openapi/extractors/rack.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Extractor for rack class << RSpec::OpenAPI::Extractors::Rack = Object.new # @param [RSpec::ExampleGroups::*] context # @param [RSpec::Core::Example] example diff --git a/lib/rspec/openapi/extractors/rails.rb b/lib/rspec/openapi/extractors/rails.rb index 5222bcd..ef269fb 100644 --- a/lib/rspec/openapi/extractors/rails.rb +++ b/lib/rspec/openapi/extractors/rails.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Extractor for rails class << RSpec::OpenAPI::Extractors::Rails = Object.new # @param [RSpec::ExampleGroups::*] context # @param [RSpec::Core::Example] example diff --git a/lib/rspec/openapi/minitest_hooks.rb b/lib/rspec/openapi/minitest_hooks.rb index 7f00f61..53b7ade 100644 --- a/lib/rspec/openapi/minitest_hooks.rb +++ b/lib/rspec/openapi/minitest_hooks.rb @@ -13,11 +13,25 @@ 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: find_extractor) RSpec::OpenAPI.path_records[path] << record if record end result end + + def find_extractor + if Bundler.load.specs.map(&:name).include?('rails') && defined?(Rails) && + Rails.respond_to?(:application) && Rails.application + RSpec::OpenAPI::Extractors::Rails + elsif Bundler.load.specs.map(&:name).include?('hanami') && 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 module ActivateOpenApiClassMethods diff --git a/lib/rspec/openapi/rspec_hooks.rb b/lib/rspec/openapi/rspec_hooks.rb index a9cd679..f3c5202 100644 --- a/lib/rspec/openapi/rspec_hooks.rb +++ b/lib/rspec/openapi/rspec_hooks.rb @@ -21,9 +21,11 @@ end def find_extractor - if defined?(Rails) && Rails.respond_to?(:application) && Rails.application + if Bundler.load.specs.map(&:name).include?('rails') && defined?(Rails) && + Rails.respond_to?(:application) && Rails.application RSpec::OpenAPI::Extractors::Rails - elsif defined?(Hanami) + elsif Bundler.load.specs.map(&:name).include?('hanami') && defined?(Hanami) && + Hanami.respond_to?(:app) && Hanami.app? RSpec::OpenAPI::Extractors::Hanami # elsif defined?(Roda) # some Roda extractor diff --git a/spec/apps/hanami/doc/openapi.json b/spec/apps/hanami/doc/openapi.json new file mode 100644 index 0000000..2da807b --- /dev/null +++ b/spec/apps/hanami/doc/openapi.json @@ -0,0 +1,1089 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "OpenAPI Documentation", + "version": "1.0.0", + "description": "My beautiful hanami API", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "servers": [ + { + "url": "http://localhost:3000" + } + ], + "paths": { + "/images": { + "get": { + "summary": "index", + "tags": [ + "Image" + ], + "responses": { + "200": { + "description": "can return an object with an attribute of empty array", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + } + } + }, + "required": [ + "name", + "tags" + ] + } + }, + "example": [ + { + "name": "file.png", + "tags": [ + + ] + } + ] + } + } + } + } + } + }, + "/images/upload": { + "post": { + "summary": "upload", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "image" + ] + }, + "example": { + "image": "test.png" + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload_multiple": { + "post": { + "summary": "upload_multiple", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "images": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + } + }, + "required": [ + "images" + ] + }, + "example": { + "images": [ + "test.png", + "test.png" + ] + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload multiple", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload_multiple_nested": { + "post": { + "summary": "upload_multiple_nested", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "image" + ] + } + } + }, + "required": [ + "images" + ] + }, + "example": { + "images": [ + { + "image": "test.png" + }, + { + "image": "test.png" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload multiple nested", + "content": { + "image/png": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/images/upload_nested": { + "post": { + "summary": "upload_nested", + "tags": [ + "Image" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "nested_image": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "binary" + }, + "caption": { + "type": "string" + } + }, + "required": [ + "image", + "caption" + ] + } + }, + "required": [ + "nested_image" + ] + }, + "example": { + "nested_image": { + "image": "test.png", + "caption": "Some caption" + } + } + } + } + }, + "responses": { + "200": { + "description": "returns a image payload with upload nested", + "content": { + "image/png": { + "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": "example", + "tags": [ + "Eng" + ], + "responses": { + "200": { + "description": "returns the block content", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "AN ENGINE TEST" + } + } + } + } + } + }, + "/my_engine/test": { + "get": { + "summary": "GET /my_engine/test", + "tags": [ + + ], + "responses": { + "200": { + "description": "returns some content from the engine", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "ANOTHER TEST" + } + } + } + } + } + }, + "/secret_items": { + "get": { + "summary": "index", + "tags": [ + "SecretItem" + ], + "security": [ + { + "SecretApiKeyAuth": [ + + ] + } + ], + "responses": { + "200": { + "description": "authorizes with secret key", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ] + }, + "example": { + "items": [ + "secrets" + ] + } + } + } + } + } + } + }, + "/tables": { + "get": { + "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", + "headers": { + "X-Cursor": { + "schema": { + "type": "integer" + } + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "null_sample": { + "nullable": true + }, + "storage_size": { + "type": "number", + "format": "float" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "database", + "null_sample", + "storage_size", + "created_at", + "updated_at" + ] + } + }, + "example": [ + { + "id": 1, + "name": "access", + "description": "logs", + "database": { + "id": 2, + "name": "production" + }, + "null_sample": null, + "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": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "example": { + "message": "Unauthorized" + } + } + } + } + } + }, + "post": { + "summary": "create", + "tags": [ + "Table" + ], + "responses": { + "201": { + "description": "returns a table", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "null_sample": { + "nullable": true + }, + "storage_size": { + "type": "number", + "format": "float" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "database", + "null_sample", + "storage_size", + "created_at", + "updated_at" + ] + }, + "example": { + "id": 1, + "name": "access", + "description": "logs", + "database": { + "id": 2, + "name": "production" + }, + "null_sample": null, + "storage_size": 12.3, + "created_at": "2020-07-17T00:00:00+00:00", + "updated_at": "2020-07-17T00:00:00+00:00" + } + } + } + }, + "422": { + "description": "fails to create a table (2)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + }, + "example": { + "error": "invalid name parameter" + } + } + } + } + }, + "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/{id}": { + "delete": { + "summary": "destroy", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 1 + } + ], + "responses": { + "200": { + "description": "returns a table", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "null_sample": { + "nullable": true + }, + "storage_size": { + "type": "number", + "format": "float" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "database", + "null_sample", + "storage_size", + "created_at", + "updated_at" + ] + }, + "example": { + "id": 1, + "name": "access", + "description": "logs", + "database": { + "id": 2, + "name": "production" + }, + "null_sample": null, + "storage_size": 12.3, + "created_at": "2020-07-17T00:00:00+00:00", + "updated_at": "2020-07-17T00:00:00+00:00" + } + } + } + }, + "202": { + "description": "returns no content if specified" + } + }, + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "no_content": { + "type": "string" + } + } + }, + "example": { + "no_content": "true" + } + } + } + } + }, + "get": { + "summary": "show", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 2 + } + ], + "responses": { + "200": { + "description": "returns a table", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "null_sample": { + "nullable": true + }, + "storage_size": { + "type": "number", + "format": "float" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "database", + "null_sample", + "storage_size", + "created_at", + "updated_at" + ] + }, + "example": { + "id": 1, + "name": "access", + "description": "logs", + "database": { + "id": 2, + "name": "production" + }, + "null_sample": null, + "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 a table if unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "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": "update", + "tags": [ + "Table" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + }, + "example": 1 + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "example": { + "name": "test" + } + } + } + }, + "responses": { + "200": { + "description": "returns a table", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "database": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "null_sample": { + "nullable": true + }, + "storage_size": { + "type": "number", + "format": "float" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "description", + "database", + "null_sample", + "storage_size", + "created_at", + "updated_at" + ] + }, + "example": { + "id": 1, + "name": "access", + "description": "logs", + "database": { + "id": 2, + "name": "production" + }, + "null_sample": null, + "storage_size": 12.3, + "created_at": "2020-07-17T00:00:00+00:00", + "updated_at": "2020-07-17T00:00:00+00:00" + } + } + } + } + } + } + }, + "/test_block": { + "get": { + "summary": "GET /test_block", + "tags": [ + + ], + "responses": { + "200": { + "description": "returns the block content", + "content": { + "text/plain": { + "schema": { + "type": "string" + }, + "example": "A TEST" + } + } + } + }, + "deprecated": true + } + } + }, + "components": { + "securitySchemes": { + "SecretApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "Secret-Key" + } + } + } +} \ No newline at end of file