lazy_api_doc 0.1.4 → 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.
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>My Application Documentation</title>
5
+ <!-- needed for adaptive design -->
6
+ <meta charset="utf-8"/>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
9
+
10
+ <!--
11
+ ReDoc doesn't change outer page styles
12
+ -->
13
+ <style>
14
+ body {
15
+ margin: 0;
16
+ padding: 0;
17
+ }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <redoc spec-url='./api.yml'></redoc>
22
+ <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,14 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: App name
4
+ version: "1.0.0"
5
+ contact:
6
+ name: User Name
7
+ email: user@example.com
8
+ url: https://app.example.com
9
+
10
+ servers:
11
+ - url: https://app.example.com
12
+ description: description
13
+
14
+ paths:
@@ -1,4 +1,5 @@
1
1
  require 'rails/generators'
2
+ require 'lazy_api_doc'
2
3
 
3
4
  module LazyApiDoc
4
5
  module Generators
@@ -7,8 +8,17 @@ module LazyApiDoc
7
8
 
8
9
  desc "Copy base configuration for LazyApiDoc"
9
10
  def install
10
- copy_file 'public/index.html', 'public/lazy_api_doc/index.html'
11
- copy_file 'public/layout.yml', 'public/lazy_api_doc/layout.yml'
11
+ copy_file 'public/index.html', "#{LazyApiDoc.path}/index.html"
12
+ copy_file 'public/layout.yml', "#{LazyApiDoc.path}/layout.yml"
13
+
14
+ append_to_file '.gitignore' do
15
+ <<~TXT
16
+
17
+ # LazyApiDoc
18
+ #{LazyApiDoc.path}/api.yml
19
+ #{LazyApiDoc.path}/examples/*.json
20
+ TXT
21
+ end
12
22
 
13
23
  install_rspec if Dir.exist?('spec')
14
24
 
@@ -21,18 +31,27 @@ module LazyApiDoc
21
31
  copy_file 'support/rspec_interceptor.rb', 'spec/support/lazy_api_doc_interceptor.rb'
22
32
 
23
33
  insert_into_file 'spec/rails_helper.rb', after: "RSpec.configure do |config|\n" do
24
- <<~RUBY
25
- if ENV['DOC']
26
- require 'lazy_api_doc'
27
- require 'support/lazy_api_doc_interceptor'
34
+ <<-RUBY
35
+ if ENV['LAZY_API_DOC']
36
+ require 'lazy_api_doc'
37
+ require 'support/lazy_api_doc_interceptor'
28
38
 
29
- config.include LazyApiDocInterceptor, type: :request
30
- config.include LazyApiDocInterceptor, type: :controller
39
+ config.include LazyApiDocInterceptor, type: :request
40
+ config.include LazyApiDocInterceptor, type: :controller
31
41
 
32
- config.after(:suite) do
33
- LazyApiDoc.save_result
34
- end
35
- end
42
+ config.after(:suite) do
43
+ # begin: Handle ParallelTests
44
+ # This peace of code handle using ParallelTests (tests runs in independent processes).
45
+ # Just delete this block if you don't use ParallelTests
46
+ if ENV['TEST_ENV_NUMBER'] && defined?(ParallelTests)
47
+ LazyApiDoc.save_examples
48
+ ParallelTests.wait_for_other_processes_to_finish if ParallelTests.first_process?
49
+ LazyApiDoc.load_examples
50
+ end
51
+ # end: Handle ParallelTests
52
+ LazyApiDoc.generate_documentation
53
+ end
54
+ end
36
55
  RUBY
37
56
  end
38
57
  end
@@ -43,7 +62,7 @@ module LazyApiDoc
43
62
  append_to_file 'test/test_helper.rb' do
44
63
  <<~RUBY
45
64
 
46
- if ENV['DOC']
65
+ if ENV['LAZY_API_DOC']
47
66
  require 'lazy_api_doc'
48
67
  require 'support/lazy_api_doc_interceptor'
49
68
 
@@ -52,7 +71,16 @@ module LazyApiDoc
52
71
  end
