From 16f37c42a73ce6d3b29b80c9c643fea84253193a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= <39093711+a-chacon@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:50:17 -0400 Subject: [PATCH] feat: add @response_example tag for document examples of responses. (#43) * feat: add @response_example tag for document examples of responses. * fix: rexml update --- Gemfile.lock | 108 +++++++++--------- README.md | 17 ++- lib/oas_rails.rb | 3 + lib/oas_rails/builders/responses_builder.rb | 5 +- lib/oas_rails/yard/example_tag.rb | 12 ++ lib/oas_rails/yard/oas_rails_factory.rb | 21 +++- .../yard/request_body_example_tag.rb | 5 +- lib/oas_rails/yard/response_example_tag.rb | 12 ++ lib/oas_rails/yard/response_tag.rb | 1 + .../dummy/app/controllers/users_controller.rb | 2 + .../oas_rails/yard/oas_rails_factory_test.rb | 18 +++ 11 files changed, 144 insertions(+), 60 deletions(-) create mode 100644 lib/oas_rails/yard/example_tag.rb create mode 100644 lib/oas_rails/yard/response_example_tag.rb create mode 100644 test/lib/oas_rails/yard/oas_rails_factory_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 51f5945..d88e13e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,29 +10,29 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (7.2.0) - actionpack (= 7.2.0) - activesupport (= 7.2.0) + actioncable (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.0) - actionpack (= 7.2.0) - activejob (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + actionmailbox (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) mail (>= 2.8.0) - actionmailer (7.2.0) - actionpack (= 7.2.0) - actionview (= 7.2.0) - activejob (= 7.2.0) - activesupport (= 7.2.0) + actionmailer (7.2.1) + actionpack (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activesupport (= 7.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.0) - actionview (= 7.2.0) - activesupport (= 7.2.0) + actionpack (7.2.1) + actionview (= 7.2.1) + activesupport (= 7.2.1) nokogiri (>= 1.8.5) racc rack (>= 2.2.4, < 3.2) @@ -41,35 +41,35 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.0) - actionpack (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + actiontext (7.2.1) + actionpack (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.0) - activesupport (= 7.2.0) + actionview (7.2.1) + activesupport (= 7.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.0) - activesupport (= 7.2.0) + activejob (7.2.1) + activesupport (= 7.2.1) globalid (>= 0.3.6) - activemodel (7.2.0) - activesupport (= 7.2.0) - activerecord (7.2.0) - activemodel (= 7.2.0) - activesupport (= 7.2.0) + activemodel (7.2.1) + activesupport (= 7.2.1) + activerecord (7.2.1) + activemodel (= 7.2.1) + activesupport (= 7.2.1) timeout (>= 0.4.0) - activestorage (7.2.0) - actionpack (= 7.2.0) - activejob (= 7.2.0) - activerecord (= 7.2.0) - activesupport (= 7.2.0) + activestorage (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activesupport (= 7.2.1) marcel (~> 1.0) - activesupport (7.2.0) + activesupport (7.2.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -169,20 +169,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.2.0) - actioncable (= 7.2.0) - actionmailbox (= 7.2.0) - actionmailer (= 7.2.0) - actionpack (= 7.2.0) - actiontext (= 7.2.0) - actionview (= 7.2.0) - activejob (= 7.2.0) - activemodel (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + rails (7.2.1) + actioncable (= 7.2.1) + actionmailbox (= 7.2.1) + actionmailer (= 7.2.1) + actionpack (= 7.2.1) + actiontext (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activemodel (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) bundler (>= 1.15.0) - railties (= 7.2.0) + railties (= 7.2.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -190,9 +190,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.2.0) - actionpack (= 7.2.0) - activesupport (= 7.2.0) + railties (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -205,7 +205,7 @@ GEM regexp_parser (2.9.2) reline (0.5.9) io-console (~> 0.5) - rexml (3.3.5) + rexml (3.3.6) strscan rubocop (1.65.1) json (~> 2.3) diff --git a/README.md b/README.md index 0f41af7..4370b89 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,22 @@ Documents the responses of the endpoint and overrides the default responses foun `# @response User not found by the provided Id(404) [Hash{success: Boolean, message: String}]` -`# @response Validation errors(422) [Hash{success: Boolean, erros: Array}>}]` +`# @response Validation errors(422) [Hash{success: Boolean, errors: Array}>}]` + + + +
+@response_example + +**Structure**: `@response_example text(code) [String Hash]` + +Documents response examples of the endpoint associated to a response code. + +**Example**: + +`# @response_example Invalida Email(422) [{success: "false", errors: [{field: "email", type: "email", detail: ["Invalid email"]}] }]` + +`# @response_example Id not exists (404) [{success: "false", message: "Nothing found with the provided ID." }]`
diff --git a/lib/oas_rails.rb b/lib/oas_rails.rb index 4570d79..8d20e32 100644 --- a/lib/oas_rails.rb +++ b/lib/oas_rails.rb @@ -47,9 +47,11 @@ module Spec module YARD autoload :RequestBodyTag, 'oas_rails/yard/request_body_tag' + autoload :ExampleTag, 'oas_rails/yard/example_tag' autoload :RequestBodyExampleTag, 'oas_rails/yard/request_body_example_tag' autoload :ParameterTag, 'oas_rails/yard/parameter_tag' autoload :ResponseTag, 'oas_rails/yard/response_tag' + autoload :ResponseExampleTag, 'oas_rails/yard/response_example_tag' autoload :OasRailsFactory, 'oas_rails/yard/oas_rails_factory' end @@ -84,6 +86,7 @@ def configure_yard! 'Request body Example' => [:request_body_example, :with_request_body_example], 'Parameter' => [:parameter, :with_parameter], 'Response' => [:response, :with_response], + 'Response Example' => [:response_example, :with_response_example], 'Endpoint Tags' => [:tags], 'Summary' => [:summary], 'No Auth' => [:no_auth], diff --git a/lib/oas_rails/builders/responses_builder.rb b/lib/oas_rails/builders/responses_builder.rb index 11e4c20..7636a04 100644 --- a/lib/oas_rails/builders/responses_builder.rb +++ b/lib/oas_rails/builders/responses_builder.rb @@ -8,7 +8,10 @@ def initialize(specification) def from_oas_route(oas_route) oas_route.docstring.tags(:response).each do |tag| - @responses.add_response(ResponseBuilder.new(@specification).from_tag(tag).build) + content = ContentBuilder.new(@specification, :outgoing).with_schema(tag.schema).with_examples_from_tags(oas_route.docstring.tags(:response_example).filter { |re| re.code == tag.name }).build + response = ResponseBuilder.new(@specification).with_code(tag.name.to_i).with_description(tag.text).with_content(content).build + + @responses.add_response(response) end self diff --git a/lib/oas_rails/yard/example_tag.rb b/lib/oas_rails/yard/example_tag.rb new file mode 100644 index 0000000..af5a6da --- /dev/null +++ b/lib/oas_rails/yard/example_tag.rb @@ -0,0 +1,12 @@ +module OasRails + module YARD + class ExampleTag < ::YARD::Tags::Tag + attr_accessor :content + + def initialize(tag_name, text, content: {}) + super(tag_name, text, nil, nil) + @content = content + end + end + end +end diff --git a/lib/oas_rails/yard/oas_rails_factory.rb b/lib/oas_rails/yard/oas_rails_factory.rb index 25f5fd6..b1687ba 100644 --- a/lib/oas_rails/yard/oas_rails_factory.rb +++ b/lib/oas_rails/yard/oas_rails_factory.rb @@ -37,6 +37,15 @@ def parse_tag_with_response(tag_name, text) ResponseTag.new(tag_name, code, name, schema) end + # Parses a tag that represents a response example. + # @param tag_name [String] The name of the tag. + # @param text [String] The tag text to parse. + # @return [ResponseExampleTag] The parsed response example tag object. + def parse_tag_with_response_example(tag_name, text) + description, code, hash = extract_name_code_and_hash(text) + ResponseExampleTag.new(tag_name, description, content: hash, code:) + end + private # Reusable method for extracting description, type, and content with an option to process content. @@ -44,7 +53,7 @@ def parse_tag_with_response(tag_name, text) # @param process_content [Boolean] Whether to evaluate the content as a hash. # @return [Array] An array containing the description, type, and content or remaining text. def extract_description_type_and_content(text, process_content: false) - match = text.match(/^(.*?)\s*\[(.*?)\]\s*(.*)$/) + match = text.match(/^(.*?)\s*\[(.*)\]\s*(.*)$/) raise ArgumentError, "Invalid tag format: #{text}" if match.nil? description = match[1].strip @@ -84,6 +93,16 @@ def extract_name_code_and_schema(text) [name, code, schema] end + # Specific method to extract name, code, and hash for responses examples. + # @param text [String] The text to parse. + # @return [Array] An array containing the name, code, and schema. + def extract_name_code_and_hash(text) + name, code = extract_text_and_parentheses_content(text) + _, type, = extract_description_type_and_content(text) + hash = eval_content(type) + [name, code, hash] + end + # Evaluates a string as a hash, handling errors gracefully. # @param content [String] The content string to evaluate. # @return [Hash] The evaluated hash, or an empty hash if an error occurs. diff --git a/lib/oas_rails/yard/request_body_example_tag.rb b/lib/oas_rails/yard/request_body_example_tag.rb index a8bc1ef..87cc3a8 100644 --- a/lib/oas_rails/yard/request_body_example_tag.rb +++ b/lib/oas_rails/yard/request_body_example_tag.rb @@ -1,11 +1,10 @@ module OasRails module YARD - class RequestBodyExampleTag < ::YARD::Tags::Tag + class RequestBodyExampleTag < ExampleTag attr_accessor :content def initialize(tag_name, text, content: {}) - super(tag_name, text, nil, nil) - @content = content + super end end end diff --git a/lib/oas_rails/yard/response_example_tag.rb b/lib/oas_rails/yard/response_example_tag.rb new file mode 100644 index 0000000..b3da1ed --- /dev/null +++ b/lib/oas_rails/yard/response_example_tag.rb @@ -0,0 +1,12 @@ +module OasRails + module YARD + class ResponseExampleTag < ExampleTag + attr_accessor :code + + def initialize(tag_name, text, content: {}, code: 200) + super(tag_name, text, content:) + @code = code + end + end + end +end diff --git a/lib/oas_rails/yard/response_tag.rb b/lib/oas_rails/yard/response_tag.rb index 21b2321..7a2c6bd 100644 --- a/lib/oas_rails/yard/response_tag.rb +++ b/lib/oas_rails/yard/response_tag.rb @@ -3,6 +3,7 @@ module YARD class ResponseTag < ::YARD::Tags::Tag attr_accessor :schema + # TODO: name == code. The name MUST be changed to code for better understanding def initialize(tag_name, name, text, schema) super(tag_name, text, nil, name) @schema = schema diff --git a/test/dummy/app/controllers/users_controller.rb b/test/dummy/app/controllers/users_controller.rb index 5b08c56..3b4efa9 100644 --- a/test/dummy/app/controllers/users_controller.rb +++ b/test/dummy/app/controllers/users_controller.rb @@ -44,6 +44,8 @@ def index # @response User not found by the provided Id(404) [Hash{success: Boolean, message: String}] # @response You dont have the rigth persmissions for access to this reasource(403) [Hash{success: Boolean, message: String}] # @response A test response from an Issue(405) [Hash{message: String, data: Hash{availabilities: Array, dates: Array}}] + # @response_example Nice 405 Error(405) [{message: "Hello", data: {availabilities: ["one", "two", "three"], dates: ["10-06-2020"]}}] + # @response_example Another 405 Error (405) [{message: "another", data: {availabilities: ["three"], dates: []}}] def show render json: @user end diff --git a/test/lib/oas_rails/yard/oas_rails_factory_test.rb b/test/lib/oas_rails/yard/oas_rails_factory_test.rb new file mode 100644 index 0000000..6695b7b --- /dev/null +++ b/test/lib/oas_rails/yard/oas_rails_factory_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +module OasRails + module YARD + class OasRailsFactoryTest < ActiveSupport::TestCase + def test_parse_tag_with_response_example + response = OasRailsFactory.new.parse_tag_with_response_example(:response_example, + '405 Error(405) [{message: "Hello", data: {availabilities: ["one", "two", "three"], dates: ["10-06-2020"]}}]') + + assert response.is_a?(ResponseExampleTag) + assert_equal "405", response.code + assert_equal '405 Error', response.text + expected_hash = { message: "Hello", data: { availabilities: %w[one two three], dates: ["10-06-2020"] } } + assert_equal expected_hash, response.content + end + end + end +end