lazy_api_doc 0.1.6 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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