rspec_api_documentation 4.9.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rspec_api_documentation/api_documentation.rb +2 -0
  3. data/lib/rspec_api_documentation/client_base.rb +8 -8
  4. data/lib/rspec_api_documentation/configuration.rb +12 -1
  5. data/lib/rspec_api_documentation/curl.rb +7 -2
  6. data/lib/rspec_api_documentation/dsl/endpoint/params.rb +19 -3
  7. data/lib/rspec_api_documentation/dsl/endpoint/set_param.rb +18 -5
  8. data/lib/rspec_api_documentation/dsl/endpoint.rb +44 -2
  9. data/lib/rspec_api_documentation/dsl/resource.rb +48 -1
  10. data/lib/rspec_api_documentation/dsl.rb +1 -1
  11. data/lib/rspec_api_documentation/example.rb +4 -0
  12. data/lib/rspec_api_documentation/open_api/contact.rb +9 -0
  13. data/lib/rspec_api_documentation/open_api/example.rb +7 -0
  14. data/lib/rspec_api_documentation/open_api/header.rb +12 -0
  15. data/lib/rspec_api_documentation/open_api/headers.rb +7 -0
  16. data/lib/rspec_api_documentation/open_api/helper.rb +29 -0
  17. data/lib/rspec_api_documentation/open_api/info.rb +12 -0
  18. data/lib/rspec_api_documentation/open_api/license.rb +8 -0
  19. data/lib/rspec_api_documentation/open_api/node.rb +112 -0
  20. data/lib/rspec_api_documentation/open_api/operation.rb +18 -0
  21. data/lib/rspec_api_documentation/open_api/parameter.rb +33 -0
  22. data/lib/rspec_api_documentation/open_api/path.rb +13 -0
  23. data/lib/rspec_api_documentation/open_api/paths.rb +7 -0
  24. data/lib/rspec_api_documentation/open_api/response.rb +10 -0
  25. data/lib/rspec_api_documentation/open_api/responses.rb +9 -0
  26. data/lib/rspec_api_documentation/open_api/root.rb +21 -0
  27. data/lib/rspec_api_documentation/open_api/schema.rb +15 -0
  28. data/lib/rspec_api_documentation/open_api/security_definitions.rb +7 -0
  29. data/lib/rspec_api_documentation/open_api/security_schema.rb +14 -0
  30. data/lib/rspec_api_documentation/open_api/tag.rb +9 -0
  31. data/lib/rspec_api_documentation/railtie.rb +1 -1
  32. data/lib/rspec_api_documentation/views/api_blueprint_example.rb +116 -0
  33. data/lib/rspec_api_documentation/views/api_blueprint_index.rb +105 -0
  34. data/lib/rspec_api_documentation/views/markdown_example.rb +1 -1
  35. data/lib/rspec_api_documentation/views/markup_example.rb +12 -3
  36. data/lib/rspec_api_documentation/views/markup_index.rb +2 -4
  37. data/lib/rspec_api_documentation/views/slate_index.rb +4 -0
  38. data/lib/rspec_api_documentation/writers/api_blueprint_writer.rb +29 -0
  39. data/lib/rspec_api_documentation/writers/combined_json_writer.rb +1 -1
  40. data/lib/rspec_api_documentation/writers/general_markup_writer.rb +20 -7
  41. data/lib/rspec_api_documentation/writers/json_iodocs_writer.rb +3 -2
  42. data/lib/rspec_api_documentation/writers/json_writer.rb +10 -6
  43. data/lib/rspec_api_documentation/writers/markdown_writer.rb +1 -1
  44. data/lib/rspec_api_documentation/writers/open_api_writer.rb +244 -0
  45. data/lib/rspec_api_documentation/writers/slate_writer.rb +2 -8
  46. data/lib/rspec_api_documentation.rb +29 -0
  47. data/templates/rspec_api_documentation/api_blueprint_index.mustache +80 -0
  48. data/templates/rspec_api_documentation/html_index.mustache +1 -1
  49. data/templates/rspec_api_documentation/markdown_example.mustache +4 -3
  50. data/templates/rspec_api_documentation/markdown_index.mustache +1 -0
  51. data/templates/rspec_api_documentation/slate_example.mustache +2 -2
  52. data/templates/rspec_api_documentation/slate_index.mustache +8 -0
  53. data/templates/rspec_api_documentation/textile_index.mustache +1 -0
  54. metadata +61 -3
