rspec-api-docs 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +55 -0
  4. data/README.md +222 -4
  5. data/Rakefile +9 -2
  6. data/bin/generate_integration_docs +14 -0
  7. data/lib/rspec_api_docs.rb +2 -16
  8. data/lib/rspec_api_docs/after.rb +20 -3
  9. data/lib/rspec_api_docs/after/type_checker.rb +56 -0
  10. data/lib/rspec_api_docs/config.rb +27 -0
  11. data/lib/rspec_api_docs/dsl.rb +35 -10
  12. data/lib/rspec_api_docs/dsl/doc_proxy.rb +71 -0
  13. data/lib/rspec_api_docs/dsl/request_store.rb +21 -0
  14. data/lib/rspec_api_docs/formatter.rb +44 -6
  15. data/lib/rspec_api_docs/formatter/renderer/json_renderer.rb +35 -0
  16. data/lib/rspec_api_docs/formatter/renderer/json_renderer/example_serializer.rb +47 -0
  17. data/lib/rspec_api_docs/formatter/renderer/json_renderer/name.rb +18 -0
  18. data/lib/rspec_api_docs/formatter/renderer/json_renderer/resource_serializer.rb +31 -0
  19. data/lib/rspec_api_docs/formatter/renderer/raddocs_renderer.rb +56 -0
  20. data/lib/rspec_api_docs/formatter/renderer/raddocs_renderer/index_serializer.rb +57 -0
  21. data/lib/rspec_api_docs/formatter/renderer/raddocs_renderer/link.rb +11 -0
  22. data/lib/rspec_api_docs/formatter/renderer/raddocs_renderer/resource_serializer.rb +58 -0
  23. data/lib/rspec_api_docs/formatter/renderer/slate_renderer.rb +29 -0
  24. data/lib/rspec_api_docs/formatter/renderer/slate_renderer/slate_index.html.md.erb +45 -0
  25. data/lib/rspec_api_docs/formatter/resource.rb +25 -111
  26. data/lib/rspec_api_docs/formatter/resource/example.rb +125 -0
  27. data/lib/rspec_api_docs/formatter/resource/parameter.rb +39 -0
  28. data/lib/rspec_api_docs/formatter/resource/response_field.rb +39 -0
  29. data/lib/rspec_api_docs/version.rb +1 -1
  30. data/rspec-api-docs.gemspec +1 -0
  31. metadata +34 -5
  32. data/lib/rspec_api_docs/formatter/index_serializer.rb +0 -37
  33. data/lib/rspec_api_docs/formatter/renderers/json_renderer.rb +0 -34
  34. data/lib/rspec_api_docs/formatter/resource_serializer.rb +0 -21
@@ -0,0 +1,27 @@
1
+ module RspecApiDocs
2
+ class << self
3
+ attr_accessor :configuration
4
+
5
+ def configuration
6
+ @configuration ||= Config.new
7
+ end
8
+ end
9
+
10
+ def self.configure
11
+ self.configuration ||= Config.new
12
+ yield configuration
13
+ end
14
+
15
+ class Config
16
+ attr_accessor \
17
+ :output_dir,
18
+ :renderer,
19
+ :validate_params
20
+
21
+ def initialize
22
+ @output_dir = 'docs'
23
+ @renderer = :json
24
+ @validate_params = true
25
+ end
26
+ end
27
+ end
@@ -3,19 +3,44 @@ require 'rspec_api_docs/dsl/request_store'
3
3
  require 'rspec_api_docs/dsl/doc_proxy'
4
4
 
5
5
  module RspecApiDocs
6
+ # This module is intended to be included in your RSpec specs to expose the
7
+ # {#doc} method.
6
8
  module Dsl
7
- def doc(&block)
8
- example.metadata[METADATA_NAMESPACE] ||= {}
9
+ # DSL method for use in your RSpec examples.
10
+ #
11
+ # Usage:
12
+ #
13
+ # it 'returns a character' do
14
+ # doc do
15
+ # title 'Returns a Character'
16
+ # description 'Allows you to return a single character.'
17
+ # path '/characters/:id'
18
+ #
19
+ # param :id, 'The id of a character', required: true
20
+ #
21
+ # field :id, 'The id of a character', scope: :character
22
+ # field :name, "The character's name", scope: :character
23
+ # end
24
+ #
25
+ # get '/characters/1'
26
+ # end
27
+ #
28
+ # For more info on the methods available in the block, see {DocProxy}.
29
+ #
30
+ # @param should_document [true, false] clear documentation metadata for the example
31
+ # @return [RequestStore, nil] an object to store request/response pairs
32
+ def doc(should_document = true, &block)
33
+ if should_document
34
+ example.metadata[METADATA_NAMESPACE] ||= {}
9
35
 
