dox 1.3.0 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,16 +1,17 @@
1
1
  module Dox
2
2
  module Entities
3
3
  class Action
4
- attr_reader :name, :desc, :verb, :path, :uri_params
4
+ attr_reader :name, :desc, :verb, :path, :resource, :params
5
5
  attr_accessor :examples
6
6
 
7
- def initialize(name, details, request)
7
+ def initialize(details, request)
8
8
  @request = request
9
- @name = name
9
+ @name = details[:action_name]
10
+ @resource = details[:resource_name]
10
11
  @desc = details[:action_desc]
11
12
  @verb = details[:action_verb] || request.method
12
13
  @path = details[:action_path] || template_path
13
- @uri_params = details[:action_params] || template_path_params
14
+ @params = template_params(details[:action_params], details[:action_query_params] || [])
14
15
  @examples = []
15
16
 
16
17
  validate!
@@ -22,25 +23,44 @@ module Dox
22
23
 
23
24
  # /pokemons/1 => pokemons/{id}
24
25
  def template_path
25
- path = request.path.dup.presence || request.fullpath.split("?").first
26
+ path = request.path.dup.presence || request.fullpath.split('?').first
26
27
  path_params.each do |key, value|
27
28
  path.sub!(%r{\/#{value}(\/|$)}, "/{#{key}}\\1")
28
29
  end
30
+
29
31
  path
30
32
  end
31
33
 
34
+ def template_params(defined_params, query_params)
35
+ acquire_path_params + acquire_defined_params(defined_params) + query_params
36
+ end
37
+
32
38
  def path_params
33
- @path_params ||=
34
- request.path_parameters.symbolize_keys.except(:action, :controller, :format, :subdomain)
39
+ request.path_parameters.symbolize_keys.except(:action, :controller, :format, :subdomain)
40
+ end
41
+
42
+ def acquire_path_params
43
+ return [] if path_params.nil?
44
+
45
+ path_params.map do |param, value|
46
+ { name: param,
47
+ in: :path,
48
+ schema: { type: guess_param_type(value) },
49
+ example: value }
50
+ end
35
51
  end
36
52
 
37
- def template_path_params
38
- h = {}
39
- path_params.each do |param, value|
40
- param_type = guess_param_type(value)
41
- h[param] = { type: param_type, required: :required, value: value }
53
+ def acquire_defined_params(defined_params)
54
+ return [] if defined_params.nil?
55
+
56
+ defined_params.map do |key, value|
57
+ { name: key,
58
+ in: 'query',
59
+ required: value[:required],
60
+ example: value[:value],
61
+ description: value[:description].presence || '',
62
+ schema: { type: value[:type] } }
42
63
  end
43
- h
44
64
  end
45
65
 
46
66
  def guess_param_type(param)
@@ -3,6 +3,8 @@ module Dox
3
3
  class Example
4
4
  extend Forwardable
5
5
 
6
+ attr_reader :desc, :name, :request_schema, :response_schema_success, :response_schema_fail
7
+
6
8
  def_delegator :response, :status, :response_status
7
9
  def_delegator :response, :content_type, :response_content_type
8
10
  def_delegator :request, :content_type, :request_content_type
@@ -10,6 +12,10 @@ module Dox
10
12
 
11
13
  def initialize(details, request, response)
12
14
  @desc = details[:description]
15
+ @name = details[:resource_name].downcase
16
+ @request_schema = details[:action_request_schema]
17
+ @response_schema_success = details[:action_response_schema_success]
18
+ @response_schema_fail = details[:action_response_schema_fail]
13
19
  @request = request
14
20
  @response = response
15
21
  end
@@ -34,6 +40,10 @@ module Dox
34
40
  @request_headers ||= filter_headers(request)
35
41
  end
36
42
 
43
+ def response_success?
44
+ response.successful?
45
+ end
46
+
37
47
  # Rails 4 includes the body params in the request_fullpath
38
48
  def request_fullpath
39
49
  if request.query_parameters.present?
@@ -67,7 +77,7 @@ module Dox
67
77
  request.path.presence || request.fullpath.split('?')[0]
68
78
  end
69
79
 
70
- attr_reader :desc, :request, :response
80
+ attr_reader :request, :response
71
81
 
72
82
  def filter_headers(obj)
73
83
  headers_whitelist.map do |header|
@@ -1,17 +1,16 @@
1
1
  module Dox
2
2
  module Entities
3
3
  class Resource
4
-
5
- attr_reader :name, :desc, :endpoint
4
+ attr_reader :name, :desc, :endpoint, :group
6
5
  attr_accessor :actions
7
6
 
8
- def initialize(name, details)
9
- @name = name
7
+ def initialize(details)
8
+ @name = details[:resource_name]
9
+ @group = details[:resource_group_name]
10
10
  @desc = details[:resource_desc]
11
11
  @endpoint = details[:resource_endpoint]
12
12
  @actions = {}
13
13
  end
14
-
15
14
  end
16
15
  end
17
16
  end
@@ -1,12 +1,11 @@
1
1
  module Dox
2
2
  module Entities
3
3
  class ResourceGroup
4
-
5
4
  attr_reader :name
6
5
  attr_accessor :desc, :resources
7
6
 
8
- def initialize(name, details)
9
- @name = name
7
+ def initialize(details)
8
+ @name = details[:resource_group_name]
10
9
  @desc = details[:resource_group_desc]
11
10
  @resources = {}
12
11
  end
data/lib/dox/formatter.rb CHANGED
@@ -20,7 +20,7 @@ module Dox
20
20
 
21
21
  def stop(notification)
22
22
  if notification.failed_examples.any?
23
- $stderr.puts(notification.fully_formatted_failed_examples)
23
+ warn(notification.fully_formatted_failed_examples)
24
24
  else
25
25
  printer.print(passed_examples)
26
26
  end
@@ -28,7 +28,8 @@ module Dox
28
28
 
29
29
  def dump_summary(summary)
30
30
  return if summary.failed_examples.none?
31
- $stderr.puts(summary.fully_formatted)
31
+
32
+ warn(summary.fully_formatted)
32
33
  exit(-1)
33
34
  end
34
35
 
@@ -44,28 +45,26 @@ module Dox
44
45
  if group
45
46
  group.tap { |g| g.desc ||= current_example.resource_group_desc }
46
47
  else
47
- passed_examples[group_name] = Entities::ResourceGroup.new(group_name,
48
- current_example.metadata)
48
+ passed_examples[group_name] = Entities::ResourceGroup.new(current_example.metadata)
49
49
  end
50
50
  end
51
51
 
52
52
  def load_or_save_resource_to_group(group)
53
- resource_name = current_example.resource_name
54
- group.resources[resource_name] ||= Entities::Resource.new(resource_name,
55
- current_example.metadata)
53
+ group.resources[current_example.resource_name] ||= Entities::Resource.new(current_example.metadata)
56
54
  end
57
55
 
58
56
  def load_or_save_action_to_resource(resource)
59
- action_name = current_example.action_name
60
- resource.actions[action_name] ||= Entities::Action.new(action_name, current_example.metadata,
61
- current_example.request)
57
+ resource.actions[current_example.action_name] ||= Entities::Action.new(current_example.metadata,
58
+ current_example.request)
62
59
  end
63
60
 
64
61
  def move_example_to_passed
65
62
  group = load_or_save_group
66
63
  resource = load_or_save_resource_to_group(group)
67
64
  action = load_or_save_action_to_resource(resource)
68
- action.examples << Entities::Example.new(current_example.metadata, current_example.request, current_example.response)
65
+ action.examples << Entities::Example.new(current_example.metadata,
66
+ current_example.request,
67
+ current_example.response)
69
68
  end
70
69
 
71
70
  def printer
@@ -3,44 +3,35 @@ module Dox
3
3
  class ActionPrinter < BasePrinter
4
4
  def print(action)
5
5
  self.action = action
6
- @output.puts action_title
7
- @output.puts action_uri_params if action.uri_params.present?
6
+ @action_hash = find_or_add(find_or_add(spec, action.path.to_s), action.verb.downcase.to_sym)
8
7
 
9
- action.examples.each do |example|
10
- example_printer.print(example)
11
- end
8
+ add_action
9
+ add_action_params
10
+
11
+ print_examples
12
12
  end
13
13
 
14
14
  private
15
15
 
16
- attr_accessor :action
17
-
18
- def action_title
19
- <<-HEREDOC
16
+ attr_accessor :action, :action_hash
20
17
 
21
- ### #{action.name} [#{action.verb.upcase} #{action.path}]
22
- #{print_desc(action.desc)}
23
- HEREDOC
18
+ def add_action
19
+ action_hash['summary'] = action.name
20
+ action_hash['tags'] = [action.resource]
21
+ action_hash['description'] = format_desc(action.desc)
24
22
  end
25
23
 
26
- def action_uri_params
27
- <<-HEREDOC
28
- + Parameters
29
- #{formatted_params(action.uri_params)}
30
- HEREDOC
31
- end
24
+ def add_action_params
25
+ return unless action.params.present?
32
26
 
33
- def example_printer
34
- @example_printer ||= ExamplePrinter.new(@output)
27
+ action_hash['parameters'] = action.params
35
28
  end
36
29
 
37
- def formatted_params(uri_params)
38
- uri_params.map do |param, details|
39
- desc = " + #{CGI.escape(param.to_s)}: `#{CGI.escape(details[:value].to_s)}` (#{details[:type]}, #{details[:required]})"
40
- desc += " - #{details[:description]}" if details[:description].present?
41
- desc += "\n + Default: #{details[:default]}" if details[:default].present?
42
- desc
43
- end.flatten.join("\n")
30
+ def print_examples
31
+ action.examples.each do |example|
32
+ ExampleRequestPrinter.new(action_hash).print(example)
33
+ ExampleResponsePrinter.new(action_hash).print(example)
34
+ end
44
35
  end
45
36
  end
46
37
  end
@@ -1,37 +1,56 @@
1
+ require 'rexml/document'
2
+
1
3
  module Dox
2
4
  module Printers
3
5
  class BasePrinter
4
- def initialize(output)
5
- @output = output
6
+ attr_reader :spec
7
+
8
+ def initialize(spec)
9
+ @spec = spec || {}
6
10
  end
7
11
 
8
12
  def print
9
13
  raise NotImplementedError
10
14
  end
11
15
 
12
- private
16
+ def find_or_add(hash, key, default = {})
17
+ return hash[key] if hash.key?(key)
13
18
 
14
- def descriptions_folder_path
15
- Dox.config.desc_folder_path
19
+ hash[key] = default
16
20
  end
17
21
 
18
- def print_desc(desc, fullpath = false)
19
- return if desc.blank?
22
+ def read_file(path, root_path: Dox.config.descriptions_location)
23
+ return '' unless root_path
24
+
25
+ File.read(File.join(root_path, path))
26
+ end
20
27
 
21
- if desc.to_s =~ /.*\.md$/
22
- path = if fullpath
23
- desc
24
- else
25
- descriptions_folder_path.join(desc).to_s
26
- end
27
- content(path)
28
+ def formatted_body(body_str, content_type)
29
+ case content_type
30
+ when %r{application\/.*json}
31
+ JSON.parse(body_str)
32
+ when /xml/
33
+ pretty_xml(body_str)
28
34
  else
29
- desc
35
+ body_str
30
36
  end
31
37
  end
32
38
 
33
- def content(path)
34
- File.read(path)
39
+ def pretty_xml(xml_string)
40
+ doc = REXML::Document.new(xml_string)
41
+ formatter = REXML::Formatters::Pretty.new
42
+ formatter.compact = true
43
+ result = ''
44
+ formatter.write(doc, result)
45
+ result
46
+ end
47
+
48
+ def format_desc(description)
49
+ desc = description
50
+ desc = '' if desc.nil?
51
+ desc = read_file(desc) if desc.end_with?('.md')
52
+
53
+ desc
35
54
  end
36
55
  end
37
56
  end
@@ -1,26 +1,46 @@
1
1
  module Dox
2
2
  module Printers
3
3
  class DocumentPrinter < BasePrinter
4
+ def initialize(output)
5
+ super(body)
6
+ @output = output
7
+ end
8
+
4
9
  def print(passed_examples)
5
- print_meta_info
10
+ spec['paths'] = {}
11
+ spec['tags'] = []
12
+ spec['x-tagGroups'] = []
6
13
 
7
14
  passed_examples.sort.each do |_, resource_group|
8
15
  group_printer.print(resource_group)
9
16
  end
17
+
18
+ @output.puts(JSON.pretty_generate(spec))
10
19
  end
11
20
 
12
21
  private
13
22
 
14
- def group_printer
15
- @group_printer ||= ResourceGroupPrinter.new(@output)
23
+ def body
24
+ {
25
+ openapi: Dox.config.openapi_version || '3.0.0',
26
+ info: {
27
+ title: Dox.config.title || 'API Documentation',
28
+ description: adjust_description(Dox.config.description || ''),
29
+ version: Dox.config.api_version || '1.0'
30
+ }
31
+ }
16
32
  end
17
33
 
18
- def print_meta_info
19
- @output.puts(print_desc(api_desc_path))
34
+ def adjust_description(description)
35
+ description.end_with?('.md') ? acquire_desc(description) : description
20
36
  end
21
37
 
22
- def api_desc_path
23
- Dox.config.header_file_path
38
+ def acquire_desc(path)
39
+ read_file(path)
40
+ end
41
+
42
+ def group_printer
43
+ @group_printer ||= ResourceGroupPrinter.new(spec)
24
44
  end
25
45
  end
26
46
  end
@@ -0,0 +1,69 @@
1
+ module Dox
2
+ module Printers
3
+ class ExampleRequestPrinter < BasePrinter
4
+ def print(example)
5
+ self.example = example
6
+ add_example_request
7
+ end
8
+
9
+ private
10
+
11
+ attr_accessor :example
12
+
13
+ def add_example_request
14
+ spec['parameters'] = add_new_header_params(find_or_add(spec, 'parameters', []))
15
+ return if example.request_body.empty?
16
+
17
+ add_content(find_or_add(spec, 'requestBody'))
18
+ end
19
+
20
+ def add_content(body)
21
+ add_content_name(body['content'] = find_or_add(body, 'content'))
22
+ end
23
+
24
+ def add_content_name(body)
25
+ req_header = find_headers(example.request_headers)
26
+ add_example(body[req_header] = find_or_add(body, req_header))
27
+ add_schema(body[req_header], Dox.config.schema_request_folder_path)
28
+ end
29
+
30
+ def add_example(body)
31
+ add_desc(body['examples'] = find_or_add(body, 'examples'))
32
+ end
33
+
34
+ def add_desc(body)
35
+ body[example.desc] = { 'summary' => example.desc,
36
+ 'value' => formatted_body(example.request_body,
37
+ example.request_content_type) }
38
+ end
39
+
40
+ def add_schema(body, path)
41
+ return if example.request_schema.nil?
42
+ return unless path
43
+
44
+ file_path = File.join(path, "#{example.request_schema}.json")
45
+
46
+ body['schema'] = File.file?(file_path) ? { '$ref' => file_path } : JSON.parse(example.request_schema)
47
+ end
48
+
49
+ def find_headers(headers)
50
+ headers.find { |key, _| key == 'Accept' }&.last || 'any'
51
+ end
52
+
53
+ def acquire_header_params
54
+ example.request_headers.map do |key, value|
55
+ { name: key, in: :header, example: value }
56
+ end
57
+ end
58
+
59
+ def add_new_header_params(header_params)
60
+ example.request_headers.each do |key, value|
61
+ header_params.push(name: key, in: :header, example: value) unless
62
+ header_params.detect { |hash| hash[:name] == key }
63
+ end
64
+
65
+ header_params
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,86 @@
1
+ module Dox
2
+ module Printers
3
+ class ExampleResponsePrinter < BasePrinter
4
+ def print(example)
5
+ self.example = example
6
+ add_example_response
7
+ end
8
+
9
+ private
10
+
11
+ attr_accessor :example
12
+
13
+ def add_example_response
14
+ add_statuses(find_or_add(find_or_add(spec, 'responses'), example.response_status.to_s))
15
+ end
16
+
17
+ def add_statuses(body)
18
+ add_status_desc(body)
19
+ add_content(body)
20
+ add_headers(body)
21
+ end
22
+
23
+ def add_status_desc(body)
24
+ body['description'] = Util::Http::HTTP_STATUS_CODES[example.response_status]
25
+ end
26
+
27
+ def add_content(body)
28
+ add_content_name(body['content'] = find_or_add(body, 'content'))
29
+ end
30
+
31
+ def add_content_name(body)
32
+ resp_header = find_headers(example.response_headers)
33
+
34
+ add_example(body[resp_header] = find_or_add(body, resp_header))
35
+ add_schema(body[resp_header], Dox.config.schema_response_folder_path)
36
+ end
37
+
38
+ def add_example(body)
39
+ return if example.response_body.empty?
40
+
41
+ add_desc(body['examples'] = find_or_add(body, 'examples'))
42
+ end
43
+
44
+ def add_desc(body)
45
+ body[example.desc] = { 'summary' => example.desc,
46
+ 'value' => formatted_body(example.response_body,
47
+ find_headers(example.response_headers)) }
48
+ end
49
+
50
+ def add_schema(body, path)
51
+ return unless path
52
+
53
+ schema = find_schema
54
+
55
+ return unless schema
56
+
57
+ add_schema_to_hash(body, path, schema)
58
+ end
59
+
60
+ def find_schema
61
+ if example.response_success?
62
+ example.response_schema_success
63
+ else
64
+ example.response_schema_fail || Dox.config.schema_response_fail_file_path
65
+ end
66
+ end
67
+
68
+ def add_schema_to_hash(body, path, schema)
69
+ body['schema'] =
70
+ if schema.is_a?(Pathname)
71
+ { '$ref' => schema }
72
+ else
73
+ { '$ref' => File.join(path, "#{schema}.json") }
74
+ end
75
+ end
76
+
77
+ def find_headers(headers)
78
+ headers.find { |key, _| key == 'Content-Type' }&.last || 'any'
79
+ end
80
+
81
+ def add_headers(body)
82
+ body['headers'] = Hash[example.response_headers.map { |key, value| [key, { description: value }] }]
83
+ end
84
+ end
85
+ end
86
+ end
@@ -3,7 +3,7 @@ module Dox
3
3
  class ResourceGroupPrinter < BasePrinter
4
4
  def print(resource_group)
5
5
  self.resource_group = resource_group
6
- @output.puts resource_group_title
6
+ add_resource_group
7
7
 
8
8
  resource_group.resources.each do |_, resource|
9
9
  resource_printer.print(resource)
@@ -14,16 +14,16 @@ module Dox
14
14
 
15
15
  attr_accessor :resource_group
16
16
 
17
- def resource_group_title
18
- <<-HEREDOC
17
+ def add_resource_group
18
+ spec['x-tagGroups'].push(name: resource_group.name, 'tags' => []) unless group_included?
19
+ end
19
20
 
20
- # Group #{resource_group.name}
21
- #{print_desc(resource_group.desc)}
22
- HEREDOC
21
+ def group_included?
22
+ spec['x-tagGroups'].find { |group| group[:name] == resource_group.name }
23
23
  end
24
24
 
25
25
  def resource_printer
26
- @resource_printer ||= ResourcePrinter.new(@output)
26
+ @resource_printer ||= ResourcePrinter.new(spec)
27
27
  end
28
28
  end
29
29
  end
@@ -3,7 +3,7 @@ module Dox
3
3
  class ResourcePrinter < BasePrinter
4
4
  def print(resource)
5
5
  self.resource = resource
6
- @output.puts resource_title
6
+ add_resources
7
7
 
8
8
  resource.actions.each do |_, action|
9
9
  action_printer.print(action)
@@ -14,16 +14,21 @@ module Dox
14
14
 
15
15
  attr_accessor :resource
16
16
 
17
- def resource_title
18
- <<-HEREDOC
17
+ def add_resources
18
+ add_to_tags
19
+ add_to_groups
20
+ end
21
+
22
+ def add_to_tags
23
+ spec['tags'] = spec['tags'].push(name: resource.name, description: format_desc(resource.desc)).uniq
24
+ end
19
25
 
20
- ## #{resource.name} [#{resource.endpoint}]
21
- #{print_desc(resource.desc)}
22
- HEREDOC
26
+ def add_to_groups
27
+ spec['x-tagGroups'].find { |group| group[:name] == resource.group }['tags'].push(resource.name)
23
28
  end
24
29
 
25
30
  def action_printer
26
- @action_printer ||= ActionPrinter.new(@output)
31
+ @action_printer ||= ActionPrinter.new(spec['paths'])
27
32
  end
28
33
  end
29
34
  end