@@ -0,0 +1,9 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class Responses < Node
4
+ CHILD_CLASS = Response
5
+
6
+ add_setting :default, :default => lambda { |responses| responses.existing_settings.size > 1 ? nil : Response.new }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class Root < Node
4
+ add_setting :swagger, :default => '2.0', :required => true
5
+ add_setting :info, :default => Info.new, :required => true, :schema => Info
6
+ add_setting :host, :default => 'localhost:3000'
7
+ add_setting :basePath
8
+ add_setting :schemes, :default => %w(http https)
9
+ add_setting :consumes, :default => %w(application/json application/xml)
10
+ add_setting :produces, :default => %w(application/json application/xml)
11
+ add_setting :paths, :default => Paths.new, :required => true, :schema => Paths
12
+ add_setting :definitions
13
+ add_setting :parameters
14
+ add_setting :responses
15
+ add_setting :securityDefinitions, :schema => SecurityDefinitions
16
+ add_setting :security
17
+ add_setting :tags, :default => [], :schema => [Tag]
18
+ add_setting :externalDocs
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class Schema < Node
4
+ add_setting :format
5
+ add_setting :title
6
+ add_setting :description, :default => ''
7
+ add_setting :required
8
+ add_setting :enum
9
+ add_setting :type
10
+ add_setting :items
11
+ add_setting :properties
12
+ add_setting :example
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class SecurityDefinitions < Node
4
+ CHILD_CLASS = SecuritySchema
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class SecuritySchema < Node
4
+ add_setting :type, :required => true
5
+ add_setting :description, :default => ''
6
+ add_setting :name
7
+ add_setting :in
8
+ add_setting :flow
9
+ add_setting :authorizationUrl
10
+ add_setting :tokenUrl
11
+ add_setting :scopes
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ module RspecApiDocumentation
2
+ module OpenApi
3
+ class Tag < Node
4
+ add_setting :name, :required => true
5
+ add_setting :description, :default => ''
6
+ add_setting :externalDocs
7
+ end
8
+ end
9
+ end
@@ -1,7 +1,7 @@
1
1
  module RspecApiDocumentation
2
2
  class Railtie < Rails::Railtie
3
3
  rake_tasks do
