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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +7 -5
- data/README.md +34 -3
- data/docs/example/api.yml +1355 -0
- data/docs/example/index.html +24 -0
- data/docs/example/layout.yml +14 -0
- data/lib/generators/lazy_api_doc/install_generator.rb +42 -14
- data/lib/generators/lazy_api_doc/templates/support/minitest_interceptor.rb +2 -2
- data/lib/generators/lazy_api_doc/templates/support/rspec_interceptor.rb +2 -2
- data/lib/lazy_api_doc/generator.rb +22 -12
- data/lib/lazy_api_doc/route_parser.rb +11 -26
- data/lib/lazy_api_doc/version.rb +1 -1
- data/lib/lazy_api_doc.rb +92 -51
- data/screenshot.png +0 -0
- metadata +10 -6
@@ -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>
|
@@ -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',
|
11
|
-
copy_file 'public/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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
34
|
+
<<-RUBY
|
35
|
+
if ENV['LAZY_API_DOC']
|
36
|
+
require 'lazy_api_doc'
|
37
|
+
require 'support/lazy_api_doc_interceptor'
|
28
38
|
|
29
|
-
|
30
|
-
|
39
|
+
config.include LazyApiDocInterceptor, type: :request
|
40
|
+
config.include LazyApiDocInterceptor, type: :controller
|
31
41
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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['
|
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
|
-
|
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[
|
12
|
+
return if example['controller'] == "anonymous" # don't handle virtual controllers
|
13
13
|
|
14
|
-
@examples <<
|
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.
|
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
|
-
|
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[
|
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[
|
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[
|
56
|
-
JSON.parse(response[
|
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[
|
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[
|
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[
|
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[
|
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[
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
@buffer = []
|
29
|
-
end
|
21
|
+
def self.format(route)
|
22
|
+
route = ActionDispatch::Routing::RouteWrapper.new(route)
|
30
23
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
data/lib/lazy_api_doc/version.rb
CHANGED
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
|
-
|
11
|
-
|
12
|
-
end
|
10
|
+
class << self
|
11
|
+
attr_accessor :path, :example_file_ttl
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def configure
|
14
|
+
yield self
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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.
|
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:
|
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.
|
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: []
|