lazy_api_doc 0.1.6 → 0.2.2

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('rspec')
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('minitest')
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
@@ -9,24 +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
14
  @examples << example
15
15
  end
16
16
 
17
+ def clear
18
+ @examples = []
19
+ end
20
+
17
21
  def result
18
22
  result = {}
19
23
  @examples.map { |example| OpenStruct.new(example) }.sort_by(&:source_location)
20
- .group_by { |ex| [ex.controller, ex.action] }.map do |_, examples|
24
+ .group_by { |ex| [ex.controller, ex.action] }
25
+ .each do |_, examples|
21
26
  first = examples.first
22
27
  route = ::LazyApiDoc::RouteParser.new(first.controller, first.action, first.verb).route
23
- 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']
24
31
  result[doc_path] ||= {}
25
32
  result[doc_path].merge!(example_group(first, examples, route))
26
33
  end
27
34
  result
28
35
  end
29
36
 
37
+ private
38
+
30
39
  def example_group(example, examples, route) # rubocop:disable Metrics/AbcSize
31
40
  {
32
41
  example['verb'].downcase => {
@@ -35,13 +44,13 @@ module LazyApiDoc
35
44
  "summary" => example.action,
36
45
  "parameters" => path_params(route, examples) + query_params(examples),
37
46
  "requestBody" => body_params(route, examples),
38
- "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|
39
48
  [
40
49
  code,
41
50
  {
42
51
  "description" => variants.first["description"].capitalize,
43
52
  "content" => {
44
- example.response[:content_type] => {
53
+ example.response['content_type'] => {
45
54
  "schema" => ::LazyApiDoc::VariantsParser.new(variants.map { |v| parse_body(v.response) }).result
46
55
  }
47
56
  }
@@ -53,17 +62,17 @@ module LazyApiDoc
53
62
  end
54
63
 
55
64
  def parse_body(response)
56
- if response[:content_type].match?("json")
57
- JSON.parse(response[:body])
65
+ if response['content_type'].match?("json")
66
+ JSON.parse(response['body'])
58
67
  else
59
68
  "Not a JSON response"
60
69
  end
61
70
  rescue JSON::ParserError
62
- response[:body]
71
+ response['body']
63
72
  end
64
73
 
65
74
  def path_params(route, examples)
66
- path_variants = examples.map { |example| example.params.slice(*route[:path_params]) }
75
+ path_variants = examples.map { |example| example.params.slice(*route['path_params']) }
67
76
  ::LazyApiDoc::VariantsParser.new(path_variants).result["properties"].map do |param_name, schema|
68
77
  {
69
78
  'in' => "path",
@@ -76,12 +85,10 @@ module LazyApiDoc
76
85
 
77
86
  def query_params(examples)
78
87
  query_variants = examples.map do |example|
79
- full_path = example.request[:full_path].split('?')
80
- next {} if full_path.size == 1
88
+ _path, query = example.request['full_path'].split('?')
89
+ next {} unless query
81
90
 
82
- # TODO: simplify it
83
- full_path.last.split('&').map { |part| part.split('=').map { |each| CGI.unescape(each) } }.group_by(&:first)
84
- .transform_values { |v| v.map(&:last) }.map { |k, v| [k, k.match?(/\[\]\z/) ? v : v.first] }.to_h
91
+ CGI.parse(query).map { |k, v| [k.gsub('[]', ''), k.match?('\[\]') ? v : v.first] }.to_h
85
92
  end
86
93
 
87
94
  parsed = ::LazyApiDoc::VariantsParser.new(query_variants).result
@@ -99,7 +106,7 @@ module LazyApiDoc
99
106
  first = examples.first
100
107
  return unless %w[POST PATCH].include?(first['verb'])
101
108
 
102
- variants = examples.map { |example| example.params.except("controller", "action", *route[:path_params]) }
109
+ variants = examples.map { |example| example.params.except("controller", "action", "format", *route['path_params']) }
103
110
  {
104
111
  'content' => {
105
112
  first.content_type => {
@@ -9,7 +9,7 @@ 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
@@ -22,11 +22,11 @@ module LazyApiDoc
22
22
  route = ActionDispatch::Routing::RouteWrapper.new(route)
23
23
 
24
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(":").to_sym },
27
- controller: route.controller,
28
- action: route.action,
29
- verb: route.verb.split('|')
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
30
  }
31
31
  end
32
32
  end
@@ -73,10 +73,10 @@ module LazyApiDoc
73
73
  result["properties"] = variant.map do |key, val|
74
74
  [
75
75
  key.to_s,
76
- parse(val, variants.compact.map { |v| v.fetch(key, OPTIONAL) })
76
+ parse(val, variants.select { |v| v.is_a?(Hash) }.map { |v| v.fetch(key, OPTIONAL) })
77
77
  ]
78
78
  end.to_h
79
- result["required"] = variant.keys.select { |key| variants.compact.all? { |v| v.key?(key) } }
79
+ result["required"] = variant.keys.select { |key| variants.select { |v| v.is_a?(Hash) }.all? { |v| v.key?(key) } }
80
80
  result
81
81
  end
82
82
 
@@ -1,3 +1,3 @@
1
1
  module LazyApiDoc
2
- VERSION = "0.1.6".freeze
2
+ VERSION = "0.2.2".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.instance_variable_get(:@request).params[:controller],
36
+ 'action' => rspec_example.instance_variable_get(:@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.instance_variable_get(:@request).method,
40
+ 'params' => rspec_example.instance_variable_get(:@request).params,
41
+ 'content_type' => rspec_example.instance_variable_get(:@request).content_type.to_s,
42
+ 'request' => {
43
+ 'query_params' => rspec_example.instance_variable_get(:@request).query_parameters,
44
+ 'full_path' => rspec_example.instance_variable_get(:@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.instance_variable_get(:@request).params[:controller],
57
+ 'action' => mini_test_example.instance_variable_get(:@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.instance_variable_get(:@request).method,
61
+ 'params' => mini_test_example.instance_variable_get(:@request).params,
62
+ 'content_type' => mini_test_example.instance_variable_get(:@request).content_type.to_s,
63
+ 'request' => {
64
+ 'query_params' => mini_test_example.instance_variable_get(:@request).query_parameters,
65
+ 'full_path' => mini_test_example.instance_variable_get(:@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(process_name)
83
+ FileUtils.mkdir("#{path}/examples") unless File.exist?("#{path}/examples")
84
+ File.write(
85
+ "#{path}/examples/#{process_name}_#{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.6
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bogdan Guban
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-02-08 00:00:00.000000000 Z
11
+ date: 2022-06-02 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