4
- load "tasks/docs.rake"
4
+ load File.join(File.dirname(__FILE__), '../tasks/docs.rake')
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,116 @@
1
+ module RspecApiDocumentation
2
+ module Views
3
+ class ApiBlueprintExample < MarkupExample
4
+ TOTAL_SPACES_INDENTATION = 12.freeze
5
+
6
+ def initialize(example, configuration)
7
+ super
8
+ self.template_name = "rspec_api_documentation/api_blueprint_example"
9
+ end
10
+
11
+ def parameters
12
+ super.map do |parameter|
13
+ parameter.merge({
14
+ :required => !!parameter[:required],
15
+ :has_example => !!parameter[:example],
16
+ :has_type => !!parameter[:type]
17
+ })
18
+ end
19
+ end
20
+
21
+ def requests
22
+ super.map do |request|
23
+ request[:request_headers_text] = remove_utf8_for_json(remove_content_type(request[:request_headers_text]))
24
+ request[:request_headers_text] = indent(request[:request_headers_text])
25
+ request[:request_content_type] = content_type(request[:request_headers])
26
+ request[:request_content_type] = remove_utf8_for_json(request[:request_content_type])
27
+ request[:request_body] = body_to_json(request, :request)
28
+ request[:request_body] = indent(request[:request_body])
29
+
30
+ request[:response_headers_text] = remove_utf8_for_json(remove_content_type(request[:response_headers_text]))
31
+ request[:response_headers_text] = indent(request[:response_headers_text])
32
+ request[:response_content_type] = content_type(request[:response_headers])
33
+ request[:response_content_type] = remove_utf8_for_json(request[:response_content_type])
34
+ request[:response_body] = body_to_json(request, :response)
35
+ request[:response_body] = indent(request[:response_body])
36
+
37
+ request[:has_request?] = has_request?(request)
38
+ request[:has_response?] = has_response?(request)
39
+ request
40
+ end
41
+ end
42
+
43
+ def extension
44
+ Writers::ApiBlueprintWriter::EXTENSION
45
+ end
46
+
47
+ private
48
+
49
+ # `Content-Type` header is removed because the information would be duplicated
50
+ # since it's already present in `request[:request_content_type]`.
51
+ def remove_content_type(headers)
52
+ return unless headers
53
+ headers
54
+ .split("\n")
55
+ .reject { |header|
56
+ header.start_with?('Content-Type:')
57
+ }
58
+ .join("\n")
59
+ end
60
+
61
+ def has_request?(metadata)
62
+ metadata.any? do |key, value|
63
+ [:request_body, :request_headers, :request_content_type].include?(key) && value
64
+ end
65
+ end
66
+
67
+ def has_response?(metadata)
68
+ metadata.any? do |key, value|
69
+ [:response_status, :response_body, :response_headers, :response_content_type].include?(key) && value
70
+ end
71
+ end
72
+
73
+ def indent(string)
74
+ string.tap do |str|
75
+ str.gsub!(/\n/, "\n" + (" " * TOTAL_SPACES_INDENTATION)) if str
76
+ end
77
+ end
78
+
79
+ # http_call: the hash that contains all information about the HTTP
80
+ # request and response.
81
+ # message_direction: either `request` or `response`.
82
+ def body_to_json(http_call, message_direction)
83
+ content_type = http_call["#{message_direction}_content_type".to_sym]
84
+ body = http_call["#{message_direction}_body".to_sym] # e.g request_body
85
+
86
+ if json?(content_type) && body
87
+ body = JSON.pretty_generate(JSON.parse(body))
88
+ end
89
+
90
+ body
91
+ end
92
+
93
+ # JSON requests should use UTF-8 by default according to
94
+ # http://www.ietf.org/rfc/rfc4627.txt, so we will remove `charset=utf-8`
95
+ # when we find it to remove noise.
96
+ def remove_utf8_for_json(headers)
97
+ return unless headers
98
+ headers
99
+ .split("\n")
100
+ .map { |header|
101
+ header.gsub!(/; *charset=utf-8/, "") if json?(header)
102
+ header
103
+ }
104
+ .join("\n")
105
+ end
106
+
107
+ def content_type(headers)
108
+ headers && headers.fetch("Content-Type", nil)
109
+ end
110
+
111
+ def json?(string)
112
+ string =~ /application\/.*json/
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,105 @@
1
+ module RspecApiDocumentation
2
+ module Views
3
+ class ApiBlueprintIndex < MarkupIndex
4
+ def initialize(index, configuration)
5
+ super
6
+ self.template_name = "rspec_api_documentation/api_blueprint_index"
7
+ end
8
+
9
+ def sections
10
+ super.map do |section|
11
+ routes = section[:examples].group_by { |e| "#{e.route_uri}#{e.route_optionals}#{e.route_name}" }.map do |route, examples|
12
+ attrs = fields(:attributes, examples)
13
+ params = fields(:parameters, examples)
14
+
15
+ methods = examples.group_by(&:http_method).map do |http_method, examples|
16
+ {
17
+ http_method: http_method,
18
+ description: examples.first.respond_to?(:action_name) && examples.first.action_name,
19
+ examples: examples
20
+ }
21
+ end
22
+
23
+ {
24
+ "has_attributes?".to_sym => attrs.size > 0,
25
+ "has_parameters?".to_sym => params.size > 0,
26
+ route: format_route(examples[0]),
27
+ route_name: examples[0][:route_name],
28
+ attributes: attrs,
29
+ parameters: params,
30
+ http_methods: methods
31
+ }
32
+ end
33
+
34
+ section.merge({
35
+ routes: routes
36
+ })
37
+ end
38
+ end
39
+
40
+ def examples
41
+ @index.examples.map do |example|
42
+ ApiBlueprintExample.new(example, @configuration)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # APIB follows the RFC 6570 to format URI templates.
49
+ # According to it, simple string expansion (used to perform variable
50
+ # expansion) should be represented by `{var}` and not by `/:var`
51
+ # For example `/posts/:id` should become `/posts/{id}`
52
+ # cf. https://github.com/apiaryio/api-blueprint/blob/format-1A/API%20Blueprint%20Specification.md#431-resource-section
53
+ # cf. https://tools.ietf.org/html/rfc6570#section-3.2.6
54
+ def format_route(example)
55
+ route_uri = example[:route_uri].gsub(/:(.*?)([.\/?{]|$)/, '{\1}\2')
56
+ "#{route_uri}#{example[:route_optionals]}"
57
+ end
58
+
59
+ # APIB has both `parameters` and `attributes`. This generates a hash
60
+ # with all of its properties, like name, description, required.
61
+ # {
62
+ # required: true,
63
+ # example: "1",
64
+ # type: "string",
65
+ # name: "id",
66
+ # description: "The id",
67
+ # properties_description: "required, string"
68
+ # }
69
+ def fields(property_name, examples)
70
+ examples
71
+ .map { |example| example.metadata[property_name] }
72
+ .flatten
73
+ .compact
74
+ .uniq { |property| property[:name] }
75
+ .map do |property|
76
+ properties = []
77
+ if property[:required] == true
78
+ properties << 'required'
79
+ else
80
+ properties << 'optional'
81
+ end
82
+ properties << property[:type] if property[:type]
83
+ if properties.count > 0
84
+ property[:properties_description] = properties.join(", ")
85
+ else
86
+ property[:properties_description] = nil
87
+ end
88
+
89
+ property[:description] = nil if description_blank?(property)
90
+ property
91
+ end
92
+ end
93
+
94
+ # When no `description` was specified for a parameter, the DSL class
95
+ # is making `description = "#{scope} #{name}"`, which is bad because it
96
+ # assumes that all formats want this behavior. To avoid changing there
97
+ # and breaking everything, I do my own check here and if description
98
+ # equals the name, I assume it is blank.
99
+ def description_blank?(property)
100
+ !property[:description] ||
101
+ property[:description].to_s.strip == property[:name].to_s.strip
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,7 +1,7 @@
1
1
  module RspecApiDocumentation
2
2
  module Views
3
3
  class MarkdownExample < MarkupExample
4
- EXTENSION = 'markdown'
4
+ EXTENSION = 'md'
5
5
 
6
6
  def initialize(example, configuration)
7
7
  super
@@ -3,6 +3,8 @@ require 'mustache'
3
3
  module RspecApiDocumentation
4
4
  module Views
5
5
  class MarkupExample < Mustache
6
+ SPECIAL_CHARS = /[<>:"\/\\|?*]/.freeze
7
+
6
8
  def initialize(example, configuration)
7
9
  @example = example
8
10
  @host = configuration.curl_host
@@ -19,12 +21,11 @@ module RspecApiDocumentation
19
21
  end
20
22
 
21
23
  def dirname
22
- resource_name.to_s.downcase.gsub(/\s+/, '_').gsub(":", "_")
24
+ sanitize(resource_name.to_s.downcase)
23
25
  end
24
26
 
25
27
  def filename
26
- special_chars = /[<>:"\/\\|?*]/
27
- basename = description.downcase.gsub(/\s+/, '_').gsub(special_chars, '')
28
+ basename = sanitize(description.downcase)
28
29
  basename = Digest::MD5.new.update(description).to_s if basename.blank?
29
30
  "#{basename}.#{extension}"
30
31
  end
@@ -83,6 +84,14 @@ module RspecApiDocumentation
83
84
  end
84
85
  end.join
85
86
  end
87
+
88
+ def content_type(headers)
89
+ headers && headers.fetch("Content-Type", nil)
90
+ end
91
+
92
+ def sanitize(name)
93
+ name.gsub(/\s+/, '_').gsub(SPECIAL_CHARS, '')
94
+ end
86
95
  end
87
96
  end
88
97
  end
@@ -3,16 +3,14 @@ require 'mustache'
3
3
  module RspecApiDocumentation
4
4
  module Views
5
5
  class MarkupIndex < Mustache
6
+ delegate :api_name, :api_explanation, to: :@configuration, prefix: false
7
+
6
8
  def initialize(index, configuration)
7
9
  @index = index
8
10
  @configuration = configuration
9
11
  self.template_path = configuration.template_path
10
12
  end
11
13
 
12
- def api_name
13
- @configuration.api_name
14
- end
15
-
16
14
  def sections
17
15
  RspecApiDocumentation::Writers::IndexHelper.sections(examples, @configuration)
18
16
  end
@@ -1,6 +1,10 @@
1
1
  module RspecApiDocumentation
2
2
  module Views
3
3
  class SlateIndex < MarkdownIndex
4
+ def initialize(index, configuration)
5
+ super
6
+ self.template_name = "rspec_api_documentation/slate_index"
7
+ end
4
8
  end
5
9
  end
6
10
  end
@@ -0,0 +1,29 @@
1
+ module RspecApiDocumentation
2
+ module Writers
3
+ class ApiBlueprintWriter < GeneralMarkupWriter
4
+ EXTENSION = 'apib'
5
+
6
+ def markup_index_class
7
+ RspecApiDocumentation::Views::ApiBlueprintIndex
8
+ end
9
+
10
+ def markup_example_class
11
+ RspecApiDocumentation::Views::ApiBlueprintExample
12
+ end
13
+
14
+ def extension
15
+ EXTENSION
16
+ end
17
+
18
+ private
19
+
20
+ # API Blueprint is a spec, not navigable like HTML, therefore we generate
21
+ # only one file with all resources.
22
+ def render_options
23
+ super.merge({
24
+ examples: false
25
+ })
26
+ end
27
+ end
28
+ end
29
+ end
@@ -7,7 +7,7 @@ module RspecApiDocumentation
7
7
  File.open(configuration.docs_dir.join("combined.json"), "w+") do |f|
8
8
  examples = []
9
9
  index.examples.each do |rspec_example|
10
- examples << Formatter.to_json(JsonExample.new(rspec_example, configuration))
10
+ examples << Formatter.to_json(JSONExample.new(rspec_example, configuration))
11
11
  end
12
12
 
13
13
  f.write "["
@@ -6,16 +6,20 @@ module RspecApiDocumentation
6
6
 
7
7
  # Write out the generated documentation
8
8
  def write
9
- File.open(configuration.docs_dir.join(index_file_name + '.' + extension), "w+") do |f|
10
- f.write markup_index_class.new(index, configuration).render
9
+ if render_options.fetch(:index, true)
10
+ File.open(configuration.docs_dir.join(index_file_name + '.' + extension), "w+") do |f|
11
+ f.write markup_index_class.new(index, configuration).render
12
+ end
11
13
  end
12
14
 
13
- index.examples.each do |example|
14
- markup_example = markup_example_class.new(example, configuration)
15
- FileUtils.mkdir_p(configuration.docs_dir.join(markup_example.dirname))
15
+ if render_options.fetch(:examples, true)
16
+ index.examples.each do |example|
17
+ markup_example = markup_example_class.new(example, configuration)
18
+ FileUtils.mkdir_p(configuration.docs_dir.join(markup_example.dirname))
16
19
 
17
- File.open(configuration.docs_dir.join(markup_example.dirname, markup_example.filename), "w+") do |f|
18
- f.write markup_example.render
20
+ File.open(configuration.docs_dir.join(markup_example.dirname, markup_example.filename), "w+") do |f|
21
+ f.write markup_example.render
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -27,6 +31,15 @@ module RspecApiDocumentation
27
31
  def extension
28
32
  raise 'Parent class. This method should not be called.'
29
33
  end
34
+
35
+ private
36
+
37
+ def render_options
38
+ {
39
+ index: true,
40
+ examples: true
41
+ }
42
+ end
30
43
  end
31
44
  end
32
45
  end
@@ -32,7 +32,7 @@ module RspecApiDocumentation
32
32
  end
33
33
 
34
34
  def examples
35
- @index.examples.map { |example| JsonExample.new(example, @configuration) }
35
+ @index.examples.map { |example| JsonIodocsExample.new(example, @configuration) }
36
36
  end
37
37
 
38
38
  def as_json(opts = nil)
@@ -48,7 +48,7 @@ module RspecApiDocumentation
48
48
  end
49
49
  end
50
50
 
51
- class JsonExample
51
+ class JsonIodocsExample
52
52
  def initialize(example, configuration)
53
53
  @example = example
54
54
  end
@@ -94,6 +94,7 @@ module RspecApiDocumentation
94
94
  {
95
95
  @api_key.to_sym => {
96
96
  :name => @configuration.api_name,
97
+ :description => @configuration.api_explanation,
97
98
  :protocol => @configuration.io_docs_protocol,
98
99
  :publicPath => "",
99
100
  :baseURL => @configuration.curl_host
@@ -2,19 +2,19 @@ require 'rspec_api_documentation/writers/formatter'
2
2
 
3
3
  module RspecApiDocumentation
4
4
  module Writers
5
- class JsonWriter < Writer
5
+ class JSONWriter < Writer
6
6
  delegate :docs_dir, :to => :configuration
7
7
 
8
8
  def write
9
9
  File.open(docs_dir.join("index.json"), "w+") do |f|
10
- f.write Formatter.to_json(JsonIndex.new(index, configuration))
10
+ f.write Formatter.to_json(JSONIndex.new(index, configuration))
11
11
  end
12
12
  write_examples
13
13
  end
14
14
 
15
15
  def write_examples
16
16
  index.examples.each do |example|
17
- json_example = JsonExample.new(example, configuration)
17
+ json_example = JSONExample.new(example, configuration)
18
18
  FileUtils.mkdir_p(docs_dir.join(json_example.dirname))
19
19
  File.open(docs_dir.join(json_example.dirname, json_example.filename), "w+") do |f|
20
20
  f.write Formatter.to_json(json_example)
@@ -23,7 +23,11 @@ module RspecApiDocumentation
23
23
  end
24
24
  end
25
25
 
26
- class JsonIndex
26
+ # https://github.com/zipmark/rspec_api_documentation/issues/382
27
+ # backward compatibilty json for configuration of config.format
28
+ class JsonWriter < JSONWriter; end
29
+
30
+ class JSONIndex
27
31
  def initialize(index, configuration)
28
32
  @index = index
29
33
  @configuration = configuration
@@ -34,7 +38,7 @@ module RspecApiDocumentation
34
38
  end
35
39
 
36
40
  def examples
37
- @index.examples.map { |example| JsonExample.new(example, @configuration) }
41
+ @index.examples.map { |example| JSONExample.new(example, @configuration) }
38
42
  end
39
43
 
40
44
  def as_json(opts = nil)
@@ -61,7 +65,7 @@ module RspecApiDocumentation
61
65
  end
62
66
  end
63
67
 
64
- class JsonExample
68
+ class JSONExample
65
69
  def initialize(example, configuration)
66
70
  @example = example
67
71
  @host = configuration.curl_host
@@ -1,7 +1,7 @@
1
1
  module RspecApiDocumentation
2
2
  module Writers
3
3
  class MarkdownWriter < GeneralMarkupWriter
4
- EXTENSION = 'markdown'
4
+ EXTENSION = 'md'
5
5
 
6
6
  def markup_index_class
7
7
  RspecApiDocumentation::Views::MarkdownIndex