diff --git a/Gemfile.lock b/Gemfile.lock index 33779a78..39dccc18 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH activesupport (>= 3.0.0) i18n (>= 0.1.0) json (>= 1.4.6) + multipart-parser mustache (>= 0.99.4) rspec (>= 2.14.0) @@ -54,6 +55,7 @@ GEM minitest (4.7.5) multi_json (1.8.2) multi_test (0.0.2) + multipart-parser (0.1.1) mustache (0.99.5) nokogiri (1.6.0) mini_portile (~> 0.5.0) diff --git a/README.md b/README.md index 390bd05d..624b6492 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,10 @@ RspecApiDocumentation.configure do |config| # Allows you to filter out headers that are not needed in the cURL request, # such as "Host" and "Cookie". Set as an array. config.curl_headers_to_filter = nil + + # Allows you to filter out headers with empty values in the generated cURL. + # Set as a boolean. + config.curl_filter_empty_headers = false # By default examples and resources are ordered by description. Set to true keep # the source order. diff --git a/lib/rspec_api_documentation/configuration.rb b/lib/rspec_api_documentation/configuration.rb index 94e99d2c..00144bc2 100644 --- a/lib/rspec_api_documentation/configuration.rb +++ b/lib/rspec_api_documentation/configuration.rb @@ -55,6 +55,7 @@ def self.add_setting(name, opts = {}) } add_setting :curl_headers_to_filter, :default => nil + add_setting :curl_filter_empty_headers, :default => false add_setting :curl_host, :default => nil add_setting :keep_source_order, :default => false add_setting :api_name, :default => "API Documentation" diff --git a/lib/rspec_api_documentation/curl.rb b/lib/rspec_api_documentation/curl.rb index 9c418a6f..025b8322 100644 --- a/lib/rspec_api_documentation/curl.rb +++ b/lib/rspec_api_documentation/curl.rb @@ -1,15 +1,25 @@ require 'active_support/core_ext/object/to_query' +require 'multipart_parser/reader' module RspecApiDocumentation + class Curl < Struct.new(:method, :path, :data, :headers) attr_accessor :host - def output(config_host, config_headers_to_filer = nil) + def output(config_host, config_headers_to_filter = nil, config_filter_empty_headers = false) self.host = config_host - @config_headers_to_filer = Array(config_headers_to_filer) + @config_headers_to_filter = config_headers_to_filter + @config_filter_empty_headers = config_filter_empty_headers + + append_filters + send(method.downcase) end + def self.format_header(header) + header.gsub(/^HTTP_/, '').titleize.split.join("-") + end + def post "curl \"#{url}\" #{post_data} -X POST #{headers}" end @@ -38,8 +48,14 @@ def url "#{host}#{path}" end + alias :original_headers :headers + + def is_multipart? + original_headers["Content-Type"].try(:match, /\Amultipart\/form-data/) + end + def headers - filter_headers(super).map do |k, v| + filter_headers(super).reject{ |k, v| k.eql?("Content-Type") && v.match(/multipart\/form-data/) }.map do |k, v| "\\\n\t-H \"#{format_full_header(k, v)}\"" end.join(" ") end @@ -49,28 +65,68 @@ def get_data end def post_data - escaped_data = data.to_s.gsub("'", "\\u0027") - "-d '#{escaped_data}'" + if is_multipart? + boundary = MultipartParser::Reader.extract_boundary_value(original_headers["Content-Type"]) + reader = MultipartParser::Reader.new(boundary) + flags = [] + reader.on_part do |part| + value = "" + unless part.filename.nil? + value = "@#{part.filename};type=#{part.mime}" + else + part.on_data do |data| + value += data + end + end + part.on_end do + flags.push "-F '#{part.name}=#{value.gsub("'", "\\u0027")}'" + end + end + reader.write(data.to_s) + flags.join(" ") + else + escaped_data = data.to_s.gsub("'", "\\u0027") + "-d '#{escaped_data}'" + end end private - def format_header(header) - header.gsub(/^HTTP_/, '').titleize.split.join("-") + def append_filters + @filters = Array.new + @filters << ConfiguredHeadersFilter.new(@config_headers_to_filter) if @config_headers_to_filter + @filters << EmptyHeaderFilter.new if @config_filter_empty_headers end def format_full_header(header, value) - formatted_value = value.gsub(/"/, "\\\"") - "#{format_header(header)}: #{formatted_value}" + formatted_value = value ? value.gsub(/"/, "\\\"") : '' + "#{Curl.format_header(header)}: #{formatted_value}" end def filter_headers(headers) - if !@config_headers_to_filer.empty? - headers.reject do |header| - @config_headers_to_filer.include?(format_header(header)) - end - else - headers + @filters.inject(headers) do |headers, filter| + filter.call(headers) + end + end + end + + class EmptyHeaderFilter + def call(headers) + headers.reject do |header, value| + value.blank? + end + end + end + + class ConfiguredHeadersFilter + + def initialize(headers_to_filter) + @headers_to_filter = Array(headers_to_filter) + end + + def call(headers) + headers.reject do |header| + @headers_to_filter.include?(Curl.format_header(header)) end end end diff --git a/lib/rspec_api_documentation/example.rb b/lib/rspec_api_documentation/example.rb index 6062d7d9..97b0042e 100644 --- a/lib/rspec_api_documentation/example.rb +++ b/lib/rspec_api_documentation/example.rb @@ -43,7 +43,17 @@ def explanation end def requests - metadata[:requests] || [] + reqs = metadata[:requests] || [] + reqs.each do |req| + if req[:request_headers]["Content-Type"].try(:match, /\Amultipart\/form-data/) + i = req[:request_body].index /^Content-Disposition: form-data.* filename=\"/ + i = req[:request_body].index "\r\n\r\n", i unless i.nil? + unless i.nil? + req[:request_body] = "#{req[:request_body][0..i+3]}...[truncated file data]..." + end + end + end + reqs end end end diff --git a/lib/rspec_api_documentation/writers/json_writer.rb b/lib/rspec_api_documentation/writers/json_writer.rb index 2b7db626..822523a3 100644 --- a/lib/rspec_api_documentation/writers/json_writer.rb +++ b/lib/rspec_api_documentation/writers/json_writer.rb @@ -63,6 +63,7 @@ def initialize(example, configuration) @example = example @host = configuration.curl_host @filter_headers = configuration.curl_headers_to_filter + @filter_empty = configuration.curl_filter_empty_headers end def method_missing(method, *args, &block) @@ -98,7 +99,7 @@ def requests super.map do |hash| if @host if hash[:curl].is_a? RspecApiDocumentation::Curl - hash[:curl] = hash[:curl].output(@host, @filter_headers) + hash[:curl] = hash[:curl].output(@host, @filter_headers, @filter_empty) end else hash[:curl] = nil diff --git a/rspec_api_documentation.gemspec b/rspec_api_documentation.gemspec index 20400277..01979c5f 100644 --- a/rspec_api_documentation.gemspec +++ b/rspec_api_documentation.gemspec @@ -18,6 +18,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency "i18n", ">= 0.1.0" s.add_runtime_dependency "mustache", ">= 0.99.4" s.add_runtime_dependency "json", ">= 1.4.6" + s.add_runtime_dependency "multipart-parser" s.add_development_dependency "fakefs" s.add_development_dependency "sinatra" diff --git a/spec/curl_spec.rb b/spec/curl_spec.rb index 9dd0d863..6a9271ab 100644 --- a/spec/curl_spec.rb +++ b/spec/curl_spec.rb @@ -15,7 +15,8 @@ "HTTP_X_HEADER" => "header", "HTTP_AUTHORIZATION" => %{Token token="mytoken"}, "HTTP_HOST" => "example.org", - "HTTP_COOKIES" => "" + "HTTP_COOKIES" => "", + "HTTP_SERVER" => nil } end @@ -26,6 +27,7 @@ it { should =~ /-H "Accept: application\/json"/ } it { should =~ /-H "X-Header: header"/ } it { should =~ /-H "Authorization: Token token=\\"mytoken\\""/ } + it { should =~ /-H "Server: "/ } it { should_not =~ /-H "Host: example\.org"/ } it { should_not =~ /-H "Cookies: "/ } @@ -171,4 +173,34 @@ curl.output(host) end end + + describe "Filter empty headers" do + subject { curl.output(host, nil, true) } + + let(:method) { "POST" } + let(:path) { "/orders" } + let(:data) { "order%5Bsize%5D=large&order%5Btype%5D=cart" } + let(:headers) do + { + "HTTP_ACCEPT" => "application/json", + "HTTP_X_HEADER" => "header", + "HTTP_AUTHORIZATION" => %{Token token="mytoken"}, + "HTTP_HOST" => "example.org", + "HTTP_COOKIES" => "", + "HTTP_SERVER" => nil + } + end + + it { should =~ /^curl/ } + it { should =~ /http:\/\/example\.com\/orders/ } + it { should =~ /-d 'order%5Bsize%5D=large&order%5Btype%5D=cart'/ } + it { should =~ /-X POST/ } + it { should =~ /-H "Accept: application\/json"/ } + it { should =~ /-H "X-Header: header"/ } + it { should =~ /-H "Authorization: Token token=\\"mytoken\\""/ } + it { should =~ /-H "Host: example\.org"/ } + it { should_not =~ /-H "Server: "/ } + it { should_not =~ /-H "Cookies: "/ } + + end end