53
72
 
54
73
  Minitest.after_run do
55
- LazyApiDoc.save_result
74
+ # begin: Handle ParallelTests
75
+ # This peace of code handle using ParallelTests (tests runs in independent processes).
76
+ # Just delete this block if you don't use ParallelTests
77
+ if ENV['TEST_ENV_NUMBER'] && defined?(ParallelTests)
78
+ LazyApiDoc.save_examples
79
+ ParallelTests.wait_for_other_processes_to_finish if ParallelTests.first_process?
80
+ LazyApiDoc.load_examples
81
+ end
82
+ # end: Handle ParallelTests
83
+ LazyApiDoc.generate_documentation
56
84
  end
57
85
  end
58
86
  RUBY
@@ -3,8 +3,8 @@ module LazyApiDocInterceptor
3
3
 
4
4
  included do
5
5
  %w[get post patch put head delete].each do |method|
6
- define_method(method) do |*args|
7
- result = super(*args)
6
+ define_method(method) do |*args, **kwargs|
7
+ result = super(*args, **kwargs)
8
8
  # self.class.metadata[:doc]
9
9
  LazyApiDoc.add_test(self)
10
10
  result
@@ -3,8 +3,8 @@ module LazyApiDocInterceptor
3
3
 
4
4
  included do
5
5
  %w[get post patch put head delete].each do |method|
6
- define_method(method) do |*args|
7
- result = super(*args)
6
+ define_method(method) do |*args, **kwargs|
7
+ result = super(*args, **kwargs)
8
8
  # self.class.metadata[:doc] can be used to document only tests with doc: true metadata
9
9
  LazyApiDoc.add_spec(self)
10
10
  result
@@ -9,23 +9,33 @@ module LazyApiDoc
9
9
  end
10
10
 
11
11
  def add(example)
12
- return if example[:controller] == "anonymous" # don't handle virtual controllers
12
+ return if example['controller'] == "anonymous" # don't handle virtual controllers
13
13
 
14
- @examples << OpenStruct.new(example)
14
+ @examples << example
15
+ end
16
+
17
+ def clear
18
+ @examples = []
15
19
  end
16
20
 
17
21
  def result
18
22
  result = {}
19
- @examples.sort_by(&:source_location).group_by { |ex| [ex.controller, ex.action] }.map do |_, examples|
23
+ @examples.map { |example| OpenStruct.new(example) }.sort_by(&:source_location)
24
+ .group_by { |ex| [ex.controller, ex.action] }
25
+ .each do |_, examples|
20
26
  first = examples.first
21
27
  route = ::LazyApiDoc::RouteParser.new(first.controller, first.action, first.verb).route
22
- doc_path = route[:doc_path]
28
+ next if route.nil? # TODO: think about adding such cases to log
29
+
30
+ doc_path = route['doc_path']
23
31
  result[doc_path] ||= {}
24
32
  result[doc_path].merge!(example_group(first, examples, route))
25
33
  end
26
34
  result
27
35
  end
28
36
 
37
+ private
38
+
29
39
  def example_group(example, examples, route) # rubocop:disable Metrics/AbcSize