10
- if block
11
- DocProxy.new(example).instance_eval(&block)
12
- end
13
-
14
- RequestStore.new(example)
15
- end
36
+ if block
37
+ DocProxy.new(example).instance_eval(&block)
38
+ end
16
39
 
17
- def no_doc
18
- example.metadata[METADATA_NAMESPACE] = nil
40
+ RequestStore.new(example)
41
+ else
42
+ example.metadata[METADATA_NAMESPACE] = nil
43
+ end
19
44
  end
20
45
 
21
46
  private
@@ -7,22 +7,74 @@ module RspecApiDocs
7
7
  @metadata = example.metadata
8
8
  end
9
9
 
10
+ # For setting the name of the example.
11
+ #
12
+ # E.g. "Returns a Character"
13
+ #
14
+ # @return [void]
10
15
  def name(value)
11
16
  metadata[METADATA_NAMESPACE][:example_name] = value
12
17
  end
13
18
 
19
+ # For setting the name of the resource.
20
+ #
21
+ # E.g. "Characters"
22
+ #
23
+ # @return [void]
14
24
  def resource_name(value)
15
25
  metadata[METADATA_NAMESPACE][:resource_name] = value
16
26
  end
17
27
 
28
+ # For setting the description of the resource.
29
+ #
30
+ # E.g. "Orders can be created, viewed, and deleted"
31
+ #
32
+ # @return [void]
33
+ def resource_description(value)
34
+ metadata[METADATA_NAMESPACE][:resource_description] = value
35
+ end
36
+
37
+ # For setting a description of the example.
38
+ #
39
+ # E.g. "Allows you to return a single character."
40
+ #
41
+ # @return [void]
18
42
  def description(value)
19
43
  metadata[METADATA_NAMESPACE][:description] = value
20
44
  end
21
45
 
46
+ # For setting the request path of the example.
47
+ #
48
+ # E.g. "/characters/:id"
49
+ #
50
+ # @return [void]
22
51
  def path(value)
23
52
  metadata[METADATA_NAMESPACE][:path] = value
24
53
  end
25
54
 
55
+ # For setting response fields of a request.
56
+ #
57
+ # Usage:
58
+ #
59
+ # doc do
60
+ # field :id, 'The id of a character', scope: :character
61
+ # field :name, "The character's name", scope: :character
62
+ # end
63
+ #
64
+ # For a response body of:
65
+ #
66
+ # {
67
+ # character: {
68
+ # id: 1,
69
+ # name: 'Finn The Human'
70
+ # }
71
+ # }
72
+ #
73
+ # @param name [Symbol] the name of the response field
74
+ # @param description [String] a description of the response field
75
+ # @param scope [Symbol, Array<Symbol>] how the field is scoped
76
+ # @param type [String]
77
+ # @return [void]
26
78
  def field(name, description, scope: [], type: nil)
27
79
  metadata[METADATA_NAMESPACE][:fields] ||= {}
28
80
  metadata[METADATA_NAMESPACE][:fields][name] = {
@@ -32,6 +84,25 @@ module RspecApiDocs
32
84
  }
33
85
  end
34
86
 
87
+ # For setting params of a request.
88
+ #
89
+ # Usage:
90
+ #
91
+ # doc do
92
+ # param :id, 'The id of a character', required: true
93
+ # param :name, 'A tag on a character', scope: :tag
94
+ # end
95
+ #
96
+ # For a path of:
97
+ #
98
+ # /characters/:id?tag[name]=:name
99
+ #
100
+ # @param name [Symbol] the name of the parameter
101
+ # @param description [String] a description of the parameter
102
+ # @param scope [Symbol, Array<Symbol>] how the parameter is scoped
103
+ # @param type [String]
104
+ # @param required [true, false] if the field is required
105
+ # @return [void]
35
106
  def param(name, description, scope: [], type: nil, required: false)
36
107
  metadata[METADATA_NAMESPACE][:parameters] ||= {}