30
40
  {
31
41
  example['verb'].downcase => {
@@ -34,13 +44,13 @@ module LazyApiDoc
34
44
  "summary" => example.action,
35
45
  "parameters" => path_params(route, examples) + query_params(examples),
36
46
  "requestBody" => body_params(route, examples),
37
- "responses" => examples.group_by { |ex| ex.response[:code] }.map do |code, variants|
47
+ "responses" => examples.group_by { |ex| ex.response['code'] }.map do |code, variants|
38
48
  [
39
49
  code,
40
50
  {
41
51
  "description" => variants.first["description"].capitalize,
42
52
  "content" => {
43
- example.response[:content_type] => {
53
+ example.response['content_type'] => {
44
54
  "schema" => ::LazyApiDoc::VariantsParser.new(variants.map { |v| parse_body(v.response) }).result
45
55
  }
46
56
  }
@@ -52,17 +62,17 @@ module LazyApiDoc
52
62
  end
53
63
 
54
64
  def parse_body(response)
55
- if response[:content_type].match?("json")
56
- JSON.parse(response[:body])
65
+ if response['content_type'].match?("json")
66
+ JSON.parse(response['body'])
57
67
  else
58
68
  "Not a JSON response"
59
69
  end
60
70
  rescue JSON::ParserError
61
- response[:body]
71
+ response['body']
62
72
  end
63
73
 
64
74
  def path_params(route, examples)
65
- path_variants = examples.map { |example| example.params.slice(*route[:path_params]) }
75
+ path_variants = examples.map { |example| example.params.slice(*route['path_params']) }
66
76
  ::LazyApiDoc::VariantsParser.new(path_variants).result["properties"].map do |param_name, schema|
67
77
  {
68
78
  'in' => "path",
@@ -75,7 +85,7 @@ module LazyApiDoc
75
85
 
76
86
  def query_params(examples)
77
87
  query_variants = examples.map do |example|
78
- full_path = example.request[:full_path].split('?')
88
+ full_path = example.request['full_path'].split('?')
79
89
  next {} if full_path.size == 1
80
90
 
81
91
  # TODO: simplify it
@@ -98,7 +108,7 @@ module LazyApiDoc
98
108
  first = examples.first
99
109
  return unless %w[POST PATCH].include?(first['verb'])
100
110
 
101
- variants = examples.map { |example| example.params.except("controller", "action", *route[:path_params]) }
111
+ variants = examples.map { |example| example.params.except("controller", "action", "format", *route['path_params']) }
102
112
  {
103
113
  'content' => {
104
114
  first.content_type => {
@@ -9,40 +9,25 @@ module LazyApiDoc
9
9
  end
10
10
 
11
11
  def route
12
- self.class.routes.find { |r| r[:action] == action && r[:controller] == controller && r[:verb].include?(verb) }
12
+ self.class.routes.find { |r| r['action'] == action && r['controller'] == controller && r['verb'].include?(verb) }
13
13
  end
14
14
 
15
15
  def self.routes
16
16
  return @routes if defined?(@routes)
17
17
 
18
- all_routes = Rails.application.routes.routes
19
- require "action_dispatch/routing/inspector"
20
- inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
21
- @routes = inspector.format(JsonRoutesFormatter.new, ENV["CONTROLLER"])
18
+ @routes = Rails.application.routes.routes.map { |route| format(route) }
22
19
  end
23
- end
24
- end
25
20
 
26
- class JsonRoutesFormatter
27
- def initialize
28
- @buffer = []
29
- end
21
+ def self.format(route)
22
+ route = ActionDispatch::Routing::RouteWrapper.new(route)
30
23
 
31
- def result
32
- @buffer
33
- end
34
-
35
- def section_title(_title); end
36
-
37
- def section(routes)
38
- @buffer = routes.map do |r|
39
- r[:doc_path] = r[:path].gsub("(.:format)", "").gsub(/(:\w+)/, '{\1}').delete(":")
40
- r[:path_params] = r[:path].gsub("(.:format)", "").scan(/:\w+/).map { |p| p.delete(":").to_sym }
41
- r[:controller] = r[:reqs].split("#").first
42
- r[:action] = r[:reqs].split("#").last.split(" ").first
43
- r
24
+ {
25
+ 'doc_path' => route.path.gsub("(.:format)", "").gsub(/(:\w+)/, '{\1}').delete(":"),
26
+ 'path_params' => route.path.gsub("(.:format)", "").scan(/:\w+/).map { |p| p.delete(":") },
27
+ 'controller' => route.controller,
28
+ 'action' => route.action,
29
+ 'verb' => route.verb.split('|')
30
+ }
44
31
  end
45
32
  end
46
-
47
- def header(_routes); end
48
33
  end
@@ -1,3 +1,3 @@
1
1
  module LazyApiDoc
2
- VERSION = "0.1.4".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
data/lib/lazy_api_doc.rb CHANGED
@@ -7,60 +7,101 @@ require "yaml"
7
7
  module LazyApiDoc
8
8
  class Error < StandardError; end
9
9
 
10
- def self.generator
11
- @generator ||= Generator.new
12
- end
10
+ class << self
11
+ attr_accessor :path, :example_file_ttl
13
12
 
14
- def self.add(example)
15
- generator.add(example)
16
- end
13
+ def configure
14
+ yield self
15
+ end
17
16
 
18
- def self.add_spec(example) # rubocop:disable Metrics/AbcSize
19
- add(
20
- controller: example.request.params[:controller],
21
- action: example.request.params[:action],
22
- description: example.class.description,
23
- source_location: [example.class.metadata[:file_path], example.class.metadata[:line_number]],
24
- verb: example.request.method,
25
- params: example.request.params,
26
- content_type: example.request.content_type.to_s,
27
- request: {
28
- query_params: example.request.query_parameters,
29
- full_path: example.request.fullpath
30
- },
31
- response: {
32
- code: example.response.status,
33
- content_type: example.response.content_type.to_s,
34
- body: example.response.body
35
- }
36
- )
37
- end
17
+ def reset!
18
+ config_file = './config/lazy_api_doc.yml'
19
+ config = File.exist?(config_file) ? YAML.safe_load(ERB.new(File.read(config_file)).result) : {}
38
20
 
39
- def self.add_test(example) # rubocop:disable Metrics/AbcSize
40
- add(
41
- controller: example.request.params[:controller],
42
- action: example.request.params[:action],
43
- description: example.name.gsub(/\Atest_/, '').humanize,
44
- source_location: example.method(example.name).source_location,
45
- verb: example.request.method,
46
- params: example.request.params,
47
- content_type: example.request.content_type.to_s,
48
- request: {
49
- query_params: example.request.query_parameters,
50
- full_path: example.request.fullpath
51
- },
52
- response: {
53
- code: example.response.status,
54
- content_type: example.response.content_type.to_s,
55
- body: example.response.body
56
- }
57
- )
58
- end
21
+ self.path = ENV['LAZY_API_DOC_PATH'] || config['path'] || 'public/lazy_api_doc'
22
+ self.example_file_ttl = ENV['LAZY_API_DOC_EXAMPLE_FILE_TTL'] || config['example_file_ttl'] || 1800 # 30 minutes
23
+ end
24
+
25
+ def generator
26
+ @generator ||= Generator.new
27
+ end
28
+
29
+ def add(lazy_example)
30
+ generator.add(lazy_example)
31
+ end
59
32
 
60
- def self.save_result(to: 'public/lazy_api_doc/api.yml', layout: 'public/lazy_api_doc/layout.yml')
61
- layout = YAML.safe_load(File.read(Rails.root.join(layout)))
62
- layout["paths"] ||= {}
63
- layout["paths"].merge!(generator.result)
64
- File.write(Rails.root.join(to), layout.to_yaml)
33
+ def add_spec(rspec_example) # rubocop:disable Metrics/AbcSize
34
+ add(
35
+ 'controller' => rspec_example.request.params[:controller],
36
+ 'action' => rspec_example.request.params[:action],
37
+ 'description' => rspec_example.class.description,
38
+ 'source_location' => [rspec_example.class.metadata[:file_path], rspec_example.class.metadata[:line_number]],
39
+ 'verb' => rspec_example.request.method,
40
+ 'params' => rspec_example.request.params,
41
+ 'content_type' => rspec_example.request.content_type.to_s,
42
+ 'request' => {
43
+ 'query_params' => rspec_example.request.query_parameters,
44
+ 'full_path' => rspec_example.request.fullpath
45
+ },
46
+ 'response' => {
47
+ 'code' => rspec_example.response.status,
48
+ 'content_type' => rspec_example.response.content_type.to_s,
49
+ 'body' => rspec_example.response.body
50
+ }
51
+ )
52
+ end
53
+
54
+ def add_test(mini_test_example) # rubocop:disable Metrics/AbcSize
55
+ add(
56
+ 'controller' => mini_test_example.request.params[:controller],
57
+ 'action' => mini_test_example.request.params[:action],
58
+ 'description' => mini_test_example.name.gsub(/\Atest_/, '').humanize,
59
+ 'source_location' => mini_test_example.method(mini_test_example.name).source_location,
60
+ 'verb' => mini_test_example.request.method,
61
+ 'params' => mini_test_example.request.params,
62
+ 'content_type' => mini_test_example.request.content_type.to_s,
63
+ 'request' => {
64
+ 'query_params' => mini_test_example.request.query_parameters,
65
+ 'full_path' => mini_test_example.request.fullpath
66
+ },
67
+ 'response' => {
68
+ 'code' => mini_test_example.response.status,
69
+ 'content_type' => mini_test_example.response.content_type.to_s,
70
+ 'body' => mini_test_example.response.body
71
+ }
72
+ )
73
+ end
74
+
75
+ def generate_documentation
76
+ layout = YAML.safe_load(File.read("#{path}/layout.yml"))
77
+ layout["paths"] ||= {}
78
+ layout["paths"].merge!(generator.result)
79
+ File.write("#{path}/api.yml", layout.to_yaml)
80
+ end
81
+
82
+ def save_examples
83
+ FileUtils.mkdir("#{path}/examples") unless File.exist?("#{path}/examples")
84
+ File.write(
85
+ "#{path}/examples/rspec_#{ENV['TEST_ENV_NUMBER'] || SecureRandom.uuid}.json",
86
+ {
87
+ created_at: Time.now.to_i,
88
+ examples: generator.examples
89
+ }.to_json
90
+ )
91
+ end
92
+
93
+ def load_examples
94
+ valid_time = Time.now.to_i - example_file_ttl
95
+ examples = Dir["#{path}/examples/*.json"].flat_map do |file|
96
+ meta = JSON.parse(File.read(file))
97
+ next [] if meta['created_at'] < valid_time # do not handle outdated files
98
+
99
+ meta['examples']
100
+ end
101
+ generator.clear
102
+ examples.each { |example| add(example) }
103
+ end
65
104
  end
66
105
  end
106
+
107
+ LazyApiDoc.reset!
data/screenshot.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lazy_api_doc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdan Guban
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-17 00:00:00.000000000 Z
11
+ date: 2022-05-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: The gem collects all requests and responses from your request specs and
14
14
  generates documentationbased on it
@@ -32,6 +32,9 @@ files:
32
32
  - Rakefile
33
33
  - bin/console
34
34
  - bin/setup
35
+ - docs/example/api.yml
36
+ - docs/example/index.html
37
+ - docs/example/layout.yml
35
38
  - lazy_api_doc.gemspec
36
39
  - lib/generators/lazy_api_doc/install_generator.rb
37
40
  - lib/generators/lazy_api_doc/templates/public/index.html
@@ -43,6 +46,7 @@ files:
43
46
  - lib/lazy_api_doc/route_parser.rb
44
47
  - lib/lazy_api_doc/variants_parser.rb
45
48
  - lib/lazy_api_doc/version.rb
49
+ - screenshot.png
46
50
  homepage: https://github.com/bguban/lazy_api_doc
47
51
  licenses:
48
52
  - MIT
@@ -51,7 +55,7 @@ metadata:
51
55
  homepage_uri: https://github.com/bguban/lazy_api_doc
52
56
  source_code_uri: https://github.com/bguban/lazy_api_doc
53
57
  changelog_uri: https://github.com/bguban/lazy_api_doc
54
- post_install_message:
58
+ post_install_message:
55
59
  rdoc_options: []
56
60
  require_paths:
57
61
  - lib
@@ -66,8 +70,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
70
  - !ruby/object:Gem::Version
67
71
  version: '0'
68
72
  requirements: []
69
- rubygems_version: 3.1.2
70
- signing_key:
73
+ rubygems_version: 3.2.32
74
+ signing_key:
71
75
  specification_version: 4
72
76
  summary: Creates openapi v3 documentation based on rspec request tests
73
77
  test_files: []