37
108
  metadata[METADATA_NAMESPACE][:parameters][name] = {
@@ -1,5 +1,6 @@
1
1
  module RspecApiDocs
2
2
  module Dsl
3
+ # Used to store request/response pairs.
3
4
  class RequestStore
4
5
  attr_reader :metadata
5
6
 
@@ -7,6 +8,26 @@ module RspecApiDocs
7
8
  @metadata = example.metadata
8
9
  end
9
10
 
11
+ # Only needed if you need to store multiple requests for a single example.
12
+ #
13
+ # Usage:
14
+ #
15
+ # it 'stores the requests a character' do
16
+ # doc do
17
+ # explanation 'Creating and requesting a character'
18
+ # end
19
+ #
20
+ # post '/characters', {name: 'Finn The Human'}
21
+ #
22
+ # doc << [last_request, last_response]
23
+ #
24
+ # get '/characters/1'
25
+ #
26
+ # # The last request/response pair is stored automatically
27
+ # end
28
+ #
29
+ # @param value [Array<Rack::Request, Rack::Response>] an array of a request and response object
30
+ # @return [void]
10
31
  def <<(value)
11
32
  metadata[METADATA_NAMESPACE][:requests] ||= []
12
33
  metadata[METADATA_NAMESPACE][:requests] << value.sort_by { |v| v.class.name }.reverse
@@ -1,27 +1,65 @@
1
1
  require 'rspec/core/formatters/base_formatter'
2
- require 'json'
3
2
 
3
+ require 'rspec_api_docs'
4
4
  require 'rspec_api_docs/formatter/resource'
5
- require 'rspec_api_docs/formatter/renderers/json_renderer'
5
+ require 'rspec_api_docs/formatter/renderer/json_renderer'
6
+ require 'rspec_api_docs/formatter/renderer/raddocs_renderer'
7
+ require 'rspec_api_docs/formatter/renderer/slate_renderer'
6
8
 
7
9
  module RspecApiDocs
10
+ # Unknown renderer configured.
11
+ UnknownRenderer = Class.new(BaseError)
12
+
13
+ # The RSpec formatter.
14
+ #
15
+ # Usage:
16
+ #
17
+ # rspec --format=RspecApiDocs::Formatter
8
18
  class Formatter < RSpec::Core::Formatters::BaseFormatter
9
19
  RSpec::Core::Formatters.register self, :example_passed, :close
10
20
 
11
21
  attr_reader :resources
12
22
 
13
23
  def initialize(*args)
14
- @resources = []
24
+ @resources = {}
15
25
  super args
16
26
  end
17
27
 
28
+ # Initializes and stores {Resource}s.
29
+ #
30
+ # @return [void]
18
31
  def example_passed(example_notification)
19
- return unless example_notification.example.metadata[METADATA_NAMESPACE]
20
- resources << Resource.new(example_notification.example)
32
+ rspec_example = example_notification.example
33
+ return unless rspec_example.metadata[METADATA_NAMESPACE]
34
+ resource = Resource.new(rspec_example)
35
+ resources[resource.name] ||= resource
36
+ resources[resource.name].examples << Resource::Example.new(rspec_example)
21
37
  end
22
38
 
39
+ # Calls the configured renderer with the stored {Resource}s.
40
+ #
41
+ # @return [void]
23
42
  def close(null_notification)
24
- JSONRender.new(resources).render
43
+ renderer.new(resources.values).render
44
+ end
45
+
46
+ private
47
+
48
+ def renderer
49
+ value = RspecApiDocs.configuration.renderer
50
+
51
+ case value
52
+ when :json
53
+ Renderer::JSONRenderer
54
+ when :raddocs
55
+ Renderer::RaddocsRenderer
56
+ when :slate
57
+ Renderer::SlateRenderer
58
+ when Class
59
+ value
60
+ else
61
+ raise UnknownRenderer, "unknown renderer #{value.inspect}"
62
+ end
25
63
  end
26
64
  end
27
65
  end
@@ -0,0 +1,35 @@
1
+ require 'json'
2
+ require 'rspec_api_docs/formatter/renderer/json_renderer/name'
3
+ require 'rspec_api_docs/formatter/renderer/json_renderer/resource_serializer'
4
+
5
+ module RspecApiDocs
6
+ module Renderer
7
+ class JSONRenderer
8
+ attr_reader :resources
9
+
10
+ def initialize(resources)
11
+ @resources = resources
12
+ end
13
+
14
+ def render
15
+ FileUtils.mkdir_p output_file.dirname
16
+
17
+ File.open(output_file, 'w') do |f|
18
+ f.write JSON.pretty_generate(output) + "\n"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def output
25
+ resources.map do |resource|
26
+ ResourceSerializer.new(resource).to_h
27
+ end
28
+ end
29
+
30
+ def output_file
31
+ Pathname.new(RspecApiDocs.configuration.output_dir) + 'index.json'
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module RspecApiDocs
2
+ module Renderer
3
+ class JSONRenderer
4
+ class ExampleSerializer
5
+ attr_reader :example
6
+
7
+ def initialize(example)
8
+ @example = example
9
+ end
10
+
11
+ def to_h # rubocop:disable Metrics/AbcSize
12
+ {
13
+ description: example.description,
14
+ name: example.name,
15
+ httpMethod: example.http_method,
16
+ parameters: parameters(example.parameters),
17
+ path: example.path,
18
+ requests: example.requests,
19
+ responseFields: response_fields(example.response_fields),
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def parameters(parameters)
26
+ parameters.map do |parameter|
27
+ {
28
+ name: Name.(name: parameter.name, scope: parameter.scope),
29
+ description: parameter.description,
30
+ required: parameter.required,
31
+ }
32
+ end
33
+ end
34
+
35
+ def response_fields(fields)
36
+ fields.map do |field|
37
+ {
38
+ name: Name.(name: field.name, scope: field.scope),
39
+ description: field.description,
40
+ type: field.type,
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ module RspecApiDocs
2
+ module Renderer
3
+ class JSONRenderer
4
+ class Name
5
+ def self.call(name:, scope:)
6
+ scope = Array(scope)
7
+ if scope.empty?
8
+ name
9
+ else
10
+ scope.each_with_index.inject('') do |str, (part, index)|
11
+ str << (index == 0 ? part : "[#{part}]").to_s
12
+ end + "[#{name}]"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ require 'rspec_api_docs/formatter/renderer/json_renderer/example_serializer'
2
+
3
+ module RspecApiDocs
4
+ module Renderer
5
+ class JSONRenderer
6
+ class ResourceSerializer
7
+ attr_reader :resource
8
+
9
+ def initialize(resource)
10
+ @resource = resource
11
+ end
12
+
13
+ def to_h
14
+ {
15
+ name: resource.name,
16
+ description: resource.description,
17
+ examples: examples,
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def examples
24
+ resource.examples.map do |example|
25
+ ExampleSerializer.new(example).to_h
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+
3
+ require 'rspec_api_docs/formatter/renderer/raddocs_renderer/index_serializer'
4
+ require 'rspec_api_docs/formatter/renderer/raddocs_renderer/link'
5
+ require 'rspec_api_docs/formatter/renderer/raddocs_renderer/resource_serializer'
6
+
7
+ module RspecApiDocs
8
+ module Renderer
9
+ class RaddocsRenderer
10
+ attr_reader :resources
11
+
12
+ def initialize(resources)
13
+ @resources = resources
14
+ end
15
+
16
+ def render
17
+ write_index
18
+ write_examples
19
+ end
20
+
21
+ private
22
+
23
+ def write_index
24
+ FileUtils.mkdir_p output_dir
25
+
26
+ File.open(output_dir + 'index.json', 'w') do |f|
27
+ f.write JSON.pretty_generate(IndexSerializer.new(resources).to_h) + "\n"
28
+ end
29
+ end
30
+
31
+ def write_examples
32
+ resources.each do |resource|
33
+ resource.examples.each do |example|
34
+ write_example(resource, example)
35
+ end
36
+ end
37
+ end
38
+
39
+ def write_example(resource, example)
40
+ FileUtils.mkdir_p file(resource, example).dirname
41
+
42
+ File.open(file(resource, example), 'w') do |f|
43
+ f.write JSON.pretty_generate(ResourceSerializer.new(resource, example).to_h) + "\n"
44
+ end
45
+ end
46
+
47
+ def output_dir
48
+ Pathname.new RspecApiDocs.configuration.output_dir
49
+ end
50
+
51
+ def file(resource, example)
52
+ output_dir + Link.(resource.name, example.name)
53
+ end
54
+ end
55
+ end
56
+ end